@openclawbrain/openclaw 0.1.12 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/src/cli.d.ts +72 -1
- package/dist/src/cli.js +1291 -21
- package/dist/src/cli.js.map +1 -1
- package/dist/src/daemon.d.ts +26 -0
- package/dist/src/daemon.js +362 -0
- package/dist/src/daemon.js.map +1 -0
- package/dist/src/import-export.d.ts +36 -0
- package/dist/src/import-export.js +171 -0
- package/dist/src/import-export.js.map +1 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +2 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/resolve-activation-root.d.ts +27 -0
- package/dist/src/resolve-activation-root.js +120 -0
- package/dist/src/resolve-activation-root.js.map +1 -0
- package/extension/index.ts +50 -0
- package/package.json +16 -14
- package/LICENSE +0 -201
package/dist/src/cli.js
CHANGED
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { execSync } from "node:child_process";
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, readSync, openSync, closeSync, realpathSync, rmSync, statSync, writeFileSync, appendFileSync } from "node:fs";
|
|
3
4
|
import path from "node:path";
|
|
4
|
-
import { pathToFileURL } from "node:url";
|
|
5
|
-
|
|
5
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = path.dirname(__filename);
|
|
8
|
+
import { parseDaemonArgs, runDaemonCommand } from "./daemon.js";
|
|
9
|
+
import { exportBrain, importBrain } from "./import-export.js";
|
|
10
|
+
import { advanceAlwaysOnLearningRuntime, createAlwaysOnLearningRuntimeState, materializeAlwaysOnLearningCandidatePack } from "@openclawbrain/learner";
|
|
11
|
+
import { inspectActivationState, promoteCandidatePack, stageCandidatePack, } from "@openclawbrain/pack-format";
|
|
12
|
+
import { resolveActivationRoot } from "./resolve-activation-root.js";
|
|
13
|
+
import { bootstrapRuntimeAttach, buildOperatorSurfaceReport, compileRuntimeContext, createAsyncTeacherLiveLoop, createRuntimeEventExportScanner, describeCurrentProfileBrainStatus, formatBootstrapRuntimeAttachReport, formatOperatorRollbackReport, loadRuntimeEventExportBundle, rollbackRuntimeAttach, scanLiveEventExport, scanRecordedSession } from "./index.js";
|
|
14
|
+
import { buildPassiveLearningStoreExportFromOpenClawSessionIndex } from "./local-session-passive-learning.js";
|
|
15
|
+
import { discoverOpenClawMainSessionStores, loadOpenClawSessionIndex, readOpenClawSessionFile } from "./session-store.js";
|
|
6
16
|
function quoteShellArg(value) {
|
|
7
17
|
return `'${value.replace(/'/g, `"'"'`)}'`;
|
|
8
18
|
}
|
|
@@ -122,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
|
-
" --
|
|
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.
|
|
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
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
|
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] === "
|
|
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,813 @@ 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
|
|
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
|
+
}
|
|
463
1631
|
export function runOperatorCli(argv = process.argv.slice(2)) {
|
|
464
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
|
+
}
|
|
465
1639
|
if (parsed.help) {
|
|
466
1640
|
console.log(operatorCliHelp());
|
|
467
1641
|
return 0;
|
|
468
1642
|
}
|
|
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
|
+
}
|
|
469
1732
|
if (parsed.command === "scan") {
|
|
470
1733
|
if (parsed.sessionPath !== null) {
|
|
471
1734
|
const result = scanRecordedSession({
|
|
@@ -499,14 +1762,16 @@ export function runOperatorCli(argv = process.argv.slice(2)) {
|
|
|
499
1762
|
}
|
|
500
1763
|
return 0;
|
|
501
1764
|
}
|
|
502
|
-
|
|
503
|
-
|
|
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") {
|
|
504
1769
|
const result = rollbackRuntimeAttach({
|
|
505
1770
|
activationRoot,
|
|
506
|
-
...(
|
|
507
|
-
dryRun:
|
|
1771
|
+
...(statusOrRollback.input.updatedAt === null ? {} : { updatedAt: statusOrRollback.input.updatedAt }),
|
|
1772
|
+
dryRun: statusOrRollback.dryRun
|
|
508
1773
|
});
|
|
509
|
-
if (
|
|
1774
|
+
if (statusOrRollback.json) {
|
|
510
1775
|
console.log(JSON.stringify(result, null, 2));
|
|
511
1776
|
}
|
|
512
1777
|
else {
|
|
@@ -515,18 +1780,23 @@ export function runOperatorCli(argv = process.argv.slice(2)) {
|
|
|
515
1780
|
return result.allowed ? 0 : 1;
|
|
516
1781
|
}
|
|
517
1782
|
const status = describeCurrentProfileBrainStatus({
|
|
518
|
-
...
|
|
1783
|
+
...statusOrRollback.input,
|
|
519
1784
|
activationRoot
|
|
520
1785
|
});
|
|
521
|
-
if (
|
|
1786
|
+
if (statusOrRollback.json) {
|
|
522
1787
|
console.log(JSON.stringify(status, null, 2));
|
|
523
1788
|
}
|
|
524
1789
|
else {
|
|
525
1790
|
const report = buildOperatorSurfaceReport({
|
|
526
|
-
...
|
|
1791
|
+
...statusOrRollback.input,
|
|
527
1792
|
activationRoot
|
|
528
1793
|
});
|
|
529
|
-
|
|
1794
|
+
if (statusOrRollback.detailed) {
|
|
1795
|
+
console.log(formatCurrentProfileStatusSummary(status, report));
|
|
1796
|
+
}
|
|
1797
|
+
else {
|
|
1798
|
+
console.log(formatHumanFriendlyStatus(status, report));
|
|
1799
|
+
}
|
|
530
1800
|
}
|
|
531
1801
|
return 0;
|
|
532
1802
|
}
|