@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 +9 -0
- package/lib/admin-daily.js +338 -0
- package/lib/admin.js +251 -0
- package/lib/commands.js +11 -0
- package/lib/constants.js +5 -0
- package/lib/report-assets/marked.min.js +69 -0
- package/lib/rewards.js +10 -4
- package/package.json +1 -1
- package/sdk/BUILD_SDK_INDEX.md +147 -4
- package/sdk/LUMINE_ADMIN.md +13 -0
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, "<")}</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={"&":"&","<":"<",">":">",'"':""","'":"'"},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
|
-
:
|
|
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
package/sdk/BUILD_SDK_INDEX.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Build SDK Index
|
|
2
2
|
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.63.0
|
|
4
4
|
Updated: 2026-09-28
|
|
5
|
-
Generated: 2026-09-
|
|
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
|
package/sdk/LUMINE_ADMIN.md
CHANGED
|
@@ -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
|