@stage5/lumine 0.2.40 → 0.2.42
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 +434 -0
- package/lib/admin.js +644 -45
- 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 +86 -9
- package/lib/constants.js +2 -0
- package/package.json +1 -1
- package/sdk/BUILD_SDK_INDEX.md +24 -3
- package/sdk/LUMINE_ADMIN.md +271 -32
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import {
|
|
3
|
+
existsSync,
|
|
4
|
+
mkdtempSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
rmSync,
|
|
8
|
+
statSync,
|
|
9
|
+
writeFileSync,
|
|
10
|
+
} from "node:fs";
|
|
11
|
+
import os from "node:os";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
|
|
14
|
+
import { requestJson } from "./http.js";
|
|
15
|
+
import { writeAdminJsonFile } from "./admin-news.js";
|
|
16
|
+
|
|
17
|
+
const DEFAULT_REVIEW_WAIT_MS = 10_000;
|
|
18
|
+
const MAX_REVIEW_WAIT_MS = 45_000;
|
|
19
|
+
const MAX_CAPTURED_LOG_LINES = 500;
|
|
20
|
+
const CDP_COMMAND_TIMEOUT_MS = 10_000;
|
|
21
|
+
|
|
22
|
+
function validationError(message, code = "CLI_ADMIN_CLI_VALIDATION") {
|
|
23
|
+
const error = new Error(message);
|
|
24
|
+
error.code = code;
|
|
25
|
+
return error;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function parseBuildReviewReceipt(filePath) {
|
|
29
|
+
let receipt;
|
|
30
|
+
try {
|
|
31
|
+
receipt = JSON.parse(readFileSync(String(filePath || ""), "utf8"));
|
|
32
|
+
} catch {
|
|
33
|
+
throw validationError(`Could not read Build review receipt ${filePath}.`);
|
|
34
|
+
}
|
|
35
|
+
const buildId = Number(receipt?.buildId || 0);
|
|
36
|
+
const publishedArtifactVersionId = Number(
|
|
37
|
+
receipt?.publishedArtifactVersionId || 0,
|
|
38
|
+
);
|
|
39
|
+
const versionAfterReview = Number(receipt?.versionAfterReview || 0);
|
|
40
|
+
const screenshotPath = String(receipt?.screenshot?.path || "");
|
|
41
|
+
const screenshotBytes =
|
|
42
|
+
screenshotPath && existsSync(screenshotPath)
|
|
43
|
+
? Number(statSync(screenshotPath).size || 0)
|
|
44
|
+
: 0;
|
|
45
|
+
if (
|
|
46
|
+
receipt?.schemaVersion !== 2 ||
|
|
47
|
+
receipt?.reviewMethod !== "runtime" ||
|
|
48
|
+
receipt?.status !== "confirmed" ||
|
|
49
|
+
!Number.isSafeInteger(buildId) ||
|
|
50
|
+
buildId <= 0 ||
|
|
51
|
+
!Number.isSafeInteger(publishedArtifactVersionId) ||
|
|
52
|
+
publishedArtifactVersionId <= 0 ||
|
|
53
|
+
versionAfterReview !== publishedArtifactVersionId ||
|
|
54
|
+
receipt?.versionStable !== true ||
|
|
55
|
+
receipt?.browser?.runtimeReadiness?.ready !== true ||
|
|
56
|
+
screenshotBytes <= 0 ||
|
|
57
|
+
screenshotBytes !== Number(receipt?.screenshot?.bytes || 0)
|
|
58
|
+
) {
|
|
59
|
+
throw validationError(
|
|
60
|
+
"The Build review receipt is not a confirmed managed-runtime review.",
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
return receipt;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function findChromeExecutable(explicitPath = "") {
|
|
67
|
+
const candidates = [
|
|
68
|
+
explicitPath,
|
|
69
|
+
process.env.CHROME_PATH,
|
|
70
|
+
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
71
|
+
"/Applications/Chromium.app/Contents/MacOS/Chromium",
|
|
72
|
+
"/usr/bin/google-chrome",
|
|
73
|
+
"/usr/bin/google-chrome-stable",
|
|
74
|
+
"/usr/bin/chromium",
|
|
75
|
+
"/usr/bin/chromium-browser",
|
|
76
|
+
].filter(Boolean);
|
|
77
|
+
const match = candidates.find((candidate) => existsSync(candidate));
|
|
78
|
+
if (!match) {
|
|
79
|
+
throw validationError(
|
|
80
|
+
"A Chrome/Chromium executable was not found. Pass --browser-path <path>.",
|
|
81
|
+
"CLI_ADMIN_BUILD_REVIEW_BROWSER_MISSING",
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
return match;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function boundedWaitMs(value) {
|
|
88
|
+
if (value === undefined || value === null || value === "") {
|
|
89
|
+
return DEFAULT_REVIEW_WAIT_MS;
|
|
90
|
+
}
|
|
91
|
+
const parsed = Number(value);
|
|
92
|
+
if (!Number.isInteger(parsed) || parsed < 1000 || parsed > MAX_REVIEW_WAIT_MS) {
|
|
93
|
+
throw validationError(
|
|
94
|
+
`--wait-ms must be an integer between 1000 and ${MAX_REVIEW_WAIT_MS}.`,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
return parsed;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function loadPublishedVersion({ apiUrl, buildId, timeoutMs }) {
|
|
101
|
+
const result = await requestJson({
|
|
102
|
+
url: `${String(apiUrl).replace(/\/$/, "")}/build/${buildId}/published-version`,
|
|
103
|
+
timeoutMs,
|
|
104
|
+
});
|
|
105
|
+
const versionId = Number(result?.publishedArtifactVersionId || 0);
|
|
106
|
+
if (!Number.isSafeInteger(versionId) || versionId <= 0) {
|
|
107
|
+
throw validationError(
|
|
108
|
+
`Build #${buildId} has no canonical published artifact to review.`,
|
|
109
|
+
"CLI_ADMIN_BUILD_REVIEW_NOT_PUBLISHED",
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
return versionId;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function delay(milliseconds) {
|
|
116
|
+
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function createCdpPipe(child) {
|
|
120
|
+
const commandPipe = child.stdio[3];
|
|
121
|
+
const eventPipe = child.stdio[4];
|
|
122
|
+
if (!commandPipe || !eventPipe) {
|
|
123
|
+
throw validationError(
|
|
124
|
+
"Chrome did not expose its managed review channel.",
|
|
125
|
+
"CLI_ADMIN_BUILD_REVIEW_BROWSER_CHANNEL_FAILED",
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
let nextId = 1;
|
|
129
|
+
let buffer = Buffer.alloc(0);
|
|
130
|
+
const pending = new Map();
|
|
131
|
+
const listeners = new Set();
|
|
132
|
+
eventPipe.on("data", (chunk) => {
|
|
133
|
+
buffer = Buffer.concat([buffer, chunk]);
|
|
134
|
+
for (let delimiter = buffer.indexOf(0); delimiter >= 0; delimiter = buffer.indexOf(0)) {
|
|
135
|
+
const payload = buffer.subarray(0, delimiter).toString("utf8");
|
|
136
|
+
buffer = buffer.subarray(delimiter + 1);
|
|
137
|
+
if (!payload) continue;
|
|
138
|
+
let message;
|
|
139
|
+
try {
|
|
140
|
+
message = JSON.parse(payload);
|
|
141
|
+
} catch {
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (message.id && pending.has(message.id)) {
|
|
145
|
+
const entry = pending.get(message.id);
|
|
146
|
+
pending.delete(message.id);
|
|
147
|
+
clearTimeout(entry.timer);
|
|
148
|
+
if (message.error) {
|
|
149
|
+
entry.reject(new Error(message.error.message || "Chrome command failed."));
|
|
150
|
+
} else {
|
|
151
|
+
entry.resolve(message.result || {});
|
|
152
|
+
}
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
for (const listener of [...listeners]) listener(message);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
const rejectPending = () => {
|
|
159
|
+
for (const entry of pending.values()) {
|
|
160
|
+
clearTimeout(entry.timer);
|
|
161
|
+
entry.reject(new Error("Chrome closed its managed review channel."));
|
|
162
|
+
}
|
|
163
|
+
pending.clear();
|
|
164
|
+
};
|
|
165
|
+
child.once("close", rejectPending);
|
|
166
|
+
child.once("error", rejectPending);
|
|
167
|
+
return {
|
|
168
|
+
onEvent(listener) {
|
|
169
|
+
listeners.add(listener);
|
|
170
|
+
return () => listeners.delete(listener);
|
|
171
|
+
},
|
|
172
|
+
send(method, params = {}, sessionId = "") {
|
|
173
|
+
const id = nextId++;
|
|
174
|
+
return new Promise((resolve, reject) => {
|
|
175
|
+
const timer = setTimeout(() => {
|
|
176
|
+
pending.delete(id);
|
|
177
|
+
reject(new Error(`Chrome timed out while running ${method}.`));
|
|
178
|
+
}, CDP_COMMAND_TIMEOUT_MS);
|
|
179
|
+
pending.set(id, { resolve, reject, timer });
|
|
180
|
+
const message = { id, method, params };
|
|
181
|
+
if (sessionId) message.sessionId = sessionId;
|
|
182
|
+
commandPipe.write(`${JSON.stringify(message)}\0`);
|
|
183
|
+
});
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async function waitForPageTarget(cdp, expectedUrl) {
|
|
189
|
+
const deadline = Date.now() + CDP_COMMAND_TIMEOUT_MS;
|
|
190
|
+
while (Date.now() < deadline) {
|
|
191
|
+
const result = await cdp.send("Target.getTargets");
|
|
192
|
+
const targets = Array.isArray(result.targetInfos) ? result.targetInfos : [];
|
|
193
|
+
const exact = targets.find(
|
|
194
|
+
(target) => target.type === "page" && target.url === expectedUrl,
|
|
195
|
+
);
|
|
196
|
+
const fallback = targets.find(
|
|
197
|
+
(target) =>
|
|
198
|
+
target.type === "page" &&
|
|
199
|
+
/^https?:/i.test(String(target.url || "")) &&
|
|
200
|
+
!String(target.url).startsWith("chrome://"),
|
|
201
|
+
);
|
|
202
|
+
if (exact || fallback) return exact || fallback;
|
|
203
|
+
await delay(100);
|
|
204
|
+
}
|
|
205
|
+
throw new Error("Chrome did not open the requested Build page.");
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const RUNTIME_READINESS_EXPRESSION = `(() => {
|
|
209
|
+
const visible = (element) => {
|
|
210
|
+
if (!element) return false;
|
|
211
|
+
const style = getComputedStyle(element);
|
|
212
|
+
const rect = element.getBoundingClientRect();
|
|
213
|
+
return style.display !== 'none' && style.visibility !== 'hidden' &&
|
|
214
|
+
Number(style.opacity || 1) > 0 && rect.width > 1 && rect.height > 1;
|
|
215
|
+
};
|
|
216
|
+
const preview = document.querySelector('iframe[title="App preview"]');
|
|
217
|
+
const progress = document.querySelector('[role="progressbar"]');
|
|
218
|
+
const runtimeError = document.querySelector('[data-agent-status="preview-error"]');
|
|
219
|
+
return {
|
|
220
|
+
documentReady: document.readyState === 'complete',
|
|
221
|
+
previewPresent: Boolean(preview),
|
|
222
|
+
previewVisible: visible(preview),
|
|
223
|
+
progressVisible: visible(progress),
|
|
224
|
+
runtimeErrorVisible: visible(runtimeError),
|
|
225
|
+
title: document.title || ''
|
|
226
|
+
};
|
|
227
|
+
})()`;
|
|
228
|
+
|
|
229
|
+
function isRuntimeReady(state) {
|
|
230
|
+
return Boolean(
|
|
231
|
+
state?.documentReady &&
|
|
232
|
+
state?.previewPresent &&
|
|
233
|
+
state?.previewVisible &&
|
|
234
|
+
!state?.progressVisible &&
|
|
235
|
+
!state?.runtimeErrorVisible,
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function remoteArgumentText(argument) {
|
|
240
|
+
if (Object.hasOwn(argument || {}, "value")) {
|
|
241
|
+
const value = argument.value;
|
|
242
|
+
return typeof value === "string" ? value : JSON.stringify(value);
|
|
243
|
+
}
|
|
244
|
+
return String(argument?.description || argument?.type || "");
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async function runChromeReview({ executable, url, screenshotPath, profileDir, waitMs }) {
|
|
248
|
+
const args = [
|
|
249
|
+
"--headless=new",
|
|
250
|
+
"--no-first-run",
|
|
251
|
+
"--no-default-browser-check",
|
|
252
|
+
"--disable-background-networking",
|
|
253
|
+
"--disable-component-update",
|
|
254
|
+
"--disable-sync",
|
|
255
|
+
"--hide-scrollbars",
|
|
256
|
+
"--enable-logging=stderr",
|
|
257
|
+
"--remote-debugging-pipe",
|
|
258
|
+
`--user-data-dir=${profileDir}`,
|
|
259
|
+
"--window-size=1440,1100",
|
|
260
|
+
url,
|
|
261
|
+
];
|
|
262
|
+
const child = spawn(executable, args, {
|
|
263
|
+
stdio: ["ignore", "pipe", "pipe", "pipe", "pipe"],
|
|
264
|
+
});
|
|
265
|
+
let stdout = "";
|
|
266
|
+
let stderr = "";
|
|
267
|
+
const consoleLines = [];
|
|
268
|
+
const append = (current, chunk) => `${current}${chunk}`.slice(-2_000_000);
|
|
269
|
+
child.stdout.on("data", (chunk) => {
|
|
270
|
+
stdout = append(stdout, chunk);
|
|
271
|
+
});
|
|
272
|
+
child.stderr.on("data", (chunk) => {
|
|
273
|
+
stderr = append(stderr, chunk);
|
|
274
|
+
});
|
|
275
|
+
const closed = new Promise((resolve, reject) => {
|
|
276
|
+
child.once("error", reject);
|
|
277
|
+
child.once("close", (code, signal) => {
|
|
278
|
+
resolve({ code: Number(code ?? -1), signal: signal || null });
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
const timer = setTimeout(() => child.kill("SIGTERM"), Math.min(waitMs + 30_000, 58_000));
|
|
282
|
+
try {
|
|
283
|
+
const cdp = createCdpPipe(child);
|
|
284
|
+
cdp.onEvent((message) => {
|
|
285
|
+
if (message.method === "Runtime.consoleAPICalled") {
|
|
286
|
+
const values = Array.isArray(message.params?.args)
|
|
287
|
+
? message.params.args.map(remoteArgumentText).filter(Boolean)
|
|
288
|
+
: [];
|
|
289
|
+
consoleLines.push(
|
|
290
|
+
`[console.${message.params?.type || "log"}] ${values.join(" ")}`,
|
|
291
|
+
);
|
|
292
|
+
} else if (message.method === "Runtime.exceptionThrown") {
|
|
293
|
+
consoleLines.push(
|
|
294
|
+
`[exception] ${message.params?.exceptionDetails?.exception?.description || message.params?.exceptionDetails?.text || "Uncaught exception"}`,
|
|
295
|
+
);
|
|
296
|
+
} else if (message.method === "Log.entryAdded") {
|
|
297
|
+
const entry = message.params?.entry;
|
|
298
|
+
consoleLines.push(
|
|
299
|
+
`[${entry?.level || "log"}] ${entry?.text || ""}${entry?.url ? ` (${entry.url})` : ""}`,
|
|
300
|
+
);
|
|
301
|
+
} else if (message.method === "Target.attachedToTarget") {
|
|
302
|
+
const childSessionId = String(message.params?.sessionId || "");
|
|
303
|
+
if (childSessionId) {
|
|
304
|
+
Promise.all([
|
|
305
|
+
cdp.send("Runtime.enable", {}, childSessionId),
|
|
306
|
+
cdp.send("Log.enable", {}, childSessionId),
|
|
307
|
+
]).catch(() => {});
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
const target = await waitForPageTarget(cdp, url);
|
|
312
|
+
const attached = await cdp.send("Target.attachToTarget", {
|
|
313
|
+
targetId: target.targetId,
|
|
314
|
+
flatten: true,
|
|
315
|
+
});
|
|
316
|
+
const sessionId = String(attached.sessionId || "");
|
|
317
|
+
if (!sessionId) throw new Error("Chrome did not attach to the Build page.");
|
|
318
|
+
await Promise.all([
|
|
319
|
+
cdp.send("Page.enable", {}, sessionId),
|
|
320
|
+
cdp.send("Runtime.enable", {}, sessionId),
|
|
321
|
+
cdp.send("Log.enable", {}, sessionId),
|
|
322
|
+
]);
|
|
323
|
+
await cdp.send(
|
|
324
|
+
"Target.setAutoAttach",
|
|
325
|
+
{
|
|
326
|
+
autoAttach: true,
|
|
327
|
+
waitForDebuggerOnStart: false,
|
|
328
|
+
flatten: true,
|
|
329
|
+
},
|
|
330
|
+
sessionId,
|
|
331
|
+
);
|
|
332
|
+
const deadline = Date.now() + waitMs;
|
|
333
|
+
let firstReadyAt = null;
|
|
334
|
+
let readiness = null;
|
|
335
|
+
do {
|
|
336
|
+
const evaluated = await cdp.send(
|
|
337
|
+
"Runtime.evaluate",
|
|
338
|
+
{ expression: RUNTIME_READINESS_EXPRESSION, returnByValue: true },
|
|
339
|
+
sessionId,
|
|
340
|
+
);
|
|
341
|
+
readiness = evaluated.result?.value || null;
|
|
342
|
+
if (!firstReadyAt && isRuntimeReady(readiness)) {
|
|
343
|
+
firstReadyAt = new Date().toISOString();
|
|
344
|
+
}
|
|
345
|
+
const remaining = deadline - Date.now();
|
|
346
|
+
if (remaining > 0) await delay(Math.min(250, remaining));
|
|
347
|
+
} while (Date.now() < deadline);
|
|
348
|
+
const screenshot = await cdp.send(
|
|
349
|
+
"Page.captureScreenshot",
|
|
350
|
+
{ format: "png", fromSurface: true, captureBeyondViewport: false },
|
|
351
|
+
sessionId,
|
|
352
|
+
);
|
|
353
|
+
if (screenshot.data) {
|
|
354
|
+
writeFileSync(screenshotPath, Buffer.from(screenshot.data, "base64"));
|
|
355
|
+
}
|
|
356
|
+
await cdp.send("Browser.close").catch(() => {});
|
|
357
|
+
const closeResult = await Promise.race([
|
|
358
|
+
closed,
|
|
359
|
+
delay(5_000).then(() => {
|
|
360
|
+
child.kill("SIGTERM");
|
|
361
|
+
return closed;
|
|
362
|
+
}),
|
|
363
|
+
]);
|
|
364
|
+
return {
|
|
365
|
+
...closeResult,
|
|
366
|
+
stdout,
|
|
367
|
+
stderr,
|
|
368
|
+
consoleLines: consoleLines.slice(-MAX_CAPTURED_LOG_LINES),
|
|
369
|
+
runtimeReadiness: {
|
|
370
|
+
...(readiness || {}),
|
|
371
|
+
ready: isRuntimeReady(readiness),
|
|
372
|
+
firstReadyAt,
|
|
373
|
+
},
|
|
374
|
+
};
|
|
375
|
+
} finally {
|
|
376
|
+
clearTimeout(timer);
|
|
377
|
+
if (child.exitCode === null && child.signalCode === null) child.kill("SIGTERM");
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function captureConsoleEvidence(stderr, cdpLines = []) {
|
|
382
|
+
const chromeLines = String(stderr || "")
|
|
383
|
+
.split(/\r?\n/)
|
|
384
|
+
.filter((line) =>
|
|
385
|
+
/(?:INFO:CONSOLE|Uncaught|Failed to load resource)/i.test(line),
|
|
386
|
+
)
|
|
387
|
+
.slice(-MAX_CAPTURED_LOG_LINES);
|
|
388
|
+
return [...new Set([...cdpLines, ...chromeLines])].slice(
|
|
389
|
+
-MAX_CAPTURED_LOG_LINES,
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
export async function runManagedBuildReview({ options, authToken, buildId }) {
|
|
394
|
+
const waitMs = boundedWaitMs(options.adminWaitMs);
|
|
395
|
+
const executable = findChromeExecutable(options.adminBrowserPath);
|
|
396
|
+
let outputDir;
|
|
397
|
+
if (options.adminOutputDir) {
|
|
398
|
+
const outputRoot = path.resolve(options.adminOutputDir);
|
|
399
|
+
mkdirSync(outputRoot, { recursive: true });
|
|
400
|
+
outputDir = mkdtempSync(path.join(outputRoot, `build-${buildId}-review-`));
|
|
401
|
+
} else {
|
|
402
|
+
outputDir = mkdtempSync(
|
|
403
|
+
path.join(os.tmpdir(), `lumine-build-${buildId}-review-`),
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
const profileDir = mkdtempSync(path.join(os.tmpdir(), "lumine-chrome-profile-"));
|
|
407
|
+
const screenshotPath = path.join(outputDir, "runtime.png");
|
|
408
|
+
const receiptPath = path.join(outputDir, "review.json");
|
|
409
|
+
const appUrl = `${String(options.siteUrl).replace(/\/$/, "")}/app/${buildId}`;
|
|
410
|
+
const beforeVersion = await loadPublishedVersion({
|
|
411
|
+
apiUrl: options.apiUrl,
|
|
412
|
+
buildId,
|
|
413
|
+
timeoutMs: options.timeoutMs,
|
|
414
|
+
authToken,
|
|
415
|
+
});
|
|
416
|
+
const startedAt = new Date().toISOString();
|
|
417
|
+
let browser;
|
|
418
|
+
try {
|
|
419
|
+
browser = await runChromeReview({
|
|
420
|
+
executable,
|
|
421
|
+
url: appUrl,
|
|
422
|
+
screenshotPath,
|
|
423
|
+
profileDir,
|
|
424
|
+
waitMs,
|
|
425
|
+
});
|
|
426
|
+
} finally {
|
|
427
|
+
rmSync(profileDir, { recursive: true, force: true });
|
|
428
|
+
}
|
|
429
|
+
const afterVersion = await loadPublishedVersion({
|
|
430
|
+
apiUrl: options.apiUrl,
|
|
431
|
+
buildId,
|
|
432
|
+
timeoutMs: options.timeoutMs,
|
|
433
|
+
authToken,
|
|
434
|
+
});
|
|
435
|
+
const screenshotBytes = existsSync(screenshotPath)
|
|
436
|
+
? Number(statSync(screenshotPath).size || 0)
|
|
437
|
+
: 0;
|
|
438
|
+
const confirmed =
|
|
439
|
+
browser.code === 0 &&
|
|
440
|
+
screenshotBytes > 0 &&
|
|
441
|
+
browser.runtimeReadiness?.ready === true &&
|
|
442
|
+
beforeVersion === afterVersion;
|
|
443
|
+
const receipt = {
|
|
444
|
+
schemaVersion: 2,
|
|
445
|
+
status: confirmed ? "confirmed" : "failed",
|
|
446
|
+
reviewMethod: "runtime",
|
|
447
|
+
buildId,
|
|
448
|
+
appUrl,
|
|
449
|
+
publishedArtifactVersionId: beforeVersion,
|
|
450
|
+
versionAfterReview: afterVersion,
|
|
451
|
+
versionStable: beforeVersion === afterVersion,
|
|
452
|
+
startedAt,
|
|
453
|
+
completedAt: new Date().toISOString(),
|
|
454
|
+
waitMs,
|
|
455
|
+
browser: {
|
|
456
|
+
executable,
|
|
457
|
+
exitCode: browser.code,
|
|
458
|
+
signal: browser.signal,
|
|
459
|
+
runtimeReadiness: browser.runtimeReadiness,
|
|
460
|
+
},
|
|
461
|
+
screenshot: {
|
|
462
|
+
path: screenshotPath,
|
|
463
|
+
bytes: screenshotBytes,
|
|
464
|
+
},
|
|
465
|
+
console: captureConsoleEvidence(browser.stderr, browser.consoleLines),
|
|
466
|
+
};
|
|
467
|
+
writeAdminJsonFile(receiptPath, receipt);
|
|
468
|
+
if (!confirmed) {
|
|
469
|
+
const error = validationError(
|
|
470
|
+
beforeVersion !== afterVersion
|
|
471
|
+
? "The Build was republished during review; review the new artifact before commenting."
|
|
472
|
+
: browser.runtimeReadiness?.runtimeErrorVisible
|
|
473
|
+
? "The isolated Build runtime exposed a runtime error instead of a reviewable app."
|
|
474
|
+
: "The isolated Build runtime did not become visibly reviewable before the observation window ended.",
|
|
475
|
+
"CLI_ADMIN_BUILD_REVIEW_FAILED",
|
|
476
|
+
);
|
|
477
|
+
error.data = {
|
|
478
|
+
ok: false,
|
|
479
|
+
status: "validation_error",
|
|
480
|
+
error: {
|
|
481
|
+
code: error.code,
|
|
482
|
+
message: error.message,
|
|
483
|
+
details: { receiptPath, screenshotPath },
|
|
484
|
+
},
|
|
485
|
+
};
|
|
486
|
+
throw error;
|
|
487
|
+
}
|
|
488
|
+
return {
|
|
489
|
+
ok: true,
|
|
490
|
+
status: "success",
|
|
491
|
+
data: { review: receipt, receiptPath, screenshotPath },
|
|
492
|
+
};
|
|
493
|
+
}
|
package/lib/commands.js
CHANGED
|
@@ -64,6 +64,8 @@ import { sdkCommand } from "./sdk.js";
|
|
|
64
64
|
import { doctorCommand, normalizePreviewUrl } from "./doctor.js";
|
|
65
65
|
import { adminCommand } from "./admin.js";
|
|
66
66
|
import { runBuildForumCommand } from "./forum.js";
|
|
67
|
+
import { agentCommand } from "./agent.js";
|
|
68
|
+
import { agentMcpCommand } from "./agent/mcp-server.js";
|
|
67
69
|
import {
|
|
68
70
|
defaultMainCheckoutDir,
|
|
69
71
|
defaultReferenceDir,
|
|
@@ -103,6 +105,12 @@ import {
|
|
|
103
105
|
|
|
104
106
|
export async function main() {
|
|
105
107
|
const options = parseArgs(process.argv.slice(2));
|
|
108
|
+
// MCP stdio must remain protocol-clean: no update checks, help text, or
|
|
109
|
+
// other stdout can run before the server takes ownership of the stream.
|
|
110
|
+
if (options.command === "agent-mcp") {
|
|
111
|
+
await agentMcpCommand(options);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
106
114
|
options.lumineCli = await loadLumineCliVersionInfo({ options });
|
|
107
115
|
if (options.help) {
|
|
108
116
|
printHelp();
|
|
@@ -117,6 +125,11 @@ export async function main() {
|
|
|
117
125
|
return;
|
|
118
126
|
}
|
|
119
127
|
|
|
128
|
+
if (options.command === "agent") {
|
|
129
|
+
await agentCommand(options, { saveWorkspace: save });
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
120
133
|
if (options.command === "workspace") {
|
|
121
134
|
await workspace(options);
|
|
122
135
|
return;
|
|
@@ -2209,6 +2222,12 @@ export function parseArgs(args) {
|
|
|
2209
2222
|
"full",
|
|
2210
2223
|
"unviewed",
|
|
2211
2224
|
"viewed",
|
|
2225
|
+
"all",
|
|
2226
|
+
"resume",
|
|
2227
|
+
"sinceRun",
|
|
2228
|
+
"includeLegacy",
|
|
2229
|
+
"includePrivateEvidence",
|
|
2230
|
+
"noReviewLoop",
|
|
2212
2231
|
]);
|
|
2213
2232
|
|
|
2214
2233
|
for (let i = 0; i < rest.length; i += 1) {
|
|
@@ -2308,7 +2327,35 @@ export function parseArgs(args) {
|
|
|
2308
2327
|
cursor: Math.max(0, Math.floor(Number(raw.cursor) || 0)),
|
|
2309
2328
|
adminCursor: raw.cursor ? String(raw.cursor) : "",
|
|
2310
2329
|
adminAfter: raw.after ? String(raw.after) : "",
|
|
2330
|
+
adminSinceRun: Boolean(raw.sinceRun),
|
|
2331
|
+
adminIncludeLegacy: Boolean(raw.includeLegacy),
|
|
2332
|
+
adminIncludePrivateEvidence: parseBoolean(raw.includePrivateEvidence, false),
|
|
2333
|
+
adminAll: Boolean(raw.all),
|
|
2334
|
+
adminResume: Boolean(raw.resume),
|
|
2335
|
+
adminCheckpoint: raw.checkpoint ? String(raw.checkpoint) : "",
|
|
2336
|
+
adminOutput: raw.output ? String(raw.output) : "",
|
|
2337
|
+
adminOutputDir: raw.outputDir ? String(raw.outputDir) : "",
|
|
2338
|
+
adminClaimFile: raw.claim ? String(raw.claim) : "",
|
|
2339
|
+
adminScaffoldFile: raw.scaffold ? String(raw.scaffold) : "",
|
|
2340
|
+
adminTargetFile: raw.targetFile ? String(raw.targetFile) : "",
|
|
2341
|
+
adminReviewReceipt: raw.reviewReceipt ? String(raw.reviewReceipt) : "",
|
|
2342
|
+
adminSeverity: raw.severity ? String(raw.severity) : "",
|
|
2343
|
+
adminStatus: raw.status ? String(raw.status) : "",
|
|
2344
|
+
adminWaitMs: raw.waitMs ? String(raw.waitMs) : "",
|
|
2345
|
+
adminBrowserPath: raw.browserPath ? String(raw.browserPath) : "",
|
|
2311
2346
|
adminEffort: raw.effort ? String(raw.effort) : "",
|
|
2347
|
+
agentEffort:
|
|
2348
|
+
command === "agent" && raw.effort ? String(raw.effort) : "",
|
|
2349
|
+
provider: raw.provider ? String(raw.provider) : "",
|
|
2350
|
+
providerPath: raw.providerPath ? String(raw.providerPath) : "",
|
|
2351
|
+
agentPrompt:
|
|
2352
|
+
command === "agent"
|
|
2353
|
+
? String(raw.prompt || positional.join(" ")).trim()
|
|
2354
|
+
: "",
|
|
2355
|
+
runtimeFile: raw.runtimeFile ? String(raw.runtimeFile) : "",
|
|
2356
|
+
traceFile: raw.traceFile ? String(raw.traceFile) : "",
|
|
2357
|
+
baseFilesHash: raw.baseFilesHash ? String(raw.baseFilesHash) : "",
|
|
2358
|
+
reviewLoop: parseBoolean(raw.noReviewLoop, false) ? false : true,
|
|
2312
2359
|
adminLevel: raw.level ? String(raw.level) : "",
|
|
2313
2360
|
adminIds: raw.subjectIds
|
|
2314
2361
|
? String(raw.subjectIds)
|
|
@@ -2620,6 +2667,7 @@ export function printHelp() {
|
|
|
2620
2667
|
lumine login
|
|
2621
2668
|
lumine whoami
|
|
2622
2669
|
lumine logout
|
|
2670
|
+
lumine agent --provider <codex|claude-code> "<build request>"
|
|
2623
2671
|
lumine new [title]
|
|
2624
2672
|
lumine rename [title] [--target <twinkle-build-url-or-id>]
|
|
2625
2673
|
lumine describe [description] [--target <twinkle-build-url-or-id>]
|
|
@@ -2662,28 +2710,34 @@ export function printHelp() {
|
|
|
2662
2710
|
lumine thumbnail generate ["<prompt>"] --model <gpt-image-2|nano-banana>
|
|
2663
2711
|
lumine doctor runtime-assets
|
|
2664
2712
|
lumine admin identity list|status|use <zero|ciel|auto> [--json]
|
|
2713
|
+
lumine admin identity inspect <user-id|username> --reason <management-reason> [--include-private-evidence] [--json]
|
|
2665
2714
|
lumine admin ai-bucket create --label <name> --note <text> [--json]
|
|
2666
2715
|
lumine admin ai-bucket get --bucket-id <id> [--json]
|
|
2667
2716
|
lumine admin ai-bucket accounts add --bucket-id <id> --user-ids <id,id,...> [--note <text>] [--json]
|
|
2668
2717
|
lumine admin ai-bucket note set --bucket-id <id> --note <text> [--json]
|
|
2669
2718
|
lumine admin daily-run start [--identity zero|ciel|auto] [--comment-mode off|draft|post] [--run-key <key>] [--json]
|
|
2670
|
-
lumine admin daily-run status|complete|fail [--reason <text>] [--json]
|
|
2671
|
-
lumine admin
|
|
2672
|
-
lumine admin
|
|
2673
|
-
lumine admin
|
|
2719
|
+
lumine admin daily-run status|report|complete|fail [--reason <text>] [--json]
|
|
2720
|
+
lumine admin daily-run escalation add --target <target> --note <summary> [--severity attention|urgent] [--json]
|
|
2721
|
+
lumine admin escalation list [--status open|acknowledged|resolved|all] [--limit <number>] [--json]
|
|
2722
|
+
lumine admin escalation set <audit-id> --status open|acknowledged|resolved --note <decision> [--json]
|
|
2723
|
+
lumine admin recommendations list [--since-run|--after <date>|--include-legacy] [--all --checkpoint <file> [--resume]] [--content-types comment,dailyReflection] [--unviewed|--viewed] [--cursor <cursor>] [--json]
|
|
2724
|
+
lumine admin builds candidates [--all --checkpoint <file> [--resume]] [--cursor <cursor>] [--limit <number>] [--json]
|
|
2725
|
+
lumine admin builds review <build-url-or-id> [--output-dir <dir>] [--wait-ms <ms>] [--browser-path <path>] [--json]
|
|
2726
|
+
lumine admin subjects candidates [--after <date>] [--effort unassigned] [--unviewed|--viewed] [--all --checkpoint <file> [--resume]] [--cursor <cursor>] [--json]
|
|
2674
2727
|
lumine admin subject get|reveal <subject-url-or-id> [--json]
|
|
2675
|
-
lumine admin subject comments <subject-url-or-id> [--unviewed|--viewed] [--cursor <cursor>] [--json]
|
|
2728
|
+
lumine admin subject comments <subject-url-or-id> [--unviewed|--viewed] [--all --checkpoint <file> [--resume]] [--cursor <cursor>] [--json]
|
|
2676
2729
|
lumine admin subject effort set <subject-id> --level <1|2|3> [--json]
|
|
2677
2730
|
lumine admin subject creator set-made-by-poster <subject-id> [--json]
|
|
2678
2731
|
lumine admin subject feature|unfeature <subject-id> [--json]
|
|
2679
2732
|
lumine admin featured list [--unviewed|--viewed] [--json]
|
|
2680
2733
|
lumine admin featured reorder --subject-ids <id,id,...> [--json]
|
|
2681
2734
|
lumine admin post get <target> [--type subject|comment|aiStory|dailyReflection] [--json]
|
|
2682
|
-
lumine admin post comments <target> [--type subject|aiStory|dailyReflection] [--unviewed|--viewed] [--cursor <cursor>] [--json]
|
|
2735
|
+
lumine admin post comments <target> [--type subject|aiStory|dailyReflection] [--unviewed|--viewed] [--all --checkpoint <file> [--resume]] [--cursor <cursor>] [--json]
|
|
2683
2736
|
lumine admin post recommend <target> [--type subject|comment|aiStory|dailyReflection] [--anyone-can-reward] [--reward-twinkles 3] [--json]
|
|
2684
2737
|
lumine admin post skip <target> [--type comment|aiStory|dailyReflection] [--reason <text>] [--json]
|
|
2738
|
+
lumine admin post skip-batch --target-file <json-or-lines> [--reason <text>] [--checkpoint <file> [--resume]] [--json]
|
|
2685
2739
|
lumine admin post reward <target> [--type subject|comment|aiStory|dailyReflection] --twinkles 3 [--json]
|
|
2686
|
-
lumine admin comment draft <target> [--type subject|comment|build|aiStory|dailyReflection] [--file <comment.md>] [--reviewed-version <id> --reviewed-via runtime|code] [--identity zero|ciel|auto] [--json]
|
|
2740
|
+
lumine admin comment draft <target> [--type subject|comment|build|aiStory|dailyReflection] [--file <comment.md>] [--review-receipt <review.json>|--reviewed-version <id> --reviewed-via runtime|code] [--identity zero|ciel|auto] [--json]
|
|
2687
2741
|
lumine admin comment reply comment:<id> [--file <reply.md>] [--reviewed-version <id> --reviewed-via runtime|code] [--identity zero|ciel|auto] [--json]
|
|
2688
2742
|
lumine admin comment post --draft-id <id> [--json]
|
|
2689
2743
|
lumine admin comment edit <comment-id> --file <comment.md> [--json]
|
|
@@ -2691,8 +2745,11 @@ export function printHelp() {
|
|
|
2691
2745
|
lumine admin bot-output [--days <1..30>] [--json]
|
|
2692
2746
|
lumine admin announcement post --file <announcement.md> [--json]
|
|
2693
2747
|
lumine admin chat send <user-id|username> --file <message.md> [--json]
|
|
2748
|
+
lumine admin news claim [--date YYYY-MM-DD] [--output <claim.json>] [--scaffold <editorial.json>] [--json]
|
|
2749
|
+
lumine admin news validate --claim <claim.json> --file <editorial.json> [--json]
|
|
2750
|
+
lumine admin news submit --claim <claim.json> --file <editorial.json> [--model <name>] [--json]
|
|
2694
2751
|
lumine admin notable add <user-id|username> --note <text> [--json]
|
|
2695
|
-
lumine admin audit [list] [--run current|last|<run-id>] [--target <target>] [--actions <a,b>] [--full] [--cursor <cursor>] [--json]
|
|
2752
|
+
lumine admin audit [list] [--run current|last|<run-id>] [--target <target>] [--actions <a,b>] [--full] [--all --checkpoint <file> [--resume]] [--cursor <cursor>] [--json]
|
|
2696
2753
|
|
|
2697
2754
|
Examples:
|
|
2698
2755
|
npx @stage5/lumine@latest
|
|
@@ -2723,6 +2780,8 @@ Examples:
|
|
|
2723
2780
|
npx @stage5/lumine@latest restore 41 --main
|
|
2724
2781
|
npx @stage5/lumine@latest update-from-main
|
|
2725
2782
|
npx @stage5/lumine@latest pull
|
|
2783
|
+
npx @stage5/lumine@latest agent --provider codex "Add keyboard controls"
|
|
2784
|
+
npx @stage5/lumine@latest agent --provider claude-code "Fix the mobile layout"
|
|
2726
2785
|
npx @stage5/lumine@latest save
|
|
2727
2786
|
npx @stage5/lumine@latest save --publish
|
|
2728
2787
|
npx @stage5/lumine@latest launch --save
|
|
@@ -2750,6 +2809,10 @@ Options:
|
|
|
2750
2809
|
--auth-file <path> Saved login path
|
|
2751
2810
|
--auth-token <token> Override saved login
|
|
2752
2811
|
--dir <path> Directory for pulled project files
|
|
2812
|
+
--provider <agent> Subscription agent for lumine agent: codex or claude-code
|
|
2813
|
+
--provider-path <p> Override the selected agent CLI executable
|
|
2814
|
+
--effort <level> Optional provider reasoning effort
|
|
2815
|
+
--no-review-loop Skip the evidence-based post-run loop retrospective
|
|
2753
2816
|
--target <build> Explicit Build URL or ID for rename/describe/upgrade
|
|
2754
2817
|
--main With pull/versions/restore: target the team project's main
|
|
2755
2818
|
--version <n> With pull: read-only checkout of previous save v<n>
|
|
@@ -2760,7 +2823,21 @@ Options:
|
|
|
2760
2823
|
--note <text> Suggestion, notable-user, or AI-bucket context
|
|
2761
2824
|
--cursor <id> Continue suggestions, Forum activity, or admin listing
|
|
2762
2825
|
--poll-ms <ms> Forum listener interval (1000-60000; default 3000)
|
|
2763
|
-
--after <date> Admin
|
|
2826
|
+
--after <date> Admin listing: inclusive Unix/ISO creation boundary
|
|
2827
|
+
--since-run Recommendations since the previous completed run (default)
|
|
2828
|
+
--include-legacy Explicitly include the full historical recommendation queue
|
|
2829
|
+
--all Automatically read every page in the canonical snapshot
|
|
2830
|
+
--checkpoint <file> Persist automatic pagination or batch progress
|
|
2831
|
+
--resume Resume the exact request recorded by --checkpoint
|
|
2832
|
+
--output <file> Save an admin result or newspaper claim as JSON
|
|
2833
|
+
--output-dir <dir> Directory for managed Build review evidence
|
|
2834
|
+
--claim <file> Newspaper claim JSON used for validation/submission
|
|
2835
|
+
--scaffold <file> Write an editable newspaper editorial scaffold
|
|
2836
|
+
--target-file <file> JSON array or newline list for audited batch skips
|
|
2837
|
+
--review-receipt <f> Confirmed managed Build runtime review receipt
|
|
2838
|
+
--severity <level> Run escalation severity: attention or urgent
|
|
2839
|
+
--wait-ms <ms> Managed Build runtime observation time (1000-45000)
|
|
2840
|
+
--browser-path <path> Chrome/Chromium executable for managed Build review
|
|
2764
2841
|
--effort unassigned Admin subjects: show only unassigned effort
|
|
2765
2842
|
--unviewed Admin content lists: retain unviewed and unknown items
|
|
2766
2843
|
--viewed Admin content lists: retain viewed and unknown items
|