@stage5/lumine 0.2.40 → 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 +66 -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 +80 -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 +95 -20
|
@@ -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,11 @@ export function parseArgs(args) {
|
|
|
2209
2222
|
"full",
|
|
2210
2223
|
"unviewed",
|
|
2211
2224
|
"viewed",
|
|
2225
|
+
"all",
|
|
2226
|
+
"resume",
|
|
2227
|
+
"sinceRun",
|
|
2228
|
+
"includeLegacy",
|
|
2229
|
+
"noReviewLoop",
|
|
2212
2230
|
]);
|
|
2213
2231
|
|
|
2214
2232
|
for (let i = 0; i < rest.length; i += 1) {
|
|
@@ -2308,7 +2326,33 @@ export function parseArgs(args) {
|
|
|
2308
2326
|
cursor: Math.max(0, Math.floor(Number(raw.cursor) || 0)),
|
|
2309
2327
|
adminCursor: raw.cursor ? String(raw.cursor) : "",
|
|
2310
2328
|
adminAfter: raw.after ? String(raw.after) : "",
|
|
2329
|
+
adminSinceRun: Boolean(raw.sinceRun),
|
|
2330
|
+
adminIncludeLegacy: Boolean(raw.includeLegacy),
|
|
2331
|
+
adminAll: Boolean(raw.all),
|
|
2332
|
+
adminResume: Boolean(raw.resume),
|
|
2333
|
+
adminCheckpoint: raw.checkpoint ? String(raw.checkpoint) : "",
|
|
2334
|
+
adminOutput: raw.output ? String(raw.output) : "",
|
|
2335
|
+
adminOutputDir: raw.outputDir ? String(raw.outputDir) : "",
|
|
2336
|
+
adminClaimFile: raw.claim ? String(raw.claim) : "",
|
|
2337
|
+
adminScaffoldFile: raw.scaffold ? String(raw.scaffold) : "",
|
|
2338
|
+
adminTargetFile: raw.targetFile ? String(raw.targetFile) : "",
|
|
2339
|
+
adminReviewReceipt: raw.reviewReceipt ? String(raw.reviewReceipt) : "",
|
|
2340
|
+
adminSeverity: raw.severity ? String(raw.severity) : "",
|
|
2341
|
+
adminWaitMs: raw.waitMs ? String(raw.waitMs) : "",
|
|
2342
|
+
adminBrowserPath: raw.browserPath ? String(raw.browserPath) : "",
|
|
2311
2343
|
adminEffort: raw.effort ? String(raw.effort) : "",
|
|
2344
|
+
agentEffort:
|
|
2345
|
+
command === "agent" && raw.effort ? String(raw.effort) : "",
|
|
2346
|
+
provider: raw.provider ? String(raw.provider) : "",
|
|
2347
|
+
providerPath: raw.providerPath ? String(raw.providerPath) : "",
|
|
2348
|
+
agentPrompt:
|
|
2349
|
+
command === "agent"
|
|
2350
|
+
? String(raw.prompt || positional.join(" ")).trim()
|
|
2351
|
+
: "",
|
|
2352
|
+
runtimeFile: raw.runtimeFile ? String(raw.runtimeFile) : "",
|
|
2353
|
+
traceFile: raw.traceFile ? String(raw.traceFile) : "",
|
|
2354
|
+
baseFilesHash: raw.baseFilesHash ? String(raw.baseFilesHash) : "",
|
|
2355
|
+
reviewLoop: parseBoolean(raw.noReviewLoop, false) ? false : true,
|
|
2312
2356
|
adminLevel: raw.level ? String(raw.level) : "",
|
|
2313
2357
|
adminIds: raw.subjectIds
|
|
2314
2358
|
? String(raw.subjectIds)
|
|
@@ -2620,6 +2664,7 @@ export function printHelp() {
|
|
|
2620
2664
|
lumine login
|
|
2621
2665
|
lumine whoami
|
|
2622
2666
|
lumine logout
|
|
2667
|
+
lumine agent --provider <codex|claude-code> "<build request>"
|
|
2623
2668
|
lumine new [title]
|
|
2624
2669
|
lumine rename [title] [--target <twinkle-build-url-or-id>]
|
|
2625
2670
|
lumine describe [description] [--target <twinkle-build-url-or-id>]
|
|
@@ -2667,23 +2712,26 @@ export function printHelp() {
|
|
|
2667
2712
|
lumine admin ai-bucket accounts add --bucket-id <id> --user-ids <id,id,...> [--note <text>] [--json]
|
|
2668
2713
|
lumine admin ai-bucket note set --bucket-id <id> --note <text> [--json]
|
|
2669
2714
|
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
|
|
2715
|
+
lumine admin daily-run status|report|complete|fail [--reason <text>] [--json]
|
|
2716
|
+
lumine admin daily-run escalation add --target <target> --note <summary> [--severity attention|urgent] [--json]
|
|
2717
|
+
lumine admin recommendations list [--since-run|--after <date>|--include-legacy] [--all --checkpoint <file> [--resume]] [--content-types comment,dailyReflection] [--unviewed|--viewed] [--cursor <cursor>] [--json]
|
|
2718
|
+
lumine admin builds candidates [--all --checkpoint <file> [--resume]] [--cursor <cursor>] [--limit <number>] [--json]
|
|
2719
|
+
lumine admin builds review <build-url-or-id> [--output-dir <dir>] [--wait-ms <ms>] [--browser-path <path>] [--json]
|
|
2720
|
+
lumine admin subjects candidates [--after <date>] [--effort unassigned] [--unviewed|--viewed] [--all --checkpoint <file> [--resume]] [--cursor <cursor>] [--json]
|
|
2674
2721
|
lumine admin subject get|reveal <subject-url-or-id> [--json]
|
|
2675
|
-
lumine admin subject comments <subject-url-or-id> [--unviewed|--viewed] [--cursor <cursor>] [--json]
|
|
2722
|
+
lumine admin subject comments <subject-url-or-id> [--unviewed|--viewed] [--all --checkpoint <file> [--resume]] [--cursor <cursor>] [--json]
|
|
2676
2723
|
lumine admin subject effort set <subject-id> --level <1|2|3> [--json]
|
|
2677
2724
|
lumine admin subject creator set-made-by-poster <subject-id> [--json]
|
|
2678
2725
|
lumine admin subject feature|unfeature <subject-id> [--json]
|
|
2679
2726
|
lumine admin featured list [--unviewed|--viewed] [--json]
|
|
2680
2727
|
lumine admin featured reorder --subject-ids <id,id,...> [--json]
|
|
2681
2728
|
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]
|
|
2729
|
+
lumine admin post comments <target> [--type subject|aiStory|dailyReflection] [--unviewed|--viewed] [--all --checkpoint <file> [--resume]] [--cursor <cursor>] [--json]
|
|
2683
2730
|
lumine admin post recommend <target> [--type subject|comment|aiStory|dailyReflection] [--anyone-can-reward] [--reward-twinkles 3] [--json]
|
|
2684
2731
|
lumine admin post skip <target> [--type comment|aiStory|dailyReflection] [--reason <text>] [--json]
|
|
2732
|
+
lumine admin post skip-batch --target-file <json-or-lines> [--reason <text>] [--checkpoint <file> [--resume]] [--json]
|
|
2685
2733
|
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]
|
|
2734
|
+
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
2735
|
lumine admin comment reply comment:<id> [--file <reply.md>] [--reviewed-version <id> --reviewed-via runtime|code] [--identity zero|ciel|auto] [--json]
|
|
2688
2736
|
lumine admin comment post --draft-id <id> [--json]
|
|
2689
2737
|
lumine admin comment edit <comment-id> --file <comment.md> [--json]
|
|
@@ -2691,8 +2739,11 @@ export function printHelp() {
|
|
|
2691
2739
|
lumine admin bot-output [--days <1..30>] [--json]
|
|
2692
2740
|
lumine admin announcement post --file <announcement.md> [--json]
|
|
2693
2741
|
lumine admin chat send <user-id|username> --file <message.md> [--json]
|
|
2742
|
+
lumine admin news claim [--date YYYY-MM-DD] [--output <claim.json>] [--scaffold <editorial.json>] [--json]
|
|
2743
|
+
lumine admin news validate --claim <claim.json> --file <editorial.json> [--json]
|
|
2744
|
+
lumine admin news submit --claim <claim.json> --file <editorial.json> [--model <name>] [--json]
|
|
2694
2745
|
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]
|
|
2746
|
+
lumine admin audit [list] [--run current|last|<run-id>] [--target <target>] [--actions <a,b>] [--full] [--all --checkpoint <file> [--resume]] [--cursor <cursor>] [--json]
|
|
2696
2747
|
|
|
2697
2748
|
Examples:
|
|
2698
2749
|
npx @stage5/lumine@latest
|
|
@@ -2723,6 +2774,8 @@ Examples:
|
|
|
2723
2774
|
npx @stage5/lumine@latest restore 41 --main
|
|
2724
2775
|
npx @stage5/lumine@latest update-from-main
|
|
2725
2776
|
npx @stage5/lumine@latest pull
|
|
2777
|
+
npx @stage5/lumine@latest agent --provider codex "Add keyboard controls"
|
|
2778
|
+
npx @stage5/lumine@latest agent --provider claude-code "Fix the mobile layout"
|
|
2726
2779
|
npx @stage5/lumine@latest save
|
|
2727
2780
|
npx @stage5/lumine@latest save --publish
|
|
2728
2781
|
npx @stage5/lumine@latest launch --save
|
|
@@ -2750,6 +2803,10 @@ Options:
|
|
|
2750
2803
|
--auth-file <path> Saved login path
|
|
2751
2804
|
--auth-token <token> Override saved login
|
|
2752
2805
|
--dir <path> Directory for pulled project files
|
|
2806
|
+
--provider <agent> Subscription agent for lumine agent: codex or claude-code
|
|
2807
|
+
--provider-path <p> Override the selected agent CLI executable
|
|
2808
|
+
--effort <level> Optional provider reasoning effort
|
|
2809
|
+
--no-review-loop Skip the evidence-based post-run loop retrospective
|
|
2753
2810
|
--target <build> Explicit Build URL or ID for rename/describe/upgrade
|
|
2754
2811
|
--main With pull/versions/restore: target the team project's main
|
|
2755
2812
|
--version <n> With pull: read-only checkout of previous save v<n>
|
|
@@ -2760,7 +2817,21 @@ Options:
|
|
|
2760
2817
|
--note <text> Suggestion, notable-user, or AI-bucket context
|
|
2761
2818
|
--cursor <id> Continue suggestions, Forum activity, or admin listing
|
|
2762
2819
|
--poll-ms <ms> Forum listener interval (1000-60000; default 3000)
|
|
2763
|
-
--after <date> Admin
|
|
2820
|
+
--after <date> Admin listing: inclusive Unix/ISO creation boundary
|
|
2821
|
+
--since-run Recommendations since the previous completed run (default)
|
|
2822
|
+
--include-legacy Explicitly include the full historical recommendation queue
|
|
2823
|
+
--all Automatically read every page in the canonical snapshot
|
|
2824
|
+
--checkpoint <file> Persist automatic pagination or batch progress
|
|
2825
|
+
--resume Resume the exact request recorded by --checkpoint
|
|
2826
|
+
--output <file> Save an admin result or newspaper claim as JSON
|
|
2827
|
+
--output-dir <dir> Directory for managed Build review evidence
|
|
2828
|
+
--claim <file> Newspaper claim JSON used for validation/submission
|
|
2829
|
+
--scaffold <file> Write an editable newspaper editorial scaffold
|
|
2830
|
+
--target-file <file> JSON array or newline list for audited batch skips
|
|
2831
|
+
--review-receipt <f> Confirmed managed Build runtime review receipt
|
|
2832
|
+
--severity <level> Run escalation severity: attention or urgent
|
|
2833
|
+
--wait-ms <ms> Managed Build runtime observation time (1000-45000)
|
|
2834
|
+
--browser-path <path> Chrome/Chromium executable for managed Build review
|
|
2764
2835
|
--effort unassigned Admin subjects: show only unassigned effort
|
|
2765
2836
|
--unviewed Admin content lists: retain unviewed and unknown items
|
|
2766
2837
|
--viewed Admin content lists: retain viewed and unknown items
|
package/lib/constants.js
CHANGED