@stage5/lumine 0.2.39 → 0.2.41
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 +78 -13
- package/lib/admin-news.js +236 -0
- package/lib/admin-workflows.js +419 -0
- package/lib/admin.js +495 -44
- package/lib/agent/mcp-server.js +138 -0
- package/lib/agent/providers/claude-code.js +214 -0
- package/lib/agent/providers/codex.js +522 -0
- package/lib/agent/providers/environment.js +40 -0
- package/lib/agent/providers/index.js +38 -0
- package/lib/agent/tool-session.js +301 -0
- package/lib/agent/trace.js +138 -0
- package/lib/agent.js +453 -0
- package/lib/api.js +81 -1
- package/lib/build-review.js +493 -0
- package/lib/commands.js +145 -11
- package/lib/constants.js +15 -0
- package/lib/forum.js +375 -0
- package/package.json +1 -1
- package/sdk/BUILD_SDK_INDEX.md +26 -3
- package/sdk/LUMINE_ADMIN.md +95 -20
package/lib/forum.js
ADDED
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
import { requestJson } from "./http.js";
|
|
2
|
+
import { sleep } from "./util.js";
|
|
3
|
+
|
|
4
|
+
const FORUM_SCOPE_MODES = new Set(["all", "branch", "main"]);
|
|
5
|
+
const FORUM_EVENT_TYPES = new Set(["thread", "reply"]);
|
|
6
|
+
const MAX_FORUM_SNAPSHOT_PAGES = 100_000;
|
|
7
|
+
|
|
8
|
+
function forumProtocolError(message) {
|
|
9
|
+
const error = new Error(`Invalid Forum response: ${message}`);
|
|
10
|
+
error.code = "lumine_forum_protocol_error";
|
|
11
|
+
error.retryable = false;
|
|
12
|
+
return error;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function normalizeForumSequence(value, label) {
|
|
16
|
+
const sequence = Number(value);
|
|
17
|
+
if (!Number.isSafeInteger(sequence) || sequence < 0) {
|
|
18
|
+
throw forumProtocolError(`${label} must be a non-negative safe integer`);
|
|
19
|
+
}
|
|
20
|
+
return sequence;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function normalizePositiveForumId(value, label) {
|
|
24
|
+
const id = Number(value);
|
|
25
|
+
if (!Number.isSafeInteger(id) || id <= 0) {
|
|
26
|
+
throw forumProtocolError(`${label} must be a positive safe integer`);
|
|
27
|
+
}
|
|
28
|
+
return id;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function buildForumScopeKey(scope) {
|
|
32
|
+
const mode = String(scope?.mode || "");
|
|
33
|
+
if (!FORUM_SCOPE_MODES.has(mode)) {
|
|
34
|
+
throw forumProtocolError("scope.mode is not recognized");
|
|
35
|
+
}
|
|
36
|
+
const rootBuildId = normalizePositiveForumId(
|
|
37
|
+
scope?.rootBuildId,
|
|
38
|
+
"scope.rootBuildId",
|
|
39
|
+
);
|
|
40
|
+
const workspaceBuildId = normalizePositiveForumId(
|
|
41
|
+
scope?.workspaceBuildId,
|
|
42
|
+
"scope.workspaceBuildId",
|
|
43
|
+
);
|
|
44
|
+
const contributionBuildId = scope?.contributionBuildId
|
|
45
|
+
? normalizePositiveForumId(
|
|
46
|
+
scope.contributionBuildId,
|
|
47
|
+
"scope.contributionBuildId",
|
|
48
|
+
)
|
|
49
|
+
: 0;
|
|
50
|
+
if (mode === "branch" && contributionBuildId !== workspaceBuildId) {
|
|
51
|
+
throw forumProtocolError(
|
|
52
|
+
"branch scope does not match its contribution workspace",
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
if (mode !== "branch" && contributionBuildId !== 0) {
|
|
56
|
+
throw forumProtocolError("non-branch scope has a contribution build");
|
|
57
|
+
}
|
|
58
|
+
return `${mode}:${rootBuildId}:${workspaceBuildId}:${contributionBuildId}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function loadBuildForumPage({
|
|
62
|
+
options,
|
|
63
|
+
auth,
|
|
64
|
+
buildId,
|
|
65
|
+
afterActivitySeq,
|
|
66
|
+
snapshotActivitySeq,
|
|
67
|
+
limit,
|
|
68
|
+
}) {
|
|
69
|
+
const url = new URL(`${options.apiUrl}/cli/build/${buildId}/forum`);
|
|
70
|
+
url.searchParams.set("afterActivitySeq", String(afterActivitySeq));
|
|
71
|
+
if (snapshotActivitySeq > 0) {
|
|
72
|
+
url.searchParams.set("snapshotActivitySeq", String(snapshotActivitySeq));
|
|
73
|
+
}
|
|
74
|
+
url.searchParams.set("limit", String(limit));
|
|
75
|
+
return await requestJson({
|
|
76
|
+
url: url.toString(),
|
|
77
|
+
authToken: auth.token,
|
|
78
|
+
timeoutMs: options.timeoutMs,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function validateForumPage({
|
|
83
|
+
page,
|
|
84
|
+
buildId,
|
|
85
|
+
pageCursor,
|
|
86
|
+
snapshotActivitySeq,
|
|
87
|
+
expectedScopeKey,
|
|
88
|
+
}) {
|
|
89
|
+
const projectId = normalizePositiveForumId(page?.project?.id, "project.id");
|
|
90
|
+
const requestedBuildId = normalizePositiveForumId(
|
|
91
|
+
page?.requestedBuildId,
|
|
92
|
+
"requestedBuildId",
|
|
93
|
+
);
|
|
94
|
+
if (requestedBuildId !== buildId) {
|
|
95
|
+
throw forumProtocolError("requestedBuildId changed during the read");
|
|
96
|
+
}
|
|
97
|
+
const scopeKey = buildForumScopeKey(page?.scope);
|
|
98
|
+
if (Number(page?.scope?.rootBuildId) !== projectId) {
|
|
99
|
+
throw forumProtocolError("project.id does not match scope.rootBuildId");
|
|
100
|
+
}
|
|
101
|
+
if (expectedScopeKey && scopeKey !== expectedScopeKey) {
|
|
102
|
+
throw forumProtocolError(
|
|
103
|
+
"the authorized Forum workspace changed; restart the listener",
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const pageSnapshotActivitySeq = normalizeForumSequence(
|
|
108
|
+
page?.pagination?.snapshotActivitySeq,
|
|
109
|
+
"pagination.snapshotActivitySeq",
|
|
110
|
+
);
|
|
111
|
+
if (
|
|
112
|
+
snapshotActivitySeq > 0 &&
|
|
113
|
+
pageSnapshotActivitySeq !== snapshotActivitySeq
|
|
114
|
+
) {
|
|
115
|
+
throw forumProtocolError("snapshotActivitySeq changed between pages");
|
|
116
|
+
}
|
|
117
|
+
if (pageSnapshotActivitySeq < pageCursor) {
|
|
118
|
+
throw forumProtocolError("snapshotActivitySeq precedes the page cursor");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const events = Array.isArray(page?.events) ? page.events : null;
|
|
122
|
+
if (!events) throw forumProtocolError("events is not an array");
|
|
123
|
+
const pageLimit = Number(page?.pagination?.limit);
|
|
124
|
+
if (
|
|
125
|
+
!Number.isSafeInteger(pageLimit) ||
|
|
126
|
+
pageLimit < 1 ||
|
|
127
|
+
pageLimit > 100 ||
|
|
128
|
+
events.length > pageLimit
|
|
129
|
+
) {
|
|
130
|
+
throw forumProtocolError("pagination.limit does not bound the page");
|
|
131
|
+
}
|
|
132
|
+
let lastActivitySeq = pageCursor;
|
|
133
|
+
for (const event of events) {
|
|
134
|
+
if (!FORUM_EVENT_TYPES.has(String(event?.type || ""))) {
|
|
135
|
+
throw forumProtocolError("event.type is not recognized");
|
|
136
|
+
}
|
|
137
|
+
normalizePositiveForumId(event?.id, "event.id");
|
|
138
|
+
normalizePositiveForumId(event?.threadId, "event.threadId");
|
|
139
|
+
const activitySeq = normalizeForumSequence(
|
|
140
|
+
event?.activitySeq,
|
|
141
|
+
"event.activitySeq",
|
|
142
|
+
);
|
|
143
|
+
if (
|
|
144
|
+
activitySeq <= lastActivitySeq ||
|
|
145
|
+
activitySeq > pageSnapshotActivitySeq
|
|
146
|
+
) {
|
|
147
|
+
throw forumProtocolError(
|
|
148
|
+
"events are not strictly ordered inside the snapshot",
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
lastActivitySeq = activitySeq;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (typeof page?.pagination?.hasMore !== "boolean") {
|
|
155
|
+
throw forumProtocolError("pagination.hasMore is not boolean");
|
|
156
|
+
}
|
|
157
|
+
const nextActivitySeq = normalizeForumSequence(
|
|
158
|
+
page?.pagination?.nextActivitySeq,
|
|
159
|
+
"pagination.nextActivitySeq",
|
|
160
|
+
);
|
|
161
|
+
if (page.pagination.hasMore) {
|
|
162
|
+
if (
|
|
163
|
+
events.length === 0 ||
|
|
164
|
+
nextActivitySeq !== lastActivitySeq ||
|
|
165
|
+
nextActivitySeq <= pageCursor ||
|
|
166
|
+
nextActivitySeq >= pageSnapshotActivitySeq
|
|
167
|
+
) {
|
|
168
|
+
throw forumProtocolError("the next Forum page cursor is not progressive");
|
|
169
|
+
}
|
|
170
|
+
} else if (nextActivitySeq !== pageSnapshotActivitySeq) {
|
|
171
|
+
throw forumProtocolError(
|
|
172
|
+
"the final Forum page did not confirm the full snapshot cursor",
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
events,
|
|
178
|
+
nextActivitySeq,
|
|
179
|
+
pageSnapshotActivitySeq,
|
|
180
|
+
scopeKey,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export async function readCompleteBuildForumSnapshot({
|
|
185
|
+
options,
|
|
186
|
+
auth,
|
|
187
|
+
buildId,
|
|
188
|
+
afterActivitySeq = 0,
|
|
189
|
+
expectedScopeKey = "",
|
|
190
|
+
loadPage = loadBuildForumPage,
|
|
191
|
+
maxPages = MAX_FORUM_SNAPSHOT_PAGES,
|
|
192
|
+
}) {
|
|
193
|
+
const normalizedBuildId = normalizePositiveForumId(buildId, "buildId");
|
|
194
|
+
const startingActivitySeq = normalizeForumSequence(
|
|
195
|
+
afterActivitySeq,
|
|
196
|
+
"afterActivitySeq",
|
|
197
|
+
);
|
|
198
|
+
let pageCursor = startingActivitySeq;
|
|
199
|
+
let snapshotActivitySeq = 0;
|
|
200
|
+
let scopeKey = expectedScopeKey;
|
|
201
|
+
let firstPage = null;
|
|
202
|
+
const events = [];
|
|
203
|
+
|
|
204
|
+
for (let pageNumber = 1; pageNumber <= maxPages; pageNumber += 1) {
|
|
205
|
+
const page = await loadPage({
|
|
206
|
+
options,
|
|
207
|
+
auth,
|
|
208
|
+
buildId: normalizedBuildId,
|
|
209
|
+
afterActivitySeq: pageCursor,
|
|
210
|
+
snapshotActivitySeq,
|
|
211
|
+
limit: options.limit,
|
|
212
|
+
});
|
|
213
|
+
const validated = validateForumPage({
|
|
214
|
+
page,
|
|
215
|
+
buildId: normalizedBuildId,
|
|
216
|
+
pageCursor,
|
|
217
|
+
snapshotActivitySeq,
|
|
218
|
+
expectedScopeKey: scopeKey,
|
|
219
|
+
});
|
|
220
|
+
if (!firstPage) firstPage = page;
|
|
221
|
+
if (!scopeKey) scopeKey = validated.scopeKey;
|
|
222
|
+
snapshotActivitySeq = validated.pageSnapshotActivitySeq;
|
|
223
|
+
events.push(...validated.events);
|
|
224
|
+
pageCursor = validated.nextActivitySeq;
|
|
225
|
+
if (!page.pagination.hasMore) {
|
|
226
|
+
return {
|
|
227
|
+
project: firstPage.project,
|
|
228
|
+
requestedBuildId: normalizedBuildId,
|
|
229
|
+
scope: firstPage.scope,
|
|
230
|
+
events,
|
|
231
|
+
pagination: {
|
|
232
|
+
fromActivitySeq: startingActivitySeq,
|
|
233
|
+
snapshotActivitySeq,
|
|
234
|
+
nextActivitySeq: pageCursor,
|
|
235
|
+
hasMore: false,
|
|
236
|
+
},
|
|
237
|
+
scopeKey,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
throw forumProtocolError("snapshot exceeded the safe pagination bound");
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export function isRetryableForumListenerError(error) {
|
|
246
|
+
if (error?.retryable === false) return false;
|
|
247
|
+
const status = Number(error?.status || 0);
|
|
248
|
+
if (!status) return true;
|
|
249
|
+
return status === 408 || status === 425 || status === 429 || status >= 500;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function formatForumTimestamp(value) {
|
|
253
|
+
const timestamp = Number(value || 0);
|
|
254
|
+
if (!Number.isFinite(timestamp) || timestamp <= 0) return "unknown time";
|
|
255
|
+
return new Date(timestamp * 1000).toISOString();
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function formatForumLocation(event) {
|
|
259
|
+
if (!event?.branch) return "Main";
|
|
260
|
+
const branchNumber = Number(event.branch.number || 0);
|
|
261
|
+
return branchNumber > 0
|
|
262
|
+
? `Branch #${branchNumber}`
|
|
263
|
+
: `Branch build #${event.branch.id}`;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function sanitizeForumTerminalText(value) {
|
|
267
|
+
// Forum text is user-authored. Preserve canonical content in JSON output,
|
|
268
|
+
// but prevent control, escape, carriage-return, and bidi override bytes from
|
|
269
|
+
// driving or visually rewriting a human reader's terminal.
|
|
270
|
+
return String(value || "").replace(
|
|
271
|
+
/[\u0000-\u0008\u000b-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/g,
|
|
272
|
+
"",
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function printIndented(value) {
|
|
277
|
+
for (const line of sanitizeForumTerminalText(value).split("\n")) {
|
|
278
|
+
console.log(` ${line}`);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export function printBuildForumSnapshot(snapshot, { json, kind }) {
|
|
283
|
+
const output = {
|
|
284
|
+
type: kind,
|
|
285
|
+
project: snapshot.project,
|
|
286
|
+
requestedBuildId: snapshot.requestedBuildId,
|
|
287
|
+
scope: snapshot.scope,
|
|
288
|
+
events: snapshot.events,
|
|
289
|
+
cursor: {
|
|
290
|
+
fromActivitySeq: snapshot.pagination.fromActivitySeq,
|
|
291
|
+
throughActivitySeq: snapshot.pagination.nextActivitySeq,
|
|
292
|
+
},
|
|
293
|
+
};
|
|
294
|
+
if (json) {
|
|
295
|
+
console.log(JSON.stringify(output));
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const projectTitle =
|
|
300
|
+
sanitizeForumTerminalText(snapshot.project?.title).trim() ||
|
|
301
|
+
`Build #${snapshot.project?.id || snapshot.requestedBuildId}`;
|
|
302
|
+
console.log(`${projectTitle} — Team Forum`);
|
|
303
|
+
if (snapshot.events.length === 0) {
|
|
304
|
+
console.log("No new visible Forum posts or replies.");
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
for (const event of snapshot.events) {
|
|
308
|
+
const author =
|
|
309
|
+
sanitizeForumTerminalText(event?.author?.username).trim() ||
|
|
310
|
+
(event?.author?.role === "lumine" ? "Lumine" : "unknown user");
|
|
311
|
+
const action = event.type === "reply" ? "replied in" : "opened";
|
|
312
|
+
console.log(
|
|
313
|
+
`${formatForumTimestamp(event.createdAt)} ${formatForumLocation(event)} ${author} ${action} #${event.threadId} “${sanitizeForumTerminalText(event.threadTitle)}”`,
|
|
314
|
+
);
|
|
315
|
+
if (event.replyTo) {
|
|
316
|
+
const target =
|
|
317
|
+
sanitizeForumTerminalText(event.replyTo.username).trim() ||
|
|
318
|
+
`reply #${event.replyTo.replyId}`;
|
|
319
|
+
console.log(` ↳ replying to ${target}`);
|
|
320
|
+
}
|
|
321
|
+
printIndented(event.body);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export async function runBuildForumCommand({ options, auth, buildId }) {
|
|
326
|
+
const listen = options.forumAction === "listen";
|
|
327
|
+
let cursor = options.forumCursor;
|
|
328
|
+
let scopeKey = "";
|
|
329
|
+
let firstSnapshot = true;
|
|
330
|
+
let consecutiveFailures = 0;
|
|
331
|
+
|
|
332
|
+
while (true) {
|
|
333
|
+
let snapshot;
|
|
334
|
+
try {
|
|
335
|
+
snapshot = await readCompleteBuildForumSnapshot({
|
|
336
|
+
options,
|
|
337
|
+
auth,
|
|
338
|
+
buildId,
|
|
339
|
+
afterActivitySeq: cursor,
|
|
340
|
+
expectedScopeKey: scopeKey,
|
|
341
|
+
});
|
|
342
|
+
} catch (error) {
|
|
343
|
+
if (!listen || !isRetryableForumListenerError(error)) throw error;
|
|
344
|
+
consecutiveFailures += 1;
|
|
345
|
+
const retryDelayMs = Math.min(
|
|
346
|
+
options.forumPollMs * 2 ** Math.min(consecutiveFailures - 1, 4),
|
|
347
|
+
30_000,
|
|
348
|
+
);
|
|
349
|
+
console.error(
|
|
350
|
+
`Forum listener temporarily lost contact (${error?.message || error}). Retrying from confirmed cursor ${cursor} in ${retryDelayMs}ms.`,
|
|
351
|
+
);
|
|
352
|
+
await sleep(retryDelayMs);
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (firstSnapshot || snapshot.events.length > 0) {
|
|
357
|
+
printBuildForumSnapshot(snapshot, {
|
|
358
|
+
json: options.json,
|
|
359
|
+
kind: firstSnapshot ? "forum.snapshot" : "forum.update",
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
cursor = snapshot.pagination.nextActivitySeq;
|
|
363
|
+
scopeKey = snapshot.scopeKey;
|
|
364
|
+
if (!listen) return;
|
|
365
|
+
|
|
366
|
+
if (firstSnapshot) {
|
|
367
|
+
console.error(
|
|
368
|
+
`Listening for canonical Forum updates from cursor ${cursor}. Press Ctrl-C to stop.`,
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
firstSnapshot = false;
|
|
372
|
+
consecutiveFailures = 0;
|
|
373
|
+
await sleep(options.forumPollMs);
|
|
374
|
+
}
|
|
375
|
+
}
|
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.
|
|
4
|
-
Updated: 2026-08-
|
|
5
|
-
Generated: 2026-08-
|
|
3
|
+
Version: 1.33.0
|
|
4
|
+
Updated: 2026-08-15
|
|
5
|
+
Generated: 2026-08-15T03:00:24.685Z
|
|
6
6
|
|
|
7
7
|
## Notes
|
|
8
8
|
- This SDK is injected into Build iframes via the Build preview/runtime.
|
|
@@ -20,6 +20,7 @@ Generated: 2026-08-13T00:13:49.827Z
|
|
|
20
20
|
- Use Twinkle.grammarbles for public Grammarbles question-bank trainer apps and optional signed-in viewer attempt-history filtering.
|
|
21
21
|
- Use Twinkle.chess for chess engine play and analysis; app code still owns chess rules, legal moves, board state, and UI.
|
|
22
22
|
- Use Twinkle.world for realtime multiplayer rooms, avatar presence, movement, emotes, and lightweight actions; world sessions are disposable and durable MMO state belongs in sharedDb/privateDb.
|
|
23
|
+
- Every Twinkle-owned profilePicUrl field returned by the SDK is an absolute HTTPS URL ready for img src, or null. Fields inside app-owned JSON such as sharedDb entry data are not rewritten.
|
|
23
24
|
- Use Twinkle.characters.chat for real Zero/Ciel NPC dialogue with shared room context and AI Energy-aware thinking modes.
|
|
24
25
|
- Twinkle.ai.chat history entries must use { role, content }; map local message.text fields to content before passing history.
|
|
25
26
|
- Live web search is enabled by default for Twinkle.ai.chat and for Medium/High Twinkle.ai.generateObject and Twinkle.characters.chat requests. App authors can pass webSearch: false to disable it for their app. Search uses the provider's live web-search tool and is included in AI Energy usage; structured and character Lite Mode remains tool-free.
|
|
@@ -75,6 +76,27 @@ files:read, user:read, users:read, dailyReflections:read, content:read, content:
|
|
|
75
76
|
- Returns: Canonical shareable deep-link URL string, or null when app info is unavailable
|
|
76
77
|
- Builds a canonical shareable deep link into this app, e.g. https://www.twin-kle.com/app/884/432-the-great-gatsby.
|
|
77
78
|
- Example: await Twinkle.app.getShareUrl('432-the-great-gatsby');
|
|
79
|
+
- history.getState() | scopes: none
|
|
80
|
+
- Returns: The current app-owned history state object, or null
|
|
81
|
+
- Read the current Build app view state stored through Twinkle.app.history.
|
|
82
|
+
- History state is local to this iframe session and never changes the parent Twinkle URL.
|
|
83
|
+
- Use this for archive/detail/page state inside one Build document; use navigate() to load another project file.
|
|
84
|
+
- history.push(state) | scopes: none
|
|
85
|
+
- Returns: A JSON-cloned copy of the stored state
|
|
86
|
+
- Add a confirmed in-app view transition to browser history so Back stays inside the Build app.
|
|
87
|
+
- State must be a JSON-serializable object no larger than 16 KB.
|
|
88
|
+
- Push only after the requested view has loaded successfully; do not synthesize server-owned state.
|
|
89
|
+
- Example: Twinkle.app.history.push({ view: 'edition', dayIndex: 2080, page: 'scores' });
|
|
90
|
+
- history.replace(state) | scopes: none
|
|
91
|
+
- Returns: A JSON-cloned copy of the stored state
|
|
92
|
+
- Replace the current in-app history entry without adding a Back step.
|
|
93
|
+
- Use this to establish the initial confirmed view or reconcile a loading-only change.
|
|
94
|
+
- history.subscribe(listener, { immediate } = {}) | scopes: none
|
|
95
|
+
- Returns: unsubscribe function
|
|
96
|
+
- Restore app views when the viewer moves through browser Back or Forward history.
|
|
97
|
+
- The listener receives a cloned app state object, or null for an entry not owned by this app.
|
|
98
|
+
- The listener is called immediately by default; pass { immediate: false } to wait for Back or Forward.
|
|
99
|
+
- Example: const off = Twinkle.app.history.subscribe((state) => restoreView(state), { immediate: false });
|
|
78
100
|
- async navigate(target) | scopes: none
|
|
79
101
|
- Returns: { success, src }
|
|
80
102
|
- Navigate to another Build preview route through the parent bridge without dropping Twinkle SDK access.
|
|
@@ -672,6 +694,7 @@ const result = await Twinkle.characters.chat({ character: 'zero', thinkingMode:
|
|
|
672
694
|
- Always available in the build iframe.
|
|
673
695
|
- World state is ephemeral and heartbeat/TTL based. Use sharedDb/privateDb for durable inventory, XP, quests, ownership, and saved progress — but write those LOW-frequency only (on a user action or an occasional snapshot, never per frame/tick); per-frame/live state stays in world presence or client memory. The server rate-limits sharedDb/privateDb writes and returns 429.
|
|
674
696
|
- Events are room-scoped and include serverTime, seq, eventId, schemaVersion, sessionId, player, and room metadata.
|
|
697
|
+
- Signed-in player identity comes from the canonical Twinkle user record; player.profilePicUrl is only used for guests and is returned only when it is a valid absolute HTTPS URL.
|
|
675
698
|
- Subscribe to session.ended and catch updatePresence/send errors. Stop using stale handles and reconnect only when Twinkle.world.isSessionEndedError(error) is true; for other Twinkle.world.isRecoverableSessionError(error) cases, drop the transient presence/action and keep the handle.
|
|
676
699
|
- Use updatePresence for live avatar snapshots and send for lightweight actions such as emotes, interactions, and chat bubbles.
|
|
677
700
|
- Throttle movement updates in app code, usually 5-15 updates per second. Do not call updatePresence from every animation frame.
|
package/sdk/LUMINE_ADMIN.md
CHANGED
|
@@ -95,6 +95,12 @@ The CLI enforces none of this — it is the standing instruction for the operato
|
|
|
95
95
|
or agent making the judgments, and it applies to every verb below: recommends,
|
|
96
96
|
rewards, effort levels, Featured, skips, comments, and replies.
|
|
97
97
|
|
|
98
|
+
Public text authored as Ciel must be English. This is an operator and generation
|
|
99
|
+
instruction, not a script or keyword test: writing systems do not identify a
|
|
100
|
+
language reliably, and the API must not pretend otherwise. This is a
|
|
101
|
+
presentation rule, not an invitation to correct or lecture a member who writes
|
|
102
|
+
in another language; reply naturally in concise English.
|
|
103
|
+
|
|
98
104
|
**Twinkle is not Reddit.** Do not rank a run's attention by popularity,
|
|
99
105
|
recommendation count, or polish. Most users here are young children, and the
|
|
100
106
|
posts that most need Zero or Ciel are the ones nobody else answered.
|
|
@@ -168,6 +174,13 @@ a human owner can decide, and a finding nobody reports is a finding that did not
|
|
|
168
174
|
happen. **Every run ends with an escalation list**, and it belongs in the run's
|
|
169
175
|
final report whether or not anyone asks for it.
|
|
170
176
|
|
|
177
|
+
Keep that list narrow enough to be useful. Escalate concrete child-safety,
|
|
178
|
+
exploitation, privacy, targeted harassment, or platform/system-abuse risk — not
|
|
179
|
+
ordinary children experimenting, arguing, making rumors, proposing informal
|
|
180
|
+
in-site loans or contests, asking where media can be found, or making an
|
|
181
|
+
unverified ownership claim. Those may merit a normal age-appropriate response,
|
|
182
|
+
but they are not escalations without credible harmful conduct or a real victim.
|
|
183
|
+
|
|
171
184
|
Escalate, with the canonical `https://www.twin-kle.com/subjects/<id>` or
|
|
172
185
|
`/comments/<id>` URL, a one-line summary, and why it needs him:
|
|
173
186
|
|
|
@@ -178,11 +191,10 @@ Escalate, with the canonical `https://www.twin-kle.com/subjects/<id>` or
|
|
|
178
191
|
happened. These outrank every other category.
|
|
179
192
|
- **Account integrity** — someone posting from another person's account,
|
|
180
193
|
impersonation, shared logins, or a user operating a set of alternate accounts.
|
|
181
|
-
- **Economy
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
kids are recommending is spreading, and that is the urgent part.
|
|
194
|
+
- **Economy exploitation** — coordinated coin or XP farming across alternate
|
|
195
|
+
accounts, coercive or deceptive arrangements, or a repeatable abuse of the
|
|
196
|
+
platform economy with concrete evidence. A child offering a voluntary loan,
|
|
197
|
+
repayment, prize, or contest is not enough by itself.
|
|
186
198
|
- **AI-cost exploits** — patterns that convert free AI allowances into farmable
|
|
187
199
|
value: clusters of young accounts with heavy AI/battery usage, one person
|
|
188
200
|
operating many accounts that feed a single build through team branches,
|
|
@@ -446,6 +458,11 @@ lumine admin daily-run start --identity auto --comment-mode off --json
|
|
|
446
458
|
lumine admin daily-run start --identity ciel --comment-mode draft \
|
|
447
459
|
--run-key daily:2026-08-06:review --json
|
|
448
460
|
lumine admin daily-run status --json
|
|
461
|
+
lumine admin daily-run escalation add --target subject:123 \
|
|
462
|
+
--note "Public contact details need owner review" --severity urgent --json
|
|
463
|
+
lumine admin daily-run escalation add --target chatMessage:3768159 \
|
|
464
|
+
--note "Concrete safety issue in a bot-authored chat message" --json
|
|
465
|
+
lumine admin daily-run report --json
|
|
449
466
|
lumine admin daily-run complete --json
|
|
450
467
|
lumine admin daily-run fail --reason "operator stopped" --json
|
|
451
468
|
```
|
|
@@ -465,6 +482,14 @@ type DailyRunComplete = Success<{
|
|
|
465
482
|
type DailyRunFail = DailyRunComplete;
|
|
466
483
|
```
|
|
467
484
|
|
|
485
|
+
Record only qualifying escalations as they are confirmed. `daily-run report`
|
|
486
|
+
then composes the active run's canonical audit events, successful mutations,
|
|
487
|
+
completed queue scans, recorded escalations, and the most useful brief deltas
|
|
488
|
+
into one result. Generate it before `complete`, because run-scoped reads require
|
|
489
|
+
the current active run. Queue coverage is written automatically only after an
|
|
490
|
+
`--all` traversal reaches canonical exhaustion; an interrupted scan remains in
|
|
491
|
+
its local checkpoint and cannot be misreported as complete.
|
|
492
|
+
|
|
468
493
|
`lastRun` makes a lost-response retry of `complete` or `fail` possible after
|
|
469
494
|
the active pointer has been cleared. Other run-scoped commands accept only the
|
|
470
495
|
current unexpired `active` run. Completion first finalizes any mutation whose
|
|
@@ -498,13 +523,17 @@ JSON error includes `details.retryIdempotencyKey` for a safe exact retry.
|
|
|
498
523
|
|
|
499
524
|
```bash
|
|
500
525
|
lumine admin recommendations list --kind recommend \
|
|
501
|
-
--content-types comment,dailyReflection --
|
|
526
|
+
--content-types comment,dailyReflection --all --json
|
|
527
|
+
lumine admin recommendations list --after 2026-08-14T00:00:00Z \
|
|
528
|
+
--all --checkpoint recommendations.json --json
|
|
529
|
+
lumine admin recommendations list --include-legacy --all --json
|
|
502
530
|
lumine admin recommendations list --unviewed --json
|
|
503
531
|
lumine admin subjects candidates --after 2026-08-01T00:00:00Z \
|
|
504
|
-
--
|
|
532
|
+
--all --checkpoint subjects.json --json
|
|
505
533
|
lumine admin subjects candidates --effort unassigned --json
|
|
506
534
|
lumine admin subjects candidates --unviewed --json
|
|
507
|
-
lumine admin builds candidates --
|
|
535
|
+
lumine admin builds candidates --all --limit 50 --json
|
|
536
|
+
lumine admin builds review build:884 --output-dir ./build-review --json
|
|
508
537
|
```
|
|
509
538
|
|
|
510
539
|
Schemas:
|
|
@@ -575,12 +604,28 @@ type BuildCandidates = Success<{
|
|
|
575
604
|
}>;
|
|
576
605
|
```
|
|
577
606
|
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
607
|
+
Subject cursors freeze a primary-key high-water mark and traverse descending
|
|
608
|
+
IDs. Bounded recommendation cursors freeze both the feed-ID high-water mark and
|
|
609
|
+
the server timestamp, then traverse the indexed `(timeStamp, id)` order; this
|
|
610
|
+
also catches a Daily Reflection whose old feed row moved forward when it was
|
|
611
|
+
reshared. Explicit legacy scans retain the descending primary-key walk. A page
|
|
612
|
+
can be empty while `hasMore` remains true; continue until `exhausted`. `--all`
|
|
613
|
+
does that automatically and writes a private checkpoint after every
|
|
614
|
+
server-confirmed page; `--resume` continues only when the checkpoint belongs to
|
|
615
|
+
the same API, run, and exact request. The final result can be copied to
|
|
616
|
+
`--output`, while `--checkpoint` is resumable operational state. Subject
|
|
617
|
+
`--after` is inclusive, and every opaque cursor is bound to its original
|
|
618
|
+
filters.
|
|
619
|
+
|
|
620
|
+
Recommendations default to `--since-run`: the server uses the previous
|
|
621
|
+
completed run's start time (or the same bounded seven-day fallback used by the
|
|
622
|
+
brief on a first run). That deliberate start-to-start overlap gives the queue
|
|
623
|
+
at-least-once coverage when content arrives after the prior snapshot but before
|
|
624
|
+
that run completes. `--after` supplies an explicit inclusive timestamp.
|
|
625
|
+
All-history traversal is deliberately available only through
|
|
626
|
+
`--include-legacy`. The CLI requires the API to echo the canonical `after`
|
|
627
|
+
boundary for bounded modes, so deploying a new CLI against an older API cannot
|
|
628
|
+
silently fall back to a million-row historical scan.
|
|
584
629
|
|
|
585
630
|
`builds candidates` is a management-agent discovery view over the canonical
|
|
586
631
|
public Build browser, ordered by the current published release. It is
|
|
@@ -592,6 +637,15 @@ genuinely try the published runtime, or pull and read an open-source project,
|
|
|
592
637
|
before making that judgment. Direct API/persona automation is never a review
|
|
593
638
|
substitute.
|
|
594
639
|
|
|
640
|
+
`builds review` is the managed runtime path: it fetches the current published
|
|
641
|
+
artifact identity, launches the app in an isolated temporary Chromium profile,
|
|
642
|
+
captures a screenshot and bounded console evidence, then fetches the identity
|
|
643
|
+
again. It writes `review.json` in a unique per-review subdirectory only when
|
|
644
|
+
the browser completed, the screenshot exists, and the artifact did not change
|
|
645
|
+
mid-review. Attach the returned `receiptPath` with
|
|
646
|
+
`comment draft ... --review-receipt review.json`; this binds the draft to the
|
|
647
|
+
exact reviewed artifact without copying a version number by hand.
|
|
648
|
+
|
|
595
649
|
During every management run, scan recent Build candidates back through the
|
|
596
650
|
run's review window alongside Subjects and the recommendation queue. An app
|
|
597
651
|
that is thin, broken, private, unchanged since a prior substantive bot
|
|
@@ -919,6 +973,10 @@ If recommendation succeeds but reward fails, the command exits nonzero with
|
|
|
919
973
|
```bash
|
|
920
974
|
lumine admin post skip dailyReflection:99 --json
|
|
921
975
|
lumine admin post skip comment:456 --reason "one-line answer, nothing to add" --json
|
|
976
|
+
lumine admin post skip-batch --target-file skip-targets.json \
|
|
977
|
+
--checkpoint skip-progress.json --json
|
|
978
|
+
lumine admin post skip-batch --target-file skip-targets.json \
|
|
979
|
+
--checkpoint skip-progress.json --resume --json
|
|
922
980
|
```
|
|
923
981
|
|
|
924
982
|
A skip records that the management rotation has judged a recommend-queue item
|
|
@@ -937,6 +995,13 @@ metadata — it is the agent's memory of the judgment, not public content.
|
|
|
937
995
|
The skip requires the `recommendation:write` scope and is audited like every
|
|
938
996
|
other mutation.
|
|
939
997
|
|
|
998
|
+
`skip-batch` accepts either a JSON array (strings or `{ "target", "reason" }`
|
|
999
|
+
objects), `{ "targets": [...] }`, or one target per text line. It deduplicates
|
|
1000
|
+
targets, submits them sequentially through the same canonical audited endpoint,
|
|
1001
|
+
and checkpoints only after each response is confirmed. `--resume` verifies the
|
|
1002
|
+
exact target-set fingerprint and run ID before continuing; it never guesses
|
|
1003
|
+
which writes succeeded.
|
|
1004
|
+
|
|
940
1005
|
```ts
|
|
941
1006
|
type PostSkip = Success<{
|
|
942
1007
|
skip: {
|
|
@@ -953,9 +1018,10 @@ type PostSkip = Success<{
|
|
|
953
1018
|
|
|
954
1019
|
```bash
|
|
955
1020
|
lumine admin news --json
|
|
956
|
-
lumine admin news claim --json
|
|
957
|
-
lumine admin news
|
|
958
|
-
|
|
1021
|
+
lumine admin news claim --output claim.json --scaffold editorial.json --json
|
|
1022
|
+
lumine admin news validate --claim claim.json --file editorial.json --json
|
|
1023
|
+
lumine admin news submit --claim claim.json --file editorial.json \
|
|
1024
|
+
--model "Ciel" --json
|
|
959
1025
|
lumine admin news print --json
|
|
960
1026
|
```
|
|
961
1027
|
|
|
@@ -968,9 +1034,18 @@ the run, and if `printedToday` is false with no edition `pending` or
|
|
|
968
1034
|
**Preferred: write the editorial yourself.** `news claim` reserves today's
|
|
969
1035
|
edition under the server's generation lease and returns the exact canonical
|
|
970
1036
|
event digest the server would otherwise send to its own model, so no provider
|
|
971
|
-
API credits are spent.
|
|
972
|
-
|
|
973
|
-
|
|
1037
|
+
API credits are spent. With `--output` and `--scaffold`, the CLI writes that
|
|
1038
|
+
lease/digest to a private claim file and creates an editable editorial shell.
|
|
1039
|
+
`news validate` runs locally, before authentication or a network request, and
|
|
1040
|
+
checks the complete citation graph plus byte-exact quote boundaries. Submit the
|
|
1041
|
+
validated pair with `news submit --claim`; the CLI reads the edition and lease
|
|
1042
|
+
from the claim file and validates again immediately before the request. The
|
|
1043
|
+
explicit `--edition-id` / `--lease-token` form remains available for backwards
|
|
1044
|
+
compatibility.
|
|
1045
|
+
|
|
1046
|
+
Write a `GeneratedEditorial` JSON and send it back within the ten-minute lease.
|
|
1047
|
+
The server still treats the editorial as untrusted regardless of author: every
|
|
1048
|
+
story must cite an exact `eventKey`
|
|
974
1049
|
from the digest, front-page `sourceQuote`s must be verbatim contiguous
|
|
975
1050
|
passages of the cited event's summary (invalid quotes are replaced with
|
|
976
1051
|
canonical text), section and page layout are server-enforced, announcements
|