@stage5/lumine 0.2.94 → 0.2.96

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
@@ -61,6 +61,15 @@ Mikey reviews with `lumine admin storage list`, `show <user>`,
61
61
  or grants directly with `lumine admin storage grant <user> --size <size>`.
62
62
  Approvals are capped at 2 GB and never lower a creator below the default.
63
63
 
64
+ Bridge Builder meetup quest crews (the website's /achievements/bridge-builder
65
+ page) are reviewed with `lumine admin meetup list [--status review|active|completed|all]`,
66
+ `show <crewId>` (includes a one-hour link to the proof video),
67
+ `approve-plan <crewId> [--note <text>]`, `send-back <crewId> --note <text>`
68
+ (sends back whatever is waiting: the video, else the plan) and
69
+ `approve <crewId> --attended <userId,userId,...>`, which unlocks the
70
+ achievement for every member ticked. At least 3 attendees from 3 different
71
+ branches are required.
72
+
64
73
  For team projects, Lumine mirrors the website workspace flow: choosing or
65
74
  pulling the owner's main project creates or reuses your contribution branch and
66
75
  checks out that branch locally. Saves go to your branch, so the project owner
@@ -0,0 +1,338 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import http from "http";
4
+ import { spawn, spawnSync } from "child_process";
5
+ import { fileURLToPath } from "url";
6
+
7
+ // Multi-step conveniences for the full daily management run. Each one runs
8
+ // the ordinary `lumine admin` commands as child processes, so pagination,
9
+ // audit, scope checks and --output handling stay exactly as documented; these
10
+ // only remove the hand-typed glue that every run used to repeat.
11
+
12
+ const LUMINE_BIN = fileURLToPath(new URL("../bin/lumine.js", import.meta.url));
13
+ const MARKED_ASSET = fileURLToPath(
14
+ new URL("./report-assets/marked.min.js", import.meta.url),
15
+ );
16
+
17
+ function runAdmin(args, { outputFile, extraEnv } = {}) {
18
+ const full = ["admin", ...args, "--json"];
19
+ if (outputFile) full.push("--output", outputFile);
20
+ const result = spawnSync(process.execPath, [LUMINE_BIN, ...full], {
21
+ encoding: "utf8",
22
+ maxBuffer: 256 * 1024 * 1024,
23
+ env: { ...process.env, ...(extraEnv || {}) },
24
+ });
25
+ let parsed = null;
26
+ try {
27
+ parsed = JSON.parse(result.stdout || "null");
28
+ } catch {
29
+ parsed = null;
30
+ }
31
+ return {
32
+ ok: result.status === 0 && parsed?.ok !== false,
33
+ status: result.status,
34
+ json: parsed,
35
+ stderr: String(result.stderr || "").slice(-2000),
36
+ };
37
+ }
38
+
39
+ function ensureDir(dir) {
40
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
41
+ return dir;
42
+ }
43
+
44
+ function utcDayKey(offsetDays = 0) {
45
+ const d = new Date(Date.now() + offsetDays * 86400000);
46
+ return d.toISOString().slice(0, 10);
47
+ }
48
+
49
+ // Bangkok midnight `days` calendar days before today, as an ISO timestamp.
50
+ export function bangkokCutoff(days, now = Date.now()) {
51
+ const nowBkk = new Date(now + 7 * 3600000);
52
+ const midnight = Date.UTC(
53
+ nowBkk.getUTCFullYear(),
54
+ nowBkk.getUTCMonth(),
55
+ nowBkk.getUTCDate(),
56
+ );
57
+ return new Date(midnight - days * 86400000 - 7 * 3600000).toISOString();
58
+ }
59
+
60
+ // Renders every Zero/Ciel row as readable text grouped by conversation, the
61
+ // file the conduct review reads end to end.
62
+ export function renderBotOutput({ chatMessages, comments }, file) {
63
+ const t = (ts) => new Date(ts * 1000).toISOString().slice(5, 16);
64
+ const lines = [];
65
+ const sorted = [...chatMessages].sort(
66
+ (a, b) => a.channelId - b.channelId || a.messageId - b.messageId,
67
+ );
68
+ for (const m of sorted) {
69
+ const r = m.recipient || {};
70
+ const g = m.generation || {};
71
+ lines.push(
72
+ `--- #${m.messageId} ${t(m.occurredAt)} ${m.bot} -> ${r.username}(${r.userId}) ch${m.channelId} ${m.surface} kind=${m.messageKind} src=${m.source} gen=${g.storedOutcome} err=${g.errorType}`,
73
+ String(m.content ?? ""),
74
+ );
75
+ }
76
+ lines.push("", "", "===== COMMENTS =====");
77
+ for (const c of comments) {
78
+ lines.push(
79
+ `--- comment ${c.commentId} ${t(c.occurredAt)} ${c.bot} root=${c.rootType}:${c.rootId} ${c.url}`,
80
+ String(c.content ?? ""),
81
+ );
82
+ }
83
+ fs.writeFileSync(file, lines.join("\n") + "\n", { mode: 0o600 });
84
+ }
85
+
86
+ export async function dailyRunGather(options) {
87
+ const dir = ensureDir(
88
+ path.resolve(options.adminOutputDir || options.adminDir || "."),
89
+ );
90
+ const yesterday = options.adminDate || utcDayKey(-1);
91
+ const steps = [];
92
+ const step = (name, args, file) => {
93
+ const outputFile = path.join(dir, file);
94
+ const res = runAdmin(args, { outputFile });
95
+ steps.push({
96
+ name,
97
+ file,
98
+ ok: res.ok,
99
+ ...(res.ok ? {} : { error: res.json?.error || res.stderr }),
100
+ });
101
+ return res;
102
+ };
103
+ step("energy-budget", ["energy-budget"], "energy-budget.json");
104
+ step("news", ["news"], "news.json");
105
+ step("brief", ["brief"], "brief.json");
106
+ step("ai-costs-monthly", ["ai-costs", "monthly"], "ai-monthly.json");
107
+ step("ai-costs-day", ["ai-costs", "day", yesterday], "ai-day.json");
108
+ step("media-costs", ["media-costs", "monthly"], "media.json");
109
+ step(
110
+ "reward-activity",
111
+ ["reward-activity", "--date", yesterday, "--days", "7"],
112
+ "reward-activity.json",
113
+ );
114
+ step("reward-review-pending", ["reward-review", "list"], "rr-pending.json");
115
+ step("todos", ["todo", "list"], "todos.json");
116
+ step("escalations", ["escalation", "list"], "escalations.json");
117
+ step("featured-list", ["featured", "list"], "featured-start.json");
118
+ step(
119
+ "sponsor-applications",
120
+ ["sponsor", "applications", "list", "--status", "pending"],
121
+ "sponsor-applications.json",
122
+ );
123
+
124
+ // Bot output: follow the cursor until both sources are exhausted.
125
+ const chatMessages = [];
126
+ const comments = [];
127
+ let cursor = "";
128
+ let pages = 0;
129
+ let botOk = true;
130
+ for (;;) {
131
+ pages += 1;
132
+ const file = `bot-output-${pages}.json`;
133
+ const res = step(
134
+ `bot-output-page-${pages}`,
135
+ cursor ? ["bot-output", "--cursor", cursor] : ["bot-output"],
136
+ file,
137
+ );
138
+ if (!res.ok) {
139
+ botOk = false;
140
+ break;
141
+ }
142
+ const data = res.json?.data || {};
143
+ chatMessages.push(...(data.chatMessages || []));
144
+ comments.push(...(data.comments || []));
145
+ const p = data.pagination || {};
146
+ if (p.exhausted && !data.chatTruncated && !data.commentsTruncated) break;
147
+ cursor = p.nextCursor || "";
148
+ if (!cursor || pages > 50) {
149
+ botOk = false;
150
+ steps.push({
151
+ name: "bot-output",
152
+ ok: false,
153
+ error: "pagination did not exhaust",
154
+ });
155
+ break;
156
+ }
157
+ }
158
+ if (botOk) renderBotOutput({ chatMessages, comments }, path.join(dir, "bot-chats.txt"));
159
+
160
+ const summary = {
161
+ dir,
162
+ yesterdayUtc: yesterday,
163
+ botOutput: {
164
+ complete: botOk,
165
+ pages,
166
+ chatRows: chatMessages.length,
167
+ comments: comments.length,
168
+ readFile: botOk ? path.join(dir, "bot-chats.txt") : null,
169
+ },
170
+ steps,
171
+ failed: steps.filter((s) => !s.ok).map((s) => s.name),
172
+ };
173
+ fs.writeFileSync(
174
+ path.join(dir, "gather.json"),
175
+ JSON.stringify(summary, null, 2),
176
+ { mode: 0o600 },
177
+ );
178
+ return {
179
+ ok: summary.failed.length === 0,
180
+ status: summary.failed.length ? "partial_failure" : "success",
181
+ data: summary,
182
+ };
183
+ }
184
+
185
+ // Recent, provably never-Featured Subjects with the context the editorial
186
+ // ranking needs (comment count, operator view state), newest first.
187
+ export async function featuredCandidates(options) {
188
+ const days = Math.max(1, Math.min(30, Number(options.adminDays || 7)));
189
+ const after = options.adminAfter || bangkokCutoff(days);
190
+ const scratch = ensureDir(
191
+ path.resolve(options.adminOutputDir || fs.mkdtempSync(path.join(process.env.TMPDIR || "/tmp", "lumine-featured-"))),
192
+ );
193
+ const subjects = runAdmin(
194
+ ["subjects", "candidates", "--after", after, "--all"],
195
+ { outputFile: path.join(scratch, "subjects.json") },
196
+ );
197
+ if (!subjects.ok) throw new Error(`subjects candidates failed: ${JSON.stringify(subjects.json?.error || subjects.stderr)}`);
198
+ const board = runAdmin(["featured", "list"]);
199
+ if (!board.ok) throw new Error("featured list failed");
200
+ const onBoard = new Set((board.json.data.subjects || []).map((s) => s.id));
201
+ const recent = (subjects.json.data.subjects || []).filter((s) => !onBoard.has(s.id));
202
+ const ids = recent.map((s) => s.id);
203
+ const history = ids.length
204
+ ? runAdmin(["featured", "history", "--subject-ids", ids.join(","), "--all"])
205
+ : { ok: true, json: { data: { subjects: [], coverage: {} } } };
206
+ if (!history.ok) throw new Error("featured history failed");
207
+ const never = new Map(
208
+ (history.json.data.subjects || []).map((s) => [s.id, s.neverFeatured]),
209
+ );
210
+ const candidates = [];
211
+ for (const s of recent) {
212
+ const detail = runAdmin(["subject", "get", String(s.id), "--include-comments"]);
213
+ const subject = detail.json?.data?.subject || {};
214
+ const humanComments = (subject.comments || []).filter(
215
+ (c) => !c.isNotification && ![7587, 8411].includes(Number(c.author?.id)),
216
+ );
217
+ candidates.push({
218
+ id: s.id,
219
+ url: s.url,
220
+ title: s.title,
221
+ author: s.author?.username || null,
222
+ createdAt: s.createdAt,
223
+ neverFeatured: never.get(s.id) ?? null,
224
+ effortLevel: s.effortLevel,
225
+ hasSecret: Boolean(s.secret?.hasSecret),
226
+ hasAttachment: Boolean(s.attachment),
227
+ description: String(s.description || "").slice(0, 300),
228
+ commentsIncluded: Boolean(subject.commentsIncluded),
229
+ humanComments: humanComments.length,
230
+ botComments: (subject.comments || []).filter((c) =>
231
+ [7587, 8411].includes(Number(c.author?.id)),
232
+ ).length,
233
+ operatorViewed: Boolean(s.operatorViewed?.viewed),
234
+ });
235
+ }
236
+ return {
237
+ ok: true,
238
+ status: "success",
239
+ data: {
240
+ postedAfter: after,
241
+ coverage: history.json.data.coverage || null,
242
+ boardCount: onBoard.size,
243
+ eligible: candidates.filter((c) => c.neverFeatured === true),
244
+ excluded: candidates
245
+ .filter((c) => c.neverFeatured !== true)
246
+ .map((c) => ({ id: c.id, title: c.title, neverFeatured: c.neverFeatured })),
247
+ note: "Eligibility only: posted after the cutoff and provably never Featured. Ranking by likely member engagement is the editor's judgment.",
248
+ },
249
+ };
250
+ }
251
+
252
+ function reportHtml(title) {
253
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>${title.replace(/</g, "&lt;")}</title>
254
+ <style>
255
+ :root{--bg:#fff;--fg:#1d2433;--muted:#5b6475;--line:#dde2ea;--nav:#f6f8fb;--code:#f1f3f7;--link:#2757c9}
256
+ @media (prefers-color-scheme:dark){:root{--bg:#14171d;--fg:#e4e8ef;--muted:#9aa3b2;--line:#2b313b;--nav:#1a1e25;--code:#222833;--link:#8fb0ff}}
257
+ *{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--fg);font:15px/1.55 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}
258
+ nav{position:fixed;top:0;left:0;bottom:0;width:250px;overflow:auto;background:var(--nav);border-right:1px solid var(--line);padding:18px 14px}
259
+ nav h4{margin:0 0 10px;font-size:12px;letter-spacing:.08em;text-transform:uppercase;color:var(--muted)}nav a{display:block;padding:4px 6px;border-radius:6px;color:var(--fg);text-decoration:none;font-size:13px}nav a:hover{background:var(--line)}
260
+ main{margin-left:250px;padding:28px 40px;max-width:1100px}a{color:var(--link)}table{border-collapse:collapse;margin:10px 0;display:block;overflow-x:auto}th,td{border:1px solid var(--line);padding:6px 9px;text-align:left;vertical-align:top}
261
+ code{background:var(--code);padding:1px 5px;border-radius:4px;font-size:.9em}pre{background:var(--code);padding:12px;border-radius:8px;overflow:auto}h2{border-bottom:1px solid var(--line);padding-bottom:4px;margin-top:34px}
262
+ @media (max-width:760px){nav{position:static;width:auto;height:auto;border-right:0;border-bottom:1px solid var(--line)}main{margin-left:0;padding:16px}}
263
+ </style></head><body><nav><h4>Sections</h4><div id="toc"></div><h4 style="margin-top:18px">Source</h4><a href="daily-management-report.md">Markdown</a></nav><main id="content">Loading…</main>
264
+ <script src="marked.min.js"></script><script>
265
+ fetch('daily-management-report.md',{cache:'no-store'}).then(r=>r.text()).then(md=>{const el=document.getElementById('content');el.innerHTML=marked.parse(md);const toc=document.getElementById('toc');let i=0;el.querySelectorAll('h1,h2').forEach(h=>{h.id='s'+(i++);if(h.tagName==='H2'){const a=document.createElement('a');a.href='#'+h.id;a.textContent=h.textContent;toc.appendChild(a)}})});
266
+ </script></body></html>`;
267
+ }
268
+
269
+ // Serves the report Markdown as a browsable page on loopback. Only the page,
270
+ // its script and the Markdown are reachable; the evidence folder is not.
271
+ export async function reportServe(options) {
272
+ const file = path.resolve(options.adminFile || "daily-management-report.md");
273
+ if (!fs.existsSync(file)) throw new Error(`Report not found: ${file}`);
274
+ const title =
275
+ (fs.readFileSync(file, "utf8").match(/^#\s+(.+)$/m) || [])[1] ||
276
+ "Daily management report";
277
+ const port = Number(options.adminPort || 0);
278
+ const routes = {
279
+ "/": () => ({ type: "text/html; charset=utf-8", body: reportHtml(title) }),
280
+ "/index.html": () => ({ type: "text/html; charset=utf-8", body: reportHtml(title) }),
281
+ "/marked.min.js": () => ({ type: "text/javascript", body: fs.readFileSync(MARKED_ASSET) }),
282
+ "/daily-management-report.md": () => ({
283
+ type: "text/markdown; charset=utf-8",
284
+ body: fs.readFileSync(file),
285
+ }),
286
+ };
287
+ if (options.adminForeground) {
288
+ const server = http.createServer((req, res) => {
289
+ const route = routes[(req.url || "/").split("?")[0]];
290
+ if (!route) {
291
+ res.writeHead(404).end("not found");
292
+ return;
293
+ }
294
+ const { type, body } = route();
295
+ res.writeHead(200, { "content-type": type, "cache-control": "no-store" });
296
+ res.end(body);
297
+ });
298
+ await new Promise((resolve) => server.listen(port, "127.0.0.1", resolve));
299
+ const url = `http://127.0.0.1:${server.address().port}/`;
300
+ console.log(JSON.stringify({ ok: true, status: "success", data: { url, file } }));
301
+ return new Promise(() => {});
302
+ }
303
+ // Detach a foreground copy of this command so the page outlives the CLI.
304
+ const child = spawn(
305
+ process.execPath,
306
+ [LUMINE_BIN, "admin", "report", "serve", "--file", file, "--port", String(port), "--foreground"],
307
+ { detached: true, stdio: ["ignore", "pipe", "ignore"] },
308
+ );
309
+ const url = await new Promise((resolve, reject) => {
310
+ let buf = "";
311
+ const timer = setTimeout(() => reject(new Error("report server did not start")), 10000);
312
+ child.stdout.on("data", (chunk) => {
313
+ buf += chunk;
314
+ const line = buf.split("\n")[0];
315
+ try {
316
+ const parsed = JSON.parse(line);
317
+ clearTimeout(timer);
318
+ resolve(parsed.data.url);
319
+ } catch {
320
+ /* wait for the full line */
321
+ }
322
+ });
323
+ child.on("exit", (code) => {
324
+ clearTimeout(timer);
325
+ reject(new Error(`report server exited (${code})`));
326
+ });
327
+ });
328
+ child.stdout.destroy();
329
+ child.unref();
330
+ if (options.adminOpen) {
331
+ spawnSync("open", ["-a", "Google Chrome", url]);
332
+ }
333
+ return {
334
+ ok: true,
335
+ status: "success",
336
+ data: { url, file, pid: child.pid, opened: Boolean(options.adminOpen) },
337
+ };
338
+ }
package/lib/admin.js CHANGED
@@ -32,6 +32,11 @@ import {
32
32
  runManagedBuildReview,
33
33
  } from "./build-review.js";
34
34
  import { runAdminRuntimeLogWorkflow } from "./admin-runtime-logs.js";
35
+ import {
36
+ dailyRunGather,
37
+ featuredCandidates,
38
+ reportServe,
39
+ } from "./admin-daily.js";
35
40
  import {
36
41
  readApprovedFeaturedPlan,
37
42
  runFeaturedWorkflow,
@@ -582,6 +587,26 @@ function readEditorialFile(filePath) {
582
587
  }
583
588
 
584
589
  export async function adminCommand(options) {
590
+ const [namespace, action] = options.positional || [];
591
+ const composite =
592
+ namespace === "daily-run" && action === "gather"
593
+ ? dailyRunGather
594
+ : namespace === "featured" && action === "candidates"
595
+ ? featuredCandidates
596
+ : namespace === "report" && action === "serve"
597
+ ? reportServe
598
+ : null;
599
+ if (composite) {
600
+ const result = await composite(options);
601
+ if (options.adminOutput) {
602
+ writeFileSync(options.adminOutput, JSON.stringify(result), {
603
+ mode: 0o600,
604
+ });
605
+ }
606
+ console.log(JSON.stringify(result, null, options.json ? 0 : 2));
607
+ if (result && result.ok === false) process.exitCode = 1;
608
+ return result;
609
+ }
585
610
  const operation = parseAdminOperation(options);
586
611
  if (operation.name === "news.validate") {
587
612
  const validation = validateNewsEditorial({
@@ -1899,6 +1924,110 @@ export function parseAdminOperation(options) {
1899
1924
  );
1900
1925
  }
1901
1926
 
1927
+ if (MEETUP_NAMESPACES.has(namespace)) {
1928
+ // Bridge Builder meetup quest crews (achievement type 'meetup'): the same
1929
+ // reviews as the admin controls on the website quest page. Mikey's own
1930
+ // decision, any time, never in a daily run.
1931
+ const usage =
1932
+ "Usage: lumine admin meetup list [--status review|active|completed|all] | 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>.";
1933
+ const note = String(options.note || "").trim();
1934
+ if (note.length > 1000) {
1935
+ throw cliValidationError("--note must be at most 1000 characters.");
1936
+ }
1937
+ if (!action || action === "list") {
1938
+ return readOperation(
1939
+ "meetup.list",
1940
+ withQuery("/cli/admin/meetup-quest/crews", {
1941
+ status: parseChoice(
1942
+ options.adminStatus || "review",
1943
+ "--status",
1944
+ ["review", "active", "completed", "all"],
1945
+ ),
1946
+ }),
1947
+ { requiresRun: false },
1948
+ );
1949
+ }
1950
+ if (action === "show" || action === "get") {
1951
+ const crewId = parseRequiredInteger(target, "Crew ID", 1);
1952
+ return readOperation(
1953
+ "meetup.show",
1954
+ `/cli/admin/meetup-quest/crews/${crewId}`,
1955
+ { requiresRun: false },
1956
+ );
1957
+ }
1958
+ if (action === "approve-plan") {
1959
+ const crewId = parseRequiredInteger(target, "Crew ID", 1);
1960
+ return writeOperation(
1961
+ "meetup.approve-plan",
1962
+ "POST",
1963
+ `/cli/admin/meetup-quest/crews/${crewId}/approve-plan`,
1964
+ { note },
1965
+ { requiresRun: false },
1966
+ );
1967
+ }
1968
+ if (action === "send-back") {
1969
+ const crewId = parseRequiredInteger(target, "Crew ID", 1);
1970
+ if (!note) {
1971
+ throw cliValidationError(
1972
+ "lumine admin meetup send-back <crewId> needs --note <what to change> (the crew sees it).",
1973
+ );
1974
+ }
1975
+ return writeOperation(
1976
+ "meetup.send-back",
1977
+ "POST",
1978
+ `/cli/admin/meetup-quest/crews/${crewId}/send-back`,
1979
+ { note },
1980
+ { requiresRun: false },
1981
+ );
1982
+ }
1983
+ if (action === "approve") {
1984
+ const crewId = parseRequiredInteger(target, "Crew ID", 1);
1985
+ const attendedUserIds = String(options.adminAttended || "")
1986
+ .split(",")
1987
+ .map((part) => part.trim())
1988
+ .filter(Boolean)
1989
+ .map((part) => parseRequiredInteger(part, "--attended user ID", 1));
1990
+ if (!attendedUserIds.length) {
1991
+ throw cliValidationError(
1992
+ "lumine admin meetup approve <crewId> needs --attended <userId,userId,...> (the members who showed up in the video).",
1993
+ );
1994
+ }
1995
+ return writeOperation(
1996
+ "meetup.approve",
1997
+ "POST",
1998
+ `/cli/admin/meetup-quest/crews/${crewId}/approve`,
1999
+ { attendedUserIds: [...new Set(attendedUserIds)], note },
2000
+ { requiresRun: false },
2001
+ );
2002
+ }
2003
+ if (action === "slot") {
2004
+ const crewId = parseRequiredInteger(target, "Crew ID", 1);
2005
+ if (options.adminConfirm === "") {
2006
+ throw cliValidationError(
2007
+ "lumine admin meetup slot <crewId> needs --confirm <slotIndex> (0 is the first offered slot; see `show`).",
2008
+ );
2009
+ }
2010
+ return writeOperation(
2011
+ "meetup.slot",
2012
+ "POST",
2013
+ `/cli/admin/meetup-quest/crews/${crewId}/slot`,
2014
+ { slotIndex: parseRequiredInteger(options.adminConfirm, "--confirm", 0, 9) },
2015
+ { requiresRun: false },
2016
+ );
2017
+ }
2018
+ if (action === "resend-email") {
2019
+ const crewId = parseRequiredInteger(target, "Crew ID", 1);
2020
+ return writeOperation(
2021
+ "meetup.resend-email",
2022
+ "POST",
2023
+ `/cli/admin/meetup-quest/crews/${crewId}/resend-email`,
2024
+ {},
2025
+ { requiresRun: false },
2026
+ );
2027
+ }
2028
+ throw cliValidationError(usage);
2029
+ }
2030
+
1902
2031
  if (STORAGE_LIMIT_NAMESPACES.has(namespace)) {
1903
2032
  // Lumine file storage (Twinkle.files) unlocks: per-creator approvals past
1904
2033
  // the default quota. Mikey's own decision, any time, never in a daily run.
@@ -4659,6 +4788,124 @@ function printCardCraftReviewResult({ operation, data }) {
4659
4788
  );
4660
4789
  }
4661
4790
 
4791
+ const MEETUP_NAMESPACES = new Set([
4792
+ "meetup",
4793
+ "meetups",
4794
+ "meetup-quest",
4795
+ "bridge-builder",
4796
+ ]);
4797
+
4798
+ function formatMeetupMembers(members) {
4799
+ return (members || [])
4800
+ .map(
4801
+ (member) =>
4802
+ `${member.username || `user ${member.userId}`} (${member.userId}, ${member.branch || "no branch"}${member.parentOk === undefined ? "" : member.parentOk ? ", parent OK" : ", parent not yet"}${member.attended ? ", attended" : ""})`,
4803
+ )
4804
+ .join("; ");
4805
+ }
4806
+
4807
+ function printMeetupCrew(crew) {
4808
+ const progress = crew.progress || {};
4809
+ console.log(
4810
+ `Crew #${crew.crewId}${crew.name ? ` "${crew.name}"` : ""} · ${crew.status}${crew.isOpen === false ? " · invite only" : ""} · step: ${progress.currentStep || "done"}${progress.blocking ? ` · ${progress.blocking}` : ""}`,
4811
+ );
4812
+ if (crew.about) console.log(` About: ${crew.about}`);
4813
+ for (const step of progress.steps || []) {
4814
+ console.log(` ${step.number}. ${step.label} [${step.state}] ${step.detail}`);
4815
+ }
4816
+ console.log(` Members: ${formatMeetupMembers(crew.members)}`);
4817
+ if (Array.isArray(crew.invites) && crew.invites.length) {
4818
+ console.log(
4819
+ ` Pending invites: ${crew.invites.map((invite) => `${invite.username} (${invite.userId}, invite ${invite.inviteId})`).join("; ")}`,
4820
+ );
4821
+ }
4822
+ if (crew.adult?.kind) {
4823
+ console.log(
4824
+ ` Adult coming: ${crew.adult.kind === "teacher" ? "Twinkle teacher" : "parent"}: ${crew.adult.name}`,
4825
+ );
4826
+ }
4827
+ const plan = crew.plan || {};
4828
+ if (plan.status && plan.status !== "none") {
4829
+ console.log(
4830
+ ` Plan (${plan.status}): ${plan.date} in ${plan.area}: ${plan.activity}${plan.note ? ` · note: ${plan.note}` : ""}`,
4831
+ );
4832
+ }
4833
+ if (crew.planApprovedBy?.username) {
4834
+ console.log(` Plan approved by ${crew.planApprovedBy.username}`);
4835
+ }
4836
+ const venue = crew.venue || {};
4837
+ if (venue.status && venue.status !== "none") {
4838
+ const slots = (venue.slots || []).map(
4839
+ (slot, index) => `[${index}] ${slot.date} ${slot.start}-${slot.end}`,
4840
+ );
4841
+ console.log(
4842
+ ` Venue (${venue.status}): ${venue.room ? `${venue.branch} · ${venue.room}` : "no classroom (meets elsewhere with the grown-up)"}${slots.length ? ` · slots ${slots.join(", ")}` : ""}${venue.confirmedSlot ? ` · SET: ${venue.confirmedSlot.date} ${venue.confirmedSlot.start}-${venue.confirmedSlot.end}` : ""}`,
4843
+ );
4844
+ }
4845
+ const video = crew.video || {};
4846
+ if (video.status && video.status !== "none") {
4847
+ console.log(
4848
+ ` Video (${video.status}): ${video.fileName}${video.note ? ` · note: ${video.note}` : ""}`,
4849
+ );
4850
+ if (video.url) console.log(` Watch (link expires in 1 hour): ${video.url}`);
4851
+ }
4852
+ }
4853
+
4854
+ function printMeetupResult({ operation, data, result }) {
4855
+ if (operation.name === "meetup.list") {
4856
+ const crews = Array.isArray(data.crews) ? data.crews : [];
4857
+ console.log(`${crews.length} meetup crew(s) (${data.filter || "review"}):`);
4858
+ for (const crew of crews) {
4859
+ console.log(
4860
+ ` #${crew.crewId} · ${crew.status}${crew.waitingFor ? ` · waiting: ${crew.waitingFor}` : ""} · founder ${crew.founderUsername || "?"} · ${formatMeetupMembers(crew.members)}${crew.blocking ? ` · ${crew.blocking}` : ""}`,
4861
+ );
4862
+ }
4863
+ if (crews.some((crew) => crew.waitingFor)) {
4864
+ console.log(
4865
+ "Review: lumine admin meetup show <crewId>, then approve-plan | send-back --note | approve --attended <ids>.",
4866
+ );
4867
+ }
4868
+ return;
4869
+ }
4870
+ if (operation.name === "meetup.show") {
4871
+ printMeetupCrew(data.crew || {});
4872
+ return;
4873
+ }
4874
+ if (operation.name === "meetup.slot") {
4875
+ console.log(
4876
+ `Slot set for crew #${data.crew?.crewId}: ${data.slot?.date} ${data.slot?.start}-${data.slot?.end}.`,
4877
+ );
4878
+ if (data.crew) printMeetupCrew(data.crew);
4879
+ return;
4880
+ }
4881
+ if (operation.name === "meetup.resend-email") {
4882
+ const email = data.email || {};
4883
+ console.log(
4884
+ email.sent
4885
+ ? `Coordinator ${email.kind} email ${email.mode === "sent" ? "sent" : `built (${email.mode})`} to ${email.to}.`
4886
+ : `No email sent: ${email.reason || "unknown"}.`,
4887
+ );
4888
+ return;
4889
+ }
4890
+ const stage = data.result?.stage === "video" ? "video" : "plan";
4891
+ if (operation.name === "meetup.approve-plan") {
4892
+ console.log(
4893
+ result?.changed === false
4894
+ ? `Crew #${data.result?.crewId}'s plan was already approved.`
4895
+ : `Approved crew #${data.result?.crewId}'s plan. They can meet up and film it now.`,
4896
+ );
4897
+ } else if (operation.name === "meetup.send-back") {
4898
+ console.log(`Sent crew #${data.result?.crewId}'s ${stage} back with your note.`);
4899
+ } else if (operation.name === "meetup.approve") {
4900
+ const unlocked = data.unlocks?.unlockedUserIds || [];
4901
+ const failed = data.unlocks?.failed || [];
4902
+ console.log(
4903
+ `${result?.changed === false ? "Already approved" : "Approved"} crew #${data.result?.crewId}: unlocked for user(s) ${unlocked.join(", ") || "none"}.${failed.length ? ` Unlock FAILED for ${failed.join(", ")}; run the same command again.` : ""}`,
4904
+ );
4905
+ }
4906
+ if (data.crew) printMeetupCrew(data.crew);
4907
+ }
4908
+
4662
4909
  const STORAGE_LIMIT_NAMESPACES = new Set([
4663
4910
  "storage",
4664
4911
  "storage-limit",
@@ -4866,6 +5113,10 @@ function printAdminResult({ operation, result }) {
4866
5113
  printStorageLimitResult({ operation, data, result });
4867
5114
  return;
4868
5115
  }
5116
+ if (operation.name.startsWith("meetup.")) {
5117
+ printMeetupResult({ operation, data, result });
5118
+ return;
5119
+ }
4869
5120
  if (operation.name === "runtime.evidence") {
4870
5121
  const evidence = data.evidence || {};
4871
5122
  console.log(
package/lib/commands.js CHANGED
@@ -2384,6 +2384,8 @@ export function parseArgs(args) {
2384
2384
  "noReviewLoop",
2385
2385
  "acceptAgreement",
2386
2386
  "show",
2387
+ "foreground",
2388
+ "open",
2387
2389
  ]);
2388
2390
 
2389
2391
  for (let i = 0; i < rest.length; i += 1) {
@@ -2602,6 +2604,8 @@ export function parseArgs(args) {
2602
2604
  twinkles: raw.twinkles ? String(raw.twinkles) : "",
2603
2605
  adminReason: raw.reason ? String(raw.reason) : "",
2604
2606
  storageSize: raw.size ? String(raw.size) : "",
2607
+ adminAttended: raw.attended ? String(raw.attended) : "",
2608
+ adminConfirm: raw.confirm === undefined ? "" : String(raw.confirm),
2605
2609
  adminContextLimit: raw.limit === undefined ? 20 : Number(raw.limit),
2606
2610
  adminRun: raw.run ? String(raw.run) : "",
2607
2611
  adminTarget: raw.target ? String(raw.target) : "",
@@ -2612,6 +2616,9 @@ export function parseArgs(args) {
2612
2616
  adminEditionId: raw.editionId ? String(raw.editionId) : "",
2613
2617
  adminLeaseToken: raw.leaseToken ? String(raw.leaseToken) : "",
2614
2618
  adminFile: raw.file ? String(raw.file) : "",
2619
+ adminPort: raw.port ? String(raw.port) : "",
2620
+ adminForeground: Boolean(raw.foreground),
2621
+ adminOpen: Boolean(raw.open),
2615
2622
  adminConfigFile: raw.config ? String(raw.config) : "",
2616
2623
  adminReviewedBuildVersion: raw.reviewedVersion
2617
2624
  ? String(raw.reviewedVersion)
@@ -3003,6 +3010,7 @@ export function printHelp() {
3003
3010
  lumine admin reward-review list [--status pending|approved|all] [--cursor <id>] [--json]
3004
3011
  lumine admin cardcraft-review list [--status pending|approved|all] [--cursor <id>] | show <id> | approve|reject|revoke <id> [--reason <text>]
3005
3012
  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>]
3013
+ lumine admin meetup list [--status review|active|completed|all] | 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)
3006
3014
  lumine admin reward-activity [--date YYYY-MM-DD] [--days <1..31>] [--build <id>] [--json]
3007
3015
  lumine admin chat-reports list [--status pending|open|reviewing|resolved|dismissed|all] [--cursor <id>] [--json]
3008
3016
  lumine admin chat-reports show <report-id> [--json] (reported message snapshot + surrounding context)
@@ -3018,6 +3026,9 @@ export function printHelp() {
3018
3026
  lumine admin reward-review reject|revoke <review-id> --reason <text> [--json]
3019
3027
  lumine admin recommendations list [--since-run|--after <date>|--include-legacy] [--all --checkpoint <file> [--resume]] [--content-types comment,dailyReflection] [--unviewed|--viewed] [--cursor <cursor>] [--json]
3020
3028
  lumine admin builds candidates [--since-run|--after <date>|--include-legacy] [--all --checkpoint <file> [--resume]] [--cursor <cursor>] [--limit <number>] [--json]
3029
+ lumine admin daily-run gather --output-dir <dir> [--date YYYY-MM-DD] [--json] (all read-only daily reports + bot-output pages + bot-chats.txt)
3030
+ lumine admin featured candidates [--days 7] [--after <iso>] [--json] (recent, provably never-Featured Subjects with comment counts)
3031
+ lumine admin report serve --file <report.md> [--port <n>] [--open] [--json] (loopback report page with section nav)
3021
3032
  lumine admin builds review <build-url-or-id> [--output-dir <dir>] [--wait-ms <ms>] [--interact <steps.json>] [--browser-path <path>] [--json]
3022
3033
  lumine admin subjects candidates [--since-run|--after <date>|--include-legacy] [--effort unassigned] [--unviewed|--viewed] [--all --checkpoint <file> [--resume]] [--cursor <cursor>] [--json]
3023
3034
  lumine admin subject get|reveal <subject-url-or-id> [--json]
package/lib/constants.js CHANGED
@@ -48,6 +48,8 @@ export const BUILD_VENDOR_THREE_MODULE_IMPORT = `${BUILD_VENDOR_THREE_PREFIX}thr
48
48
  export const BUILD_VENDOR_THREE_WEBGPU_MODULE_IMPORT = `${BUILD_VENDOR_THREE_PREFIX}three.webgpu.min.js`;
49
49
  export const BUILD_VENDOR_THREE_TSL_MODULE_IMPORT = `${BUILD_VENDOR_THREE_PREFIX}three.tsl.min.js`;
50
50
  export const BUILD_VENDOR_THREE_ADDONS_IMPORT_PREFIX = `${BUILD_VENDOR_THREE_PREFIX}addons/`;
51
+ export const BUILD_VENDOR_KATEX_VERSION = "0.18.9";
52
+ export const BUILD_VENDOR_KATEX_PREFIX = `/build/vendor/katex/${BUILD_VENDOR_KATEX_VERSION}/`;
51
53
  export const PROJECT_METADATA_DIR = ".twinkle";
52
54
  export const PROJECT_METADATA_FILE = "lumine-project.json";
53
55
  export const ASSETS_METADATA_FILE = "assets.json";
@@ -157,6 +159,8 @@ export const LUMINE_THREE_VENDOR_GUIDANCE = `- For Three.js, use the first-party
157
159
  - Keep one Three.js version throughout a project. If a project using ${BUILD_VENDOR_THREE_LEGACY_VERSION} needs current addons, migrate every Three.js import to ${BUILD_VENDOR_THREE_VERSION} in one coherent change; never mix the legacy core with current addons.
158
160
  - This vendor surface covers supported official Three.js modules, not arbitrary third-party Three.js packages. Do not paste library source into project files or use npm/CDN copies. Loader runtime assets are served too: point decoder/transcoder paths at the addon prefix (example: dracoLoader.setDecoderPath('${BUILD_VENDOR_THREE_ADDONS_IMPORT_PREFIX}libs/draco/');).
159
161
  - Size both WebGLRenderer and EffectComposer from Twinkle.preview layout dimensions, coalesce resize work, and render with composer.render() when a composer owns the pass chain.`;
162
+ export const LUMINE_MATH_VENDOR_GUIDANCE = `- For math formulas, Twinkle serves KaTeX ${BUILD_VENDOR_KATEX_VERSION}: link <link rel="stylesheet" href="${BUILD_VENDOR_KATEX_PREFIX}katex.min.css"> (its fonts load from the same place), then import katex from '${BUILD_VENDOR_KATEX_PREFIX}katex.js' and call katex.render(tex, element, { displayMode, throwOnError: false }), or import renderMathInElement from '${BUILD_VENDOR_KATEX_PREFIX}contrib/auto-render.js' to typeset $...$ and $$...$$ inside existing text.
163
+ - Use it when an app shows fractions, roots, exponents, integrals, matrices or other formulas that plain text renders poorly; keep plain text for simple arithmetic. Do not paste KaTeX source into project files or use npm/CDN copies.`;
160
164
  export const LUMINE_AGENT_INSTRUCTIONS = `${LUMINE_AGENT_INSTRUCTIONS_MARKER}
161
165
  # Lumine Project Agent Guide
162
166
 
@@ -333,6 +337,7 @@ ${LUMINE_MOBILE_SELECTION_GUIDANCE}
333
337
  - CAUTION: the preview runtime AUTO-DETECTS "game apps" — any <canvas> in the body (even a decorative background canvas) or game-y words in visible text switch the app to viewport-app mode: html/body get overflow:hidden !important and body becomes a centering flexbox, so tall document-flow pages clip and stop scrolling. Document-style apps that use a canvas must call Twinkle.preview.subscribe (or getLayout/reserveInsets) early at boot — any of those opts out of auto game mode — then pad by layout.safeInsets and scroll within layout.viewport.height.
334
338
  - For canvas, WebGL, Three.js, fullscreen, or game builds, use Twinkle.preview for layout. Do not size roots from 100vh, 100vw, 100dvh, 100dvw, window.innerWidth, window.innerHeight, visualViewport, or document viewport dimensions.
335
339
  ${LUMINE_THREE_VENDOR_GUIDANCE}
340
+ ${LUMINE_MATH_VENDOR_GUIDANCE}
336
341
  - Do not invent or guess Twinkle.* SDK method names. Use ${SDK_REFERENCE_FILE} as the local SDK reference and prefer Twinkle.capabilities checks for gated features.
337
342
  - Match storage to update frequency. Twinkle.privateDb and Twinkle.sharedDb are for LOW-frequency durable state only — things that change on a user action (settings, inventory checkpoints, completed quests, saved progress; comments, votes, room settings, submitted records). NEVER write high-frequency or per-frame/per-tick state to them (camera or cursor position, animation state, live movement, presence, autosave every frame/tick). Keep live state in client memory, broadcast realtime/presence via Twinkle.world, and for durable per-user state flush an occasional snapshot on an interval or on exit (never per frame) — e.g. the viewer/user DB or a single latest-snapshot key. The server rate-limits these writes per key and returns 429 on excess; never retry-loop a 429.
338
343
  ${LUMINE_WORLD_UPDATE_GUIDANCE}
@@ -0,0 +1,69 @@
1
+ /**
2
+ * marked v15.0.12 - a markdown parser
3
+ * Copyright (c) 2011-2025, Christopher Jeffrey. (MIT Licensed)
4
+ * https://github.com/markedjs/marked
5
+ */
6
+
7
+ /**
8
+ * DO NOT EDIT THIS FILE
9
+ * The code in this file is generated from files in ./src/
10
+ */
11
+ (function(g,f){if(typeof exports=="object"&&typeof module<"u"){module.exports=f()}else if("function"==typeof define && define.amd){define("marked",f)}else {g["marked"]=f()}}(typeof globalThis < "u" ? globalThis : typeof self < "u" ? self : this,function(){var exports={};var __exports=exports;var module={exports};
12
+ "use strict";var H=Object.defineProperty;var be=Object.getOwnPropertyDescriptor;var Te=Object.getOwnPropertyNames;var we=Object.prototype.hasOwnProperty;var ye=(l,e)=>{for(var t in e)H(l,t,{get:e[t],enumerable:!0})},Re=(l,e,t,n)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of Te(e))!we.call(l,s)&&s!==t&&H(l,s,{get:()=>e[s],enumerable:!(n=be(e,s))||n.enumerable});return l};var Se=l=>Re(H({},"__esModule",{value:!0}),l);var kt={};ye(kt,{Hooks:()=>L,Lexer:()=>x,Marked:()=>E,Parser:()=>b,Renderer:()=>$,TextRenderer:()=>_,Tokenizer:()=>S,defaults:()=>w,getDefaults:()=>z,lexer:()=>ht,marked:()=>k,options:()=>it,parse:()=>pt,parseInline:()=>ct,parser:()=>ut,setOptions:()=>ot,use:()=>lt,walkTokens:()=>at});module.exports=Se(kt);function z(){return{async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null}}var w=z();function N(l){w=l}var I={exec:()=>null};function h(l,e=""){let t=typeof l=="string"?l:l.source,n={replace:(s,i)=>{let r=typeof i=="string"?i:i.source;return r=r.replace(m.caret,"$1"),t=t.replace(s,r),n},getRegex:()=>new RegExp(t,e)};return n}var m={codeRemoveIndent:/^(?: {1,4}| {0,3}\t)/gm,outputLinkReplace:/\\([\[\]])/g,indentCodeCompensation:/^(\s+)(?:```)/,beginningSpace:/^\s+/,endingHash:/#$/,startingSpaceChar:/^ /,endingSpaceChar:/ $/,nonSpaceChar:/[^ ]/,newLineCharGlobal:/\n/g,tabCharGlobal:/\t/g,multipleSpaceGlobal:/\s+/g,blankLine:/^[ \t]*$/,doubleBlankLine:/\n[ \t]*\n[ \t]*$/,blockquoteStart:/^ {0,3}>/,blockquoteSetextReplace:/\n {0,3}((?:=+|-+) *)(?=\n|$)/g,blockquoteSetextReplace2:/^ {0,3}>[ \t]?/gm,listReplaceTabs:/^\t+/,listReplaceNesting:/^ {1,4}(?=( {4})*[^ ])/g,listIsTask:/^\[[ xX]\] /,listReplaceTask:/^\[[ xX]\] +/,anyLine:/\n.*\n/,hrefBrackets:/^<(.*)>$/,tableDelimiter:/[:|]/,tableAlignChars:/^\||\| *$/g,tableRowBlankLine:/\n[ \t]*$/,tableAlignRight:/^ *-+: *$/,tableAlignCenter:/^ *:-+: *$/,tableAlignLeft:/^ *:-+ *$/,startATag:/^<a /i,endATag:/^<\/a>/i,startPreScriptTag:/^<(pre|code|kbd|script)(\s|>)/i,endPreScriptTag:/^<\/(pre|code|kbd|script)(\s|>)/i,startAngleBracket:/^</,endAngleBracket:/>$/,pedanticHrefTitle:/^([^'"]*[^\s])\s+(['"])(.*)\2/,unicodeAlphaNumeric:/[\p{L}\p{N}]/u,escapeTest:/[&<>"']/,escapeReplace:/[&<>"']/g,escapeTestNoEncode:/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/,escapeReplaceNoEncode:/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/g,unescapeTest:/&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/ig,caret:/(^|[^\[])\^/g,percentDecode:/%25/g,findPipe:/\|/g,splitPipe:/ \|/,slashPipe:/\\\|/g,carriageReturn:/\r\n|\r/g,spaceLine:/^ +$/gm,notSpaceStart:/^\S*/,endingNewline:/\n$/,listItemRegex:l=>new RegExp(`^( {0,3}${l})((?:[ ][^\\n]*)?(?:\\n|$))`),nextBulletRegex:l=>new RegExp(`^ {0,${Math.min(3,l-1)}}(?:[*+-]|\\d{1,9}[.)])((?:[ ][^\\n]*)?(?:\\n|$))`),hrRegex:l=>new RegExp(`^ {0,${Math.min(3,l-1)}}((?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$)`),fencesBeginRegex:l=>new RegExp(`^ {0,${Math.min(3,l-1)}}(?:\`\`\`|~~~)`),headingBeginRegex:l=>new RegExp(`^ {0,${Math.min(3,l-1)}}#`),htmlBeginRegex:l=>new RegExp(`^ {0,${Math.min(3,l-1)}}<(?:[a-z].*>|!--)`,"i")},$e=/^(?:[ \t]*(?:\n|$))+/,_e=/^((?: {4}| {0,3}\t)[^\n]+(?:\n(?:[ \t]*(?:\n|$))*)?)+/,Le=/^ {0,3}(`{3,}(?=[^`\n]*(?:\n|$))|~{3,})([^\n]*)(?:\n|$)(?:|([\s\S]*?)(?:\n|$))(?: {0,3}\1[~`]* *(?=\n|$)|$)/,O=/^ {0,3}((?:-[\t ]*){3,}|(?:_[ \t]*){3,}|(?:\*[ \t]*){3,})(?:\n+|$)/,ze=/^ {0,3}(#{1,6})(?=\s|$)(.*)(?:\n+|$)/,F=/(?:[*+-]|\d{1,9}[.)])/,ie=/^(?!bull |blockCode|fences|blockquote|heading|html|table)((?:.|\n(?!\s*?\n|bull |blockCode|fences|blockquote|heading|html|table))+?)\n {0,3}(=+|-+) *(?:\n+|$)/,oe=h(ie).replace(/bull/g,F).replace(/blockCode/g,/(?: {4}| {0,3}\t)/).replace(/fences/g,/ {0,3}(?:`{3,}|~{3,})/).replace(/blockquote/g,/ {0,3}>/).replace(/heading/g,/ {0,3}#{1,6}/).replace(/html/g,/ {0,3}<[^\n>]+>\n/).replace(/\|table/g,"").getRegex(),Me=h(ie).replace(/bull/g,F).replace(/blockCode/g,/(?: {4}| {0,3}\t)/).replace(/fences/g,/ {0,3}(?:`{3,}|~{3,})/).replace(/blockquote/g,/ {0,3}>/).replace(/heading/g,/ {0,3}#{1,6}/).replace(/html/g,/ {0,3}<[^\n>]+>\n/).replace(/table/g,/ {0,3}\|?(?:[:\- ]*\|)+[\:\- ]*\n/).getRegex(),Q=/^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html|table| +\n)[^\n]+)*)/,Pe=/^[^\n]+/,U=/(?!\s*\])(?:\\.|[^\[\]\\])+/,Ae=h(/^ {0,3}\[(label)\]: *(?:\n[ \t]*)?([^<\s][^\s]*|<.*?>)(?:(?: +(?:\n[ \t]*)?| *\n[ \t]*)(title))? *(?:\n+|$)/).replace("label",U).replace("title",/(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/).getRegex(),Ee=h(/^( {0,3}bull)([ \t][^\n]+?)?(?:\n|$)/).replace(/bull/g,F).getRegex(),v="address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|search|section|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul",K=/<!--(?:-?>|[\s\S]*?(?:-->|$))/,Ce=h("^ {0,3}(?:<(script|pre|style|textarea)[\\s>][\\s\\S]*?(?:</\\1>[^\\n]*\\n+|$)|comment[^\\n]*(\\n+|$)|<\\?[\\s\\S]*?(?:\\?>\\n*|$)|<![A-Z][\\s\\S]*?(?:>\\n*|$)|<!\\[CDATA\\[[\\s\\S]*?(?:\\]\\]>\\n*|$)|</?(tag)(?: +|\\n|/?>)[\\s\\S]*?(?:(?:\\n[ ]*)+\\n|$)|<(?!script|pre|style|textarea)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n[ ]*)+\\n|$)|</(?!script|pre|style|textarea)[a-z][\\w-]*\\s*>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n[ ]*)+\\n|$))","i").replace("comment",K).replace("tag",v).replace("attribute",/ +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/).getRegex(),le=h(Q).replace("hr",O).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("|lheading","").replace("|table","").replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html","</?(?:tag)(?: +|\\n|/?>)|<(?:script|pre|style|textarea|!--)").replace("tag",v).getRegex(),Ie=h(/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/).replace("paragraph",le).getRegex(),X={blockquote:Ie,code:_e,def:Ae,fences:Le,heading:ze,hr:O,html:Ce,lheading:oe,list:Ee,newline:$e,paragraph:le,table:I,text:Pe},re=h("^ *([^\\n ].*)\\n {0,3}((?:\\| *)?:?-+:? *(?:\\| *:?-+:? *)*(?:\\| *)?)(?:\\n((?:(?! *\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)").replace("hr",O).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("blockquote"," {0,3}>").replace("code","(?: {4}| {0,3} )[^\\n]").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html","</?(?:tag)(?: +|\\n|/?>)|<(?:script|pre|style|textarea|!--)").replace("tag",v).getRegex(),Oe={...X,lheading:Me,table:re,paragraph:h(Q).replace("hr",O).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("|lheading","").replace("table",re).replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html","</?(?:tag)(?: +|\\n|/?>)|<(?:script|pre|style|textarea|!--)").replace("tag",v).getRegex()},Be={...X,html:h(`^ *(?:comment *(?:\\n|\\s*$)|<(tag)[\\s\\S]+?</\\1> *(?:\\n{2,}|\\s*$)|<tag(?:"[^"]*"|'[^']*'|\\s[^'"/>\\s]*)*?/?> *(?:\\n{2,}|\\s*$))`).replace("comment",K).replace(/tag/g,"(?!(?:a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)\\b)\\w+(?!:|[^\\w\\s@]*@)\\b").getRegex(),def:/^ *\[([^\]]+)\]: *<?([^\s>]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/,heading:/^(#{1,6})(.*)(?:\n+|$)/,fences:I,lheading:/^(.+?)\n {0,3}(=+|-+) *(?:\n+|$)/,paragraph:h(Q).replace("hr",O).replace("heading",` *#{1,6} *[^
13
+ ]`).replace("lheading",oe).replace("|table","").replace("blockquote"," {0,3}>").replace("|fences","").replace("|list","").replace("|html","").replace("|tag","").getRegex()},qe=/^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/,ve=/^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/,ae=/^( {2,}|\\)\n(?!\s*$)/,De=/^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\<!\[`*_]|\b_|$)|[^ ](?= {2,}\n)))/,D=/[\p{P}\p{S}]/u,W=/[\s\p{P}\p{S}]/u,ce=/[^\s\p{P}\p{S}]/u,Ze=h(/^((?![*_])punctSpace)/,"u").replace(/punctSpace/g,W).getRegex(),pe=/(?!~)[\p{P}\p{S}]/u,Ge=/(?!~)[\s\p{P}\p{S}]/u,He=/(?:[^\s\p{P}\p{S}]|~)/u,Ne=/\[[^[\]]*?\]\((?:\\.|[^\\\(\)]|\((?:\\.|[^\\\(\)])*\))*\)|`[^`]*?`|<[^<>]*?>/g,ue=/^(?:\*+(?:((?!\*)punct)|[^\s*]))|^_+(?:((?!_)punct)|([^\s_]))/,je=h(ue,"u").replace(/punct/g,D).getRegex(),Fe=h(ue,"u").replace(/punct/g,pe).getRegex(),he="^[^_*]*?__[^_*]*?\\*[^_*]*?(?=__)|[^*]+(?=[^*])|(?!\\*)punct(\\*+)(?=[\\s]|$)|notPunctSpace(\\*+)(?!\\*)(?=punctSpace|$)|(?!\\*)punctSpace(\\*+)(?=notPunctSpace)|[\\s](\\*+)(?!\\*)(?=punct)|(?!\\*)punct(\\*+)(?!\\*)(?=punct)|notPunctSpace(\\*+)(?=notPunctSpace)",Qe=h(he,"gu").replace(/notPunctSpace/g,ce).replace(/punctSpace/g,W).replace(/punct/g,D).getRegex(),Ue=h(he,"gu").replace(/notPunctSpace/g,He).replace(/punctSpace/g,Ge).replace(/punct/g,pe).getRegex(),Ke=h("^[^_*]*?\\*\\*[^_*]*?_[^_*]*?(?=\\*\\*)|[^_]+(?=[^_])|(?!_)punct(_+)(?=[\\s]|$)|notPunctSpace(_+)(?!_)(?=punctSpace|$)|(?!_)punctSpace(_+)(?=notPunctSpace)|[\\s](_+)(?!_)(?=punct)|(?!_)punct(_+)(?!_)(?=punct)","gu").replace(/notPunctSpace/g,ce).replace(/punctSpace/g,W).replace(/punct/g,D).getRegex(),Xe=h(/\\(punct)/,"gu").replace(/punct/g,D).getRegex(),We=h(/^<(scheme:[^\s\x00-\x1f<>]*|email)>/).replace("scheme",/[a-zA-Z][a-zA-Z0-9+.-]{1,31}/).replace("email",/[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/).getRegex(),Je=h(K).replace("(?:-->|$)","-->").getRegex(),Ve=h("^comment|^</[a-zA-Z][\\w:-]*\\s*>|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>|^<\\?[\\s\\S]*?\\?>|^<![a-zA-Z]+\\s[\\s\\S]*?>|^<!\\[CDATA\\[[\\s\\S]*?\\]\\]>").replace("comment",Je).replace("attribute",/\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/).getRegex(),q=/(?:\[(?:\\.|[^\[\]\\])*\]|\\.|`[^`]*`|[^\[\]\\`])*?/,Ye=h(/^!?\[(label)\]\(\s*(href)(?:(?:[ \t]*(?:\n[ \t]*)?)(title))?\s*\)/).replace("label",q).replace("href",/<(?:\\.|[^\n<>\\])+>|[^ \t\n\x00-\x1f]*/).replace("title",/"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/).getRegex(),ke=h(/^!?\[(label)\]\[(ref)\]/).replace("label",q).replace("ref",U).getRegex(),ge=h(/^!?\[(ref)\](?:\[\])?/).replace("ref",U).getRegex(),et=h("reflink|nolink(?!\\()","g").replace("reflink",ke).replace("nolink",ge).getRegex(),J={_backpedal:I,anyPunctuation:Xe,autolink:We,blockSkip:Ne,br:ae,code:ve,del:I,emStrongLDelim:je,emStrongRDelimAst:Qe,emStrongRDelimUnd:Ke,escape:qe,link:Ye,nolink:ge,punctuation:Ze,reflink:ke,reflinkSearch:et,tag:Ve,text:De,url:I},tt={...J,link:h(/^!?\[(label)\]\((.*?)\)/).replace("label",q).getRegex(),reflink:h(/^!?\[(label)\]\s*\[([^\]]*)\]/).replace("label",q).getRegex()},j={...J,emStrongRDelimAst:Ue,emStrongLDelim:Fe,url:h(/^((?:ftp|https?):\/\/|www\.)(?:[a-zA-Z0-9\-]+\.?)+[^\s<]*|^email/,"i").replace("email",/[A-Za-z0-9._+-]+(@)[a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]*[a-zA-Z0-9])+(?![-_])/).getRegex(),_backpedal:/(?:[^?!.,:;*_'"~()&]+|\([^)]*\)|&(?![a-zA-Z0-9]+;$)|[?!.,:;*_'"~)]+(?!$))+/,del:/^(~~?)(?=[^\s~])((?:\\.|[^\\])*?(?:\\.|[^\s~\\]))\1(?=[^~]|$)/,text:/^([`~]+|[^`~])(?:(?= {2,}\n)|(?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@)|[\s\S]*?(?:(?=[\\<!\[`*~_]|\b_|https?:\/\/|ftp:\/\/|www\.|$)|[^ ](?= {2,}\n)|[^a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-](?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@)))/},nt={...j,br:h(ae).replace("{2,}","*").getRegex(),text:h(j.text).replace("\\b_","\\b_| {2,}\\n").replace(/\{2,\}/g,"*").getRegex()},B={normal:X,gfm:Oe,pedantic:Be},P={normal:J,gfm:j,breaks:nt,pedantic:tt};var st={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"},fe=l=>st[l];function R(l,e){if(e){if(m.escapeTest.test(l))return l.replace(m.escapeReplace,fe)}else if(m.escapeTestNoEncode.test(l))return l.replace(m.escapeReplaceNoEncode,fe);return l}function V(l){try{l=encodeURI(l).replace(m.percentDecode,"%")}catch{return null}return l}function Y(l,e){let t=l.replace(m.findPipe,(i,r,o)=>{let a=!1,c=r;for(;--c>=0&&o[c]==="\\";)a=!a;return a?"|":" |"}),n=t.split(m.splitPipe),s=0;if(n[0].trim()||n.shift(),n.length>0&&!n.at(-1)?.trim()&&n.pop(),e)if(n.length>e)n.splice(e);else for(;n.length<e;)n.push("");for(;s<n.length;s++)n[s]=n[s].trim().replace(m.slashPipe,"|");return n}function A(l,e,t){let n=l.length;if(n===0)return"";let s=0;for(;s<n;){let i=l.charAt(n-s-1);if(i===e&&!t)s++;else if(i!==e&&t)s++;else break}return l.slice(0,n-s)}function de(l,e){if(l.indexOf(e[1])===-1)return-1;let t=0;for(let n=0;n<l.length;n++)if(l[n]==="\\")n++;else if(l[n]===e[0])t++;else if(l[n]===e[1]&&(t--,t<0))return n;return t>0?-2:-1}function me(l,e,t,n,s){let i=e.href,r=e.title||null,o=l[1].replace(s.other.outputLinkReplace,"$1");n.state.inLink=!0;let a={type:l[0].charAt(0)==="!"?"image":"link",raw:t,href:i,title:r,text:o,tokens:n.inlineTokens(o)};return n.state.inLink=!1,a}function rt(l,e,t){let n=l.match(t.other.indentCodeCompensation);if(n===null)return e;let s=n[1];return e.split(`
14
+ `).map(i=>{let r=i.match(t.other.beginningSpace);if(r===null)return i;let[o]=r;return o.length>=s.length?i.slice(s.length):i}).join(`
15
+ `)}var S=class{options;rules;lexer;constructor(e){this.options=e||w}space(e){let t=this.rules.block.newline.exec(e);if(t&&t[0].length>0)return{type:"space",raw:t[0]}}code(e){let t=this.rules.block.code.exec(e);if(t){let n=t[0].replace(this.rules.other.codeRemoveIndent,"");return{type:"code",raw:t[0],codeBlockStyle:"indented",text:this.options.pedantic?n:A(n,`
16
+ `)}}}fences(e){let t=this.rules.block.fences.exec(e);if(t){let n=t[0],s=rt(n,t[3]||"",this.rules);return{type:"code",raw:n,lang:t[2]?t[2].trim().replace(this.rules.inline.anyPunctuation,"$1"):t[2],text:s}}}heading(e){let t=this.rules.block.heading.exec(e);if(t){let n=t[2].trim();if(this.rules.other.endingHash.test(n)){let s=A(n,"#");(this.options.pedantic||!s||this.rules.other.endingSpaceChar.test(s))&&(n=s.trim())}return{type:"heading",raw:t[0],depth:t[1].length,text:n,tokens:this.lexer.inline(n)}}}hr(e){let t=this.rules.block.hr.exec(e);if(t)return{type:"hr",raw:A(t[0],`
17
+ `)}}blockquote(e){let t=this.rules.block.blockquote.exec(e);if(t){let n=A(t[0],`
18
+ `).split(`
19
+ `),s="",i="",r=[];for(;n.length>0;){let o=!1,a=[],c;for(c=0;c<n.length;c++)if(this.rules.other.blockquoteStart.test(n[c]))a.push(n[c]),o=!0;else if(!o)a.push(n[c]);else break;n=n.slice(c);let p=a.join(`
20
+ `),u=p.replace(this.rules.other.blockquoteSetextReplace,`
21
+ $1`).replace(this.rules.other.blockquoteSetextReplace2,"");s=s?`${s}
22
+ ${p}`:p,i=i?`${i}
23
+ ${u}`:u;let d=this.lexer.state.top;if(this.lexer.state.top=!0,this.lexer.blockTokens(u,r,!0),this.lexer.state.top=d,n.length===0)break;let g=r.at(-1);if(g?.type==="code")break;if(g?.type==="blockquote"){let T=g,f=T.raw+`
24
+ `+n.join(`
25
+ `),y=this.blockquote(f);r[r.length-1]=y,s=s.substring(0,s.length-T.raw.length)+y.raw,i=i.substring(0,i.length-T.text.length)+y.text;break}else if(g?.type==="list"){let T=g,f=T.raw+`
26
+ `+n.join(`
27
+ `),y=this.list(f);r[r.length-1]=y,s=s.substring(0,s.length-g.raw.length)+y.raw,i=i.substring(0,i.length-T.raw.length)+y.raw,n=f.substring(r.at(-1).raw.length).split(`
28
+ `);continue}}return{type:"blockquote",raw:s,tokens:r,text:i}}}list(e){let t=this.rules.block.list.exec(e);if(t){let n=t[1].trim(),s=n.length>1,i={type:"list",raw:"",ordered:s,start:s?+n.slice(0,-1):"",loose:!1,items:[]};n=s?`\\d{1,9}\\${n.slice(-1)}`:`\\${n}`,this.options.pedantic&&(n=s?n:"[*+-]");let r=this.rules.other.listItemRegex(n),o=!1;for(;e;){let c=!1,p="",u="";if(!(t=r.exec(e))||this.rules.block.hr.test(e))break;p=t[0],e=e.substring(p.length);let d=t[2].split(`
29
+ `,1)[0].replace(this.rules.other.listReplaceTabs,Z=>" ".repeat(3*Z.length)),g=e.split(`
30
+ `,1)[0],T=!d.trim(),f=0;if(this.options.pedantic?(f=2,u=d.trimStart()):T?f=t[1].length+1:(f=t[2].search(this.rules.other.nonSpaceChar),f=f>4?1:f,u=d.slice(f),f+=t[1].length),T&&this.rules.other.blankLine.test(g)&&(p+=g+`
31
+ `,e=e.substring(g.length+1),c=!0),!c){let Z=this.rules.other.nextBulletRegex(f),te=this.rules.other.hrRegex(f),ne=this.rules.other.fencesBeginRegex(f),se=this.rules.other.headingBeginRegex(f),xe=this.rules.other.htmlBeginRegex(f);for(;e;){let G=e.split(`
32
+ `,1)[0],C;if(g=G,this.options.pedantic?(g=g.replace(this.rules.other.listReplaceNesting," "),C=g):C=g.replace(this.rules.other.tabCharGlobal," "),ne.test(g)||se.test(g)||xe.test(g)||Z.test(g)||te.test(g))break;if(C.search(this.rules.other.nonSpaceChar)>=f||!g.trim())u+=`
33
+ `+C.slice(f);else{if(T||d.replace(this.rules.other.tabCharGlobal," ").search(this.rules.other.nonSpaceChar)>=4||ne.test(d)||se.test(d)||te.test(d))break;u+=`
34
+ `+g}!T&&!g.trim()&&(T=!0),p+=G+`
35
+ `,e=e.substring(G.length+1),d=C.slice(f)}}i.loose||(o?i.loose=!0:this.rules.other.doubleBlankLine.test(p)&&(o=!0));let y=null,ee;this.options.gfm&&(y=this.rules.other.listIsTask.exec(u),y&&(ee=y[0]!=="[ ] ",u=u.replace(this.rules.other.listReplaceTask,""))),i.items.push({type:"list_item",raw:p,task:!!y,checked:ee,loose:!1,text:u,tokens:[]}),i.raw+=p}let a=i.items.at(-1);if(a)a.raw=a.raw.trimEnd(),a.text=a.text.trimEnd();else return;i.raw=i.raw.trimEnd();for(let c=0;c<i.items.length;c++)if(this.lexer.state.top=!1,i.items[c].tokens=this.lexer.blockTokens(i.items[c].text,[]),!i.loose){let p=i.items[c].tokens.filter(d=>d.type==="space"),u=p.length>0&&p.some(d=>this.rules.other.anyLine.test(d.raw));i.loose=u}if(i.loose)for(let c=0;c<i.items.length;c++)i.items[c].loose=!0;return i}}html(e){let t=this.rules.block.html.exec(e);if(t)return{type:"html",block:!0,raw:t[0],pre:t[1]==="pre"||t[1]==="script"||t[1]==="style",text:t[0]}}def(e){let t=this.rules.block.def.exec(e);if(t){let n=t[1].toLowerCase().replace(this.rules.other.multipleSpaceGlobal," "),s=t[2]?t[2].replace(this.rules.other.hrefBrackets,"$1").replace(this.rules.inline.anyPunctuation,"$1"):"",i=t[3]?t[3].substring(1,t[3].length-1).replace(this.rules.inline.anyPunctuation,"$1"):t[3];return{type:"def",tag:n,raw:t[0],href:s,title:i}}}table(e){let t=this.rules.block.table.exec(e);if(!t||!this.rules.other.tableDelimiter.test(t[2]))return;let n=Y(t[1]),s=t[2].replace(this.rules.other.tableAlignChars,"").split("|"),i=t[3]?.trim()?t[3].replace(this.rules.other.tableRowBlankLine,"").split(`
36
+ `):[],r={type:"table",raw:t[0],header:[],align:[],rows:[]};if(n.length===s.length){for(let o of s)this.rules.other.tableAlignRight.test(o)?r.align.push("right"):this.rules.other.tableAlignCenter.test(o)?r.align.push("center"):this.rules.other.tableAlignLeft.test(o)?r.align.push("left"):r.align.push(null);for(let o=0;o<n.length;o++)r.header.push({text:n[o],tokens:this.lexer.inline(n[o]),header:!0,align:r.align[o]});for(let o of i)r.rows.push(Y(o,r.header.length).map((a,c)=>({text:a,tokens:this.lexer.inline(a),header:!1,align:r.align[c]})));return r}}lheading(e){let t=this.rules.block.lheading.exec(e);if(t)return{type:"heading",raw:t[0],depth:t[2].charAt(0)==="="?1:2,text:t[1],tokens:this.lexer.inline(t[1])}}paragraph(e){let t=this.rules.block.paragraph.exec(e);if(t){let n=t[1].charAt(t[1].length-1)===`
37
+ `?t[1].slice(0,-1):t[1];return{type:"paragraph",raw:t[0],text:n,tokens:this.lexer.inline(n)}}}text(e){let t=this.rules.block.text.exec(e);if(t)return{type:"text",raw:t[0],text:t[0],tokens:this.lexer.inline(t[0])}}escape(e){let t=this.rules.inline.escape.exec(e);if(t)return{type:"escape",raw:t[0],text:t[1]}}tag(e){let t=this.rules.inline.tag.exec(e);if(t)return!this.lexer.state.inLink&&this.rules.other.startATag.test(t[0])?this.lexer.state.inLink=!0:this.lexer.state.inLink&&this.rules.other.endATag.test(t[0])&&(this.lexer.state.inLink=!1),!this.lexer.state.inRawBlock&&this.rules.other.startPreScriptTag.test(t[0])?this.lexer.state.inRawBlock=!0:this.lexer.state.inRawBlock&&this.rules.other.endPreScriptTag.test(t[0])&&(this.lexer.state.inRawBlock=!1),{type:"html",raw:t[0],inLink:this.lexer.state.inLink,inRawBlock:this.lexer.state.inRawBlock,block:!1,text:t[0]}}link(e){let t=this.rules.inline.link.exec(e);if(t){let n=t[2].trim();if(!this.options.pedantic&&this.rules.other.startAngleBracket.test(n)){if(!this.rules.other.endAngleBracket.test(n))return;let r=A(n.slice(0,-1),"\\");if((n.length-r.length)%2===0)return}else{let r=de(t[2],"()");if(r===-2)return;if(r>-1){let a=(t[0].indexOf("!")===0?5:4)+t[1].length+r;t[2]=t[2].substring(0,r),t[0]=t[0].substring(0,a).trim(),t[3]=""}}let s=t[2],i="";if(this.options.pedantic){let r=this.rules.other.pedanticHrefTitle.exec(s);r&&(s=r[1],i=r[3])}else i=t[3]?t[3].slice(1,-1):"";return s=s.trim(),this.rules.other.startAngleBracket.test(s)&&(this.options.pedantic&&!this.rules.other.endAngleBracket.test(n)?s=s.slice(1):s=s.slice(1,-1)),me(t,{href:s&&s.replace(this.rules.inline.anyPunctuation,"$1"),title:i&&i.replace(this.rules.inline.anyPunctuation,"$1")},t[0],this.lexer,this.rules)}}reflink(e,t){let n;if((n=this.rules.inline.reflink.exec(e))||(n=this.rules.inline.nolink.exec(e))){let s=(n[2]||n[1]).replace(this.rules.other.multipleSpaceGlobal," "),i=t[s.toLowerCase()];if(!i){let r=n[0].charAt(0);return{type:"text",raw:r,text:r}}return me(n,i,n[0],this.lexer,this.rules)}}emStrong(e,t,n=""){let s=this.rules.inline.emStrongLDelim.exec(e);if(!s||s[3]&&n.match(this.rules.other.unicodeAlphaNumeric))return;if(!(s[1]||s[2]||"")||!n||this.rules.inline.punctuation.exec(n)){let r=[...s[0]].length-1,o,a,c=r,p=0,u=s[0][0]==="*"?this.rules.inline.emStrongRDelimAst:this.rules.inline.emStrongRDelimUnd;for(u.lastIndex=0,t=t.slice(-1*e.length+r);(s=u.exec(t))!=null;){if(o=s[1]||s[2]||s[3]||s[4]||s[5]||s[6],!o)continue;if(a=[...o].length,s[3]||s[4]){c+=a;continue}else if((s[5]||s[6])&&r%3&&!((r+a)%3)){p+=a;continue}if(c-=a,c>0)continue;a=Math.min(a,a+c+p);let d=[...s[0]][0].length,g=e.slice(0,r+s.index+d+a);if(Math.min(r,a)%2){let f=g.slice(1,-1);return{type:"em",raw:g,text:f,tokens:this.lexer.inlineTokens(f)}}let T=g.slice(2,-2);return{type:"strong",raw:g,text:T,tokens:this.lexer.inlineTokens(T)}}}}codespan(e){let t=this.rules.inline.code.exec(e);if(t){let n=t[2].replace(this.rules.other.newLineCharGlobal," "),s=this.rules.other.nonSpaceChar.test(n),i=this.rules.other.startingSpaceChar.test(n)&&this.rules.other.endingSpaceChar.test(n);return s&&i&&(n=n.substring(1,n.length-1)),{type:"codespan",raw:t[0],text:n}}}br(e){let t=this.rules.inline.br.exec(e);if(t)return{type:"br",raw:t[0]}}del(e){let t=this.rules.inline.del.exec(e);if(t)return{type:"del",raw:t[0],text:t[2],tokens:this.lexer.inlineTokens(t[2])}}autolink(e){let t=this.rules.inline.autolink.exec(e);if(t){let n,s;return t[2]==="@"?(n=t[1],s="mailto:"+n):(n=t[1],s=n),{type:"link",raw:t[0],text:n,href:s,tokens:[{type:"text",raw:n,text:n}]}}}url(e){let t;if(t=this.rules.inline.url.exec(e)){let n,s;if(t[2]==="@")n=t[0],s="mailto:"+n;else{let i;do i=t[0],t[0]=this.rules.inline._backpedal.exec(t[0])?.[0]??"";while(i!==t[0]);n=t[0],t[1]==="www."?s="http://"+t[0]:s=t[0]}return{type:"link",raw:t[0],text:n,href:s,tokens:[{type:"text",raw:n,text:n}]}}}inlineText(e){let t=this.rules.inline.text.exec(e);if(t){let n=this.lexer.state.inRawBlock;return{type:"text",raw:t[0],text:t[0],escaped:n}}}};var x=class l{tokens;options;state;tokenizer;inlineQueue;constructor(e){this.tokens=[],this.tokens.links=Object.create(null),this.options=e||w,this.options.tokenizer=this.options.tokenizer||new S,this.tokenizer=this.options.tokenizer,this.tokenizer.options=this.options,this.tokenizer.lexer=this,this.inlineQueue=[],this.state={inLink:!1,inRawBlock:!1,top:!0};let t={other:m,block:B.normal,inline:P.normal};this.options.pedantic?(t.block=B.pedantic,t.inline=P.pedantic):this.options.gfm&&(t.block=B.gfm,this.options.breaks?t.inline=P.breaks:t.inline=P.gfm),this.tokenizer.rules=t}static get rules(){return{block:B,inline:P}}static lex(e,t){return new l(t).lex(e)}static lexInline(e,t){return new l(t).inlineTokens(e)}lex(e){e=e.replace(m.carriageReturn,`
38
+ `),this.blockTokens(e,this.tokens);for(let t=0;t<this.inlineQueue.length;t++){let n=this.inlineQueue[t];this.inlineTokens(n.src,n.tokens)}return this.inlineQueue=[],this.tokens}blockTokens(e,t=[],n=!1){for(this.options.pedantic&&(e=e.replace(m.tabCharGlobal," ").replace(m.spaceLine,""));e;){let s;if(this.options.extensions?.block?.some(r=>(s=r.call({lexer:this},e,t))?(e=e.substring(s.raw.length),t.push(s),!0):!1))continue;if(s=this.tokenizer.space(e)){e=e.substring(s.raw.length);let r=t.at(-1);s.raw.length===1&&r!==void 0?r.raw+=`
39
+ `:t.push(s);continue}if(s=this.tokenizer.code(e)){e=e.substring(s.raw.length);let r=t.at(-1);r?.type==="paragraph"||r?.type==="text"?(r.raw+=`
40
+ `+s.raw,r.text+=`
41
+ `+s.text,this.inlineQueue.at(-1).src=r.text):t.push(s);continue}if(s=this.tokenizer.fences(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.heading(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.hr(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.blockquote(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.list(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.html(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.def(e)){e=e.substring(s.raw.length);let r=t.at(-1);r?.type==="paragraph"||r?.type==="text"?(r.raw+=`
42
+ `+s.raw,r.text+=`
43
+ `+s.raw,this.inlineQueue.at(-1).src=r.text):this.tokens.links[s.tag]||(this.tokens.links[s.tag]={href:s.href,title:s.title});continue}if(s=this.tokenizer.table(e)){e=e.substring(s.raw.length),t.push(s);continue}if(s=this.tokenizer.lheading(e)){e=e.substring(s.raw.length),t.push(s);continue}let i=e;if(this.options.extensions?.startBlock){let r=1/0,o=e.slice(1),a;this.options.extensions.startBlock.forEach(c=>{a=c.call({lexer:this},o),typeof a=="number"&&a>=0&&(r=Math.min(r,a))}),r<1/0&&r>=0&&(i=e.substring(0,r+1))}if(this.state.top&&(s=this.tokenizer.paragraph(i))){let r=t.at(-1);n&&r?.type==="paragraph"?(r.raw+=`
44
+ `+s.raw,r.text+=`
45
+ `+s.text,this.inlineQueue.pop(),this.inlineQueue.at(-1).src=r.text):t.push(s),n=i.length!==e.length,e=e.substring(s.raw.length);continue}if(s=this.tokenizer.text(e)){e=e.substring(s.raw.length);let r=t.at(-1);r?.type==="text"?(r.raw+=`
46
+ `+s.raw,r.text+=`
47
+ `+s.text,this.inlineQueue.pop(),this.inlineQueue.at(-1).src=r.text):t.push(s);continue}if(e){let r="Infinite loop on byte: "+e.charCodeAt(0);if(this.options.silent){console.error(r);break}else throw new Error(r)}}return this.state.top=!0,t}inline(e,t=[]){return this.inlineQueue.push({src:e,tokens:t}),t}inlineTokens(e,t=[]){let n=e,s=null;if(this.tokens.links){let o=Object.keys(this.tokens.links);if(o.length>0)for(;(s=this.tokenizer.rules.inline.reflinkSearch.exec(n))!=null;)o.includes(s[0].slice(s[0].lastIndexOf("[")+1,-1))&&(n=n.slice(0,s.index)+"["+"a".repeat(s[0].length-2)+"]"+n.slice(this.tokenizer.rules.inline.reflinkSearch.lastIndex))}for(;(s=this.tokenizer.rules.inline.anyPunctuation.exec(n))!=null;)n=n.slice(0,s.index)+"++"+n.slice(this.tokenizer.rules.inline.anyPunctuation.lastIndex);for(;(s=this.tokenizer.rules.inline.blockSkip.exec(n))!=null;)n=n.slice(0,s.index)+"["+"a".repeat(s[0].length-2)+"]"+n.slice(this.tokenizer.rules.inline.blockSkip.lastIndex);let i=!1,r="";for(;e;){i||(r=""),i=!1;let o;if(this.options.extensions?.inline?.some(c=>(o=c.call({lexer:this},e,t))?(e=e.substring(o.raw.length),t.push(o),!0):!1))continue;if(o=this.tokenizer.escape(e)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.tag(e)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.link(e)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.reflink(e,this.tokens.links)){e=e.substring(o.raw.length);let c=t.at(-1);o.type==="text"&&c?.type==="text"?(c.raw+=o.raw,c.text+=o.text):t.push(o);continue}if(o=this.tokenizer.emStrong(e,n,r)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.codespan(e)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.br(e)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.del(e)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.autolink(e)){e=e.substring(o.raw.length),t.push(o);continue}if(!this.state.inLink&&(o=this.tokenizer.url(e))){e=e.substring(o.raw.length),t.push(o);continue}let a=e;if(this.options.extensions?.startInline){let c=1/0,p=e.slice(1),u;this.options.extensions.startInline.forEach(d=>{u=d.call({lexer:this},p),typeof u=="number"&&u>=0&&(c=Math.min(c,u))}),c<1/0&&c>=0&&(a=e.substring(0,c+1))}if(o=this.tokenizer.inlineText(a)){e=e.substring(o.raw.length),o.raw.slice(-1)!=="_"&&(r=o.raw.slice(-1)),i=!0;let c=t.at(-1);c?.type==="text"?(c.raw+=o.raw,c.text+=o.text):t.push(o);continue}if(e){let c="Infinite loop on byte: "+e.charCodeAt(0);if(this.options.silent){console.error(c);break}else throw new Error(c)}}return t}};var $=class{options;parser;constructor(e){this.options=e||w}space(e){return""}code({text:e,lang:t,escaped:n}){let s=(t||"").match(m.notSpaceStart)?.[0],i=e.replace(m.endingNewline,"")+`
48
+ `;return s?'<pre><code class="language-'+R(s)+'">'+(n?i:R(i,!0))+`</code></pre>
49
+ `:"<pre><code>"+(n?i:R(i,!0))+`</code></pre>
50
+ `}blockquote({tokens:e}){return`<blockquote>
51
+ ${this.parser.parse(e)}</blockquote>
52
+ `}html({text:e}){return e}heading({tokens:e,depth:t}){return`<h${t}>${this.parser.parseInline(e)}</h${t}>
53
+ `}hr(e){return`<hr>
54
+ `}list(e){let t=e.ordered,n=e.start,s="";for(let o=0;o<e.items.length;o++){let a=e.items[o];s+=this.listitem(a)}let i=t?"ol":"ul",r=t&&n!==1?' start="'+n+'"':"";return"<"+i+r+`>
55
+ `+s+"</"+i+`>
56
+ `}listitem(e){let t="";if(e.task){let n=this.checkbox({checked:!!e.checked});e.loose?e.tokens[0]?.type==="paragraph"?(e.tokens[0].text=n+" "+e.tokens[0].text,e.tokens[0].tokens&&e.tokens[0].tokens.length>0&&e.tokens[0].tokens[0].type==="text"&&(e.tokens[0].tokens[0].text=n+" "+R(e.tokens[0].tokens[0].text),e.tokens[0].tokens[0].escaped=!0)):e.tokens.unshift({type:"text",raw:n+" ",text:n+" ",escaped:!0}):t+=n+" "}return t+=this.parser.parse(e.tokens,!!e.loose),`<li>${t}</li>
57
+ `}checkbox({checked:e}){return"<input "+(e?'checked="" ':"")+'disabled="" type="checkbox">'}paragraph({tokens:e}){return`<p>${this.parser.parseInline(e)}</p>
58
+ `}table(e){let t="",n="";for(let i=0;i<e.header.length;i++)n+=this.tablecell(e.header[i]);t+=this.tablerow({text:n});let s="";for(let i=0;i<e.rows.length;i++){let r=e.rows[i];n="";for(let o=0;o<r.length;o++)n+=this.tablecell(r[o]);s+=this.tablerow({text:n})}return s&&(s=`<tbody>${s}</tbody>`),`<table>
59
+ <thead>
60
+ `+t+`</thead>
61
+ `+s+`</table>
62
+ `}tablerow({text:e}){return`<tr>
63
+ ${e}</tr>
64
+ `}tablecell(e){let t=this.parser.parseInline(e.tokens),n=e.header?"th":"td";return(e.align?`<${n} align="${e.align}">`:`<${n}>`)+t+`</${n}>
65
+ `}strong({tokens:e}){return`<strong>${this.parser.parseInline(e)}</strong>`}em({tokens:e}){return`<em>${this.parser.parseInline(e)}</em>`}codespan({text:e}){return`<code>${R(e,!0)}</code>`}br(e){return"<br>"}del({tokens:e}){return`<del>${this.parser.parseInline(e)}</del>`}link({href:e,title:t,tokens:n}){let s=this.parser.parseInline(n),i=V(e);if(i===null)return s;e=i;let r='<a href="'+e+'"';return t&&(r+=' title="'+R(t)+'"'),r+=">"+s+"</a>",r}image({href:e,title:t,text:n,tokens:s}){s&&(n=this.parser.parseInline(s,this.parser.textRenderer));let i=V(e);if(i===null)return R(n);e=i;let r=`<img src="${e}" alt="${n}"`;return t&&(r+=` title="${R(t)}"`),r+=">",r}text(e){return"tokens"in e&&e.tokens?this.parser.parseInline(e.tokens):"escaped"in e&&e.escaped?e.text:R(e.text)}};var _=class{strong({text:e}){return e}em({text:e}){return e}codespan({text:e}){return e}del({text:e}){return e}html({text:e}){return e}text({text:e}){return e}link({text:e}){return""+e}image({text:e}){return""+e}br(){return""}};var b=class l{options;renderer;textRenderer;constructor(e){this.options=e||w,this.options.renderer=this.options.renderer||new $,this.renderer=this.options.renderer,this.renderer.options=this.options,this.renderer.parser=this,this.textRenderer=new _}static parse(e,t){return new l(t).parse(e)}static parseInline(e,t){return new l(t).parseInline(e)}parse(e,t=!0){let n="";for(let s=0;s<e.length;s++){let i=e[s];if(this.options.extensions?.renderers?.[i.type]){let o=i,a=this.options.extensions.renderers[o.type].call({parser:this},o);if(a!==!1||!["space","hr","heading","code","table","blockquote","list","html","paragraph","text"].includes(o.type)){n+=a||"";continue}}let r=i;switch(r.type){case"space":{n+=this.renderer.space(r);continue}case"hr":{n+=this.renderer.hr(r);continue}case"heading":{n+=this.renderer.heading(r);continue}case"code":{n+=this.renderer.code(r);continue}case"table":{n+=this.renderer.table(r);continue}case"blockquote":{n+=this.renderer.blockquote(r);continue}case"list":{n+=this.renderer.list(r);continue}case"html":{n+=this.renderer.html(r);continue}case"paragraph":{n+=this.renderer.paragraph(r);continue}case"text":{let o=r,a=this.renderer.text(o);for(;s+1<e.length&&e[s+1].type==="text";)o=e[++s],a+=`
66
+ `+this.renderer.text(o);t?n+=this.renderer.paragraph({type:"paragraph",raw:a,text:a,tokens:[{type:"text",raw:a,text:a,escaped:!0}]}):n+=a;continue}default:{let o='Token with "'+r.type+'" type was not found.';if(this.options.silent)return console.error(o),"";throw new Error(o)}}}return n}parseInline(e,t=this.renderer){let n="";for(let s=0;s<e.length;s++){let i=e[s];if(this.options.extensions?.renderers?.[i.type]){let o=this.options.extensions.renderers[i.type].call({parser:this},i);if(o!==!1||!["escape","html","link","image","strong","em","codespan","br","del","text"].includes(i.type)){n+=o||"";continue}}let r=i;switch(r.type){case"escape":{n+=t.text(r);break}case"html":{n+=t.html(r);break}case"link":{n+=t.link(r);break}case"image":{n+=t.image(r);break}case"strong":{n+=t.strong(r);break}case"em":{n+=t.em(r);break}case"codespan":{n+=t.codespan(r);break}case"br":{n+=t.br(r);break}case"del":{n+=t.del(r);break}case"text":{n+=t.text(r);break}default:{let o='Token with "'+r.type+'" type was not found.';if(this.options.silent)return console.error(o),"";throw new Error(o)}}}return n}};var L=class{options;block;constructor(e){this.options=e||w}static passThroughHooks=new Set(["preprocess","postprocess","processAllTokens"]);preprocess(e){return e}postprocess(e){return e}processAllTokens(e){return e}provideLexer(){return this.block?x.lex:x.lexInline}provideParser(){return this.block?b.parse:b.parseInline}};var E=class{defaults=z();options=this.setOptions;parse=this.parseMarkdown(!0);parseInline=this.parseMarkdown(!1);Parser=b;Renderer=$;TextRenderer=_;Lexer=x;Tokenizer=S;Hooks=L;constructor(...e){this.use(...e)}walkTokens(e,t){let n=[];for(let s of e)switch(n=n.concat(t.call(this,s)),s.type){case"table":{let i=s;for(let r of i.header)n=n.concat(this.walkTokens(r.tokens,t));for(let r of i.rows)for(let o of r)n=n.concat(this.walkTokens(o.tokens,t));break}case"list":{let i=s;n=n.concat(this.walkTokens(i.items,t));break}default:{let i=s;this.defaults.extensions?.childTokens?.[i.type]?this.defaults.extensions.childTokens[i.type].forEach(r=>{let o=i[r].flat(1/0);n=n.concat(this.walkTokens(o,t))}):i.tokens&&(n=n.concat(this.walkTokens(i.tokens,t)))}}return n}use(...e){let t=this.defaults.extensions||{renderers:{},childTokens:{}};return e.forEach(n=>{let s={...n};if(s.async=this.defaults.async||s.async||!1,n.extensions&&(n.extensions.forEach(i=>{if(!i.name)throw new Error("extension name required");if("renderer"in i){let r=t.renderers[i.name];r?t.renderers[i.name]=function(...o){let a=i.renderer.apply(this,o);return a===!1&&(a=r.apply(this,o)),a}:t.renderers[i.name]=i.renderer}if("tokenizer"in i){if(!i.level||i.level!=="block"&&i.level!=="inline")throw new Error("extension level must be 'block' or 'inline'");let r=t[i.level];r?r.unshift(i.tokenizer):t[i.level]=[i.tokenizer],i.start&&(i.level==="block"?t.startBlock?t.startBlock.push(i.start):t.startBlock=[i.start]:i.level==="inline"&&(t.startInline?t.startInline.push(i.start):t.startInline=[i.start]))}"childTokens"in i&&i.childTokens&&(t.childTokens[i.name]=i.childTokens)}),s.extensions=t),n.renderer){let i=this.defaults.renderer||new $(this.defaults);for(let r in n.renderer){if(!(r in i))throw new Error(`renderer '${r}' does not exist`);if(["options","parser"].includes(r))continue;let o=r,a=n.renderer[o],c=i[o];i[o]=(...p)=>{let u=a.apply(i,p);return u===!1&&(u=c.apply(i,p)),u||""}}s.renderer=i}if(n.tokenizer){let i=this.defaults.tokenizer||new S(this.defaults);for(let r in n.tokenizer){if(!(r in i))throw new Error(`tokenizer '${r}' does not exist`);if(["options","rules","lexer"].includes(r))continue;let o=r,a=n.tokenizer[o],c=i[o];i[o]=(...p)=>{let u=a.apply(i,p);return u===!1&&(u=c.apply(i,p)),u}}s.tokenizer=i}if(n.hooks){let i=this.defaults.hooks||new L;for(let r in n.hooks){if(!(r in i))throw new Error(`hook '${r}' does not exist`);if(["options","block"].includes(r))continue;let o=r,a=n.hooks[o],c=i[o];L.passThroughHooks.has(r)?i[o]=p=>{if(this.defaults.async)return Promise.resolve(a.call(i,p)).then(d=>c.call(i,d));let u=a.call(i,p);return c.call(i,u)}:i[o]=(...p)=>{let u=a.apply(i,p);return u===!1&&(u=c.apply(i,p)),u}}s.hooks=i}if(n.walkTokens){let i=this.defaults.walkTokens,r=n.walkTokens;s.walkTokens=function(o){let a=[];return a.push(r.call(this,o)),i&&(a=a.concat(i.call(this,o))),a}}this.defaults={...this.defaults,...s}}),this}setOptions(e){return this.defaults={...this.defaults,...e},this}lexer(e,t){return x.lex(e,t??this.defaults)}parser(e,t){return b.parse(e,t??this.defaults)}parseMarkdown(e){return(n,s)=>{let i={...s},r={...this.defaults,...i},o=this.onError(!!r.silent,!!r.async);if(this.defaults.async===!0&&i.async===!1)return o(new Error("marked(): The async option was set to true by an extension. Remove async: false from the parse options object to return a Promise."));if(typeof n>"u"||n===null)return o(new Error("marked(): input parameter is undefined or null"));if(typeof n!="string")return o(new Error("marked(): input parameter is of type "+Object.prototype.toString.call(n)+", string expected"));r.hooks&&(r.hooks.options=r,r.hooks.block=e);let a=r.hooks?r.hooks.provideLexer():e?x.lex:x.lexInline,c=r.hooks?r.hooks.provideParser():e?b.parse:b.parseInline;if(r.async)return Promise.resolve(r.hooks?r.hooks.preprocess(n):n).then(p=>a(p,r)).then(p=>r.hooks?r.hooks.processAllTokens(p):p).then(p=>r.walkTokens?Promise.all(this.walkTokens(p,r.walkTokens)).then(()=>p):p).then(p=>c(p,r)).then(p=>r.hooks?r.hooks.postprocess(p):p).catch(o);try{r.hooks&&(n=r.hooks.preprocess(n));let p=a(n,r);r.hooks&&(p=r.hooks.processAllTokens(p)),r.walkTokens&&this.walkTokens(p,r.walkTokens);let u=c(p,r);return r.hooks&&(u=r.hooks.postprocess(u)),u}catch(p){return o(p)}}}onError(e,t){return n=>{if(n.message+=`
67
+ Please report this to https://github.com/markedjs/marked.`,e){let s="<p>An error occurred:</p><pre>"+R(n.message+"",!0)+"</pre>";return t?Promise.resolve(s):s}if(t)return Promise.reject(n);throw n}}};var M=new E;function k(l,e){return M.parse(l,e)}k.options=k.setOptions=function(l){return M.setOptions(l),k.defaults=M.defaults,N(k.defaults),k};k.getDefaults=z;k.defaults=w;k.use=function(...l){return M.use(...l),k.defaults=M.defaults,N(k.defaults),k};k.walkTokens=function(l,e){return M.walkTokens(l,e)};k.parseInline=M.parseInline;k.Parser=b;k.parser=b.parse;k.Renderer=$;k.TextRenderer=_;k.Lexer=x;k.lexer=x.lex;k.Tokenizer=S;k.Hooks=L;k.parse=k;var it=k.options,ot=k.setOptions,lt=k.use,at=k.walkTokens,ct=k.parseInline,pt=k,ut=b.parse,ht=x.lex;
68
+
69
+ if(__exports != exports)module.exports = exports;return module.exports}));
package/lib/rewards.js CHANGED
@@ -69,7 +69,9 @@ function printDeclaration(result, { prefix = "" } = {}) {
69
69
  const what =
70
70
  rule.verifier === "completion"
71
71
  ? `completion · at least ${rule.minSeconds || 0}s`
72
- : `quiz · ${rule.questionSets} set(s)${rule.progression ? ` · ${rule.progression}` : ""}${rule.standingQuestions ? ` · ${rule.standingQuestions} standing` : ""}`;
72
+ : rule.verifier === "generated-quiz"
73
+ ? "question bank · server-written, double-checked questions on a step ladder"
74
+ : `quiz · ${rule.questionSets} set(s)${rule.progression ? ` · ${rule.progression}` : ""}${rule.standingQuestions ? ` · ${rule.standingQuestions} standing` : ""}`;
73
75
  console.log(
74
76
  `${prefix} ${rule.id}: ${rule.title} · ${rule.xp} XP + ${rule.coins} Coins · ${what}`,
75
77
  );
@@ -239,8 +241,12 @@ function printRewardsHelp() {
239
241
  rewards.json (project root) declares the economy the reviewer approves:
240
242
  { "userDailyXP", "userDailyCoins", "userDailyClaims"?,
241
243
  "rules": [{ "id", "title", "howTo"? (how to earn it, in the player's words), "category"? (section header, up to 40 characters, e.g. "Bring friends"),
242
- "series"? (lowercase key for repeat rules like friend-1/2/3; they must share category, verifier and amounts), "xp", "coins", "verifier": "numeric-quiz" | "completion",
243
- "maxAttempts"?, "retry"?: { "xpPercent", "coinsPercent", "paidAttempts"? }, "minSeconds"? (completion), "progression"?: "dated" | "until-earned" (quiz) }] }
244
+ "series"? (lowercase key for repeat rules like friend-1/2/3; they must share category, verifier and amounts), "xp", "coins", "verifier": "numeric-quiz" | "completion" | "generated-quiz",
245
+ "maxAttempts"?, "retry"?: { "xpPercent", "coinsPercent", "paidAttempts"? }, "minSeconds"? (completion), "progression"?: "dated" | "until-earned" (quiz),
246
+ "bank"? (generated-quiz): { "steps": [percent, ...] rising to 100, "startStep"?, "instructions", "topics": [{ "key", "title", "description" }] } }] }
244
247
  The Earn page shows rules under their category headers and each series as one row ("up to 3 a day").
245
- Questions and answer keys never go in project files; they belong in the sheet.`);
248
+ Questions and answer keys never go in project files; they belong in the sheet.
249
+ generated-quiz rules need no sheet: the server writes each question for a topic at the learner's step
250
+ (GPT-6 Sol, billed to that learner's AI Energy), checks it with a second independent solve, and pays
251
+ the step's percent of xp/coins. The reviewer approves the ladder, topics and instructions.`);
246
252
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stage5/lumine",
3
- "version": "0.2.94",
3
+ "version": "0.2.96",
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.61.0
3
+ Version: 1.63.0
4
4
  Updated: 2026-09-28
5
- Generated: 2026-09-27T17:21:08.257Z
5
+ Generated: 2026-09-28T07:33:56.595Z
6
6
 
7
7
  ## Notes
8
8
  - This SDK is injected into Build iframes via the Build preview/runtime.
@@ -33,8 +33,8 @@ Generated: 2026-09-27T17:21:08.257Z
33
33
  - Use Twinkle.live for one-way app livestreams and Twinkle.chat for the accompanying thread. Free livestreams require a verified host, end after at most 15 minutes, and issue at most 10 private viewer grants. Twinkle keeps platform-owned live-status/end controls above active hosts, so app code cannot hide or replace the broadcaster's Stop path.
34
34
  - Media Energy is separate from AI Energy. Replace Media Energy UI only from canonical mediaEnergy/getUsage responses; never decrement, reserve, or synthesize it in app code.
35
35
  - Twinkle.rewards awards real XP and Coins only in the current approved published release. Drafts, local previews, private apps and superseded releases cannot earn. The server supplies a published-runtime grant; app code cannot choose a recipient or award amount.
36
- - The creator's agent designs the rewards. Declare the economy in a project file `rewards.json` at the root: budgets (userDailyXP, userDailyCoins, optional userDailyClaims; there is no app-wide daily or lifetime budget, only what one learner can earn per day) and rules [{ id, title, howTo?, category? (section header, up to 40 characters), series? (key for repeat rules that share category and amounts), xp, coins, verifier: 'numeric-quiz' | 'completion', maxAttempts?, retry?: { xpPercent, coinsPercent, paidAttempts? }, minSeconds? (completion), progression?: 'dated' | 'until-earned' (quiz) }]. Wire the matching Twinkle.rewards calls with those literal rule ids. Questions and answer keys NEVER go in project files (published source is readable by every player): quiz rules get them from the private question sheet uploaded with `lumine rewards sheet <file.json>` ({ rules: { <ruleId>: { questions?, sets? } } }); `lumine rewards check` validates both together. A review request freezes the code and proposes rewards.json merged with the sheet; the administrator reads the code, checks the amounts and whether the app is exploitable, may change any amount, and approves. Creators are kids and teens: show approval status and one Send for review action; do not ask them to fill in technical forms. Every code update that retains rewards needs a new approval before publishing. Removing the SDK automatically clears its gate. Apps read amounts, tries and sets from getStatus, never from their own file.
37
- - Verifiers: 'numeric-quiz' pays for server-checked numeric answers (retry share, attempt limits, dated sets or until-earned sets that stay up until somebody earns them, after-answer guides). 'completion' pays when the app reports an activity finished — a cleared stage, a finished round — at least minSeconds after start({ ruleId }); the server checks only the elapsed time, once per learner per site day (UTC midnight), and the budgets. Call start when the activity begins and claim({ challengeId }) with no answers when it ends; keep completion amounts and userDailyXP small enough that a player scripting the calls would not matter, because nothing else is verified.
36
+ - The creator's agent designs the rewards. Declare the economy in a project file `rewards.json` at the root: budgets (userDailyXP, userDailyCoins, optional userDailyClaims; there is no app-wide daily or lifetime budget, only what one learner can earn per day) and rules [{ id, title, howTo?, category? (section header, up to 40 characters), series? (key for repeat rules that share category and amounts), xp, coins, verifier: 'numeric-quiz' | 'completion' | 'generated-quiz', maxAttempts?, retry?: { xpPercent, coinsPercent, paidAttempts? }, minSeconds? (completion), progression?: 'dated' | 'until-earned' (quiz), bank? (generated-quiz): { steps: [percent, ...] rising to 100, startStep?, instructions, topics: [{ key, title, description }] } }]. generated-quiz rules need no question sheet. Wire the matching Twinkle.rewards calls with those literal rule ids. Questions and answer keys NEVER go in project files (published source is readable by every player): quiz rules get them from the private question sheet uploaded with `lumine rewards sheet <file.json>` ({ rules: { <ruleId>: { questions?, sets? } } }); `lumine rewards check` validates both together. A review request freezes the code and proposes rewards.json merged with the sheet; the administrator reads the code, checks the amounts and whether the app is exploitable, may change any amount, and approves. Creators are kids and teens: show approval status and one Send for review action; do not ask them to fill in technical forms. Every code update that retains rewards needs a new approval before publishing. Removing the SDK automatically clears its gate. Apps read amounts, tries and sets from getStatus, never from their own file.
37
+ - Verifiers: 'numeric-quiz' pays for server-checked numeric answers (retry share, attempt limits, dated sets or until-earned sets that stay up until somebody earns them, after-answer guides). 'completion' pays when the app reports an activity finished — a cleared stage, a finished round — at least minSeconds after start({ ruleId }); the server checks only the elapsed time, once per learner per site day (UTC midnight), and the budgets. Call start when the activity begins and claim({ challengeId }) with no answers when it ends; keep completion amounts and userDailyXP small enough that a player scripting the calls would not matter, because nothing else is verified. 'generated-quiz' pays from a question bank the server fills itself: for one declared topic at the learner's step of the ladder, the server writes a question with GPT-6 Sol (billed to that learner's AI Energy), solves it again independently and keeps it only when both answers agree; every learner who has not seen a question gets it before anything new is written. `xp`/`coins` are the top step's amounts and each step pays its `steps` percent of them (then the usual retry share). Two first-try answers in a row climb a step; two misses in a row step down. Learners may earn many bank questions a day, bounded by userDailyXP/userDailyCoins. The reviewer approves the ladder, amounts, topics and instructions, not each question; learner reports and solve rates hide bad questions. Use the rewards bank methods (getBankStatus, startBankQuestion, generateBankQuestion, claimBankQuestion, reportBankQuestion); start and claim refuse bank rules.
38
38
  - Numeric quiz answers are verified on the server; client scores, privateDb state, timers and completion booleans are not verified reward evidence. Limits reset at UTC midnight. Rules are earned once per viewer per UTC day; attempt limits and retry payouts come from the approved rule. Challenges expire at the UTC day boundary. Budgets apply across release changes.
39
39
  - Optional reward rule controls: maxLifetimeClaims caps one learner’s receipts for that rule across every day and release; completionProof: classic-tower-v1 requires a server-simulated Classic Tower finish in addition to minSeconds. These are server-enforced controls. Existing completion rules without completionProof still verify elapsed time only. Registered proof profiles also include breadface-v1, breadface-v2 and breadface-v3 (server-simulated Breadface inputs; each is one reviewed release's exact physics, chosen by the reviewer) study-record-v1 (a private study record reviewed by JEV, billed to the learner’s AI Energy), and Groove Lab's groove-lab-song-v1 (a song the learner published today, checked on the server for length, notes and originality) and groove-lab-heard-v1 (three established accounts finished the learner's songs today), Ashen Vigil's vigil-guest-coplay-v1 (a new guest spent 10 minutes with the learner in a private world room, measured by the world relay; each guest pays once) and minecraft-first-link-v1 (the learner is the first Twinkle account ever linked to that Minecraft player; each player pays once). Profiles are platform-owned; an app cannot invent a verifier or authorize its own reward.
40
40
 
@@ -1064,6 +1064,122 @@ const result = await Twinkle.characters.chat({ character: 'zero', thinkingMode:
1064
1064
  - Others get 403 minecraft_news_forbidden.
1065
1065
  - Example: await Twinkle.minecraft.deleteNews({ id: post.id });
1066
1066
 
1067
+ ### Twinkle.town
1068
+ - async getTown() | scopes: sharedDb:read
1069
+ - Returns: { town, clock: { now, townMinute, day, weekday, kstTime }, places: [{ id, name, kind, district, door: [x, z], y, built, spots }], housing: { homes, waitingList }, residents: [publicCharacter], gazette: [{ id, townMinute, text }], goods, myCharacterId, cursor }
1070
+ - Load Starhaven Life's shared town: places (Minecraft coordinates), the homes board, every resident's public state and the Town Gazette.
1071
+ - Every resident is a real member's character living on its own every 15 minutes on the real KST clock (school mornings, work afternoons, rent on Mondays).
1072
+ - publicCharacter = { characterId, name, look, pet, home, place, spot, move: { points, depart, arrive } | null, activity: { id, label, icon, place, start, until, with, withName } | null, mood: { emotion, icon, label }, title, job, knownFor, guided }. start/until/depart/arrive are town minutes (clock.townMinute is now).
1073
+ - Pass cursor to getChanges.
1074
+ - Limited rollout: app 2610 (Twinkle Minecraft Live) only; other apps get 403 with code town_sdk_not_enabled.
1075
+ - Example: const town = await Twinkle.town.getTown(); let cursor = town.cursor; setInterval(async () => { const ch = await Twinkle.town.getChanges({ since: cursor }); cursor = ch.cursor; /* move ch.residents */ }, 25000);
1076
+ - async getChanges({ since }) | scopes: sharedDb:read
1077
+ - Returns: { clock, events: [{ id, townMinute, type, salience, characterId, withCharacterId, text, mine }], residents: [publicCharacter], newDiaryLines, hasMore, cursor }
1078
+ - Poll what changed since a cursor: public town events, events about your own character, and residents whose state changed.
1079
+ - Poll every 20-30 seconds; always continue from the returned cursor. hasMore: true means call again right away.
1080
+ - Also marks the viewer as online (their character earns full pay while its guardian is around).
1081
+ - Limited rollout: app 2610 (Twinkle Minecraft Live) only; other apps get 403 with code town_sdk_not_enabled.
1082
+ - async getMyCharacter() | scopes: sharedDb:read
1083
+ - Returns: { character: { characterId, name, status, profile, petName, public, life, movingIn } | null, guardian: { priorities, direction, focus, civicCard, optIns, aspiration }, home, waitingListPosition, control, guidedActions, clock } | { character: null, create: { suggestedNames, suggestedPetNames, petSpecies, likeTags, axes, nameCheck } }
1084
+ - The guardian's view of the viewer's own character (needs, feelings, wishes, worries, skills, job, plan, money, friends, proposals waiting for an answer), or the options for creating one.
1085
+ - life is the server's canonical state; never compute needs, money or relationships in the app.
1086
+ - life.proposals are big life changes the character asks its guardian first (for example asking someone to be best friends): answer with answerProposal; silence for 24 hours lets the character decide.
1087
+ - Limited rollout: app 2610 (Twinkle Minecraft Live) only; other apps get 403 with code town_sdk_not_enabled.
1088
+ - async createCharacter({ name, petName, petSpecies, personality: { neat, outgoing, active, playful, kind }, likes, dislike, look: { skin, hair, shirt, pants, hood }, homeId }) | scopes: sharedDb:write
1089
+ - Returns: { characterId, name, petName, welcomeBucks, housing: { homeId, weeklyRent, rentDueMinute } | { waitingListPosition, error? } }
1090
+ - Create the viewer's one character in Starhaven (safety-checked names, 200 welcome Bucks) and optionally claim a vacant home.
1091
+ - Names are 2-16 letters. Suggested names (getMyCharacter().create) always pass; any other name needs a clear safety check and may be refused (400 town_name_not_allowed) or be unavailable (503: offer the suggestions).
1092
+ - personality values are -2..2; likes: 3 different interest tags; dislike: 1 other tag; look colours are 0xRRGGBB numbers.
1093
+ - Without a free home the character lodges in the Fivefold guest rooms and joins the waiting list.
1094
+ - Limited rollout: app 2610 (Twinkle Minecraft Live) only; other apps get 403 with code town_sdk_not_enabled.
1095
+ - async setPriorities({ priorities }) | scopes: sharedDb:write
1096
+ - Returns: { priorities }
1097
+ - Set the guardian's 8 priority sliders (-2..2): saving, pets, work, social, learning, fun, tidy, civic.
1098
+ - Priorities tilt what the character chooses; they never force it (characters have free will).
1099
+ - Limited rollout: app 2610 (Twinkle Minecraft Live) only; other apps get 403 with code town_sdk_not_enabled.
1100
+ - async setDirections({ text }) | scopes: sharedDb:write
1101
+ - Returns: { ok: true, text, understoodAs, weights, neighbour } | { ok: false, reason }
1102
+ - Give the character plain-words guidance (up to 200 characters, English). JEV checks it is safe and kind, then shows back how it was understood.
1103
+ - Show understoodAs to the guardian. ok: false keeps the old guidance (unsafe wording, too many changes today (3 a day), or guidance reading paused).
1104
+ - Directions can only raise time with a neighbour, never avoid or target anyone. They are private to the owner.
1105
+ - Limited rollout: app 2610 (Twinkle Minecraft Live) only; other apps get 403 with code town_sdk_not_enabled.
1106
+ - async setOptIns({ dontVoteForMe, voteForMe, spendLimit, savingsFloor, visitors, workOffline, tendPets }) | scopes: sharedDb:write
1107
+ - Returns: { optIns }
1108
+ - Guardian opt-ins: JEV votes for the character by default (dontVoteForMe: true opts out), daily spend limit (0-500 Bucks, default 50), savings floor, visitors (friends | residents | nobody).
1109
+ - visitors: nobody also stops the character hosting a moved-out friend.
1110
+ - Limited rollout: app 2610 (Twinkle Minecraft Live) only; other apps get 403 with code town_sdk_not_enabled.
1111
+ - async setFocus({ skills }) | scopes: sharedDb:write
1112
+ - Returns: { focus }
1113
+ - Pick up to 2 Focus skills; the character spends more time on them and grows them faster.
1114
+ - Scholarship grows only from the owner's real learning on Twinkle and cannot be a Focus.
1115
+ - Limited rollout: app 2610 (Twinkle Minecraft Live) only; other apps get 403 with code town_sdk_not_enabled.
1116
+ - async setCivicCard({ focus, tax, change }) | scopes: sharedDb:write
1117
+ - Returns: { civicCard }
1118
+ - The 3-question civic card the autopilot vote reads (focus: fun | learning | pets | saving | civic; tax: lower | keep | higher; change: new | keep).
1119
+ - Limited rollout: app 2610 (Twinkle Minecraft Live) only; other apps get 403 with code town_sdk_not_enabled.
1120
+ - async confirmAspiration({ aspirationId }) | scopes: sharedDb:write
1121
+ - Returns: { aspiration: { id, label, confirmed } }
1122
+ - Confirm the life goal the character proposed (life.aspirationProposed) or choose another; milestones give titles only.
1123
+ - Limited rollout: app 2610 (Twinkle Minecraft Live) only; other apps get 403 with code town_sdk_not_enabled.
1124
+ - async takeControl({ leaseId, immediate }) | scopes: sharedDb:write
1125
+ - Returns: { leaseId, expiresAt, heartbeatMs, renewed }
1126
+ - Guide the character directly. Returns a lease; call takeControl({ leaseId }) about every 60 s to keep it.
1127
+ - The lease lapses after 3 missed heartbeats (stop renewing when the tab is hidden) and the character goes back to living on its own. immediate: true stops the current activity now instead of at its end.
1128
+ - 409 town_control_elsewhere: another window is guiding this character.
1129
+ - Limited rollout: app 2610 (Twinkle Minecraft Live) only; other apps get 403 with code town_sdk_not_enabled.
1130
+ - async releaseControl({ leaseId }) | scopes: sharedDb:write
1131
+ - Returns: { released, livingOnAutopilot }
1132
+ - "Let them live": hand the character back to its own free will.
1133
+ - Limited rollout: app 2610 (Twinkle Minecraft Live) only; other apps get 403 with code town_sdk_not_enabled.
1134
+ - async act({ leaseId, operationId, activity } | { leaseId, operationId, outing, withCharacterId }) | scopes: sharedDb:write
1135
+ - Returns: { operationId, status: queued | applied | rejected, result }
1136
+ - While guiding, tell the character what to do next: an activity (from getMyCharacter().guidedActions.activities) or an outing with another resident.
1137
+ - The server checks it is possible (hours, weather, money, the other resident being free) and applies it within a few seconds; watch getChanges/getMyCharacter for the result. Reuse the same operationId when retrying.
1138
+ - Limited rollout: app 2610 (Twinkle Minecraft Live) only; other apps get 403 with code town_sdk_not_enabled.
1139
+ - async answerProposal({ proposalId, answer, operationId }) | scopes: sharedDb:write
1140
+ - Returns: { status, result }
1141
+ - Answer a big-life-change question the character asked its guardian: yes, no, or decide (let them decide).
1142
+ - Limited rollout: app 2610 (Twinkle Minecraft Live) only; other apps get 403 with code town_sdk_not_enabled.
1143
+ - async getDiary({ since, limit }) | scopes: sharedDb:read
1144
+ - Returns: { since, moments, decisions: [{ text, by: jev | code, at }], lines, numbers: { earned, shifts, meals, sleptHours, friendsSeen, skillUps } }
1145
+ - "While you were away": the top moments, the decisions made for you (and whether JEV or the code made them), and the numbers since a time (ms).
1146
+ - Text comes from fixed English templates; "(quiet stretch)" marks time the town caught up after a pause.
1147
+ - Limited rollout: app 2610 (Twinkle Minecraft Live) only; other apps get 403 with code town_sdk_not_enabled.
1148
+ - async getElections() | scopes: sharedDb:read
1149
+ - Returns: { offices, elections: [{ id, office, officeLabel, status, votingOpensAt, closesAt, candidates, result, myBallot }], pledges, mottos, minEligibleVoters }
1150
+ - Town offices and elections (Town Chief, Headmaster, Market Warden, Pet Warden; 14-day terms).
1151
+ - Results show counts only (secret ballot), including how many were autopilot votes.
1152
+ - Limited rollout: app 2610 (Twinkle Minecraft Live) only; other apps get 403 with code town_sdk_not_enabled.
1153
+ - async vote({ electionId, approve }) | scopes: sharedDb:write
1154
+ - Returns: { electionId, approve, abstain }
1155
+ - Approval vote: approve any number of candidates (characterIds). One ballot per account; voting again replaces it until the close.
1156
+ - Voters: account 30+ days old, living in Starhaven 7+ days, active 3 of the last 14 days (403 town_not_eligible otherwise).
1157
+ - Without a manual vote, JEV votes for the character at the close unless the guardian opted out (setOptIns({ dontVoteForMe: true })).
1158
+ - Limited rollout: app 2610 (Twinkle Minecraft Live) only; other apps get 403 with code town_sdk_not_enabled.
1159
+ - async declareCandidacy({ electionId, pledges, motto }) | scopes: sharedDb:write
1160
+ - Returns: { electionId, pledges, motto }
1161
+ - Stand for office during the candidacy days: exactly 3 pledges from the list and an optional templated motto id (no free text).
1162
+ - Only the owner can declare, never the character.
1163
+ - Limited rollout: app 2610 (Twinkle Minecraft Live) only; other apps get 403 with code town_sdk_not_enabled.
1164
+ - async getHousing() | scopes: sharedDb:read
1165
+ - Returns: { homes: [{ homeId, plot, district, name, type, readyMade, weeklyRent, vacant, residentName }], waitingList: [{ position, name, since, movedOut }], mine: { home, rentJar, bucks, waitingListPosition, inStorage } | null, rentDay, graceDays }
1166
+ - The homes board (10 Harbor Row + 10 Lantern Hill plots), the public waiting list and the viewer's rent status.
1167
+ - Rent is weekly in Bucks, due Monday 00:00 KST, with 3 days of grace; a character that can't pay moves out gently (belongings to storage) and gets the next home it can cover from the waiting list.
1168
+ - Limited rollout: app 2610 (Twinkle Minecraft Live) only; other apps get 403 with code town_sdk_not_enabled.
1169
+ - async claimHome({ homeId }) | scopes: sharedDb:write
1170
+ - Returns: { homeId, weeklyRent, rentDueMinute, rentDueAt }
1171
+ - Claim a vacant home, first come first served. The first rent is due on a Monday at least 3 days away.
1172
+ - 409 town_home_taken when someone else got there first.
1173
+ - Limited rollout: app 2610 (Twinkle Minecraft Live) only; other apps get 403 with code town_sdk_not_enabled.
1174
+ - async joinWaitingList() | scopes: sharedDb:write
1175
+ - Returns: { waitingListPosition }
1176
+ - Join the public housing waiting list.
1177
+ - Limited rollout: app 2610 (Twinkle Minecraft Live) only; other apps get 403 with code town_sdk_not_enabled.
1178
+ - async leaveWaitingList() | scopes: sharedDb:write
1179
+ - Returns: { waitingListPosition: null }
1180
+ - Leave the housing waiting list.
1181
+ - Limited rollout: app 2610 (Twinkle Minecraft Live) only; other apps get 403 with code town_sdk_not_enabled.
1182
+
1067
1183
  ### Twinkle.leaderboards
1068
1184
  - async get({ boardKey = 'default', limit, cursor } = {}) | scopes: none
1069
1185
  - Returns: { entries: [{ rank, id, buildId, boardKey, viewerKind, userId, displayName, score, meta, achievedAt, createdAt, updatedAt }], scores, cursor, hasMore, personalBest: { id, buildId, boardKey, viewerKind, userId, displayName, score, meta, achievedAt, createdAt, updatedAt } | null }
@@ -1388,6 +1504,33 @@ world.updatePresence({ x, y, z, facing });
1388
1504
  - Read a retired bounty’s original questions and guides from the approval attached to its solve receipt.
1389
1505
  - Use a receiptId returned by getTimeline. Both reads independently check retirement and app ownership; guessing an active or other app’s receipt cannot reveal its questions or guides. Returns build_reward_archive_unavailable (404) if unavailable.
1390
1506
  - Uses that receipt’s frozen approved sheet, never today’s edited question or the mutable draft. Answer keys and tolerances are never returned. Render the question first and offer Reveal guide for learning, without a reward-claim button.
1507
+ - await Twinkle.rewards.getBankStatus({ ruleId }) | scopes: rewards:claim
1508
+ - Returns: { mode, ruleId, dayKey, startStep, steps: [{ step, reward: { xp, coins } }], topics: [{ key, title, step, questionsAtStep }], today: { xp, coins, claims }, budgets: { userDailyXP, userDailyCoins } }
1509
+ - Read a generated-quiz rule's ladder: what each step pays, the learner's step on every topic, and today's earnings.
1510
+ - Read on load and after each claim; never compute amounts or steps in the app. Drafts return mode 'preview' from the owner's own rewards.json.
1511
+ - await Twinkle.rewards.startBankQuestion({ ruleId, topicKey, skip? }) | scopes: rewards:claim
1512
+ - Returns: { needsQuestion: true, step, reward } | { challengeId, step, question: { id, topicKey, step, prompt, hint?, guide?, answer? }, attempts, maxAttempts, attemptsRemaining, firstTryAvailable, solved, reward, retryReward, stats: { learners, solved, firstSolver: { id, username, solvedAt } | null } }
1513
+ - Serve the learner's open question on a topic, else the next question at their step that they have not seen.
1514
+ - needsQuestion: true means no unseen question exists at the learner's step: offer to write one with generateBankQuestion, saying it uses their AI Energy.
1515
+ - The prompt and guide use LaTeX between $...$ / $$...$$; render them with the Twinkle-served KaTeX (see the vendor guidance). The answer appears only after the learner solves it or runs out of tries; the guide after their first answer.
1516
+ - skip: true gives up on the open question (it counts as a miss on the ladder) and serves the next one.
1517
+ - await Twinkle.rewards.generateBankQuestion({ ruleId, topicKey, onProgress?: ({ stage, phase, percent, round? }) => void }) | scopes: rewards:claim
1518
+ - Returns: Same as startBankQuestion with the new question, or { needsQuestion: false } when an unseen question already exists (call startBankQuestion).
1519
+ - Have the server write, double-check and serve a new question at the learner's step. Uses the learner's AI Energy.
1520
+ - Show a progress bar from onProgress: stage is 'pending', 'writing', 'checking' (the independent solve), 'saving' or 'done'; percent follows the server's real phases and the model's streamed output, never goes backwards, and reaches 100 only when the question is served. Silent model thinking inside a phase cannot be measured, so the bar eases forward there rather than guessing.
1521
+ - Takes as long as two GPT-6 Sol calls (tens of seconds): show progress and keep one request in flight. Both calls are billed to the signed-in learner's AI Energy; an empty battery rejects before any work.
1522
+ - build_reward_bank_unverified (503) means two tries did not produce a question whose answers agreed; nothing is stored. build_reward_bank_generation_limit (429) caps questions one learner writes per day.
1523
+ - The question joins the shared bank: every other learner at that step gets it before anything new is written.
1524
+ - await Twinkle.rewards.claimBankQuestion({ challengeId, answer: number }) | scopes: rewards:claim
1525
+ - Returns: { correct, awarded, duplicate?, budgetReached?, receipt: { id, questionId, step, xp, coins, attempt, firstTry } | null, balances?, ladder?: { step, previousStep }, question: { ..., guide?, answer? }, attempts, attemptsRemaining, stats }
1526
+ - Answer a bank question. The first answer moves the ladder; a correct answer pays the question's step share.
1527
+ - Send one number (convert a typed fraction yourself). A wrong answer within two seconds of the previous one is refused with build_reward_throttled (429).
1528
+ - ladder appears on the learner's first answer to a question and gives their new step on that topic. Retrying a solved challengeId returns duplicate: true with the original receipt.
1529
+ - budgetReached: true means the solve counted but today's userDailyXP/userDailyCoins is used up. In preview mode nothing is paid and previewReward shows what the live app would pay.
1530
+ - await Twinkle.rewards.reportBankQuestion({ questionId, reason }) | scopes: rewards:claim
1531
+ - Returns: { reported: true, duplicate, hidden }
1532
+ - Report a wrong or unclear bank question the learner has seen.
1533
+ - One report per learner per question; three reports hide it from everyone. Show a small 'report this problem' action next to the question.
1391
1534
 
1392
1535
  ### Twinkle.cardCraft
1393
1536
  - await Twinkle.cardCraft.getStatus() | scopes: cardCraft:read
@@ -1018,6 +1018,19 @@ Mikey gives his go-ahead; a pending proposal is not a completed refresh. After
1018
1018
  approval, execute the entire approved plan and verify it without asking for
1019
1019
  each swap again. Never omit this section from a full-run report.
1020
1020
 
1021
+ ### Daily-run conveniences (CLI 0.2.95)
1022
+
1023
+ ```bash
1024
+ lumine admin daily-run gather --output-dir <dir> [--date YYYY-MM-DD] --json
1025
+ lumine admin featured candidates [--days 7] [--after <iso>] --json
1026
+ lumine admin report serve --file <dir>/daily-management-report.md [--port <n>] [--open] --json
1027
+ ```
1028
+
1029
+ These compose the documented commands in this contract, running them as child processes, so every scope check, audit and pagination rule applies unchanged.
1030
+ - **`gather`** saves each read-only report into `<dir>`. It follows `bot-output` to exhaustion and renders `bot-chats.txt`, then lists any failed step in `gather.json`. It needs the active run for run-scoped reads.
1031
+ - **`featured candidates`** returns Subjects posted after the Bangkok-midnight cutoff that are off the board and provably never Featured. Each one comes with its human/bot comment counts and whether Mikey has viewed it. Eligibility only: ranking stays editorial.
1032
+ - **`report serve`** detaches a loopback server that answers only `/`, `marked.min.js` and the report Markdown. `--open` opens Chrome.
1033
+
1021
1034
  ### Full management report in Chrome
1022
1035
 
1023
1036
  Mikey's standing delivery preference (2026-09-15): after a full daily run, open