@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/dist/src/cli.js CHANGED
@@ -1,8 +1,18 @@
1
1
  #!/usr/bin/env node
2
- import { realpathSync } from "node:fs";
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
- import { buildOperatorSurfaceReport, describeCurrentProfileBrainStatus, formatOperatorRollbackReport, rollbackRuntimeAttach } from "./index.js";
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
- " --activation-root <path> Activation root to inspect.",
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. bootstrap call bootstrapRuntimeAttach({ profileSelector: \"current_profile\", ... }) to attach an initial pack",
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
- `principal latest=${formatPrincipalLatest(report)} pending=${report.principal.pendingCount ?? "none"} downstream=${yesNo(report.principal.servingDownstreamOfLatestCorrection)} lag=${report.learning.principalLagToPromotion.sequenceLag ?? "none"}`,
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
- function requireActivationRoot(input, command) {
186
- if (input.activationRoot.trim().length === 0) {
187
- throw new Error(`--activation-root is required for ${command}`);
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 path.resolve(input.activationRoot);
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] === "status" || args[0] === "rollback") {
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
- const activationRoot = requireActivationRoot(parsed.input, parsed.command);
300
- if (parsed.command === "rollback") {
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
- ...(parsed.input.updatedAt === null ? {} : { updatedAt: parsed.input.updatedAt }),
304
- dryRun: parsed.dryRun
1771
+ ...(statusOrRollback.input.updatedAt === null ? {} : { updatedAt: statusOrRollback.input.updatedAt }),
1772
+ dryRun: statusOrRollback.dryRun
305
1773
  });
306
- if (parsed.json) {
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
- ...parsed.input,
1783
+ ...statusOrRollback.input,
316
1784
  activationRoot
317
1785
  });
318
- if (parsed.json) {
1786
+ if (statusOrRollback.json) {
319
1787
  console.log(JSON.stringify(status, null, 2));
320
1788
  }
321
1789
  else {
322
1790
  const report = buildOperatorSurfaceReport({
323
- ...parsed.input,
1791
+ ...statusOrRollback.input,
324
1792
  activationRoot
325
1793
  });
326
- console.log(formatCurrentProfileStatusSummary(status, report));
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
  }