@openclawbrain/openclaw 0.1.11 → 0.2.0
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 +13 -6
- package/dist/src/cli.d.ts +85 -1
- package/dist/src/cli.js +1500 -27
- package/dist/src/cli.js.map +1 -1
- package/dist/src/daemon.d.ts +26 -0
- package/dist/src/daemon.js +362 -0
- package/dist/src/daemon.js.map +1 -0
- package/dist/src/import-export.d.ts +36 -0
- package/dist/src/import-export.js +171 -0
- package/dist/src/import-export.js.map +1 -0
- package/dist/src/index.d.ts +361 -4
- package/dist/src/index.js +1629 -77
- package/dist/src/index.js.map +1 -1
- package/dist/src/local-session-passive-learning.d.ts +60 -0
- package/dist/src/local-session-passive-learning.js +359 -0
- package/dist/src/local-session-passive-learning.js.map +1 -0
- package/dist/src/resolve-activation-root.d.ts +27 -0
- package/dist/src/resolve-activation-root.js +120 -0
- package/dist/src/resolve-activation-root.js.map +1 -0
- package/dist/src/session-store.d.ts +150 -0
- package/dist/src/session-store.js +199 -0
- package/dist/src/session-store.js.map +1 -0
- package/dist/src/session-tail.d.ts +68 -0
- package/dist/src/session-tail.js +519 -0
- package/dist/src/session-tail.js.map +1 -0
- package/extension/index.ts +50 -0
- package/package.json +16 -13
- package/LICENSE +0 -201
package/dist/src/cli.js
CHANGED
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { execSync } from "node:child_process";
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, readSync, openSync, closeSync, realpathSync, rmSync, statSync, writeFileSync, appendFileSync } from "node:fs";
|
|
3
4
|
import path from "node:path";
|
|
4
|
-
import { pathToFileURL } from "node:url";
|
|
5
|
-
|
|
5
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = path.dirname(__filename);
|
|
8
|
+
import { parseDaemonArgs, runDaemonCommand } from "./daemon.js";
|
|
9
|
+
import { exportBrain, importBrain } from "./import-export.js";
|
|
10
|
+
import { advanceAlwaysOnLearningRuntime, createAlwaysOnLearningRuntimeState, materializeAlwaysOnLearningCandidatePack } from "@openclawbrain/learner";
|
|
11
|
+
import { inspectActivationState, promoteCandidatePack, stageCandidatePack, } from "@openclawbrain/pack-format";
|
|
12
|
+
import { resolveActivationRoot } from "./resolve-activation-root.js";
|
|
13
|
+
import { bootstrapRuntimeAttach, buildOperatorSurfaceReport, compileRuntimeContext, createAsyncTeacherLiveLoop, createRuntimeEventExportScanner, describeCurrentProfileBrainStatus, formatBootstrapRuntimeAttachReport, formatOperatorRollbackReport, loadRuntimeEventExportBundle, rollbackRuntimeAttach, scanLiveEventExport, scanRecordedSession } from "./index.js";
|
|
14
|
+
import { buildPassiveLearningStoreExportFromOpenClawSessionIndex } from "./local-session-passive-learning.js";
|
|
15
|
+
import { discoverOpenClawMainSessionStores, loadOpenClawSessionIndex, readOpenClawSessionFile } from "./session-store.js";
|
|
6
16
|
function quoteShellArg(value) {
|
|
7
17
|
return `'${value.replace(/'/g, `"'"'`)}'`;
|
|
8
18
|
}
|
|
@@ -122,29 +132,59 @@ function buildDoctorDeletedMessage(args) {
|
|
|
122
132
|
function operatorCliHelp() {
|
|
123
133
|
return [
|
|
124
134
|
"Usage:",
|
|
135
|
+
" openclawbrain setup --openclaw-home <path> [options]",
|
|
136
|
+
" openclawbrain attach --activation-root <path> [options]",
|
|
125
137
|
" openclawbrain <status|rollback> --activation-root <path> [options]",
|
|
138
|
+
" openclawbrain context \"message\" [--activation-root <path>]",
|
|
139
|
+
" openclawbrain history [--activation-root <path>] [--limit N] [--json]",
|
|
140
|
+
" openclawbrain scan --session <trace.json> --root <path> [options]",
|
|
141
|
+
" openclawbrain scan --live <event-export-path> --workspace <workspace.json> [options]",
|
|
142
|
+
" openclawbrain learn [--activation-root <path>] [--json]",
|
|
143
|
+
" openclawbrain watch [--activation-root <path>] [--scan-root <path>] [--interval <seconds>]",
|
|
144
|
+
" openclawbrain daemon <start|stop|status|logs> [--activation-root <path>]",
|
|
126
145
|
" openclawbrain-ops <status|rollback> --activation-root <path> [options] # compatibility alias",
|
|
146
|
+
" openclawbrain-ops scan --session <trace.json> --root <path> [options] # compatibility alias",
|
|
127
147
|
"",
|
|
128
148
|
"Options:",
|
|
129
|
-
" --
|
|
149
|
+
" --openclaw-home <path> OpenClaw profile home dir for setup (e.g. ~/.openclaw-Tern).",
|
|
150
|
+
" --shared Set brain-attachment-policy to shared instead of dedicated (setup only).",
|
|
151
|
+
" --activation-root <path> Activation root to bootstrap or inspect.",
|
|
152
|
+
" --pack-root <path> Initial pack root directory (attach only; defaults to <activation-root>/packs/initial).",
|
|
153
|
+
" --workspace-id <id> Workspace identifier for attach provenance (attach only; defaults to 'workspace').",
|
|
130
154
|
" --event-export <path> Event-export bundle root or normalized export JSON payload.",
|
|
131
|
-
" --teacher-snapshot <path> Async teacher snapshot JSON from teacherLoop.snapshot()/flush().",
|
|
155
|
+
" --teacher-snapshot <path> Async teacher snapshot JSON from teacherLoop.snapshot()/flush(); keeps live-first, principal-priority, and passive-backfill learner truth explicit.",
|
|
132
156
|
" --updated-at <iso> Observation time to use for freshness checks.",
|
|
133
157
|
" --brain-attachment-policy <undeclared|dedicated|shared> Override attachment policy semantics for status inspection.",
|
|
158
|
+
" --detailed Show verbose diagnostic output for status (default is human-friendly summary).",
|
|
134
159
|
" --dry-run Preview rollback pointer movement without writing activation state.",
|
|
160
|
+
" --session <path> Sanitized recorded-session trace JSON to replay.",
|
|
161
|
+
" --live <path> Runtime event-export bundle root or normalized export JSON to scan once.",
|
|
162
|
+
" --root <path> Output root for scan --session replay artifacts.",
|
|
163
|
+
" --workspace <path> Workspace metadata JSON for scan --live candidate provenance.",
|
|
164
|
+
" --pack-label <label> Candidate-pack label for scan --live. Defaults to scanner-live-cli.",
|
|
165
|
+
" --observed-at <iso> Observation time for scan --live freshness checks.",
|
|
166
|
+
" --snapshot-out <path> Write the one-shot scan --live snapshot JSON.",
|
|
167
|
+
" --limit <N> Maximum number of history entries to show (default: 20, history only).",
|
|
168
|
+
" --scan-root <path> Event-export scan root for watch mode (defaults to <activation-root>/event-exports).",
|
|
169
|
+
" --interval <seconds> Polling interval for watch mode (default: 30).",
|
|
135
170
|
" --json Emit machine-readable JSON instead of text.",
|
|
136
171
|
" --help Show this help.",
|
|
137
172
|
"",
|
|
138
173
|
"Common flow:",
|
|
139
|
-
" 0.
|
|
174
|
+
" 0. context openclawbrain context \"hello\" — preview the brain context that would be injected for a message",
|
|
175
|
+
" 0. attach openclawbrain attach --activation-root <path>",
|
|
140
176
|
" 1. status answer \"How's the brain?\" for the current profile on that activation root",
|
|
141
177
|
" 2. status --json read the canonical current_profile_brain_status.v1 object for that same boundary",
|
|
142
178
|
" 3. rollback --dry-run preview active <- previous, active -> candidate",
|
|
143
179
|
" 4. rollback apply the rollback when the preview says ready",
|
|
180
|
+
" 5. scan --session replay one sanitized session trace across no_brain, seed_pack, and learned_replay",
|
|
181
|
+
" 6. scan --live scan one live event export into teacher/learner state without claiming a daemon is running",
|
|
182
|
+
" status --teacher-snapshot keeps the current live-first / principal-priority / passive-backfill learner order visible when that snapshot exists",
|
|
144
183
|
"",
|
|
145
184
|
"Exit codes:",
|
|
146
185
|
" status: 0 on successful inspection, 1 on input/read failure.",
|
|
147
|
-
" rollback: 0 when ready/applied, 1 when blocked or on input/read failure."
|
|
186
|
+
" rollback: 0 when ready/applied, 1 when blocked or on input/read failure.",
|
|
187
|
+
" scan: 0 on successful replay/scan, 1 on input/read failure."
|
|
148
188
|
].join("\n");
|
|
149
189
|
}
|
|
150
190
|
function yesNo(value) {
|
|
@@ -157,36 +197,134 @@ function formatPrincipalLatest(report) {
|
|
|
157
197
|
const latest = report.principal.latestFeedback;
|
|
158
198
|
return latest === null ? "none" : `${latest.teacherIdentity}/${latest.kind}`;
|
|
159
199
|
}
|
|
200
|
+
function formatPrincipalCheckpointFrontier(report) {
|
|
201
|
+
const checkpoint = report.learning.leadingPrincipalCheckpoint;
|
|
202
|
+
if (checkpoint === null) {
|
|
203
|
+
return "none";
|
|
204
|
+
}
|
|
205
|
+
const learnedThrough = checkpoint.learnedThroughSequence ?? "none";
|
|
206
|
+
const newestPending = checkpoint.newestPendingSequence ?? "none";
|
|
207
|
+
return `${checkpoint.teacherIdentity}:${learnedThrough}->${newestPending}`;
|
|
208
|
+
}
|
|
160
209
|
function formatStructuralOps(report) {
|
|
161
210
|
const structuralOps = report.graph.structuralOps;
|
|
162
211
|
return structuralOps === null
|
|
163
212
|
? "none"
|
|
164
213
|
: `split:${structuralOps.split},merge:${structuralOps.merge},prune:${structuralOps.prune},connect:${structuralOps.connect}`;
|
|
165
214
|
}
|
|
215
|
+
function formatScannerSurfaces(report) {
|
|
216
|
+
return report.supervision.scanSurfaces.length === 0 ? "none" : report.supervision.scanSurfaces.join("|");
|
|
217
|
+
}
|
|
218
|
+
function formatLearningBuckets(report) {
|
|
219
|
+
const buckets = report.learning.pendingByBucket;
|
|
220
|
+
if (buckets === null) {
|
|
221
|
+
return "none";
|
|
222
|
+
}
|
|
223
|
+
return `pi:${buckets.principal_immediate},pb:${buckets.principal_backfill},live:${buckets.live},backfill:${buckets.backfill}`;
|
|
224
|
+
}
|
|
225
|
+
function formatLearningWarnings(report) {
|
|
226
|
+
return report.learning.warningStates.length === 0 ? "none" : report.learning.warningStates.join("|");
|
|
227
|
+
}
|
|
166
228
|
function formatCurrentProfileStatusSummary(status, report) {
|
|
229
|
+
const profileIdSuffix = status.profile.profileId === null ? "" : ` id=${status.profile.profileId}`;
|
|
167
230
|
return [
|
|
168
231
|
`STATUS ${status.brainStatus.status}`,
|
|
169
232
|
`answer ${status.brain.summary}`,
|
|
170
233
|
`host runtime=${status.host.runtimeOwner} activation=${status.host.activationRoot}`,
|
|
171
|
-
`profile selector=${status.profile.selector} attachment=${status.attachment.state} policy=${status.attachment.policyMode}`,
|
|
234
|
+
`profile selector=${status.profile.selector}${profileIdSuffix} attachment=${status.attachment.state} policy=${status.attachment.policyMode}`,
|
|
235
|
+
`manyProfile surface=${report.manyProfile.operatorSurface} policy=${report.manyProfile.declaredAttachmentPolicy} intent=${report.manyProfile.sameGatewayIntent} checkedProof=${report.manyProfile.checkedInProofTopology} sameGatewayProof=${yesNo(report.manyProfile.sameGatewayProof)} sharedWriteProof=${yesNo(report.manyProfile.sharedWriteSafetyProof)}`,
|
|
172
236
|
`brain pack=${status.brain.activePackId ?? "none"} state=${status.brain.state} init=${status.brain.initMode ?? "unknown"} routeFreshness=${status.brain.routeFreshness} lastPromotion=${status.brain.lastPromotionAt ?? "none"} router=${status.brain.routerIdentity ?? "none"}`,
|
|
173
237
|
`serve state=${status.brainStatus.serveState} failOpen=${yesNo(status.brainStatus.failOpen)} hardFail=${yesNo(report.servePath.hardRequirementViolated)} usedRouteFn=${yesNo(status.brainStatus.usedLearnedRouteFn)} awaitingFirstExport=${yesNo(status.brainStatus.awaitingFirstExport)} detail=${status.brainStatus.detail}`,
|
|
174
238
|
`route router=${report.servePath.routerIdentity ?? status.brain.routerIdentity ?? "none"} supervision=${report.servePath.refreshStatus ?? status.brain.routeFreshness} freshness=${report.servePath.freshnessChecksum ?? "none"}`,
|
|
175
|
-
`budget requested=${report.servePath.requestedBudgetStrategy ?? "none"} resolved=${report.servePath.resolvedBudgetStrategy ?? "none"} maxBlocks=${report.servePath.resolvedMaxContextBlocks ?? "none"} source=${report.servePath.structuralBudgetSource ?? "none"}`,
|
|
176
|
-
`
|
|
239
|
+
`budget requested=${report.servePath.requestedBudgetStrategy ?? "none"} resolved=${report.servePath.resolvedBudgetStrategy ?? "none"} maxBlocks=${report.servePath.resolvedMaxContextBlocks ?? "none"} source=${report.servePath.structuralBudgetSource ?? "none"} origin=${status.brainStatus.structuralDecision.origin} basis=${status.brainStatus.structuralDecision.basis}`,
|
|
240
|
+
`decision ${status.brainStatus.structuralDecision.detail}`,
|
|
241
|
+
`principal latest=${formatPrincipalLatest(report)} pending=${report.principal.pendingCount ?? report.learning.pendingPrincipalCount ?? "none"} checkpoint=${formatPrincipalCheckpointFrontier(report)} downstream=${yesNo(report.principal.servingDownstreamOfLatestCorrection)} lag=${report.learning.principalLagToPromotion.sequenceLag ?? "none"}`,
|
|
242
|
+
`scanner flowing=${yesNo(report.supervision.flowing)} scan=${report.supervision.scanPolicy ?? "none"} surfaces=${formatScannerSurfaces(report)} labels=${report.supervision.humanLabelCount ?? "none"}/${report.supervision.selfLabelCount ?? "none"} attributable=${report.supervision.attributedEventCount ?? "none"}/${report.supervision.totalEventCount ?? "none"} digests=${report.supervision.selectionDigestCount ?? "none"}`,
|
|
177
243
|
`graph source=${report.graph.runtimePlasticitySource ?? "none"} ops=${formatStructuralOps(report)} changed=${yesNo(report.graph.changed)} pruned=${report.graph.prunedBlockCount ?? "none"} strongest=${report.graph.strongestBlockId ?? "none"} summary=${report.graph.operatorSummary ?? report.graph.detail}`,
|
|
178
|
-
`learning bootstrapped=${yesNo(report.learning.bootstrapped)} mode=${report.learning.mode} next=${report.learning.nextPriorityLane} lastPack=${report.learning.lastMaterializedPackId ?? "none"}`,
|
|
244
|
+
`learning state=${report.learning.backlogState} bootstrapped=${yesNo(report.learning.bootstrapped)} mode=${report.learning.mode} next=${report.learning.nextPriorityLane} priority=${report.learning.nextPriorityBucket} pending=${report.learning.pendingLive ?? "none"}/${report.learning.pendingBackfill ?? "none"} buckets=${formatLearningBuckets(report)} warn=${formatLearningWarnings(report)} lastPack=${report.learning.lastMaterializedPackId ?? "none"} detail=${report.learning.detail}`,
|
|
179
245
|
`rollback ready=${yesNo(report.rollback.allowed)} state=${report.rollback.state} previous=${report.rollback.previousPackId ?? "none"}`,
|
|
180
246
|
`proof lastExport=${status.brain.lastExportAt ?? "none"} lastLearningUpdate=${status.brain.lastLearningUpdateAt ?? "none"} lastPromotion=${status.brain.lastPromotionAt ?? "none"}`,
|
|
181
247
|
`logs root=${status.brain.logRoot ?? "none"}`,
|
|
182
248
|
`turn attribution=${status.currentTurnAttribution === null ? "none" : status.currentTurnAttribution.contract}`
|
|
183
249
|
].join("\n");
|
|
184
250
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
251
|
+
// Auto-detection of activation root is now handled by the shared
|
|
252
|
+
// resolveActivationRoot() helper in resolve-activation-root.ts.
|
|
253
|
+
// It is imported at the top and used by requireActivationRoot below.
|
|
254
|
+
function shortenPath(fullPath) {
|
|
255
|
+
const homeDir = process.env.HOME ?? "";
|
|
256
|
+
if (homeDir.length > 0 && fullPath.startsWith(homeDir)) {
|
|
257
|
+
return "~" + fullPath.slice(homeDir.length);
|
|
188
258
|
}
|
|
189
|
-
return
|
|
259
|
+
return fullPath;
|
|
260
|
+
}
|
|
261
|
+
function formatHumanFriendlyStatus(status, report) {
|
|
262
|
+
// Brain status line
|
|
263
|
+
const brainActive = status.brainStatus.status === "ok" || status.brainStatus.serveState === "serving_active_pack";
|
|
264
|
+
const brainIcon = brainActive ? "Active ✓" : status.brainStatus.status === "fail" ? "Inactive ✗" : `${status.brainStatus.status}`;
|
|
265
|
+
// Pack line
|
|
266
|
+
const packId = status.brain.activePackId ?? "none";
|
|
267
|
+
const packShort = packId.length > 9 ? packId.slice(0, 9) : packId;
|
|
268
|
+
const state = status.brain.state ?? "unknown";
|
|
269
|
+
// Activation root
|
|
270
|
+
const activationPath = shortenPath(status.host.activationRoot);
|
|
271
|
+
// Policy
|
|
272
|
+
const policy = status.attachment.policyMode ?? report.manyProfile.declaredAttachmentPolicy ?? "undeclared";
|
|
273
|
+
const lines = [
|
|
274
|
+
`Brain: ${brainIcon}`,
|
|
275
|
+
`Pack: ${packShort} (${state})`,
|
|
276
|
+
`Activation: ${activationPath}`,
|
|
277
|
+
`Policy: ${policy}`
|
|
278
|
+
];
|
|
279
|
+
// Add learning/serve warnings if relevant
|
|
280
|
+
if (report.learning.warningStates.length > 0) {
|
|
281
|
+
lines.push(`Warnings: ${report.learning.warningStates.join(", ")}`);
|
|
282
|
+
}
|
|
283
|
+
if (status.brainStatus.awaitingFirstExport) {
|
|
284
|
+
lines.push(`Note: Awaiting first event export`);
|
|
285
|
+
}
|
|
286
|
+
return lines.join("\n");
|
|
287
|
+
}
|
|
288
|
+
function requireActivationRoot(input, _command) {
|
|
289
|
+
// Use the shared auto-detect chain for ALL commands:
|
|
290
|
+
// explicit flag → ~/.openclawbrain/activation → extension scan → clear error
|
|
291
|
+
return resolveActivationRoot({
|
|
292
|
+
explicit: input.activationRoot.trim().length > 0 ? input.activationRoot : null,
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
function readJsonFile(filePath) {
|
|
296
|
+
return JSON.parse(readFileSync(path.resolve(filePath), "utf8"));
|
|
297
|
+
}
|
|
298
|
+
function loadCliScanLiveExport(livePath) {
|
|
299
|
+
const resolvedPath = path.resolve(livePath);
|
|
300
|
+
const stats = statSync(resolvedPath);
|
|
301
|
+
if (stats.isDirectory()) {
|
|
302
|
+
return loadRuntimeEventExportBundle(resolvedPath).normalizedEventExport;
|
|
303
|
+
}
|
|
304
|
+
return readJsonFile(resolvedPath);
|
|
305
|
+
}
|
|
306
|
+
function formatScanSessionSummary(result) {
|
|
307
|
+
return [
|
|
308
|
+
"SCAN session ok",
|
|
309
|
+
`trace ${result.bundle.traceId}`,
|
|
310
|
+
`winner ${result.bundle.summary.winnerMode ?? "none"}`,
|
|
311
|
+
`scores ${result.bundle.modes.map((mode) => `${mode.mode}=${mode.summary.qualityScore}`).join(" ")}`,
|
|
312
|
+
`turns ${result.bundle.modes[0]?.turns.length ?? 0}`,
|
|
313
|
+
`hashes fixture=${result.fixtureHash} score=${result.bundle.scoreHash}`,
|
|
314
|
+
`root ${result.rootDir}`
|
|
315
|
+
].join("\n");
|
|
316
|
+
}
|
|
317
|
+
function formatScanLiveSummary(result, snapshotOutPath) {
|
|
318
|
+
const materializedPackId = result.snapshot.learner.lastMaterialization?.candidate.summary.packId ?? "none";
|
|
319
|
+
const materializationReason = result.snapshot.learner.lastMaterialization?.reason ?? "none";
|
|
320
|
+
return [
|
|
321
|
+
"SCAN live ok",
|
|
322
|
+
`source digest=${result.supervision.exportDigest} session=${result.supervision.sessionId ?? "none"} channel=${result.supervision.channel ?? "none"} range=${result.supervision.eventRange.start}-${result.supervision.eventRange.end}/${result.supervision.eventRange.count}`,
|
|
323
|
+
`teacher artifacts=${result.snapshot.teacher.artifactCount} freshness=${result.snapshot.teacher.latestFreshness} humanLabels=${result.supervision.humanLabelCount} noop=${result.snapshot.diagnostics.lastNoOpReason}`,
|
|
324
|
+
`learner packLabel=${result.packLabel} materialized=${materializedPackId} reason=${materializationReason}`,
|
|
325
|
+
`observed ${result.observedAt}`,
|
|
326
|
+
`snapshot ${snapshotOutPath ?? "none"}`
|
|
327
|
+
].join("\n");
|
|
190
328
|
}
|
|
191
329
|
export function parseOperatorCliArgs(argv) {
|
|
192
330
|
let command = "status";
|
|
@@ -195,16 +333,341 @@ export function parseOperatorCliArgs(argv) {
|
|
|
195
333
|
let teacherSnapshotPath = null;
|
|
196
334
|
let updatedAt = null;
|
|
197
335
|
let brainAttachmentPolicy = null;
|
|
336
|
+
let sessionPath = null;
|
|
337
|
+
let livePath = null;
|
|
338
|
+
let rootDir = null;
|
|
339
|
+
let workspacePath = null;
|
|
340
|
+
let packLabel = null;
|
|
341
|
+
let packRoot = null;
|
|
342
|
+
let workspaceId = null;
|
|
343
|
+
let observedAt = null;
|
|
344
|
+
let snapshotOutPath = null;
|
|
345
|
+
let openclawHome = null;
|
|
346
|
+
let shared = false;
|
|
198
347
|
let json = false;
|
|
199
348
|
let help = false;
|
|
200
349
|
let dryRun = false;
|
|
350
|
+
let detailed = false;
|
|
201
351
|
const args = [...argv];
|
|
202
352
|
if (args[0] === "doctor") {
|
|
203
353
|
throw new Error(buildDoctorDeletedMessage(args.slice(1)));
|
|
204
354
|
}
|
|
205
|
-
if (args[0] === "
|
|
355
|
+
if (args[0] === "daemon") {
|
|
356
|
+
args.shift();
|
|
357
|
+
return parseDaemonArgs(args);
|
|
358
|
+
}
|
|
359
|
+
if (args[0] === "status" || args[0] === "rollback" || args[0] === "scan" || args[0] === "attach" || args[0] === "setup" || args[0] === "context" || args[0] === "history" || args[0] === "learn" || args[0] === "watch" || args[0] === "export" || args[0] === "import" || args[0] === "reset") {
|
|
206
360
|
command = args.shift();
|
|
207
361
|
}
|
|
362
|
+
if (command === "learn") {
|
|
363
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
364
|
+
const arg = args[index];
|
|
365
|
+
if (arg === "--help" || arg === "-h") {
|
|
366
|
+
help = true;
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
if (arg === "--json") {
|
|
370
|
+
json = true;
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
if (arg === "--activation-root") {
|
|
374
|
+
const next = args[index + 1];
|
|
375
|
+
if (next === undefined) {
|
|
376
|
+
throw new Error("--activation-root requires a value");
|
|
377
|
+
}
|
|
378
|
+
activationRoot = next;
|
|
379
|
+
index += 1;
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
if (arg.startsWith("--")) {
|
|
383
|
+
throw new Error(`unknown argument for learn: ${arg}`);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
if (help) {
|
|
387
|
+
return { command, activationRoot: "", json, help };
|
|
388
|
+
}
|
|
389
|
+
return {
|
|
390
|
+
command,
|
|
391
|
+
activationRoot: resolveActivationRoot({ explicit: activationRoot }),
|
|
392
|
+
json,
|
|
393
|
+
help
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
if (command === "watch") {
|
|
397
|
+
let watchScanRoot = null;
|
|
398
|
+
let watchInterval = 30;
|
|
399
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
400
|
+
const arg = args[index];
|
|
401
|
+
if (arg === "--help" || arg === "-h") {
|
|
402
|
+
help = true;
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
if (arg === "--json") {
|
|
406
|
+
json = true;
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
if (arg === "--activation-root") {
|
|
410
|
+
const next = args[index + 1];
|
|
411
|
+
if (next === undefined) {
|
|
412
|
+
throw new Error("--activation-root requires a value");
|
|
413
|
+
}
|
|
414
|
+
activationRoot = next;
|
|
415
|
+
index += 1;
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
if (arg === "--scan-root") {
|
|
419
|
+
const next = args[index + 1];
|
|
420
|
+
if (next === undefined) {
|
|
421
|
+
throw new Error("--scan-root requires a value");
|
|
422
|
+
}
|
|
423
|
+
watchScanRoot = next;
|
|
424
|
+
index += 1;
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
if (arg === "--interval") {
|
|
428
|
+
const next = args[index + 1];
|
|
429
|
+
if (next === undefined) {
|
|
430
|
+
throw new Error("--interval requires a value");
|
|
431
|
+
}
|
|
432
|
+
const parsed = Number.parseInt(next, 10);
|
|
433
|
+
if (!Number.isInteger(parsed) || parsed < 1) {
|
|
434
|
+
throw new Error("--interval must be a positive integer (seconds)");
|
|
435
|
+
}
|
|
436
|
+
watchInterval = parsed;
|
|
437
|
+
index += 1;
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
if (arg.startsWith("--")) {
|
|
441
|
+
throw new Error(`unknown argument for watch: ${arg}`);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
if (help) {
|
|
445
|
+
return { command, activationRoot: "", scanRoot: null, interval: 30, json, help };
|
|
446
|
+
}
|
|
447
|
+
return {
|
|
448
|
+
command,
|
|
449
|
+
activationRoot: resolveActivationRoot({ explicit: activationRoot }),
|
|
450
|
+
scanRoot: watchScanRoot,
|
|
451
|
+
interval: watchInterval,
|
|
452
|
+
json,
|
|
453
|
+
help
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
if (command === "context") {
|
|
457
|
+
const messageParts = [];
|
|
458
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
459
|
+
const arg = args[index];
|
|
460
|
+
if (arg === "--help" || arg === "-h") {
|
|
461
|
+
help = true;
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
464
|
+
if (arg === "--json") {
|
|
465
|
+
json = true;
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
if (arg === "--activation-root") {
|
|
469
|
+
const next = args[index + 1];
|
|
470
|
+
if (next === undefined) {
|
|
471
|
+
throw new Error("--activation-root requires a value");
|
|
472
|
+
}
|
|
473
|
+
activationRoot = next;
|
|
474
|
+
index += 1;
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
477
|
+
if (arg.startsWith("--")) {
|
|
478
|
+
throw new Error(`unknown argument for context: ${arg}`);
|
|
479
|
+
}
|
|
480
|
+
messageParts.push(arg);
|
|
481
|
+
}
|
|
482
|
+
if (help) {
|
|
483
|
+
return { command, message: "", activationRoot: "", json, help };
|
|
484
|
+
}
|
|
485
|
+
if (messageParts.length === 0) {
|
|
486
|
+
throw new Error("context requires a message argument: openclawbrain context \"your message\"");
|
|
487
|
+
}
|
|
488
|
+
return {
|
|
489
|
+
command,
|
|
490
|
+
message: messageParts.join(" "),
|
|
491
|
+
activationRoot: resolveActivationRoot({ explicit: activationRoot }),
|
|
492
|
+
json,
|
|
493
|
+
help
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
if (command === "history") {
|
|
497
|
+
let historyLimit = 20;
|
|
498
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
499
|
+
const arg = args[index];
|
|
500
|
+
if (arg === "--help" || arg === "-h") {
|
|
501
|
+
help = true;
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
if (arg === "--json") {
|
|
505
|
+
json = true;
|
|
506
|
+
continue;
|
|
507
|
+
}
|
|
508
|
+
if (arg === "--activation-root") {
|
|
509
|
+
const next = args[index + 1];
|
|
510
|
+
if (next === undefined) {
|
|
511
|
+
throw new Error("--activation-root requires a value");
|
|
512
|
+
}
|
|
513
|
+
activationRoot = next;
|
|
514
|
+
index += 1;
|
|
515
|
+
continue;
|
|
516
|
+
}
|
|
517
|
+
if (arg === "--limit") {
|
|
518
|
+
const next = args[index + 1];
|
|
519
|
+
if (next === undefined) {
|
|
520
|
+
throw new Error("--limit requires a value");
|
|
521
|
+
}
|
|
522
|
+
const parsed = Number.parseInt(next, 10);
|
|
523
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
524
|
+
throw new Error("--limit must be a positive integer");
|
|
525
|
+
}
|
|
526
|
+
historyLimit = parsed;
|
|
527
|
+
index += 1;
|
|
528
|
+
continue;
|
|
529
|
+
}
|
|
530
|
+
if (arg.startsWith("--")) {
|
|
531
|
+
throw new Error(`unknown argument for history: ${arg}`);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
if (help) {
|
|
535
|
+
return { command, activationRoot: "", limit: historyLimit, json, help };
|
|
536
|
+
}
|
|
537
|
+
return {
|
|
538
|
+
command,
|
|
539
|
+
activationRoot: resolveActivationRoot({ explicit: activationRoot }),
|
|
540
|
+
limit: historyLimit,
|
|
541
|
+
json,
|
|
542
|
+
help
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
if (command === "export") {
|
|
546
|
+
let outputPath = null;
|
|
547
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
548
|
+
const arg = args[index];
|
|
549
|
+
if (arg === "--help" || arg === "-h") {
|
|
550
|
+
help = true;
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
if (arg === "--json") {
|
|
554
|
+
json = true;
|
|
555
|
+
continue;
|
|
556
|
+
}
|
|
557
|
+
if (arg === "--activation-root") {
|
|
558
|
+
const next = args[index + 1];
|
|
559
|
+
if (next === undefined)
|
|
560
|
+
throw new Error("--activation-root requires a value");
|
|
561
|
+
activationRoot = next;
|
|
562
|
+
index += 1;
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
if (arg === "-o" || arg === "--output") {
|
|
566
|
+
const next = args[index + 1];
|
|
567
|
+
if (next === undefined)
|
|
568
|
+
throw new Error("-o / --output requires a value");
|
|
569
|
+
outputPath = next;
|
|
570
|
+
index += 1;
|
|
571
|
+
continue;
|
|
572
|
+
}
|
|
573
|
+
if (arg.startsWith("--"))
|
|
574
|
+
throw new Error(`unknown argument for export: ${arg}`);
|
|
575
|
+
}
|
|
576
|
+
if (help)
|
|
577
|
+
return { command, activationRoot: "", outputPath: "", json, help };
|
|
578
|
+
if (outputPath === null)
|
|
579
|
+
throw new Error("export requires -o <output.tar.gz>");
|
|
580
|
+
return {
|
|
581
|
+
command,
|
|
582
|
+
activationRoot: resolveActivationRoot({ explicit: activationRoot }),
|
|
583
|
+
outputPath: path.resolve(outputPath),
|
|
584
|
+
json,
|
|
585
|
+
help,
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
if (command === "import") {
|
|
589
|
+
let archivePath = null;
|
|
590
|
+
let force = false;
|
|
591
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
592
|
+
const arg = args[index];
|
|
593
|
+
if (arg === "--help" || arg === "-h") {
|
|
594
|
+
help = true;
|
|
595
|
+
continue;
|
|
596
|
+
}
|
|
597
|
+
if (arg === "--json") {
|
|
598
|
+
json = true;
|
|
599
|
+
continue;
|
|
600
|
+
}
|
|
601
|
+
if (arg === "--force") {
|
|
602
|
+
force = true;
|
|
603
|
+
continue;
|
|
604
|
+
}
|
|
605
|
+
if (arg === "--activation-root") {
|
|
606
|
+
const next = args[index + 1];
|
|
607
|
+
if (next === undefined)
|
|
608
|
+
throw new Error("--activation-root requires a value");
|
|
609
|
+
activationRoot = next;
|
|
610
|
+
index += 1;
|
|
611
|
+
continue;
|
|
612
|
+
}
|
|
613
|
+
if (arg.startsWith("--"))
|
|
614
|
+
throw new Error(`unknown argument for import: ${arg}`);
|
|
615
|
+
if (archivePath === null) {
|
|
616
|
+
archivePath = arg;
|
|
617
|
+
}
|
|
618
|
+
else {
|
|
619
|
+
throw new Error(`unexpected positional argument: ${arg}`);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
if (help)
|
|
623
|
+
return { command, archivePath: "", activationRoot: "", force: false, json, help };
|
|
624
|
+
if (archivePath === null)
|
|
625
|
+
throw new Error("import requires <backup.tar.gz> argument");
|
|
626
|
+
return {
|
|
627
|
+
command,
|
|
628
|
+
archivePath: path.resolve(archivePath),
|
|
629
|
+
activationRoot: resolveActivationRoot({ explicit: activationRoot }),
|
|
630
|
+
force,
|
|
631
|
+
json,
|
|
632
|
+
help,
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
if (command === "reset") {
|
|
636
|
+
let yes = false;
|
|
637
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
638
|
+
const arg = args[index];
|
|
639
|
+
if (arg === "--help" || arg === "-h") {
|
|
640
|
+
help = true;
|
|
641
|
+
continue;
|
|
642
|
+
}
|
|
643
|
+
if (arg === "--json") {
|
|
644
|
+
json = true;
|
|
645
|
+
continue;
|
|
646
|
+
}
|
|
647
|
+
if (arg === "--yes" || arg === "-y") {
|
|
648
|
+
yes = true;
|
|
649
|
+
continue;
|
|
650
|
+
}
|
|
651
|
+
if (arg === "--activation-root") {
|
|
652
|
+
const next = args[index + 1];
|
|
653
|
+
if (next === undefined)
|
|
654
|
+
throw new Error("--activation-root requires a value");
|
|
655
|
+
activationRoot = next;
|
|
656
|
+
index += 1;
|
|
657
|
+
continue;
|
|
658
|
+
}
|
|
659
|
+
throw new Error(`unknown argument for reset: ${arg}`);
|
|
660
|
+
}
|
|
661
|
+
if (help)
|
|
662
|
+
return { command, activationRoot: "", yes: false, json, help };
|
|
663
|
+
return {
|
|
664
|
+
command,
|
|
665
|
+
activationRoot: resolveActivationRoot({ explicit: activationRoot }),
|
|
666
|
+
yes,
|
|
667
|
+
json,
|
|
668
|
+
help
|
|
669
|
+
};
|
|
670
|
+
}
|
|
208
671
|
for (let index = 0; index < args.length; index += 1) {
|
|
209
672
|
const arg = args[index];
|
|
210
673
|
if (arg === "--help" || arg === "-h") {
|
|
@@ -219,7 +682,23 @@ export function parseOperatorCliArgs(argv) {
|
|
|
219
682
|
dryRun = true;
|
|
220
683
|
continue;
|
|
221
684
|
}
|
|
685
|
+
if (arg === "--shared") {
|
|
686
|
+
shared = true;
|
|
687
|
+
continue;
|
|
688
|
+
}
|
|
689
|
+
if (arg === "--detailed") {
|
|
690
|
+
detailed = true;
|
|
691
|
+
continue;
|
|
692
|
+
}
|
|
222
693
|
const next = args[index + 1];
|
|
694
|
+
if (arg === "--openclaw-home") {
|
|
695
|
+
if (next === undefined) {
|
|
696
|
+
throw new Error("--openclaw-home requires a value");
|
|
697
|
+
}
|
|
698
|
+
openclawHome = next;
|
|
699
|
+
index += 1;
|
|
700
|
+
continue;
|
|
701
|
+
}
|
|
223
702
|
if (arg === "--activation-root") {
|
|
224
703
|
if (next === undefined) {
|
|
225
704
|
throw new Error("--activation-root requires a value");
|
|
@@ -263,10 +742,162 @@ export function parseOperatorCliArgs(argv) {
|
|
|
263
742
|
index += 1;
|
|
264
743
|
continue;
|
|
265
744
|
}
|
|
745
|
+
if (arg === "--session") {
|
|
746
|
+
if (next === undefined) {
|
|
747
|
+
throw new Error("--session requires a value");
|
|
748
|
+
}
|
|
749
|
+
sessionPath = next;
|
|
750
|
+
index += 1;
|
|
751
|
+
continue;
|
|
752
|
+
}
|
|
753
|
+
if (arg === "--live") {
|
|
754
|
+
if (next === undefined) {
|
|
755
|
+
throw new Error("--live requires a value");
|
|
756
|
+
}
|
|
757
|
+
livePath = next;
|
|
758
|
+
index += 1;
|
|
759
|
+
continue;
|
|
760
|
+
}
|
|
761
|
+
if (arg === "--root") {
|
|
762
|
+
if (next === undefined) {
|
|
763
|
+
throw new Error("--root requires a value");
|
|
764
|
+
}
|
|
765
|
+
rootDir = next;
|
|
766
|
+
index += 1;
|
|
767
|
+
continue;
|
|
768
|
+
}
|
|
769
|
+
if (arg === "--workspace") {
|
|
770
|
+
if (next === undefined) {
|
|
771
|
+
throw new Error("--workspace requires a value");
|
|
772
|
+
}
|
|
773
|
+
workspacePath = next;
|
|
774
|
+
index += 1;
|
|
775
|
+
continue;
|
|
776
|
+
}
|
|
777
|
+
if (arg === "--pack-label") {
|
|
778
|
+
if (next === undefined) {
|
|
779
|
+
throw new Error("--pack-label requires a value");
|
|
780
|
+
}
|
|
781
|
+
packLabel = next;
|
|
782
|
+
index += 1;
|
|
783
|
+
continue;
|
|
784
|
+
}
|
|
785
|
+
if (arg === "--observed-at") {
|
|
786
|
+
if (next === undefined) {
|
|
787
|
+
throw new Error("--observed-at requires a value");
|
|
788
|
+
}
|
|
789
|
+
observedAt = next;
|
|
790
|
+
index += 1;
|
|
791
|
+
continue;
|
|
792
|
+
}
|
|
793
|
+
if (arg === "--snapshot-out") {
|
|
794
|
+
if (next === undefined) {
|
|
795
|
+
throw new Error("--snapshot-out requires a value");
|
|
796
|
+
}
|
|
797
|
+
snapshotOutPath = next;
|
|
798
|
+
index += 1;
|
|
799
|
+
continue;
|
|
800
|
+
}
|
|
801
|
+
if (arg === "--pack-root") {
|
|
802
|
+
if (next === undefined) {
|
|
803
|
+
throw new Error("--pack-root requires a value");
|
|
804
|
+
}
|
|
805
|
+
packRoot = next;
|
|
806
|
+
index += 1;
|
|
807
|
+
continue;
|
|
808
|
+
}
|
|
809
|
+
if (arg === "--workspace-id") {
|
|
810
|
+
if (next === undefined) {
|
|
811
|
+
throw new Error("--workspace-id requires a value");
|
|
812
|
+
}
|
|
813
|
+
workspaceId = next;
|
|
814
|
+
index += 1;
|
|
815
|
+
continue;
|
|
816
|
+
}
|
|
266
817
|
throw new Error(`unknown argument: ${arg}`);
|
|
267
818
|
}
|
|
819
|
+
if (command === "setup") {
|
|
820
|
+
if (help) {
|
|
821
|
+
return { command, openclawHome: "", activationRoot: "", shared: false, workspaceId: "", json, help };
|
|
822
|
+
}
|
|
823
|
+
if (openclawHome === null || openclawHome.trim().length === 0) {
|
|
824
|
+
throw new Error("--openclaw-home is required for setup");
|
|
825
|
+
}
|
|
826
|
+
const resolvedOpenclawHome = path.resolve(openclawHome);
|
|
827
|
+
const defaultActivationRoot = path.resolve(process.env.HOME ?? "~", ".openclawbrain", "activation");
|
|
828
|
+
const resolvedActivationRoot = activationRoot !== null ? path.resolve(activationRoot) : defaultActivationRoot;
|
|
829
|
+
const dirName = path.basename(resolvedOpenclawHome);
|
|
830
|
+
const derivedWorkspaceId = dirName.startsWith(".openclaw-") ? dirName.slice(".openclaw-".length) : dirName;
|
|
831
|
+
const resolvedWorkspaceId = workspaceId ?? derivedWorkspaceId;
|
|
832
|
+
return {
|
|
833
|
+
command,
|
|
834
|
+
openclawHome: resolvedOpenclawHome,
|
|
835
|
+
activationRoot: resolvedActivationRoot,
|
|
836
|
+
shared,
|
|
837
|
+
workspaceId: resolvedWorkspaceId,
|
|
838
|
+
json,
|
|
839
|
+
help
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
if (command === "attach") {
|
|
843
|
+
if (help) {
|
|
844
|
+
return { command, activationRoot: "", packRoot: "", packLabel: "", workspaceId: "", brainAttachmentPolicy: null, json, help };
|
|
845
|
+
}
|
|
846
|
+
if (activationRoot === null || activationRoot.trim().length === 0) {
|
|
847
|
+
throw new Error("--activation-root is required for attach");
|
|
848
|
+
}
|
|
849
|
+
const resolvedActivationRoot = path.resolve(activationRoot);
|
|
850
|
+
const resolvedPackRoot = packRoot !== null
|
|
851
|
+
? path.resolve(packRoot)
|
|
852
|
+
: path.resolve(resolvedActivationRoot, "packs", "initial");
|
|
853
|
+
const resolvedWorkspaceId = workspaceId ?? "workspace";
|
|
854
|
+
const resolvedPackLabel = packLabel ?? "cli-attach";
|
|
855
|
+
return {
|
|
856
|
+
command,
|
|
857
|
+
activationRoot: resolvedActivationRoot,
|
|
858
|
+
packRoot: resolvedPackRoot,
|
|
859
|
+
packLabel: resolvedPackLabel,
|
|
860
|
+
workspaceId: resolvedWorkspaceId,
|
|
861
|
+
brainAttachmentPolicy: brainAttachmentPolicy,
|
|
862
|
+
json,
|
|
863
|
+
help
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
if (command === "scan") {
|
|
867
|
+
if ((sessionPath === null && livePath === null) || (sessionPath !== null && livePath !== null)) {
|
|
868
|
+
throw new Error("scan requires exactly one of --session or --live");
|
|
869
|
+
}
|
|
870
|
+
if (sessionPath !== null) {
|
|
871
|
+
if (rootDir === null) {
|
|
872
|
+
throw new Error("--root is required for scan --session");
|
|
873
|
+
}
|
|
874
|
+
if (workspacePath !== null || packLabel !== null || observedAt !== null || snapshotOutPath !== null) {
|
|
875
|
+
throw new Error("--workspace, --pack-label, --observed-at, and --snapshot-out only apply to scan --live");
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
if (livePath !== null) {
|
|
879
|
+
if (workspacePath === null) {
|
|
880
|
+
throw new Error("--workspace is required for scan --live");
|
|
881
|
+
}
|
|
882
|
+
if (rootDir !== null) {
|
|
883
|
+
throw new Error("--root only applies to scan --session");
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
return {
|
|
887
|
+
command,
|
|
888
|
+
json,
|
|
889
|
+
help,
|
|
890
|
+
sessionPath,
|
|
891
|
+
livePath,
|
|
892
|
+
rootDir,
|
|
893
|
+
workspacePath,
|
|
894
|
+
packLabel,
|
|
895
|
+
observedAt,
|
|
896
|
+
snapshotOutPath
|
|
897
|
+
};
|
|
898
|
+
}
|
|
268
899
|
return {
|
|
269
|
-
command,
|
|
900
|
+
command: command,
|
|
270
901
|
input: {
|
|
271
902
|
activationRoot: activationRoot ?? "",
|
|
272
903
|
eventExportPath,
|
|
@@ -276,7 +907,8 @@ export function parseOperatorCliArgs(argv) {
|
|
|
276
907
|
},
|
|
277
908
|
json,
|
|
278
909
|
help,
|
|
279
|
-
dryRun
|
|
910
|
+
dryRun,
|
|
911
|
+
detailed
|
|
280
912
|
};
|
|
281
913
|
}
|
|
282
914
|
function isDirectCliRun(entryArg, moduleUrl) {
|
|
@@ -290,20 +922,856 @@ function isDirectCliRun(entryArg, moduleUrl) {
|
|
|
290
922
|
return pathToFileURL(path.resolve(entryArg)).href === moduleUrl;
|
|
291
923
|
}
|
|
292
924
|
}
|
|
925
|
+
/**
|
|
926
|
+
* Resolve the path to the pre-built extension template shipped with this package.
|
|
927
|
+
* Falls back to a generated string if the template file is missing (e.g. in tests).
|
|
928
|
+
*/
|
|
929
|
+
function resolveExtensionTemplatePath() {
|
|
930
|
+
const candidates = [
|
|
931
|
+
path.resolve(__dirname, "..", "extension", "index.ts"),
|
|
932
|
+
path.resolve(__dirname, "..", "..", "extension", "index.ts"),
|
|
933
|
+
];
|
|
934
|
+
for (const candidate of candidates) {
|
|
935
|
+
if (existsSync(candidate)) {
|
|
936
|
+
return candidate;
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
throw new Error("Pre-built extension template not found. Searched:\n" +
|
|
940
|
+
candidates.map((c) => ` - ${c}`).join("\n"));
|
|
941
|
+
}
|
|
942
|
+
function buildExtensionIndexTs(activationRoot) {
|
|
943
|
+
const templatePath = resolveExtensionTemplatePath();
|
|
944
|
+
const template = readFileSync(templatePath, "utf8");
|
|
945
|
+
return template.replace(/const ACTIVATION_ROOT = "__ACTIVATION_ROOT__";/, `const ACTIVATION_ROOT = ${JSON.stringify(activationRoot)};`);
|
|
946
|
+
}
|
|
947
|
+
function buildExtensionPackageJson() {
|
|
948
|
+
return JSON.stringify({
|
|
949
|
+
name: "openclawbrain-extension",
|
|
950
|
+
version: "0.1.0",
|
|
951
|
+
private: true,
|
|
952
|
+
type: "module",
|
|
953
|
+
dependencies: {
|
|
954
|
+
"@openclawbrain/openclaw": ">=0.2.0"
|
|
955
|
+
}
|
|
956
|
+
}, null, 2) + "\n";
|
|
957
|
+
}
|
|
958
|
+
function buildExtensionPluginManifest() {
|
|
959
|
+
return JSON.stringify({
|
|
960
|
+
id: "openclawbrain",
|
|
961
|
+
name: "OpenClawBrain",
|
|
962
|
+
description: "Learned memory and context from OpenClawBrain",
|
|
963
|
+
version: "0.2.0"
|
|
964
|
+
}, null, 2) + "\n";
|
|
965
|
+
}
|
|
966
|
+
function formatContextForHuman(result) {
|
|
967
|
+
if (!result.ok) {
|
|
968
|
+
if (result.fallbackToStaticContext) {
|
|
969
|
+
return "No learned context yet. Talk to your agent and check back.";
|
|
970
|
+
}
|
|
971
|
+
return `Brain error: ${result.error}`;
|
|
972
|
+
}
|
|
973
|
+
if (result.brainContext.trim().length === 0) {
|
|
974
|
+
return "No learned context yet. Talk to your agent and check back.";
|
|
975
|
+
}
|
|
976
|
+
return result.brainContext;
|
|
977
|
+
}
|
|
978
|
+
function runContextCommand(parsed) {
|
|
979
|
+
const result = compileRuntimeContext({
|
|
980
|
+
activationRoot: parsed.activationRoot,
|
|
981
|
+
message: parsed.message
|
|
982
|
+
});
|
|
983
|
+
if (parsed.json) {
|
|
984
|
+
console.log(JSON.stringify({
|
|
985
|
+
ok: result.ok,
|
|
986
|
+
activationRoot: result.activationRoot,
|
|
987
|
+
activePackId: result.ok ? result.activePackId : null,
|
|
988
|
+
brainContext: result.brainContext,
|
|
989
|
+
fallbackToStaticContext: result.ok ? false : result.fallbackToStaticContext,
|
|
990
|
+
hardRequirementViolated: result.ok ? false : result.hardRequirementViolated,
|
|
991
|
+
error: result.ok ? null : result.error
|
|
992
|
+
}, null, 2));
|
|
993
|
+
}
|
|
994
|
+
else {
|
|
995
|
+
console.log(formatContextForHuman(result));
|
|
996
|
+
}
|
|
997
|
+
return 0;
|
|
998
|
+
}
|
|
999
|
+
function formatHistoryTimestamp(iso) {
|
|
1000
|
+
const date = new Date(iso);
|
|
1001
|
+
const year = date.getFullYear();
|
|
1002
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
1003
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
1004
|
+
const hours = String(date.getHours()).padStart(2, "0");
|
|
1005
|
+
const minutes = String(date.getMinutes()).padStart(2, "0");
|
|
1006
|
+
return `${year}-${month}-${day} ${hours}:${minutes}`;
|
|
1007
|
+
}
|
|
1008
|
+
function loadManifestSafe(manifestPath) {
|
|
1009
|
+
try {
|
|
1010
|
+
if (!existsSync(manifestPath)) {
|
|
1011
|
+
return null;
|
|
1012
|
+
}
|
|
1013
|
+
return JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
1014
|
+
}
|
|
1015
|
+
catch {
|
|
1016
|
+
return null;
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
function buildHistoryEntry(record, slot, isActive) {
|
|
1020
|
+
const manifest = loadManifestSafe(record.manifestPath);
|
|
1021
|
+
const eventCount = record.eventRange.count;
|
|
1022
|
+
// Count corrections from the learning surface in the manifest provenance
|
|
1023
|
+
let correctionCount = 0;
|
|
1024
|
+
if (manifest !== null) {
|
|
1025
|
+
const learningSurface = manifest.provenance?.learningSurface;
|
|
1026
|
+
if (learningSurface?.labelHarvest) {
|
|
1027
|
+
correctionCount = learningSurface.labelHarvest.humanLabels;
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
// Determine the label: seed packs have 0 events, promoted packs have events
|
|
1031
|
+
const label = eventCount === 0 ? "seed" : "promoted";
|
|
1032
|
+
return {
|
|
1033
|
+
packId: record.packId,
|
|
1034
|
+
slot,
|
|
1035
|
+
label,
|
|
1036
|
+
builtAt: record.builtAt,
|
|
1037
|
+
updatedAt: record.updatedAt,
|
|
1038
|
+
eventCount,
|
|
1039
|
+
correctionCount,
|
|
1040
|
+
current: isActive
|
|
1041
|
+
};
|
|
1042
|
+
}
|
|
1043
|
+
function runHistoryCommand(parsed) {
|
|
1044
|
+
const activationRoot = parsed.activationRoot;
|
|
1045
|
+
const pointersPath = path.join(activationRoot, "activation-pointers.json");
|
|
1046
|
+
if (!existsSync(pointersPath)) {
|
|
1047
|
+
if (parsed.json) {
|
|
1048
|
+
console.log(JSON.stringify({ entries: [], empty: true, message: "No history yet. Run: openclawbrain setup" }, null, 2));
|
|
1049
|
+
}
|
|
1050
|
+
else {
|
|
1051
|
+
console.log("No history yet. Run: openclawbrain setup");
|
|
1052
|
+
}
|
|
1053
|
+
return 0;
|
|
1054
|
+
}
|
|
1055
|
+
let pointers;
|
|
1056
|
+
try {
|
|
1057
|
+
pointers = JSON.parse(readFileSync(pointersPath, "utf8"));
|
|
1058
|
+
}
|
|
1059
|
+
catch (error) {
|
|
1060
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1061
|
+
console.error(`Failed to read activation pointers: ${message}`);
|
|
1062
|
+
return 1;
|
|
1063
|
+
}
|
|
1064
|
+
// Build history entries from pointers: active is most recent, then previous
|
|
1065
|
+
const entries = [];
|
|
1066
|
+
if (pointers.active !== null) {
|
|
1067
|
+
entries.push(buildHistoryEntry(pointers.active, "active", true));
|
|
1068
|
+
}
|
|
1069
|
+
if (pointers.previous !== null) {
|
|
1070
|
+
// Only add if different from active
|
|
1071
|
+
if (pointers.active === null || pointers.previous.packId !== pointers.active.packId) {
|
|
1072
|
+
entries.push(buildHistoryEntry(pointers.previous, "previous", false));
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
if (pointers.candidate !== null) {
|
|
1076
|
+
// Only add if different from active and previous
|
|
1077
|
+
const isDuplicate = entries.some((e) => e.packId === pointers.candidate.packId);
|
|
1078
|
+
if (!isDuplicate) {
|
|
1079
|
+
entries.push(buildHistoryEntry(pointers.candidate, "candidate", false));
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
if (entries.length === 0) {
|
|
1083
|
+
if (parsed.json) {
|
|
1084
|
+
console.log(JSON.stringify({ entries: [], empty: true, message: "No history yet. Run: openclawbrain setup" }, null, 2));
|
|
1085
|
+
}
|
|
1086
|
+
else {
|
|
1087
|
+
console.log("No history yet. Run: openclawbrain setup");
|
|
1088
|
+
}
|
|
1089
|
+
return 0;
|
|
1090
|
+
}
|
|
1091
|
+
// Sort by updatedAt descending (most recent first)
|
|
1092
|
+
entries.sort((a, b) => Date.parse(b.updatedAt) - Date.parse(a.updatedAt));
|
|
1093
|
+
// Apply limit
|
|
1094
|
+
const limited = entries.slice(0, parsed.limit);
|
|
1095
|
+
if (parsed.json) {
|
|
1096
|
+
console.log(JSON.stringify({
|
|
1097
|
+
entries: limited,
|
|
1098
|
+
activationRoot,
|
|
1099
|
+
empty: false
|
|
1100
|
+
}, null, 2));
|
|
1101
|
+
return 0;
|
|
1102
|
+
}
|
|
1103
|
+
// Human-readable output
|
|
1104
|
+
for (const entry of limited) {
|
|
1105
|
+
const packShort = entry.packId.length > 9 ? entry.packId.slice(0, 9) : entry.packId;
|
|
1106
|
+
const timestamp = formatHistoryTimestamp(entry.updatedAt);
|
|
1107
|
+
const tag = entry.current ? "(current)" : "(previous)";
|
|
1108
|
+
let line = `${packShort.padEnd(10)} ${entry.label.padEnd(10)} ${timestamp} ${tag}`;
|
|
1109
|
+
// Add stats suffix for promoted packs
|
|
1110
|
+
if (entry.label === "promoted" && (entry.correctionCount > 0 || entry.eventCount > 0)) {
|
|
1111
|
+
const parts = [];
|
|
1112
|
+
if (entry.correctionCount > 0) {
|
|
1113
|
+
parts.push(`${entry.correctionCount} corrections`);
|
|
1114
|
+
}
|
|
1115
|
+
if (entry.eventCount > 0) {
|
|
1116
|
+
parts.push(`${entry.eventCount} events`);
|
|
1117
|
+
}
|
|
1118
|
+
line += ` — ${parts.join(", ")}`;
|
|
1119
|
+
}
|
|
1120
|
+
console.log(line);
|
|
1121
|
+
}
|
|
1122
|
+
return 0;
|
|
1123
|
+
}
|
|
1124
|
+
function runSetupCommand(parsed) {
|
|
1125
|
+
const steps = [];
|
|
1126
|
+
// 1. Validate --openclaw-home exists and has openclaw.json
|
|
1127
|
+
if (!existsSync(parsed.openclawHome)) {
|
|
1128
|
+
throw new Error(`--openclaw-home directory does not exist: ${parsed.openclawHome}`);
|
|
1129
|
+
}
|
|
1130
|
+
const openclawJsonPath = path.join(parsed.openclawHome, "openclaw.json");
|
|
1131
|
+
if (!existsSync(openclawJsonPath)) {
|
|
1132
|
+
throw new Error(`openclaw.json not found in ${parsed.openclawHome}`);
|
|
1133
|
+
}
|
|
1134
|
+
// 2. Create activation root if needed
|
|
1135
|
+
if (!existsSync(parsed.activationRoot)) {
|
|
1136
|
+
mkdirSync(parsed.activationRoot, { recursive: true });
|
|
1137
|
+
steps.push(`Created activation root: ${parsed.activationRoot}`);
|
|
1138
|
+
}
|
|
1139
|
+
else {
|
|
1140
|
+
steps.push(`Activation root exists: ${parsed.activationRoot}`);
|
|
1141
|
+
}
|
|
1142
|
+
// 3. Run bootstrapRuntimeAttach if not already attached
|
|
1143
|
+
const activationPointersPath = path.join(parsed.activationRoot, "activation-pointers.json");
|
|
1144
|
+
if (existsSync(activationPointersPath)) {
|
|
1145
|
+
steps.push("Brain already attached (activation-pointers.json exists), skipping bootstrap.");
|
|
1146
|
+
}
|
|
1147
|
+
else {
|
|
1148
|
+
const packRoot = path.resolve(parsed.activationRoot, "packs", "initial");
|
|
1149
|
+
mkdirSync(packRoot, { recursive: true });
|
|
1150
|
+
const brainAttachmentPolicy = parsed.shared ? "shared" : "dedicated";
|
|
1151
|
+
const result = bootstrapRuntimeAttach({
|
|
1152
|
+
profileSelector: "current_profile",
|
|
1153
|
+
brainAttachmentPolicy,
|
|
1154
|
+
activationRoot: parsed.activationRoot,
|
|
1155
|
+
packRoot,
|
|
1156
|
+
packLabel: "setup-cli",
|
|
1157
|
+
workspace: {
|
|
1158
|
+
workspaceId: parsed.workspaceId,
|
|
1159
|
+
snapshotId: `${parsed.workspaceId}@setup-${new Date().toISOString().slice(0, 10)}`,
|
|
1160
|
+
capturedAt: new Date().toISOString(),
|
|
1161
|
+
rootDir: parsed.openclawHome,
|
|
1162
|
+
revision: "cli-setup-v1"
|
|
1163
|
+
},
|
|
1164
|
+
interactionEvents: [],
|
|
1165
|
+
feedbackEvents: []
|
|
1166
|
+
});
|
|
1167
|
+
steps.push(`Bootstrapped brain attach: ${result.status}`);
|
|
1168
|
+
}
|
|
1169
|
+
// 4-7. Write extension files
|
|
1170
|
+
const extensionDir = path.join(parsed.openclawHome, "extensions", "openclawbrain");
|
|
1171
|
+
mkdirSync(extensionDir, { recursive: true });
|
|
1172
|
+
// 4. Write index.ts
|
|
1173
|
+
const indexTsPath = path.join(extensionDir, "index.ts");
|
|
1174
|
+
writeFileSync(indexTsPath, buildExtensionIndexTs(parsed.activationRoot), "utf8");
|
|
1175
|
+
steps.push(`Wrote extension: ${indexTsPath}`);
|
|
1176
|
+
// 5. Write package.json
|
|
1177
|
+
const packageJsonPath = path.join(extensionDir, "package.json");
|
|
1178
|
+
writeFileSync(packageJsonPath, buildExtensionPackageJson(), "utf8");
|
|
1179
|
+
steps.push(`Wrote package.json: ${packageJsonPath}`);
|
|
1180
|
+
// 6. npm install
|
|
1181
|
+
try {
|
|
1182
|
+
execSync("npm install --ignore-scripts", { cwd: extensionDir, stdio: "pipe" });
|
|
1183
|
+
steps.push("Ran npm install --ignore-scripts");
|
|
1184
|
+
}
|
|
1185
|
+
catch (err) {
|
|
1186
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1187
|
+
steps.push(`npm install failed (non-fatal): ${message}`);
|
|
1188
|
+
}
|
|
1189
|
+
// 7. Write plugin manifest
|
|
1190
|
+
const manifestPath = path.join(extensionDir, "openclaw.plugin.json");
|
|
1191
|
+
writeFileSync(manifestPath, buildExtensionPluginManifest(), "utf8");
|
|
1192
|
+
steps.push(`Wrote manifest: ${manifestPath}`);
|
|
1193
|
+
// 8. Write BRAIN.md to workspace directories
|
|
1194
|
+
const brainMdContent = [
|
|
1195
|
+
"## OpenClawBrain",
|
|
1196
|
+
`You have a learning brain attached at ${parsed.activationRoot}.`,
|
|
1197
|
+
"- It learns automatically from your conversations",
|
|
1198
|
+
'- Corrections matter — "no, actually X" teaches the brain X',
|
|
1199
|
+
"- You don't manage it — background daemon handles learning",
|
|
1200
|
+
"- Check: `openclawbrain status`",
|
|
1201
|
+
"- Rollback: `openclawbrain rollback`",
|
|
1202
|
+
'- See what brain knows: `openclawbrain context "your question"`',
|
|
1203
|
+
""
|
|
1204
|
+
].join("\n");
|
|
1205
|
+
const agentsMdBrainRef = "\n5. Read `BRAIN.md` — your learning brain context\n";
|
|
1206
|
+
try {
|
|
1207
|
+
const entries = readdirSync(parsed.openclawHome, { withFileTypes: true });
|
|
1208
|
+
const workspaceDirs = entries
|
|
1209
|
+
.filter(e => e.isDirectory() && e.name.startsWith("workspace-"))
|
|
1210
|
+
.map(e => path.join(parsed.openclawHome, e.name));
|
|
1211
|
+
// If no workspace-* dirs found, check if openclawHome itself is a workspace
|
|
1212
|
+
if (workspaceDirs.length === 0) {
|
|
1213
|
+
workspaceDirs.push(parsed.openclawHome);
|
|
1214
|
+
}
|
|
1215
|
+
for (const wsDir of workspaceDirs) {
|
|
1216
|
+
const brainMdPath = path.join(wsDir, "BRAIN.md");
|
|
1217
|
+
writeFileSync(brainMdPath, brainMdContent, "utf8");
|
|
1218
|
+
steps.push(`Wrote BRAIN.md: ${brainMdPath}`);
|
|
1219
|
+
// If AGENTS.md exists, append brain reference to startup sequence
|
|
1220
|
+
const agentsMdPath = path.join(wsDir, "AGENTS.md");
|
|
1221
|
+
if (existsSync(agentsMdPath)) {
|
|
1222
|
+
const agentsContent = readFileSync(agentsMdPath, "utf8");
|
|
1223
|
+
if (!agentsContent.includes("BRAIN.md")) {
|
|
1224
|
+
// Find the startup sequence section and append after the last numbered item
|
|
1225
|
+
const startupMarker = "## Session Startup";
|
|
1226
|
+
if (agentsContent.includes(startupMarker)) {
|
|
1227
|
+
// Find the numbered list in the startup section and append after last item
|
|
1228
|
+
const lines = agentsContent.split("\n");
|
|
1229
|
+
let lastNumberedIdx = -1;
|
|
1230
|
+
let inStartup = false;
|
|
1231
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1232
|
+
const line = lines[i] ?? "";
|
|
1233
|
+
if (line.includes(startupMarker)) {
|
|
1234
|
+
inStartup = true;
|
|
1235
|
+
continue;
|
|
1236
|
+
}
|
|
1237
|
+
if (inStartup && /^\d+\.\s/.test(line.trim())) {
|
|
1238
|
+
lastNumberedIdx = i;
|
|
1239
|
+
}
|
|
1240
|
+
if (inStartup && line.startsWith("## ") && !line.includes(startupMarker)) {
|
|
1241
|
+
break;
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
if (lastNumberedIdx >= 0) {
|
|
1245
|
+
lines.splice(lastNumberedIdx + 1, 0, agentsMdBrainRef.trimEnd());
|
|
1246
|
+
writeFileSync(agentsMdPath, lines.join("\n"), "utf8");
|
|
1247
|
+
steps.push(`Updated AGENTS.md startup sequence: ${agentsMdPath}`);
|
|
1248
|
+
}
|
|
1249
|
+
else {
|
|
1250
|
+
appendFileSync(agentsMdPath, agentsMdBrainRef, "utf8");
|
|
1251
|
+
steps.push(`Appended BRAIN.md reference to AGENTS.md: ${agentsMdPath}`);
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
else {
|
|
1255
|
+
appendFileSync(agentsMdPath, agentsMdBrainRef, "utf8");
|
|
1256
|
+
steps.push(`Appended BRAIN.md reference to AGENTS.md: ${agentsMdPath}`);
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
else {
|
|
1260
|
+
steps.push(`AGENTS.md already references BRAIN.md: ${agentsMdPath}`);
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
catch (err) {
|
|
1266
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1267
|
+
steps.push(`BRAIN.md generation failed (non-fatal): ${message}`);
|
|
1268
|
+
}
|
|
1269
|
+
// 9. Print summary
|
|
1270
|
+
if (parsed.json) {
|
|
1271
|
+
console.log(JSON.stringify({
|
|
1272
|
+
command: "setup",
|
|
1273
|
+
openclawHome: parsed.openclawHome,
|
|
1274
|
+
activationRoot: parsed.activationRoot,
|
|
1275
|
+
workspaceId: parsed.workspaceId,
|
|
1276
|
+
shared: parsed.shared,
|
|
1277
|
+
extensionDir,
|
|
1278
|
+
steps
|
|
1279
|
+
}, null, 2));
|
|
1280
|
+
}
|
|
1281
|
+
else {
|
|
1282
|
+
console.log("SETUP complete\n");
|
|
1283
|
+
for (const step of steps) {
|
|
1284
|
+
console.log(` ✓ ${step}`);
|
|
1285
|
+
}
|
|
1286
|
+
console.log("");
|
|
1287
|
+
console.log(`Check status: openclawbrain status --activation-root ${quoteShellArg(parsed.activationRoot)}`);
|
|
1288
|
+
console.log("Next step: Restart your OpenClaw gateway to activate the extension.");
|
|
1289
|
+
}
|
|
1290
|
+
return 0;
|
|
1291
|
+
}
|
|
1292
|
+
function runLearnCommand(parsed) {
|
|
1293
|
+
const activationRoot = parsed.activationRoot;
|
|
1294
|
+
// 1. Discover local session stores
|
|
1295
|
+
const stores = discoverOpenClawMainSessionStores();
|
|
1296
|
+
if (stores.length === 0) {
|
|
1297
|
+
if (parsed.json) {
|
|
1298
|
+
console.log(JSON.stringify({ command: "learn", activationRoot, scannedSessions: 0, newEvents: 0, materialized: null, promoted: false, message: "No local session stores found." }));
|
|
1299
|
+
}
|
|
1300
|
+
else {
|
|
1301
|
+
console.log("No new session data. Brain is up to date.");
|
|
1302
|
+
}
|
|
1303
|
+
return 0;
|
|
1304
|
+
}
|
|
1305
|
+
// 2. Build passive learning export from ALL discovered sessions
|
|
1306
|
+
let totalSessions = 0;
|
|
1307
|
+
let totalInteractionEvents = 0;
|
|
1308
|
+
let totalFeedbackEvents = 0;
|
|
1309
|
+
const allInteractionEvents = [];
|
|
1310
|
+
const allFeedbackEvents = [];
|
|
1311
|
+
for (const store of stores) {
|
|
1312
|
+
const sessionIndex = loadOpenClawSessionIndex(store.indexPath);
|
|
1313
|
+
const storeExport = buildPassiveLearningStoreExportFromOpenClawSessionIndex({
|
|
1314
|
+
sessionIndex,
|
|
1315
|
+
readSessionRecords: (_sessionKey, entry) => {
|
|
1316
|
+
const sessionFile = entry.sessionFile;
|
|
1317
|
+
if (typeof sessionFile !== "string" || sessionFile.trim().length === 0) {
|
|
1318
|
+
return [];
|
|
1319
|
+
}
|
|
1320
|
+
try {
|
|
1321
|
+
return readOpenClawSessionFile(sessionFile);
|
|
1322
|
+
}
|
|
1323
|
+
catch {
|
|
1324
|
+
return [];
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
});
|
|
1328
|
+
totalSessions += storeExport.sessions.length;
|
|
1329
|
+
totalInteractionEvents += storeExport.interactionEvents.length;
|
|
1330
|
+
totalFeedbackEvents += storeExport.feedbackEvents.length;
|
|
1331
|
+
allInteractionEvents.push(...storeExport.interactionEvents);
|
|
1332
|
+
allFeedbackEvents.push(...storeExport.feedbackEvents);
|
|
1333
|
+
}
|
|
1334
|
+
const totalEvents = totalInteractionEvents + totalFeedbackEvents;
|
|
1335
|
+
if (totalEvents === 0) {
|
|
1336
|
+
if (parsed.json) {
|
|
1337
|
+
console.log(JSON.stringify({ command: "learn", activationRoot, scannedSessions: totalSessions, newEvents: 0, materialized: null, promoted: false, message: "No new session data. Brain is up to date." }));
|
|
1338
|
+
}
|
|
1339
|
+
else {
|
|
1340
|
+
console.log("No new session data. Brain is up to date.");
|
|
1341
|
+
}
|
|
1342
|
+
return 0;
|
|
1343
|
+
}
|
|
1344
|
+
// 3. Run single learning cycle
|
|
1345
|
+
const now = new Date().toISOString();
|
|
1346
|
+
const learnerResult = advanceAlwaysOnLearningRuntime({
|
|
1347
|
+
packLabel: "learn-cli",
|
|
1348
|
+
workspace: {
|
|
1349
|
+
workspaceId: "learn-cli",
|
|
1350
|
+
snapshotId: `learn-cli@${now.slice(0, 10)}`,
|
|
1351
|
+
capturedAt: now,
|
|
1352
|
+
rootDir: activationRoot,
|
|
1353
|
+
revision: "learn-cli-v1"
|
|
1354
|
+
},
|
|
1355
|
+
interactionEvents: allInteractionEvents,
|
|
1356
|
+
feedbackEvents: allFeedbackEvents,
|
|
1357
|
+
learnedRouting: true,
|
|
1358
|
+
state: createAlwaysOnLearningRuntimeState(),
|
|
1359
|
+
builtAt: now
|
|
1360
|
+
});
|
|
1361
|
+
// 4. If materialization produced, materialize → stage → promote
|
|
1362
|
+
if (learnerResult.materialization !== null) {
|
|
1363
|
+
const candidatePackRoot = path.join(activationRoot, "packs", `learn-cli-${Date.now()}`);
|
|
1364
|
+
mkdirSync(candidatePackRoot, { recursive: true });
|
|
1365
|
+
const candidateDescriptor = materializeAlwaysOnLearningCandidatePack(candidatePackRoot, learnerResult.materialization);
|
|
1366
|
+
stageCandidatePack(activationRoot, candidatePackRoot, {
|
|
1367
|
+
updatedAt: now,
|
|
1368
|
+
reason: "learn_cli_stage"
|
|
1369
|
+
});
|
|
1370
|
+
promoteCandidatePack(activationRoot, {
|
|
1371
|
+
updatedAt: now,
|
|
1372
|
+
reason: "learn_cli_promote"
|
|
1373
|
+
});
|
|
1374
|
+
const packId = candidateDescriptor.manifest.packId;
|
|
1375
|
+
if (parsed.json) {
|
|
1376
|
+
console.log(JSON.stringify({
|
|
1377
|
+
command: "learn",
|
|
1378
|
+
activationRoot,
|
|
1379
|
+
scannedSessions: totalSessions,
|
|
1380
|
+
newEvents: totalEvents,
|
|
1381
|
+
materialized: packId,
|
|
1382
|
+
promoted: true,
|
|
1383
|
+
message: `Scanned ${totalSessions} sessions, ${totalEvents} new events, materialized ${packId}, promoted.`
|
|
1384
|
+
}, null, 2));
|
|
1385
|
+
}
|
|
1386
|
+
else {
|
|
1387
|
+
console.log(`Scanned ${totalSessions} sessions, ${totalEvents} new events, materialized ${packId}, promoted.`);
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
else {
|
|
1391
|
+
if (parsed.json) {
|
|
1392
|
+
console.log(JSON.stringify({
|
|
1393
|
+
command: "learn",
|
|
1394
|
+
activationRoot,
|
|
1395
|
+
scannedSessions: totalSessions,
|
|
1396
|
+
newEvents: totalEvents,
|
|
1397
|
+
materialized: null,
|
|
1398
|
+
promoted: false,
|
|
1399
|
+
message: "No new session data. Brain is up to date."
|
|
1400
|
+
}, null, 2));
|
|
1401
|
+
}
|
|
1402
|
+
else {
|
|
1403
|
+
console.log("No new session data. Brain is up to date.");
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
return 0;
|
|
1407
|
+
}
|
|
1408
|
+
function formatTimestamp() {
|
|
1409
|
+
const now = new Date();
|
|
1410
|
+
return `[${now.toTimeString().slice(0, 8)}]`;
|
|
1411
|
+
}
|
|
1412
|
+
function watchLog(message) {
|
|
1413
|
+
console.log(`${formatTimestamp()} ${message}`);
|
|
1414
|
+
}
|
|
1415
|
+
async function runWatchCommand(parsed) {
|
|
1416
|
+
const activationRoot = parsed.activationRoot;
|
|
1417
|
+
const scanRoot = parsed.scanRoot !== null
|
|
1418
|
+
? path.resolve(parsed.scanRoot)
|
|
1419
|
+
: path.resolve(activationRoot, "event-exports");
|
|
1420
|
+
const intervalMs = parsed.interval * 1000;
|
|
1421
|
+
watchLog(`Watch starting — activation: ${shortenPath(activationRoot)}`);
|
|
1422
|
+
watchLog(`Scan root: ${shortenPath(scanRoot)} interval: ${parsed.interval}s`);
|
|
1423
|
+
const scanner = createRuntimeEventExportScanner({ scanRoot });
|
|
1424
|
+
const teacherLoop = createAsyncTeacherLiveLoop({
|
|
1425
|
+
packLabel: "watch-cli",
|
|
1426
|
+
workspace: {
|
|
1427
|
+
workspaceId: "watch-cli",
|
|
1428
|
+
snapshotId: `watch-cli@${new Date().toISOString().slice(0, 10)}`,
|
|
1429
|
+
capturedAt: new Date().toISOString(),
|
|
1430
|
+
rootDir: activationRoot,
|
|
1431
|
+
revision: "watch-cli-v1"
|
|
1432
|
+
},
|
|
1433
|
+
learnedRouting: true
|
|
1434
|
+
});
|
|
1435
|
+
let stopping = false;
|
|
1436
|
+
const onSignal = () => {
|
|
1437
|
+
if (stopping) {
|
|
1438
|
+
process.exit(1);
|
|
1439
|
+
}
|
|
1440
|
+
stopping = true;
|
|
1441
|
+
watchLog("Stopping... (Ctrl+C again to force)");
|
|
1442
|
+
};
|
|
1443
|
+
process.on("SIGINT", onSignal);
|
|
1444
|
+
process.on("SIGTERM", onSignal);
|
|
1445
|
+
while (!stopping) {
|
|
1446
|
+
try {
|
|
1447
|
+
const scanResult = scanner.scanOnce();
|
|
1448
|
+
const liveCount = scanResult.live.length;
|
|
1449
|
+
const backfillCount = scanResult.backfill.length;
|
|
1450
|
+
const totalSelected = scanResult.selected.length;
|
|
1451
|
+
if (totalSelected === 0) {
|
|
1452
|
+
watchLog("Scanning... no changes");
|
|
1453
|
+
}
|
|
1454
|
+
else {
|
|
1455
|
+
const totalEvents = scanResult.selected.reduce((sum, hit) => sum + hit.eventRange.count, 0);
|
|
1456
|
+
watchLog(`Scanning... ${totalSelected} session${totalSelected === 1 ? "" : "s"} changed, ${totalEvents} new event${totalEvents === 1 ? "" : "s"}`);
|
|
1457
|
+
// Feed exports into teacher/learner pipeline
|
|
1458
|
+
const ingestResult = await teacherLoop.ingestRuntimeEventExportScannerScan(scanResult);
|
|
1459
|
+
const snapshot = ingestResult.snapshot;
|
|
1460
|
+
const materialization = snapshot.learner.lastMaterialization;
|
|
1461
|
+
if (materialization !== null) {
|
|
1462
|
+
const packId = materialization.candidate.summary.packId;
|
|
1463
|
+
const shortPackId = packId.length > 16 ? packId.slice(0, 16) : packId;
|
|
1464
|
+
watchLog(`Learning: materialized ${shortPackId}`);
|
|
1465
|
+
// Attempt stage + promote
|
|
1466
|
+
try {
|
|
1467
|
+
const candidateRootDir = path.resolve(activationRoot, "packs", packId);
|
|
1468
|
+
mkdirSync(candidateRootDir, { recursive: true });
|
|
1469
|
+
materializeAlwaysOnLearningCandidatePack(candidateRootDir, materialization);
|
|
1470
|
+
const now = new Date().toISOString();
|
|
1471
|
+
stageCandidatePack(activationRoot, candidateRootDir, {
|
|
1472
|
+
updatedAt: now,
|
|
1473
|
+
reason: `watch_stage:${materialization.reason}:${materialization.lane}`
|
|
1474
|
+
});
|
|
1475
|
+
const inspection = inspectActivationState(activationRoot, now);
|
|
1476
|
+
if (inspection.promotion.allowed) {
|
|
1477
|
+
promoteCandidatePack(activationRoot, {
|
|
1478
|
+
updatedAt: now,
|
|
1479
|
+
reason: `watch_promote:${materialization.reason}:${materialization.lane}`
|
|
1480
|
+
});
|
|
1481
|
+
watchLog(`Promoted ${shortPackId} → active`);
|
|
1482
|
+
}
|
|
1483
|
+
else {
|
|
1484
|
+
watchLog(`Staged ${shortPackId} (promotion blocked: ${inspection.promotion.findings.join(", ")})`);
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
catch (error) {
|
|
1488
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1489
|
+
watchLog(`Promotion failed: ${message}`);
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
if (parsed.json) {
|
|
1493
|
+
console.log(JSON.stringify({
|
|
1494
|
+
timestamp: new Date().toISOString(),
|
|
1495
|
+
selected: totalSelected,
|
|
1496
|
+
events: totalEvents,
|
|
1497
|
+
live: liveCount,
|
|
1498
|
+
backfill: backfillCount,
|
|
1499
|
+
materialized: materialization?.candidate.summary.packId ?? null,
|
|
1500
|
+
diagnostics: snapshot.diagnostics
|
|
1501
|
+
}));
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
catch (error) {
|
|
1506
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1507
|
+
watchLog(`Error: ${message}`);
|
|
1508
|
+
}
|
|
1509
|
+
// Wait for the next interval, checking for stop signal periodically
|
|
1510
|
+
const deadline = Date.now() + intervalMs;
|
|
1511
|
+
while (!stopping && Date.now() < deadline) {
|
|
1512
|
+
await new Promise((resolve) => {
|
|
1513
|
+
setTimeout(resolve, Math.min(1000, deadline - Date.now()));
|
|
1514
|
+
});
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
watchLog("Watch stopped.");
|
|
1518
|
+
process.removeListener("SIGINT", onSignal);
|
|
1519
|
+
process.removeListener("SIGTERM", onSignal);
|
|
1520
|
+
return 0;
|
|
1521
|
+
}
|
|
1522
|
+
function promptSyncLine(prompt) {
|
|
1523
|
+
process.stdout.write(prompt);
|
|
1524
|
+
const buf = Buffer.alloc(256);
|
|
1525
|
+
let input = "";
|
|
1526
|
+
const fd = openSync("/dev/tty", "r");
|
|
1527
|
+
try {
|
|
1528
|
+
const bytesRead = readSync(fd, buf, 0, buf.length, null);
|
|
1529
|
+
input = buf.toString("utf8", 0, bytesRead).replace(/\r?\n$/, "");
|
|
1530
|
+
}
|
|
1531
|
+
finally {
|
|
1532
|
+
closeSync(fd);
|
|
1533
|
+
}
|
|
1534
|
+
return input;
|
|
1535
|
+
}
|
|
1536
|
+
function resetActivationRoot(activationRoot) {
|
|
1537
|
+
const resolvedRoot = path.resolve(activationRoot);
|
|
1538
|
+
const removedPacks = [];
|
|
1539
|
+
const packsDir = path.join(resolvedRoot, "packs");
|
|
1540
|
+
if (existsSync(packsDir)) {
|
|
1541
|
+
try {
|
|
1542
|
+
const entries = readdirSync(packsDir);
|
|
1543
|
+
for (const entry of entries) {
|
|
1544
|
+
const packPath = path.join(packsDir, entry);
|
|
1545
|
+
rmSync(packPath, { recursive: true, force: true });
|
|
1546
|
+
removedPacks.push(entry);
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
catch {
|
|
1550
|
+
// packs dir may not be readable
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
const logsDir = path.join(resolvedRoot, "logs");
|
|
1554
|
+
if (existsSync(logsDir)) {
|
|
1555
|
+
rmSync(logsDir, { recursive: true, force: true });
|
|
1556
|
+
}
|
|
1557
|
+
const seedPointers = {
|
|
1558
|
+
contract: "activation_pointers.v1",
|
|
1559
|
+
active: null,
|
|
1560
|
+
candidate: null,
|
|
1561
|
+
previous: null
|
|
1562
|
+
};
|
|
1563
|
+
const pointersPath = path.join(resolvedRoot, "activation-pointers.json");
|
|
1564
|
+
mkdirSync(resolvedRoot, { recursive: true });
|
|
1565
|
+
writeFileSync(pointersPath, JSON.stringify(seedPointers, null, 2) + "\n", "utf8");
|
|
1566
|
+
return { removedPacks, pointersReset: true };
|
|
1567
|
+
}
|
|
1568
|
+
function runResetCommand(parsed) {
|
|
1569
|
+
if (parsed.help) {
|
|
1570
|
+
console.log([
|
|
1571
|
+
"Usage: openclawbrain reset [--activation-root <path>] [--yes] [--json]",
|
|
1572
|
+
"",
|
|
1573
|
+
"Wipes all learned state and returns the brain to seed state.",
|
|
1574
|
+
"",
|
|
1575
|
+
"Options:",
|
|
1576
|
+
" --activation-root <path> Activation root (auto-detected if omitted)",
|
|
1577
|
+
" --yes, -y Skip confirmation prompt",
|
|
1578
|
+
" --json Emit machine-readable JSON output",
|
|
1579
|
+
" --help Show this help"
|
|
1580
|
+
].join("\n"));
|
|
1581
|
+
return 0;
|
|
1582
|
+
}
|
|
1583
|
+
const activationRoot = parsed.activationRoot;
|
|
1584
|
+
if (!existsSync(activationRoot)) {
|
|
1585
|
+
const msg = `Activation root does not exist: ${activationRoot}`;
|
|
1586
|
+
if (parsed.json) {
|
|
1587
|
+
console.log(JSON.stringify({ ok: false, error: msg }, null, 2));
|
|
1588
|
+
}
|
|
1589
|
+
else {
|
|
1590
|
+
console.error(msg);
|
|
1591
|
+
}
|
|
1592
|
+
return 1;
|
|
1593
|
+
}
|
|
1594
|
+
if (!parsed.yes) {
|
|
1595
|
+
let answer;
|
|
1596
|
+
try {
|
|
1597
|
+
answer = promptSyncLine("This will delete all learned context. Type 'reset' to confirm: ");
|
|
1598
|
+
}
|
|
1599
|
+
catch {
|
|
1600
|
+
console.error("Cannot prompt for confirmation in non-interactive mode. Use --yes to skip.");
|
|
1601
|
+
return 1;
|
|
1602
|
+
}
|
|
1603
|
+
if (answer.trim() !== "reset") {
|
|
1604
|
+
console.log("Reset cancelled.");
|
|
1605
|
+
return 1;
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
const result = resetActivationRoot(activationRoot);
|
|
1609
|
+
if (parsed.json) {
|
|
1610
|
+
console.log(JSON.stringify({
|
|
1611
|
+
ok: true,
|
|
1612
|
+
activationRoot,
|
|
1613
|
+
removedPacks: result.removedPacks,
|
|
1614
|
+
pointersReset: result.pointersReset
|
|
1615
|
+
}, null, 2));
|
|
1616
|
+
}
|
|
1617
|
+
else {
|
|
1618
|
+
console.log("RESET complete\n");
|
|
1619
|
+
if (result.removedPacks.length > 0) {
|
|
1620
|
+
console.log(` Removed ${result.removedPacks.length} pack(s): ${result.removedPacks.join(", ")}`);
|
|
1621
|
+
}
|
|
1622
|
+
else {
|
|
1623
|
+
console.log(" No packs to remove.");
|
|
1624
|
+
}
|
|
1625
|
+
console.log(" Activation pointers reset to seed state.");
|
|
1626
|
+
console.log(`\nBrain at ${shortenPath(activationRoot)} is now in seed state.`);
|
|
1627
|
+
console.log("Run `openclawbrain status` to verify.");
|
|
1628
|
+
}
|
|
1629
|
+
return 0;
|
|
1630
|
+
}
|
|
293
1631
|
export function runOperatorCli(argv = process.argv.slice(2)) {
|
|
294
1632
|
const parsed = parseOperatorCliArgs(argv);
|
|
1633
|
+
if (parsed.command === "context") {
|
|
1634
|
+
return runContextCommand(parsed);
|
|
1635
|
+
}
|
|
1636
|
+
if (parsed.command === "reset") {
|
|
1637
|
+
return runResetCommand(parsed);
|
|
1638
|
+
}
|
|
295
1639
|
if (parsed.help) {
|
|
296
1640
|
console.log(operatorCliHelp());
|
|
297
1641
|
return 0;
|
|
298
1642
|
}
|
|
299
|
-
|
|
300
|
-
|
|
1643
|
+
if (parsed.command === "export") {
|
|
1644
|
+
const result = exportBrain({
|
|
1645
|
+
activationRoot: parsed.activationRoot,
|
|
1646
|
+
outputPath: parsed.outputPath,
|
|
1647
|
+
});
|
|
1648
|
+
if (parsed.json) {
|
|
1649
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1650
|
+
}
|
|
1651
|
+
else if (result.ok) {
|
|
1652
|
+
console.log(`EXPORT ok`);
|
|
1653
|
+
console.log(` Archive: ${result.outputPath}`);
|
|
1654
|
+
console.log(` Source: ${result.activationRoot}`);
|
|
1655
|
+
}
|
|
1656
|
+
else {
|
|
1657
|
+
console.error(`EXPORT failed: ${result.error}`);
|
|
1658
|
+
}
|
|
1659
|
+
return result.ok ? 0 : 1;
|
|
1660
|
+
}
|
|
1661
|
+
if (parsed.command === "import") {
|
|
1662
|
+
const result = importBrain({
|
|
1663
|
+
archivePath: parsed.archivePath,
|
|
1664
|
+
activationRoot: parsed.activationRoot,
|
|
1665
|
+
force: parsed.force,
|
|
1666
|
+
});
|
|
1667
|
+
if (parsed.json) {
|
|
1668
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1669
|
+
}
|
|
1670
|
+
else if (result.ok) {
|
|
1671
|
+
console.log(`IMPORT ok`);
|
|
1672
|
+
console.log(` Activation root: ${result.activationRoot}`);
|
|
1673
|
+
console.log(` Archive: ${result.archivePath}`);
|
|
1674
|
+
if (result.warning) {
|
|
1675
|
+
console.log(` Warning: ${result.warning}`);
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
else {
|
|
1679
|
+
console.error(`IMPORT failed: ${result.error}`);
|
|
1680
|
+
}
|
|
1681
|
+
return result.ok ? 0 : 1;
|
|
1682
|
+
}
|
|
1683
|
+
if (parsed.command === "daemon") {
|
|
1684
|
+
return runDaemonCommand(parsed);
|
|
1685
|
+
}
|
|
1686
|
+
if (parsed.command === "history") {
|
|
1687
|
+
return runHistoryCommand(parsed);
|
|
1688
|
+
}
|
|
1689
|
+
if (parsed.command === "learn") {
|
|
1690
|
+
return runLearnCommand(parsed);
|
|
1691
|
+
}
|
|
1692
|
+
if (parsed.command === "watch") {
|
|
1693
|
+
// Watch is async — bridge to sync CLI entry by scheduling and returning 0.
|
|
1694
|
+
// The process stays alive due to the interval loop and exits via SIGINT or error.
|
|
1695
|
+
runWatchCommand(parsed).then((code) => { process.exitCode = code; }, (error) => {
|
|
1696
|
+
console.error("[openclawbrain] watch failed");
|
|
1697
|
+
console.error(error instanceof Error ? error.stack ?? error.message : String(error));
|
|
1698
|
+
process.exitCode = 1;
|
|
1699
|
+
});
|
|
1700
|
+
return 0;
|
|
1701
|
+
}
|
|
1702
|
+
if (parsed.command === "setup") {
|
|
1703
|
+
return runSetupCommand(parsed);
|
|
1704
|
+
}
|
|
1705
|
+
if (parsed.command === "attach") {
|
|
1706
|
+
mkdirSync(parsed.activationRoot, { recursive: true });
|
|
1707
|
+
mkdirSync(parsed.packRoot, { recursive: true });
|
|
1708
|
+
const result = bootstrapRuntimeAttach({
|
|
1709
|
+
profileSelector: "current_profile",
|
|
1710
|
+
...(parsed.brainAttachmentPolicy != null ? { brainAttachmentPolicy: parsed.brainAttachmentPolicy } : {}),
|
|
1711
|
+
activationRoot: parsed.activationRoot,
|
|
1712
|
+
packRoot: parsed.packRoot,
|
|
1713
|
+
packLabel: parsed.packLabel,
|
|
1714
|
+
workspace: {
|
|
1715
|
+
workspaceId: parsed.workspaceId,
|
|
1716
|
+
snapshotId: `${parsed.workspaceId}@bootstrap-${new Date().toISOString().slice(0, 10)}`,
|
|
1717
|
+
capturedAt: new Date().toISOString(),
|
|
1718
|
+
rootDir: process.cwd(),
|
|
1719
|
+
revision: "cli-bootstrap-v1"
|
|
1720
|
+
},
|
|
1721
|
+
interactionEvents: [],
|
|
1722
|
+
feedbackEvents: []
|
|
1723
|
+
});
|
|
1724
|
+
if (parsed.json) {
|
|
1725
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1726
|
+
}
|
|
1727
|
+
else {
|
|
1728
|
+
console.log(formatBootstrapRuntimeAttachReport(result));
|
|
1729
|
+
}
|
|
1730
|
+
return 0;
|
|
1731
|
+
}
|
|
1732
|
+
if (parsed.command === "scan") {
|
|
1733
|
+
if (parsed.sessionPath !== null) {
|
|
1734
|
+
const result = scanRecordedSession({
|
|
1735
|
+
rootDir: parsed.rootDir,
|
|
1736
|
+
trace: readJsonFile(parsed.sessionPath)
|
|
1737
|
+
});
|
|
1738
|
+
if (parsed.json) {
|
|
1739
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1740
|
+
}
|
|
1741
|
+
else {
|
|
1742
|
+
console.log(formatScanSessionSummary(result));
|
|
1743
|
+
}
|
|
1744
|
+
return 0;
|
|
1745
|
+
}
|
|
1746
|
+
const result = scanLiveEventExport({
|
|
1747
|
+
normalizedEventExport: loadCliScanLiveExport(parsed.livePath),
|
|
1748
|
+
workspace: readJsonFile(parsed.workspacePath),
|
|
1749
|
+
...(parsed.packLabel === null ? {} : { packLabel: parsed.packLabel }),
|
|
1750
|
+
...(parsed.observedAt === null ? {} : { observedAt: parsed.observedAt })
|
|
1751
|
+
});
|
|
1752
|
+
const snapshotOutPath = parsed.snapshotOutPath === null ? null : path.resolve(parsed.snapshotOutPath);
|
|
1753
|
+
if (snapshotOutPath !== null) {
|
|
1754
|
+
mkdirSync(path.dirname(snapshotOutPath), { recursive: true });
|
|
1755
|
+
writeFileSync(snapshotOutPath, JSON.stringify(result.snapshot, null, 2), "utf8");
|
|
1756
|
+
}
|
|
1757
|
+
if (parsed.json) {
|
|
1758
|
+
console.log(JSON.stringify({ ...result, snapshotOutPath }, null, 2));
|
|
1759
|
+
}
|
|
1760
|
+
else {
|
|
1761
|
+
console.log(formatScanLiveSummary(result, snapshotOutPath));
|
|
1762
|
+
}
|
|
1763
|
+
return 0;
|
|
1764
|
+
}
|
|
1765
|
+
// At this point only status/rollback commands remain
|
|
1766
|
+
const statusOrRollback = parsed;
|
|
1767
|
+
const activationRoot = requireActivationRoot(statusOrRollback.input, statusOrRollback.command);
|
|
1768
|
+
if (statusOrRollback.command === "rollback") {
|
|
301
1769
|
const result = rollbackRuntimeAttach({
|
|
302
1770
|
activationRoot,
|
|
303
|
-
...(
|
|
304
|
-
dryRun:
|
|
1771
|
+
...(statusOrRollback.input.updatedAt === null ? {} : { updatedAt: statusOrRollback.input.updatedAt }),
|
|
1772
|
+
dryRun: statusOrRollback.dryRun
|
|
305
1773
|
});
|
|
306
|
-
if (
|
|
1774
|
+
if (statusOrRollback.json) {
|
|
307
1775
|
console.log(JSON.stringify(result, null, 2));
|
|
308
1776
|
}
|
|
309
1777
|
else {
|
|
@@ -312,18 +1780,23 @@ export function runOperatorCli(argv = process.argv.slice(2)) {
|
|
|
312
1780
|
return result.allowed ? 0 : 1;
|
|
313
1781
|
}
|
|
314
1782
|
const status = describeCurrentProfileBrainStatus({
|
|
315
|
-
...
|
|
1783
|
+
...statusOrRollback.input,
|
|
316
1784
|
activationRoot
|
|
317
1785
|
});
|
|
318
|
-
if (
|
|
1786
|
+
if (statusOrRollback.json) {
|
|
319
1787
|
console.log(JSON.stringify(status, null, 2));
|
|
320
1788
|
}
|
|
321
1789
|
else {
|
|
322
1790
|
const report = buildOperatorSurfaceReport({
|
|
323
|
-
...
|
|
1791
|
+
...statusOrRollback.input,
|
|
324
1792
|
activationRoot
|
|
325
1793
|
});
|
|
326
|
-
|
|
1794
|
+
if (statusOrRollback.detailed) {
|
|
1795
|
+
console.log(formatCurrentProfileStatusSummary(status, report));
|
|
1796
|
+
}
|
|
1797
|
+
else {
|
|
1798
|
+
console.log(formatHumanFriendlyStatus(status, report));
|
|
1799
|
+
}
|
|
327
1800
|
}
|
|
328
1801
|
return 0;
|
|
329
1802
|
}
|