@openclawbrain/openclaw 0.1.12 → 0.2.1

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 { mkdirSync, readFileSync, realpathSync, statSync, writeFileSync } 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, loadRuntimeEventExportBundle, rollbackRuntimeAttach, scanLiveEventExport, scanRecordedSession } 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 { buildPassiveLearningSessionExportFromOpenClawSessionStore } 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,18 +132,30 @@ 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]",
126
140
  " openclawbrain scan --session <trace.json> --root <path> [options]",
127
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>]",
128
145
  " openclawbrain-ops <status|rollback> --activation-root <path> [options] # compatibility alias",
129
146
  " openclawbrain-ops scan --session <trace.json> --root <path> [options] # compatibility alias",
130
147
  "",
131
148
  "Options:",
132
- " --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').",
133
154
  " --event-export <path> Event-export bundle root or normalized export JSON payload.",
134
155
  " --teacher-snapshot <path> Async teacher snapshot JSON from teacherLoop.snapshot()/flush(); keeps live-first, principal-priority, and passive-backfill learner truth explicit.",
135
156
  " --updated-at <iso> Observation time to use for freshness checks.",
136
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).",
137
159
  " --dry-run Preview rollback pointer movement without writing activation state.",
138
160
  " --session <path> Sanitized recorded-session trace JSON to replay.",
139
161
  " --live <path> Runtime event-export bundle root or normalized export JSON to scan once.",
@@ -142,11 +164,15 @@ function operatorCliHelp() {
142
164
  " --pack-label <label> Candidate-pack label for scan --live. Defaults to scanner-live-cli.",
143
165
  " --observed-at <iso> Observation time for scan --live freshness checks.",
144
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).",
145
170
  " --json Emit machine-readable JSON instead of text.",
146
171
  " --help Show this help.",
147
172
  "",
148
173
  "Common flow:",
149
- " 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>",
150
176
  " 1. status answer \"How's the brain?\" for the current profile on that activation root",
151
177
  " 2. status --json read the canonical current_profile_brain_status.v1 object for that same boundary",
152
178
  " 3. rollback --dry-run preview active <- previous, active -> candidate",
@@ -222,11 +248,49 @@ function formatCurrentProfileStatusSummary(status, report) {
222
248
  `turn attribution=${status.currentTurnAttribution === null ? "none" : status.currentTurnAttribution.contract}`
223
249
  ].join("\n");
224
250
  }
225
- function requireActivationRoot(input, command) {
226
- if (input.activationRoot.trim().length === 0) {
227
- 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);
228
258
  }
229
- 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
+ });
230
294
  }
231
295
  function readJsonFile(filePath) {
232
296
  return JSON.parse(readFileSync(path.resolve(filePath), "utf8"));
@@ -274,18 +338,336 @@ export function parseOperatorCliArgs(argv) {
274
338
  let rootDir = null;
275
339
  let workspacePath = null;
276
340
  let packLabel = null;
341
+ let packRoot = null;
342
+ let workspaceId = null;
277
343
  let observedAt = null;
278
344
  let snapshotOutPath = null;
345
+ let openclawHome = null;
346
+ let shared = false;
279
347
  let json = false;
280
348
  let help = false;
281
349
  let dryRun = false;
350
+ let detailed = false;
282
351
  const args = [...argv];
283
352
  if (args[0] === "doctor") {
284
353
  throw new Error(buildDoctorDeletedMessage(args.slice(1)));
285
354
  }
286
- if (args[0] === "status" || args[0] === "rollback" || args[0] === "scan") {
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") {
287
360
  command = args.shift();
288
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
+ }
289
671
  for (let index = 0; index < args.length; index += 1) {
290
672
  const arg = args[index];
291
673
  if (arg === "--help" || arg === "-h") {
@@ -300,7 +682,23 @@ export function parseOperatorCliArgs(argv) {
300
682
  dryRun = true;
301
683
  continue;
302
684
  }
685
+ if (arg === "--shared") {
686
+ shared = true;
687
+ continue;
688
+ }
689
+ if (arg === "--detailed") {
690
+ detailed = true;
691
+ continue;
692
+ }
303
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
+ }
304
702
  if (arg === "--activation-root") {
305
703
  if (next === undefined) {
306
704
  throw new Error("--activation-root requires a value");
@@ -400,8 +798,71 @@ export function parseOperatorCliArgs(argv) {
400
798
  index += 1;
401
799
  continue;
402
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
+ }
403
817
  throw new Error(`unknown argument: ${arg}`);
404
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
+ }
405
866
  if (command === "scan") {
406
867
  if ((sessionPath === null && livePath === null) || (sessionPath !== null && livePath !== null)) {
407
868
  throw new Error("scan requires exactly one of --session or --live");
@@ -436,7 +897,7 @@ export function parseOperatorCliArgs(argv) {
436
897
  };
437
898
  }
438
899
  return {
439
- command,
900
+ command: command,
440
901
  input: {
441
902
  activationRoot: activationRoot ?? "",
442
903
  eventExportPath,
@@ -446,7 +907,8 @@ export function parseOperatorCliArgs(argv) {
446
907
  },
447
908
  json,
448
909
  help,
449
- dryRun
910
+ dryRun,
911
+ detailed
450
912
  };
451
913
  }
452
914
  function isDirectCliRun(entryArg, moduleUrl) {
@@ -460,12 +922,835 @@ function isDirectCliRun(entryArg, moduleUrl) {
460
922
  return pathToFileURL(path.resolve(entryArg)).href === moduleUrl;
461
923
  }
462
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 in one monotonic sequence space
1306
+ let totalSessions = 0;
1307
+ let totalInteractionEvents = 0;
1308
+ let totalFeedbackEvents = 0;
1309
+ const allInteractionEvents = [];
1310
+ const allFeedbackEvents = [];
1311
+ let nextSequence = 1;
1312
+ const discoveredSessions = stores
1313
+ .flatMap((store) => {
1314
+ const sessionIndex = loadOpenClawSessionIndex(store.indexPath);
1315
+ return Object.entries(sessionIndex).map(([sessionKey, entry]) => ({
1316
+ store,
1317
+ sessionKey,
1318
+ entry
1319
+ }));
1320
+ })
1321
+ .sort((left, right) => {
1322
+ if (left.entry.updatedAt !== right.entry.updatedAt) {
1323
+ return left.entry.updatedAt - right.entry.updatedAt;
1324
+ }
1325
+ if (left.store.indexPath !== right.store.indexPath) {
1326
+ return left.store.indexPath.localeCompare(right.store.indexPath);
1327
+ }
1328
+ return left.sessionKey.localeCompare(right.sessionKey);
1329
+ });
1330
+ for (const session of discoveredSessions) {
1331
+ const sessionFile = session.entry.sessionFile;
1332
+ const records = typeof sessionFile !== "string" || sessionFile.trim().length === 0
1333
+ ? []
1334
+ : (() => {
1335
+ try {
1336
+ return readOpenClawSessionFile(sessionFile);
1337
+ }
1338
+ catch {
1339
+ return [];
1340
+ }
1341
+ })();
1342
+ const sessionExport = buildPassiveLearningSessionExportFromOpenClawSessionStore({
1343
+ sessionKey: session.sessionKey,
1344
+ indexEntry: session.entry,
1345
+ records,
1346
+ agentId: session.store.agentId,
1347
+ sequenceStart: nextSequence
1348
+ });
1349
+ nextSequence = sessionExport.nextSequence;
1350
+ totalSessions += 1;
1351
+ totalInteractionEvents += sessionExport.interactionEvents.length;
1352
+ totalFeedbackEvents += sessionExport.feedbackEvents.length;
1353
+ allInteractionEvents.push(...sessionExport.interactionEvents);
1354
+ allFeedbackEvents.push(...sessionExport.feedbackEvents);
1355
+ }
1356
+ const totalEvents = totalInteractionEvents + totalFeedbackEvents;
1357
+ if (totalEvents === 0) {
1358
+ if (parsed.json) {
1359
+ 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." }));
1360
+ }
1361
+ else {
1362
+ console.log("No new session data. Brain is up to date.");
1363
+ }
1364
+ return 0;
1365
+ }
1366
+ // 3. Run single learning cycle
1367
+ const now = new Date().toISOString();
1368
+ const learnerResult = advanceAlwaysOnLearningRuntime({
1369
+ packLabel: "learn-cli",
1370
+ workspace: {
1371
+ workspaceId: "learn-cli",
1372
+ snapshotId: `learn-cli@${now.slice(0, 10)}`,
1373
+ capturedAt: now,
1374
+ rootDir: activationRoot,
1375
+ revision: "learn-cli-v1"
1376
+ },
1377
+ interactionEvents: allInteractionEvents,
1378
+ feedbackEvents: allFeedbackEvents,
1379
+ learnedRouting: true,
1380
+ state: createAlwaysOnLearningRuntimeState(),
1381
+ builtAt: now
1382
+ });
1383
+ // 4. If materialization produced, materialize → stage → promote
1384
+ if (learnerResult.materialization !== null) {
1385
+ const candidatePackRoot = path.join(activationRoot, "packs", `learn-cli-${Date.now()}`);
1386
+ mkdirSync(candidatePackRoot, { recursive: true });
1387
+ const candidateDescriptor = materializeAlwaysOnLearningCandidatePack(candidatePackRoot, learnerResult.materialization);
1388
+ stageCandidatePack(activationRoot, candidatePackRoot, {
1389
+ updatedAt: now,
1390
+ reason: "learn_cli_stage"
1391
+ });
1392
+ promoteCandidatePack(activationRoot, {
1393
+ updatedAt: now,
1394
+ reason: "learn_cli_promote"
1395
+ });
1396
+ const packId = candidateDescriptor.manifest.packId;
1397
+ if (parsed.json) {
1398
+ console.log(JSON.stringify({
1399
+ command: "learn",
1400
+ activationRoot,
1401
+ scannedSessions: totalSessions,
1402
+ newEvents: totalEvents,
1403
+ materialized: packId,
1404
+ promoted: true,
1405
+ message: `Scanned ${totalSessions} sessions, ${totalEvents} new events, materialized ${packId}, promoted.`
1406
+ }, null, 2));
1407
+ }
1408
+ else {
1409
+ console.log(`Scanned ${totalSessions} sessions, ${totalEvents} new events, materialized ${packId}, promoted.`);
1410
+ }
1411
+ }
1412
+ else {
1413
+ if (parsed.json) {
1414
+ console.log(JSON.stringify({
1415
+ command: "learn",
1416
+ activationRoot,
1417
+ scannedSessions: totalSessions,
1418
+ newEvents: totalEvents,
1419
+ materialized: null,
1420
+ promoted: false,
1421
+ message: "No new session data. Brain is up to date."
1422
+ }, null, 2));
1423
+ }
1424
+ else {
1425
+ console.log("No new session data. Brain is up to date.");
1426
+ }
1427
+ }
1428
+ return 0;
1429
+ }
1430
+ function formatTimestamp() {
1431
+ const now = new Date();
1432
+ return `[${now.toTimeString().slice(0, 8)}]`;
1433
+ }
1434
+ function watchLog(message) {
1435
+ console.log(`${formatTimestamp()} ${message}`);
1436
+ }
1437
+ async function runWatchCommand(parsed) {
1438
+ const activationRoot = parsed.activationRoot;
1439
+ const scanRoot = parsed.scanRoot !== null
1440
+ ? path.resolve(parsed.scanRoot)
1441
+ : path.resolve(activationRoot, "event-exports");
1442
+ const intervalMs = parsed.interval * 1000;
1443
+ watchLog(`Watch starting — activation: ${shortenPath(activationRoot)}`);
1444
+ watchLog(`Scan root: ${shortenPath(scanRoot)} interval: ${parsed.interval}s`);
1445
+ const scanner = createRuntimeEventExportScanner({ scanRoot });
1446
+ const teacherLoop = createAsyncTeacherLiveLoop({
1447
+ packLabel: "watch-cli",
1448
+ workspace: {
1449
+ workspaceId: "watch-cli",
1450
+ snapshotId: `watch-cli@${new Date().toISOString().slice(0, 10)}`,
1451
+ capturedAt: new Date().toISOString(),
1452
+ rootDir: activationRoot,
1453
+ revision: "watch-cli-v1"
1454
+ },
1455
+ learnedRouting: true
1456
+ });
1457
+ let stopping = false;
1458
+ const onSignal = () => {
1459
+ if (stopping) {
1460
+ process.exit(1);
1461
+ }
1462
+ stopping = true;
1463
+ watchLog("Stopping... (Ctrl+C again to force)");
1464
+ };
1465
+ process.on("SIGINT", onSignal);
1466
+ process.on("SIGTERM", onSignal);
1467
+ while (!stopping) {
1468
+ try {
1469
+ const scanResult = scanner.scanOnce();
1470
+ const liveCount = scanResult.live.length;
1471
+ const backfillCount = scanResult.backfill.length;
1472
+ const totalSelected = scanResult.selected.length;
1473
+ if (totalSelected === 0) {
1474
+ watchLog("Scanning... no changes");
1475
+ }
1476
+ else {
1477
+ const totalEvents = scanResult.selected.reduce((sum, hit) => sum + hit.eventRange.count, 0);
1478
+ watchLog(`Scanning... ${totalSelected} session${totalSelected === 1 ? "" : "s"} changed, ${totalEvents} new event${totalEvents === 1 ? "" : "s"}`);
1479
+ // Feed exports into teacher/learner pipeline
1480
+ const ingestResult = await teacherLoop.ingestRuntimeEventExportScannerScan(scanResult);
1481
+ const snapshot = ingestResult.snapshot;
1482
+ const materialization = snapshot.learner.lastMaterialization;
1483
+ if (materialization !== null) {
1484
+ const packId = materialization.candidate.summary.packId;
1485
+ const shortPackId = packId.length > 16 ? packId.slice(0, 16) : packId;
1486
+ watchLog(`Learning: materialized ${shortPackId}`);
1487
+ // Attempt stage + promote
1488
+ try {
1489
+ const candidateRootDir = path.resolve(activationRoot, "packs", packId);
1490
+ mkdirSync(candidateRootDir, { recursive: true });
1491
+ materializeAlwaysOnLearningCandidatePack(candidateRootDir, materialization);
1492
+ const now = new Date().toISOString();
1493
+ stageCandidatePack(activationRoot, candidateRootDir, {
1494
+ updatedAt: now,
1495
+ reason: `watch_stage:${materialization.reason}:${materialization.lane}`
1496
+ });
1497
+ const inspection = inspectActivationState(activationRoot, now);
1498
+ if (inspection.promotion.allowed) {
1499
+ promoteCandidatePack(activationRoot, {
1500
+ updatedAt: now,
1501
+ reason: `watch_promote:${materialization.reason}:${materialization.lane}`
1502
+ });
1503
+ watchLog(`Promoted ${shortPackId} → active`);
1504
+ }
1505
+ else {
1506
+ watchLog(`Staged ${shortPackId} (promotion blocked: ${inspection.promotion.findings.join(", ")})`);
1507
+ }
1508
+ }
1509
+ catch (error) {
1510
+ const message = error instanceof Error ? error.message : String(error);
1511
+ watchLog(`Promotion failed: ${message}`);
1512
+ }
1513
+ }
1514
+ if (parsed.json) {
1515
+ console.log(JSON.stringify({
1516
+ timestamp: new Date().toISOString(),
1517
+ selected: totalSelected,
1518
+ events: totalEvents,
1519
+ live: liveCount,
1520
+ backfill: backfillCount,
1521
+ materialized: materialization?.candidate.summary.packId ?? null,
1522
+ diagnostics: snapshot.diagnostics
1523
+ }));
1524
+ }
1525
+ }
1526
+ }
1527
+ catch (error) {
1528
+ const message = error instanceof Error ? error.message : String(error);
1529
+ watchLog(`Error: ${message}`);
1530
+ }
1531
+ // Wait for the next interval, checking for stop signal periodically
1532
+ const deadline = Date.now() + intervalMs;
1533
+ while (!stopping && Date.now() < deadline) {
1534
+ await new Promise((resolve) => {
1535
+ setTimeout(resolve, Math.min(1000, deadline - Date.now()));
1536
+ });
1537
+ }
1538
+ }
1539
+ watchLog("Watch stopped.");
1540
+ process.removeListener("SIGINT", onSignal);
1541
+ process.removeListener("SIGTERM", onSignal);
1542
+ return 0;
1543
+ }
1544
+ function promptSyncLine(prompt) {
1545
+ process.stdout.write(prompt);
1546
+ const buf = Buffer.alloc(256);
1547
+ let input = "";
1548
+ const fd = openSync("/dev/tty", "r");
1549
+ try {
1550
+ const bytesRead = readSync(fd, buf, 0, buf.length, null);
1551
+ input = buf.toString("utf8", 0, bytesRead).replace(/\r?\n$/, "");
1552
+ }
1553
+ finally {
1554
+ closeSync(fd);
1555
+ }
1556
+ return input;
1557
+ }
1558
+ function resetActivationRoot(activationRoot) {
1559
+ const resolvedRoot = path.resolve(activationRoot);
1560
+ const removedPacks = [];
1561
+ const packsDir = path.join(resolvedRoot, "packs");
1562
+ if (existsSync(packsDir)) {
1563
+ try {
1564
+ const entries = readdirSync(packsDir);
1565
+ for (const entry of entries) {
1566
+ const packPath = path.join(packsDir, entry);
1567
+ rmSync(packPath, { recursive: true, force: true });
1568
+ removedPacks.push(entry);
1569
+ }
1570
+ }
1571
+ catch {
1572
+ // packs dir may not be readable
1573
+ }
1574
+ }
1575
+ const logsDir = path.join(resolvedRoot, "logs");
1576
+ if (existsSync(logsDir)) {
1577
+ rmSync(logsDir, { recursive: true, force: true });
1578
+ }
1579
+ const seedPointers = {
1580
+ contract: "activation_pointers.v1",
1581
+ active: null,
1582
+ candidate: null,
1583
+ previous: null
1584
+ };
1585
+ const pointersPath = path.join(resolvedRoot, "activation-pointers.json");
1586
+ mkdirSync(resolvedRoot, { recursive: true });
1587
+ writeFileSync(pointersPath, JSON.stringify(seedPointers, null, 2) + "\n", "utf8");
1588
+ return { removedPacks, pointersReset: true };
1589
+ }
1590
+ function runResetCommand(parsed) {
1591
+ if (parsed.help) {
1592
+ console.log([
1593
+ "Usage: openclawbrain reset [--activation-root <path>] [--yes] [--json]",
1594
+ "",
1595
+ "Wipes all learned state and returns the brain to seed state.",
1596
+ "",
1597
+ "Options:",
1598
+ " --activation-root <path> Activation root (auto-detected if omitted)",
1599
+ " --yes, -y Skip confirmation prompt",
1600
+ " --json Emit machine-readable JSON output",
1601
+ " --help Show this help"
1602
+ ].join("\n"));
1603
+ return 0;
1604
+ }
1605
+ const activationRoot = parsed.activationRoot;
1606
+ if (!existsSync(activationRoot)) {
1607
+ const msg = `Activation root does not exist: ${activationRoot}`;
1608
+ if (parsed.json) {
1609
+ console.log(JSON.stringify({ ok: false, error: msg }, null, 2));
1610
+ }
1611
+ else {
1612
+ console.error(msg);
1613
+ }
1614
+ return 1;
1615
+ }
1616
+ if (!parsed.yes) {
1617
+ let answer;
1618
+ try {
1619
+ answer = promptSyncLine("This will delete all learned context. Type 'reset' to confirm: ");
1620
+ }
1621
+ catch {
1622
+ console.error("Cannot prompt for confirmation in non-interactive mode. Use --yes to skip.");
1623
+ return 1;
1624
+ }
1625
+ if (answer.trim() !== "reset") {
1626
+ console.log("Reset cancelled.");
1627
+ return 1;
1628
+ }
1629
+ }
1630
+ const result = resetActivationRoot(activationRoot);
1631
+ if (parsed.json) {
1632
+ console.log(JSON.stringify({
1633
+ ok: true,
1634
+ activationRoot,
1635
+ removedPacks: result.removedPacks,
1636
+ pointersReset: result.pointersReset
1637
+ }, null, 2));
1638
+ }
1639
+ else {
1640
+ console.log("RESET complete\n");
1641
+ if (result.removedPacks.length > 0) {
1642
+ console.log(` Removed ${result.removedPacks.length} pack(s): ${result.removedPacks.join(", ")}`);
1643
+ }
1644
+ else {
1645
+ console.log(" No packs to remove.");
1646
+ }
1647
+ console.log(" Activation pointers reset to seed state.");
1648
+ console.log(`\nBrain at ${shortenPath(activationRoot)} is now in seed state.`);
1649
+ console.log("Run `openclawbrain status` to verify.");
1650
+ }
1651
+ return 0;
1652
+ }
463
1653
  export function runOperatorCli(argv = process.argv.slice(2)) {
464
1654
  const parsed = parseOperatorCliArgs(argv);
1655
+ if (parsed.command === "context") {
1656
+ return runContextCommand(parsed);
1657
+ }
1658
+ if (parsed.command === "reset") {
1659
+ return runResetCommand(parsed);
1660
+ }
465
1661
  if (parsed.help) {
466
1662
  console.log(operatorCliHelp());
467
1663
  return 0;
468
1664
  }
1665
+ if (parsed.command === "export") {
1666
+ const result = exportBrain({
1667
+ activationRoot: parsed.activationRoot,
1668
+ outputPath: parsed.outputPath,
1669
+ });
1670
+ if (parsed.json) {
1671
+ console.log(JSON.stringify(result, null, 2));
1672
+ }
1673
+ else if (result.ok) {
1674
+ console.log(`EXPORT ok`);
1675
+ console.log(` Archive: ${result.outputPath}`);
1676
+ console.log(` Source: ${result.activationRoot}`);
1677
+ }
1678
+ else {
1679
+ console.error(`EXPORT failed: ${result.error}`);
1680
+ }
1681
+ return result.ok ? 0 : 1;
1682
+ }
1683
+ if (parsed.command === "import") {
1684
+ const result = importBrain({
1685
+ archivePath: parsed.archivePath,
1686
+ activationRoot: parsed.activationRoot,
1687
+ force: parsed.force,
1688
+ });
1689
+ if (parsed.json) {
1690
+ console.log(JSON.stringify(result, null, 2));
1691
+ }
1692
+ else if (result.ok) {
1693
+ console.log(`IMPORT ok`);
1694
+ console.log(` Activation root: ${result.activationRoot}`);
1695
+ console.log(` Archive: ${result.archivePath}`);
1696
+ if (result.warning) {
1697
+ console.log(` Warning: ${result.warning}`);
1698
+ }
1699
+ }
1700
+ else {
1701
+ console.error(`IMPORT failed: ${result.error}`);
1702
+ }
1703
+ return result.ok ? 0 : 1;
1704
+ }
1705
+ if (parsed.command === "daemon") {
1706
+ return runDaemonCommand(parsed);
1707
+ }
1708
+ if (parsed.command === "history") {
1709
+ return runHistoryCommand(parsed);
1710
+ }
1711
+ if (parsed.command === "learn") {
1712
+ return runLearnCommand(parsed);
1713
+ }
1714
+ if (parsed.command === "watch") {
1715
+ // Watch is async — bridge to sync CLI entry by scheduling and returning 0.
1716
+ // The process stays alive due to the interval loop and exits via SIGINT or error.
1717
+ runWatchCommand(parsed).then((code) => { process.exitCode = code; }, (error) => {
1718
+ console.error("[openclawbrain] watch failed");
1719
+ console.error(error instanceof Error ? error.stack ?? error.message : String(error));
1720
+ process.exitCode = 1;
1721
+ });
1722
+ return 0;
1723
+ }
1724
+ if (parsed.command === "setup") {
1725
+ return runSetupCommand(parsed);
1726
+ }
1727
+ if (parsed.command === "attach") {
1728
+ mkdirSync(parsed.activationRoot, { recursive: true });
1729
+ mkdirSync(parsed.packRoot, { recursive: true });
1730
+ const result = bootstrapRuntimeAttach({
1731
+ profileSelector: "current_profile",
1732
+ ...(parsed.brainAttachmentPolicy != null ? { brainAttachmentPolicy: parsed.brainAttachmentPolicy } : {}),
1733
+ activationRoot: parsed.activationRoot,
1734
+ packRoot: parsed.packRoot,
1735
+ packLabel: parsed.packLabel,
1736
+ workspace: {
1737
+ workspaceId: parsed.workspaceId,
1738
+ snapshotId: `${parsed.workspaceId}@bootstrap-${new Date().toISOString().slice(0, 10)}`,
1739
+ capturedAt: new Date().toISOString(),
1740
+ rootDir: process.cwd(),
1741
+ revision: "cli-bootstrap-v1"
1742
+ },
1743
+ interactionEvents: [],
1744
+ feedbackEvents: []
1745
+ });
1746
+ if (parsed.json) {
1747
+ console.log(JSON.stringify(result, null, 2));
1748
+ }
1749
+ else {
1750
+ console.log(formatBootstrapRuntimeAttachReport(result));
1751
+ }
1752
+ return 0;
1753
+ }
469
1754
  if (parsed.command === "scan") {
470
1755
  if (parsed.sessionPath !== null) {
471
1756
  const result = scanRecordedSession({
@@ -499,14 +1784,16 @@ export function runOperatorCli(argv = process.argv.slice(2)) {
499
1784
  }
500
1785
  return 0;
501
1786
  }
502
- const activationRoot = requireActivationRoot(parsed.input, parsed.command);
503
- if (parsed.command === "rollback") {
1787
+ // At this point only status/rollback commands remain
1788
+ const statusOrRollback = parsed;
1789
+ const activationRoot = requireActivationRoot(statusOrRollback.input, statusOrRollback.command);
1790
+ if (statusOrRollback.command === "rollback") {
504
1791
  const result = rollbackRuntimeAttach({
505
1792
  activationRoot,
506
- ...(parsed.input.updatedAt === null ? {} : { updatedAt: parsed.input.updatedAt }),
507
- dryRun: parsed.dryRun
1793
+ ...(statusOrRollback.input.updatedAt === null ? {} : { updatedAt: statusOrRollback.input.updatedAt }),
1794
+ dryRun: statusOrRollback.dryRun
508
1795
  });
509
- if (parsed.json) {
1796
+ if (statusOrRollback.json) {
510
1797
  console.log(JSON.stringify(result, null, 2));
511
1798
  }
512
1799
  else {
@@ -515,18 +1802,23 @@ export function runOperatorCli(argv = process.argv.slice(2)) {
515
1802
  return result.allowed ? 0 : 1;
516
1803
  }
517
1804
  const status = describeCurrentProfileBrainStatus({
518
- ...parsed.input,
1805
+ ...statusOrRollback.input,
519
1806
  activationRoot
520
1807
  });
521
- if (parsed.json) {
1808
+ if (statusOrRollback.json) {
522
1809
  console.log(JSON.stringify(status, null, 2));
523
1810
  }
524
1811
  else {
525
1812
  const report = buildOperatorSurfaceReport({
526
- ...parsed.input,
1813
+ ...statusOrRollback.input,
527
1814
  activationRoot
528
1815
  });
529
- console.log(formatCurrentProfileStatusSummary(status, report));
1816
+ if (statusOrRollback.detailed) {
1817
+ console.log(formatCurrentProfileStatusSummary(status, report));
1818
+ }
1819
+ else {
1820
+ console.log(formatHumanFriendlyStatus(status, report));
1821
+ }
530
1822
  }
531
1823
  return 0;
532
1824
  }