@openclawbrain/cli 0.4.10 → 0.4.12
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 +18 -17
- package/dist/src/cli.js +53 -10
- package/dist/src/daemon.d.ts +6 -1
- package/dist/src/daemon.js +229 -41
- package/dist/src/index.js +5 -2
- package/dist/src/local-learner.d.ts +4 -0
- package/dist/src/local-learner.js +212 -5
- package/dist/src/proof-command.js +654 -0
- package/dist/src/status-learning-path.js +28 -0
- package/package.json +1 -2
|
@@ -0,0 +1,654 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import process from "node:process";
|
|
6
|
+
|
|
7
|
+
export const DEFAULT_OPERATOR_PROOF_PLUGIN_ID = "openclawbrain";
|
|
8
|
+
export const DEFAULT_OPERATOR_PROOF_TIMEOUT_MS = 120_000;
|
|
9
|
+
|
|
10
|
+
function quoteShellArg(value) {
|
|
11
|
+
return `'${value.replace(/'/g, `"'"'`)}'`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function normalizeOptionalCliString(value) {
|
|
15
|
+
if (typeof value !== "string") {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
const trimmed = value.trim();
|
|
19
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function canonicalizeExistingProofPath(filePath) {
|
|
23
|
+
const resolvedPath = path.resolve(filePath);
|
|
24
|
+
try {
|
|
25
|
+
return realpathSync(resolvedPath);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return resolvedPath;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function shellJoin(parts) {
|
|
33
|
+
return parts
|
|
34
|
+
.map((part) => {
|
|
35
|
+
if (/^[A-Za-z0-9_./:@=-]+$/.test(part)) {
|
|
36
|
+
return part;
|
|
37
|
+
}
|
|
38
|
+
return JSON.stringify(part);
|
|
39
|
+
})
|
|
40
|
+
.join(" ");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function timestampToken(date = new Date()) {
|
|
44
|
+
return date.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z").replace("T", "-");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function resolveProofOutputDir(options) {
|
|
48
|
+
if (options.outputDir !== null) {
|
|
49
|
+
return path.resolve(options.outputDir);
|
|
50
|
+
}
|
|
51
|
+
return path.resolve(options.cwd ?? process.cwd(), "artifacts", `operator-proof-${timestampToken()}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function writeText(filePath, text) {
|
|
55
|
+
mkdirSync(path.dirname(filePath), { recursive: true });
|
|
56
|
+
writeFileSync(filePath, text, "utf8");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function writeJson(filePath, value) {
|
|
60
|
+
writeText(filePath, `${JSON.stringify(value, null, 2)}\n`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function buildCurrentCliInvocation(cliEntryPath = process.argv[1]) {
|
|
64
|
+
const normalizedEntryPath = normalizeOptionalCliString(cliEntryPath);
|
|
65
|
+
if (normalizedEntryPath === null) {
|
|
66
|
+
return {
|
|
67
|
+
command: "openclawbrain",
|
|
68
|
+
args: []
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
command: process.execPath,
|
|
73
|
+
args: [path.resolve(normalizedEntryPath)]
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function defaultRunCapture(command, args, options = {}) {
|
|
78
|
+
const startedAt = new Date();
|
|
79
|
+
const result = spawnSync(command, args, {
|
|
80
|
+
cwd: options.cwd ?? process.cwd(),
|
|
81
|
+
env: options.env ?? process.env,
|
|
82
|
+
encoding: "utf8",
|
|
83
|
+
timeout: options.timeoutMs,
|
|
84
|
+
});
|
|
85
|
+
const endedAt = new Date();
|
|
86
|
+
return {
|
|
87
|
+
label: options.label ?? command,
|
|
88
|
+
command,
|
|
89
|
+
argv: args,
|
|
90
|
+
shellCommand: shellJoin([command, ...args]),
|
|
91
|
+
startedAt: startedAt.toISOString(),
|
|
92
|
+
endedAt: endedAt.toISOString(),
|
|
93
|
+
durationMs: endedAt.getTime() - startedAt.getTime(),
|
|
94
|
+
exitCode: typeof result.status === "number" ? result.status : null,
|
|
95
|
+
signal: result.signal ?? null,
|
|
96
|
+
stdout: result.stdout ?? "",
|
|
97
|
+
stderr: result.stderr ?? "",
|
|
98
|
+
error: result.error ? String(result.error) : null,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function summarizeCapture(step) {
|
|
103
|
+
let resultClass = "unknown";
|
|
104
|
+
if (step.exitCode === 0 && !step.error) {
|
|
105
|
+
resultClass = "success";
|
|
106
|
+
}
|
|
107
|
+
else if (step.signal === "SIGTERM" || step.signal === "SIGKILL") {
|
|
108
|
+
resultClass = "interrupted";
|
|
109
|
+
}
|
|
110
|
+
else if (step.error && /timed out/i.test(step.error)) {
|
|
111
|
+
resultClass = "timed_out";
|
|
112
|
+
}
|
|
113
|
+
else if (step.exitCode !== 0 || step.error) {
|
|
114
|
+
resultClass = "command_failed";
|
|
115
|
+
}
|
|
116
|
+
const captureState = step.exitCode === null && !step.stdout && !step.stderr
|
|
117
|
+
? "missing"
|
|
118
|
+
: step.exitCode === null
|
|
119
|
+
? "partial"
|
|
120
|
+
: "complete";
|
|
121
|
+
return { resultClass, captureState };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function writeStepBundle(bundleDir, stepId, capture) {
|
|
125
|
+
const stdoutName = `${stepId}.stdout.log`;
|
|
126
|
+
const stderrName = `${stepId}.stderr.log`;
|
|
127
|
+
writeText(path.join(bundleDir, stdoutName), capture.stdout);
|
|
128
|
+
writeText(path.join(bundleDir, stderrName), capture.stderr);
|
|
129
|
+
return { stdoutName, stderrName };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function extractGatewayLogPath(text) {
|
|
133
|
+
const match = text.match(/^File logs:\s+(.+)$/m);
|
|
134
|
+
return match ? match[1].trim() : null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function extractActivationRoot(statusText, override) {
|
|
138
|
+
if (override !== null) {
|
|
139
|
+
return path.resolve(override);
|
|
140
|
+
}
|
|
141
|
+
const targetMatch = statusText.match(/^target\s+activation=([^\s]+)\s+/m);
|
|
142
|
+
if (targetMatch) {
|
|
143
|
+
return targetMatch[1].trim();
|
|
144
|
+
}
|
|
145
|
+
const hostMatch = statusText.match(/^host\s+runtime=[^\s]+\s+activation=([^\s]+)$/m);
|
|
146
|
+
if (hostMatch) {
|
|
147
|
+
return hostMatch[1].trim();
|
|
148
|
+
}
|
|
149
|
+
return path.join(homedir(), ".openclawbrain", "activation");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function readTextIfExists(filePath) {
|
|
153
|
+
if (filePath === null || !existsSync(filePath)) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
return readFileSync(filePath, "utf8");
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function readJsonSnapshot(filePath) {
|
|
160
|
+
if (filePath === null) {
|
|
161
|
+
return {
|
|
162
|
+
path: null,
|
|
163
|
+
exists: false,
|
|
164
|
+
error: "proof path unresolved",
|
|
165
|
+
value: null
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
if (!existsSync(filePath)) {
|
|
169
|
+
return {
|
|
170
|
+
path: filePath,
|
|
171
|
+
exists: false,
|
|
172
|
+
error: null,
|
|
173
|
+
value: null
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
try {
|
|
177
|
+
return {
|
|
178
|
+
path: filePath,
|
|
179
|
+
exists: true,
|
|
180
|
+
error: null,
|
|
181
|
+
value: JSON.parse(readFileSync(filePath, "utf8"))
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
return {
|
|
186
|
+
path: filePath,
|
|
187
|
+
exists: true,
|
|
188
|
+
error: error instanceof Error ? error.message : String(error),
|
|
189
|
+
value: null
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function extractStartupBreadcrumbs(logText, bundleStartedAtIso) {
|
|
195
|
+
if (!logText) {
|
|
196
|
+
return { all: [], afterBundleStart: [] };
|
|
197
|
+
}
|
|
198
|
+
const bundleStartMs = Date.parse(bundleStartedAtIso);
|
|
199
|
+
const out = [];
|
|
200
|
+
for (const line of logText.split(/\r?\n/)) {
|
|
201
|
+
if (!line.includes("[openclawbrain] BRAIN LOADED") && !line.includes("[openclawbrain] BRAIN NOT YET LOADED")) {
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
let parsed = null;
|
|
205
|
+
try {
|
|
206
|
+
parsed = JSON.parse(line);
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
parsed = null;
|
|
210
|
+
}
|
|
211
|
+
const timestamp = parsed?._meta?.date ?? parsed?.time ?? null;
|
|
212
|
+
out.push({
|
|
213
|
+
line,
|
|
214
|
+
timestamp,
|
|
215
|
+
kind: line.includes("BRAIN LOADED") ? "loaded" : "not_yet_loaded",
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
return {
|
|
219
|
+
all: out,
|
|
220
|
+
afterBundleStart: out.filter((entry) => entry.timestamp && Date.parse(entry.timestamp) >= bundleStartMs),
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function extractStatusSignals(statusText) {
|
|
225
|
+
return {
|
|
226
|
+
statusOk: /^STATUS ok$/m.test(statusText),
|
|
227
|
+
loadProofReady: /loadProof=status_probe_ready/.test(statusText),
|
|
228
|
+
runtimeProven: /attachTruth .*runtime=proven/.test(statusText),
|
|
229
|
+
pluginInstalled: /hook\s+install=installed/.test(statusText),
|
|
230
|
+
serveActivePack: /serve\s+state=serving_active_pack/.test(statusText),
|
|
231
|
+
routeFnAvailable: /routeFn\s+available=yes/.test(statusText),
|
|
232
|
+
proofPath: statusText.match(/proofPath=([^\s]+)/)?.[1] ?? null,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function hasPackagedHookSource(pluginInspectText) {
|
|
237
|
+
return /Source:\s+.*(?:@openclawbrain[\\/]+openclaw|openclawbrain)[\\/]+dist[\\/]+extension[\\/]+index\.js/m.test(pluginInspectText);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function buildVerdict({ steps, gatewayStatus, pluginInspect, statusSignals, breadcrumbs, runtimeLoadProofSnapshot, openclawHome }) {
|
|
241
|
+
const failedStep = steps.find((step) => step.resultClass !== "success" && step.skipped !== true);
|
|
242
|
+
if (failedStep) {
|
|
243
|
+
return {
|
|
244
|
+
verdict: "command_failed",
|
|
245
|
+
severity: "blocking",
|
|
246
|
+
why: `${failedStep.stepId} exited as ${failedStep.resultClass}`,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
const gatewayHealthy = /Runtime:\s+running/m.test(gatewayStatus) && /RPC probe:\s+ok/m.test(gatewayStatus);
|
|
250
|
+
const pluginLoaded = /Status:\s+loaded/m.test(pluginInspect);
|
|
251
|
+
const packagedHookPath = hasPackagedHookSource(pluginInspect);
|
|
252
|
+
const breadcrumbLoaded = breadcrumbs.afterBundleStart.some((entry) => entry.kind === "loaded");
|
|
253
|
+
const runtimeProofMatched = Array.isArray(runtimeLoadProofSnapshot?.value?.profiles)
|
|
254
|
+
&& runtimeLoadProofSnapshot.value.profiles.some((profile) => canonicalizeExistingProofPath(profile?.openclawHome ?? "") === canonicalizeExistingProofPath(openclawHome));
|
|
255
|
+
const missingProofs = [];
|
|
256
|
+
if (!gatewayHealthy)
|
|
257
|
+
missingProofs.push("gateway_health");
|
|
258
|
+
if (!pluginLoaded)
|
|
259
|
+
missingProofs.push("plugin_loaded");
|
|
260
|
+
if (!packagedHookPath)
|
|
261
|
+
missingProofs.push("packaged_hook_path");
|
|
262
|
+
if (!statusSignals.statusOk)
|
|
263
|
+
missingProofs.push("status_ok");
|
|
264
|
+
if (!statusSignals.loadProofReady)
|
|
265
|
+
missingProofs.push("load_proof");
|
|
266
|
+
if (!statusSignals.runtimeProven)
|
|
267
|
+
missingProofs.push("runtime_proven");
|
|
268
|
+
if (!statusSignals.serveActivePack)
|
|
269
|
+
missingProofs.push("serve_active_pack");
|
|
270
|
+
if (!statusSignals.routeFnAvailable)
|
|
271
|
+
missingProofs.push("route_fn");
|
|
272
|
+
if (!breadcrumbLoaded)
|
|
273
|
+
missingProofs.push("startup_breadcrumb");
|
|
274
|
+
if (!runtimeProofMatched)
|
|
275
|
+
missingProofs.push("runtime_load_proof_record");
|
|
276
|
+
if (missingProofs.length === 0) {
|
|
277
|
+
return {
|
|
278
|
+
verdict: "success_and_proven",
|
|
279
|
+
severity: "none",
|
|
280
|
+
why: "install, restart, gateway health, plugin load, startup breadcrumb, runtime-load-proof record, and detailed status all aligned",
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
const blocking = missingProofs.some((item) => [
|
|
284
|
+
"gateway_health",
|
|
285
|
+
"plugin_loaded",
|
|
286
|
+
"packaged_hook_path",
|
|
287
|
+
"status_ok",
|
|
288
|
+
"load_proof",
|
|
289
|
+
"runtime_proven",
|
|
290
|
+
"serve_active_pack",
|
|
291
|
+
"route_fn",
|
|
292
|
+
].includes(item));
|
|
293
|
+
return {
|
|
294
|
+
verdict: blocking ? "degraded_or_failed_proof" : "success_but_proof_incomplete",
|
|
295
|
+
severity: blocking ? "blocking" : "degraded",
|
|
296
|
+
why: `missing or conflicting proofs: ${missingProofs.join(", ")}`,
|
|
297
|
+
missingProofs,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function buildSummary({ options, steps, verdict, gatewayStatusText, pluginInspectText, statusSignals, breadcrumbs, runtimeLoadProofSnapshot }) {
|
|
302
|
+
const passed = [];
|
|
303
|
+
const missing = [];
|
|
304
|
+
if (steps.find((step) => step.stepId === "01-install")?.resultClass === "success") {
|
|
305
|
+
passed.push("install command succeeded");
|
|
306
|
+
}
|
|
307
|
+
if (steps.find((step) => step.stepId === "02-restart")?.skipped === true || steps.find((step) => step.stepId === "02-restart")?.resultClass === "success") {
|
|
308
|
+
passed.push("restart step completed or was intentionally skipped");
|
|
309
|
+
}
|
|
310
|
+
if (/Runtime:\s+running/m.test(gatewayStatusText) && /RPC probe:\s+ok/m.test(gatewayStatusText)) {
|
|
311
|
+
passed.push("gateway status showed runtime running and RPC probe ok");
|
|
312
|
+
}
|
|
313
|
+
if (/Status:\s+loaded/m.test(pluginInspectText)) {
|
|
314
|
+
passed.push("plugin inspect showed OpenClawBrain loaded");
|
|
315
|
+
}
|
|
316
|
+
if (statusSignals.statusOk) {
|
|
317
|
+
passed.push("detailed status returned STATUS ok");
|
|
318
|
+
}
|
|
319
|
+
if (statusSignals.loadProofReady) {
|
|
320
|
+
passed.push("detailed status reported loadProof=status_probe_ready");
|
|
321
|
+
}
|
|
322
|
+
if (statusSignals.serveActivePack) {
|
|
323
|
+
passed.push("detailed status reported serve state=serving_active_pack");
|
|
324
|
+
}
|
|
325
|
+
if (statusSignals.routeFnAvailable) {
|
|
326
|
+
passed.push("detailed status reported routeFn available=yes");
|
|
327
|
+
}
|
|
328
|
+
if (breadcrumbs.afterBundleStart.some((entry) => entry.kind === "loaded")) {
|
|
329
|
+
passed.push("startup log contained a post-bundle [openclawbrain] BRAIN LOADED breadcrumb");
|
|
330
|
+
}
|
|
331
|
+
if (!statusSignals.loadProofReady)
|
|
332
|
+
missing.push("detailed status did not prove hook load");
|
|
333
|
+
if (!breadcrumbs.afterBundleStart.some((entry) => entry.kind === "loaded"))
|
|
334
|
+
missing.push("no post-bundle startup breadcrumb was found");
|
|
335
|
+
if (runtimeLoadProofSnapshot.path === null)
|
|
336
|
+
missing.push("runtime-load-proof path could not be resolved");
|
|
337
|
+
if (runtimeLoadProofSnapshot.error !== null)
|
|
338
|
+
missing.push(`runtime-load-proof snapshot was unreadable: ${runtimeLoadProofSnapshot.error}`);
|
|
339
|
+
const lines = [
|
|
340
|
+
"# OpenClawBrain operator proof summary",
|
|
341
|
+
"",
|
|
342
|
+
`- openclaw home: \`${options.openclawHome}\``,
|
|
343
|
+
`- bundle verdict: **${verdict.verdict}**`,
|
|
344
|
+
`- severity: **${verdict.severity}**`,
|
|
345
|
+
`- why: ${verdict.why}`,
|
|
346
|
+
"",
|
|
347
|
+
"## Passed",
|
|
348
|
+
...passed.map((item) => `- ${item}`),
|
|
349
|
+
"",
|
|
350
|
+
"## Missing / incomplete",
|
|
351
|
+
...(missing.length === 0 ? ["- none"] : missing.map((item) => `- ${item}`)),
|
|
352
|
+
"",
|
|
353
|
+
"## Step ledger",
|
|
354
|
+
...steps.map((step) => `- ${step.stepId}: ${step.skipped ? "skipped" : `${step.resultClass} (${step.captureState})`} - ${step.summary}`),
|
|
355
|
+
];
|
|
356
|
+
if (runtimeLoadProofSnapshot.path !== null) {
|
|
357
|
+
lines.push("", "## Runtime proof file", `- ${runtimeLoadProofSnapshot.path}`);
|
|
358
|
+
}
|
|
359
|
+
return `${lines.join("\n")}\n`;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function readOpenClawProfileName(openclawHome) {
|
|
363
|
+
try {
|
|
364
|
+
const openclawJsonPath = path.join(openclawHome, "openclaw.json");
|
|
365
|
+
if (!existsSync(openclawJsonPath)) {
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
const parsed = JSON.parse(readFileSync(openclawJsonPath, "utf8"));
|
|
369
|
+
return normalizeOptionalCliString(parsed?.profile);
|
|
370
|
+
}
|
|
371
|
+
catch {
|
|
372
|
+
return null;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function buildGatewayArgs(action, profileName) {
|
|
377
|
+
return profileName === null
|
|
378
|
+
? ["gateway", action]
|
|
379
|
+
: ["gateway", action, "--profile", profileName];
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
export function buildProofCommandForOpenClawHome(openclawHome) {
|
|
383
|
+
return `openclawbrain proof --openclaw-home ${quoteShellArg(path.resolve(openclawHome))}`;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
export function buildProofCommandHelpSection() {
|
|
387
|
+
return {
|
|
388
|
+
usage: " openclawbrain proof --openclaw-home <path> [options]",
|
|
389
|
+
optionLines: [
|
|
390
|
+
" --output-dir <path> Bundle directory for proof artifacts (proof only). Defaults to ./artifacts/operator-proof-<timestamp>.",
|
|
391
|
+
" --skip-install Capture proof without rerunning install first (proof only).",
|
|
392
|
+
" --skip-restart Capture proof without restarting OpenClaw first (proof only).",
|
|
393
|
+
` --plugin-id <id> Plugin id for \`openclaw plugins inspect\` (proof only; default: ${DEFAULT_OPERATOR_PROOF_PLUGIN_ID}).`,
|
|
394
|
+
` --timeout-ms <ms> Per-step timeout in ms for proof capture (proof only; default: ${DEFAULT_OPERATOR_PROOF_TIMEOUT_MS}).`,
|
|
395
|
+
],
|
|
396
|
+
lifecycle: " 5. proof openclawbrain proof --openclaw-home <path> - capture one durable operator proof bundle after install/restart/status",
|
|
397
|
+
advanced: " proof capture one durable operator proof bundle with step logs, startup breadcrumbs, and a runtime-load-proof snapshot",
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
export function parseProofCliArgs(argv, options = {}) {
|
|
402
|
+
const existsSyncImpl = options.existsSyncImpl ?? existsSync;
|
|
403
|
+
let openclawHome = null;
|
|
404
|
+
let activationRoot = null;
|
|
405
|
+
let outputDir = null;
|
|
406
|
+
let skipInstall = false;
|
|
407
|
+
let skipRestart = false;
|
|
408
|
+
let pluginId = DEFAULT_OPERATOR_PROOF_PLUGIN_ID;
|
|
409
|
+
let timeoutMs = DEFAULT_OPERATOR_PROOF_TIMEOUT_MS;
|
|
410
|
+
let json = false;
|
|
411
|
+
let help = false;
|
|
412
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
413
|
+
const arg = argv[index];
|
|
414
|
+
if (arg === "--help" || arg === "-h") {
|
|
415
|
+
help = true;
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
if (arg === "--json") {
|
|
419
|
+
json = true;
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
if (arg === "--skip-install") {
|
|
423
|
+
skipInstall = true;
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
if (arg === "--skip-restart") {
|
|
427
|
+
skipRestart = true;
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
if (arg === "--openclaw-home") {
|
|
431
|
+
const next = argv[index + 1];
|
|
432
|
+
if (next === undefined) {
|
|
433
|
+
throw new Error("--openclaw-home requires a value");
|
|
434
|
+
}
|
|
435
|
+
openclawHome = next;
|
|
436
|
+
index += 1;
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
if (arg === "--activation-root") {
|
|
440
|
+
const next = argv[index + 1];
|
|
441
|
+
if (next === undefined) {
|
|
442
|
+
throw new Error("--activation-root requires a value");
|
|
443
|
+
}
|
|
444
|
+
activationRoot = next;
|
|
445
|
+
index += 1;
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
if (arg === "--output-dir") {
|
|
449
|
+
const next = argv[index + 1];
|
|
450
|
+
if (next === undefined) {
|
|
451
|
+
throw new Error("--output-dir requires a value");
|
|
452
|
+
}
|
|
453
|
+
outputDir = next;
|
|
454
|
+
index += 1;
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
if (arg === "--plugin-id") {
|
|
458
|
+
const next = argv[index + 1];
|
|
459
|
+
if (next === undefined) {
|
|
460
|
+
throw new Error("--plugin-id requires a value");
|
|
461
|
+
}
|
|
462
|
+
pluginId = next;
|
|
463
|
+
index += 1;
|
|
464
|
+
continue;
|
|
465
|
+
}
|
|
466
|
+
if (arg === "--timeout-ms") {
|
|
467
|
+
const next = argv[index + 1];
|
|
468
|
+
if (next === undefined) {
|
|
469
|
+
throw new Error("--timeout-ms requires a value");
|
|
470
|
+
}
|
|
471
|
+
const parsed = Number.parseInt(next, 10);
|
|
472
|
+
if (!Number.isInteger(parsed) || parsed < 1) {
|
|
473
|
+
throw new Error("--timeout-ms must be a positive integer");
|
|
474
|
+
}
|
|
475
|
+
timeoutMs = parsed;
|
|
476
|
+
index += 1;
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
throw new Error(`unknown argument for proof: ${arg}`);
|
|
480
|
+
}
|
|
481
|
+
if (help) {
|
|
482
|
+
return {
|
|
483
|
+
command: "proof",
|
|
484
|
+
openclawHome: "",
|
|
485
|
+
activationRoot: null,
|
|
486
|
+
outputDir: null,
|
|
487
|
+
skipInstall,
|
|
488
|
+
skipRestart,
|
|
489
|
+
pluginId,
|
|
490
|
+
timeoutMs,
|
|
491
|
+
json,
|
|
492
|
+
help
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
if (openclawHome === null) {
|
|
496
|
+
throw new Error("proof requires --openclaw-home <path>");
|
|
497
|
+
}
|
|
498
|
+
const resolvedOpenClawHome = path.resolve(openclawHome);
|
|
499
|
+
if (!existsSyncImpl(resolvedOpenClawHome)) {
|
|
500
|
+
throw new Error(`--openclaw-home directory does not exist: ${resolvedOpenClawHome}`);
|
|
501
|
+
}
|
|
502
|
+
return {
|
|
503
|
+
command: "proof",
|
|
504
|
+
openclawHome: resolvedOpenClawHome,
|
|
505
|
+
activationRoot: activationRoot === null ? null : path.resolve(activationRoot),
|
|
506
|
+
outputDir: outputDir === null ? null : path.resolve(outputDir),
|
|
507
|
+
skipInstall,
|
|
508
|
+
skipRestart,
|
|
509
|
+
pluginId,
|
|
510
|
+
timeoutMs,
|
|
511
|
+
json,
|
|
512
|
+
help
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
export function captureOperatorProofBundle(options) {
|
|
517
|
+
const cliInvocation = options.cliInvocation ?? buildCurrentCliInvocation();
|
|
518
|
+
const runCapture = options.runCapture ?? defaultRunCapture;
|
|
519
|
+
const bundleStartedAt = new Date().toISOString();
|
|
520
|
+
const bundleDir = resolveProofOutputDir(options);
|
|
521
|
+
mkdirSync(bundleDir, { recursive: true });
|
|
522
|
+
const steps = [];
|
|
523
|
+
const gatewayProfile = readOpenClawProfileName(options.openclawHome);
|
|
524
|
+
function addStep(stepId, label, command, args, { skipped = false } = {}) {
|
|
525
|
+
if (skipped) {
|
|
526
|
+
steps.push({
|
|
527
|
+
stepId,
|
|
528
|
+
label,
|
|
529
|
+
shellCommand: shellJoin([command, ...args]),
|
|
530
|
+
skipped: true,
|
|
531
|
+
captureState: "complete",
|
|
532
|
+
resultClass: "success",
|
|
533
|
+
summary: "step intentionally skipped",
|
|
534
|
+
stdoutPath: null,
|
|
535
|
+
stderrPath: null,
|
|
536
|
+
});
|
|
537
|
+
return { stdout: "", stderr: "", exitCode: 0, signal: null, error: null };
|
|
538
|
+
}
|
|
539
|
+
const capture = runCapture(command, args, {
|
|
540
|
+
label,
|
|
541
|
+
cwd: options.cwd ?? process.cwd(),
|
|
542
|
+
env: options.env ?? process.env,
|
|
543
|
+
timeoutMs: options.timeoutMs,
|
|
544
|
+
});
|
|
545
|
+
const { stdoutName, stderrName } = writeStepBundle(bundleDir, stepId, capture);
|
|
546
|
+
const summary = summarizeCapture(capture);
|
|
547
|
+
steps.push({
|
|
548
|
+
stepId,
|
|
549
|
+
label,
|
|
550
|
+
shellCommand: capture.shellCommand ?? shellJoin([command, ...args]),
|
|
551
|
+
startedAt: capture.startedAt ?? null,
|
|
552
|
+
endedAt: capture.endedAt ?? null,
|
|
553
|
+
durationMs: capture.durationMs ?? null,
|
|
554
|
+
exitCode: capture.exitCode ?? null,
|
|
555
|
+
signal: capture.signal ?? null,
|
|
556
|
+
resultClass: summary.resultClass,
|
|
557
|
+
captureState: summary.captureState,
|
|
558
|
+
summary: summary.resultClass === "success"
|
|
559
|
+
? `${label} completed successfully`
|
|
560
|
+
: `${label} ended as ${summary.resultClass}`,
|
|
561
|
+
stdoutPath: stdoutName,
|
|
562
|
+
stderrPath: stderrName,
|
|
563
|
+
});
|
|
564
|
+
return capture;
|
|
565
|
+
}
|
|
566
|
+
addStep("01-install", "install", cliInvocation.command, [...cliInvocation.args, "install", "--openclaw-home", options.openclawHome], { skipped: options.skipInstall === true });
|
|
567
|
+
addStep("02-restart", "gateway restart", "openclaw", buildGatewayArgs("restart", gatewayProfile), { skipped: options.skipRestart === true });
|
|
568
|
+
const gatewayStatusCapture = addStep("03-gateway-status", "gateway status", "openclaw", buildGatewayArgs("status", gatewayProfile));
|
|
569
|
+
const pluginInspectCapture = addStep("04-plugin-inspect", "plugin inspect", "openclaw", ["plugins", "inspect", options.pluginId]);
|
|
570
|
+
const statusCapture = addStep("05-detailed-status", "detailed status", cliInvocation.command, [...cliInvocation.args, "status", "--openclaw-home", options.openclawHome, "--detailed"]);
|
|
571
|
+
const gatewayLogPath = extractGatewayLogPath(gatewayStatusCapture.stdout);
|
|
572
|
+
const activationRoot = extractActivationRoot(statusCapture.stdout, options.activationRoot ?? null);
|
|
573
|
+
const runtimeLoadProofPath = path.join(activationRoot, "attachment-truth", "runtime-load-proofs.json");
|
|
574
|
+
const runtimeLoadProofSnapshot = readJsonSnapshot(runtimeLoadProofPath);
|
|
575
|
+
const gatewayLogText = readTextIfExists(gatewayLogPath);
|
|
576
|
+
const breadcrumbs = extractStartupBreadcrumbs(gatewayLogText, bundleStartedAt);
|
|
577
|
+
const statusSignals = extractStatusSignals(statusCapture.stdout);
|
|
578
|
+
writeText(path.join(bundleDir, "extracted-startup-breadcrumbs.log"), breadcrumbs.all.length === 0
|
|
579
|
+
? "<no matching breadcrumbs found>\n"
|
|
580
|
+
: `${breadcrumbs.all.map((entry) => entry.line).join("\n")}\n`);
|
|
581
|
+
writeJson(path.join(bundleDir, "runtime-load-proof.json"), runtimeLoadProofSnapshot);
|
|
582
|
+
const verdict = buildVerdict({
|
|
583
|
+
steps,
|
|
584
|
+
gatewayStatus: gatewayStatusCapture.stdout,
|
|
585
|
+
pluginInspect: pluginInspectCapture.stdout,
|
|
586
|
+
statusSignals,
|
|
587
|
+
breadcrumbs,
|
|
588
|
+
runtimeLoadProofSnapshot,
|
|
589
|
+
openclawHome: options.openclawHome,
|
|
590
|
+
});
|
|
591
|
+
writeJson(path.join(bundleDir, "steps.json"), {
|
|
592
|
+
bundleStartedAt,
|
|
593
|
+
openclawHome: canonicalizeExistingProofPath(options.openclawHome),
|
|
594
|
+
activationRoot,
|
|
595
|
+
gatewayProfile,
|
|
596
|
+
gatewayLogPath,
|
|
597
|
+
steps,
|
|
598
|
+
});
|
|
599
|
+
writeJson(path.join(bundleDir, "verdict.json"), {
|
|
600
|
+
bundleStartedAt,
|
|
601
|
+
verdict,
|
|
602
|
+
statusSignals,
|
|
603
|
+
breadcrumbs: {
|
|
604
|
+
allCount: breadcrumbs.all.length,
|
|
605
|
+
postBundleCount: breadcrumbs.afterBundleStart.length,
|
|
606
|
+
postBundleKinds: breadcrumbs.afterBundleStart.map((entry) => entry.kind),
|
|
607
|
+
},
|
|
608
|
+
runtimeLoadProofPath,
|
|
609
|
+
runtimeLoadProofError: runtimeLoadProofSnapshot.error,
|
|
610
|
+
});
|
|
611
|
+
writeText(path.join(bundleDir, "summary.md"), buildSummary({
|
|
612
|
+
options,
|
|
613
|
+
steps,
|
|
614
|
+
verdict,
|
|
615
|
+
gatewayStatusText: gatewayStatusCapture.stdout,
|
|
616
|
+
pluginInspectText: pluginInspectCapture.stdout,
|
|
617
|
+
statusSignals,
|
|
618
|
+
breadcrumbs,
|
|
619
|
+
runtimeLoadProofSnapshot,
|
|
620
|
+
}));
|
|
621
|
+
return {
|
|
622
|
+
ok: true,
|
|
623
|
+
bundleDir,
|
|
624
|
+
bundleStartedAt,
|
|
625
|
+
activationRoot,
|
|
626
|
+
gatewayProfile,
|
|
627
|
+
gatewayLogPath,
|
|
628
|
+
runtimeLoadProofPath,
|
|
629
|
+
runtimeLoadProofSnapshot,
|
|
630
|
+
verdict,
|
|
631
|
+
statusSignals,
|
|
632
|
+
steps,
|
|
633
|
+
summaryPath: path.join(bundleDir, "summary.md"),
|
|
634
|
+
stepsPath: path.join(bundleDir, "steps.json"),
|
|
635
|
+
verdictPath: path.join(bundleDir, "verdict.json"),
|
|
636
|
+
breadcrumbPath: path.join(bundleDir, "extracted-startup-breadcrumbs.log"),
|
|
637
|
+
runtimeLoadProofSnapshotPath: path.join(bundleDir, "runtime-load-proof.json"),
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
export function formatOperatorProofResult(result) {
|
|
642
|
+
const lines = [
|
|
643
|
+
`PROOF ${result.verdict.verdict}`,
|
|
644
|
+
` Severity: ${result.verdict.severity}`,
|
|
645
|
+
` Why: ${result.verdict.why}`,
|
|
646
|
+
` Bundle: ${result.bundleDir}`,
|
|
647
|
+
` Summary: ${result.summaryPath}`,
|
|
648
|
+
` Steps: ${result.stepsPath}`,
|
|
649
|
+
` Verdict: ${result.verdictPath}`,
|
|
650
|
+
` Breadcrumbs: ${result.breadcrumbPath}`,
|
|
651
|
+
` Runtime proof: ${result.runtimeLoadProofSnapshotPath}`,
|
|
652
|
+
];
|
|
653
|
+
return lines.join("\n");
|
|
654
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
function formatRawLearningPathSummary(learningPath) {
|
|
2
|
+
return `source=${learningPath.source} pg=${learningPath.policyGradientVersion} method=${learningPath.policyGradientMethod ?? "none"} target=${learningPath.targetConstruction ?? "none"} connect=${learningPath.connectOpsFired ?? "none"} trajectories=${learningPath.reconstructedTrajectoryCount ?? "none"}`;
|
|
3
|
+
}
|
|
4
|
+
function isSeedAwaitingFirstPromotion(status) {
|
|
5
|
+
return status?.brain?.state === "seed_state_authoritative" && status?.brainStatus?.awaitingFirstExport === true;
|
|
6
|
+
}
|
|
7
|
+
function normalizeOptionalString(value) {
|
|
8
|
+
return typeof value === "string" && value.trim().length > 0 ? value : null;
|
|
9
|
+
}
|
|
10
|
+
export function formatOperatorLearningPathSummary({ status, learningPath, tracedLearning }) {
|
|
11
|
+
if (!isSeedAwaitingFirstPromotion(status)) {
|
|
12
|
+
return formatRawLearningPathSummary(learningPath);
|
|
13
|
+
}
|
|
14
|
+
const detailParts = [
|
|
15
|
+
"detail=seed_state_awaiting_first_promotion",
|
|
16
|
+
`tracedPg=${normalizeOptionalString(tracedLearning?.pgVersionUsed) ?? "none"}`,
|
|
17
|
+
`tracedPack=${normalizeOptionalString(tracedLearning?.materializedPackId) ?? "none"}`
|
|
18
|
+
];
|
|
19
|
+
return [
|
|
20
|
+
"source=seed_state",
|
|
21
|
+
"pg=seed",
|
|
22
|
+
"method=not_yet_promoted",
|
|
23
|
+
"target=not_yet_promoted",
|
|
24
|
+
"connect=none",
|
|
25
|
+
"trajectories=none",
|
|
26
|
+
...detailParts
|
|
27
|
+
].join(" ");
|
|
28
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openclawbrain/cli",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.12",
|
|
4
4
|
"description": "OpenClawBrain operator CLI package with install/status helpers, daemon controls, and import/export tooling.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/src/index.js",
|
|
@@ -62,4 +62,3 @@
|
|
|
62
62
|
"test": "node --test dist/test/*.test.js"
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
|
-
|