@remnic/cli 1.0.3 → 1.0.5

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/index.js CHANGED
@@ -1,7 +1,30 @@
1
+ import {
2
+ buildBenchmarkPublishFeed,
3
+ checkRegression,
4
+ compareResults,
5
+ defaultBenchmarkBaselineDir,
6
+ defaultBenchmarkPublishPath,
7
+ deleteBenchmarkResults,
8
+ discoverAllProviders,
9
+ getBenchmarkLowerIsBetter,
10
+ listBenchmarkBaselines,
11
+ listBenchmarkResults,
12
+ loadBaseline,
13
+ loadBenchmarkBaseline,
14
+ loadBenchmarkResult,
15
+ renderBenchmarkResultExport,
16
+ resolveBenchmarkResultReference,
17
+ runBenchSuite,
18
+ runExplain,
19
+ saveBenchmarkBaseline,
20
+ writeBenchmarkPublishFeed
21
+ } from "./chunk-GAZ3DFWX.js";
22
+
1
23
  // src/index.ts
2
- import fs2 from "fs";
24
+ import fs from "fs";
3
25
  import path2 from "path";
4
26
  import * as childProcess from "child_process";
27
+ import { fileURLToPath } from "url";
5
28
  import {
6
29
  parseConfig,
7
30
  Orchestrator,
@@ -34,291 +57,1309 @@ import {
34
57
  getManifestPath,
35
58
  generateContextTree,
36
59
  migrateFromEngram,
37
- rollbackFromEngramMigration
60
+ rollbackFromEngramMigration,
61
+ buildBriefing,
62
+ parseBriefingWindow,
63
+ parseBriefingFocus,
64
+ validateBriefingFormat,
65
+ resolveBriefingSaveDir,
66
+ briefingFilename,
67
+ FileCalendarSource,
68
+ listVersions,
69
+ getVersion,
70
+ revertToVersion,
71
+ diffVersions,
72
+ readManifest,
73
+ createBackend,
74
+ runBinaryLifecyclePipeline,
75
+ DEFAULT_SCAN_PATTERNS,
76
+ DEFAULT_MAX_BINARY_SIZE_BYTES,
77
+ publisherForConnector,
78
+ hostIdForConnector,
79
+ registerPublisher,
80
+ PUBLISHERS,
81
+ CodexMemoryExtensionPublisher,
82
+ ClaudeCodeMemoryExtensionPublisher,
83
+ HermesMemoryExtensionPublisher,
84
+ DEFAULT_TAXONOMY,
85
+ resolveCategory,
86
+ generateResolverDocument,
87
+ loadTaxonomy,
88
+ saveTaxonomy,
89
+ validateSlug,
90
+ validateTaxonomy,
91
+ generateMarketplaceManifest,
92
+ checkMarketplaceManifest,
93
+ writeMarketplaceManifest,
94
+ installFromMarketplace,
95
+ EnrichmentProviderRegistry,
96
+ WebSearchProvider,
97
+ runEnrichmentPipeline,
98
+ appendAuditEntry,
99
+ readAuditLog,
100
+ defaultEnrichmentPipelineConfig,
101
+ discoverMemoryExtensions,
102
+ resolveExtensionsRoot,
103
+ coerceInstallExtension
104
+ } from "@remnic/core";
105
+ import {
106
+ convertMemoriesToRecords,
107
+ getTrainingExportAdapter as getTrainingExportAdapter2,
108
+ listTrainingExportAdapters,
109
+ parseStrictCliDate
38
110
  } from "@remnic/core";
39
111
 
40
- // ../bench/src/benchmark.ts
41
- import fs from "fs";
112
+ // ../export-weclone/dist/index.js
113
+ import {
114
+ getTrainingExportAdapter,
115
+ registerTrainingExportAdapter
116
+ } from "@remnic/core";
117
+ var wecloneExportAdapter = {
118
+ name: "weclone",
119
+ fileExtension: ".json",
120
+ formatRecords(records) {
121
+ const alpacaRecords = records.map((r) => ({
122
+ instruction: r.instruction,
123
+ input: r.input,
124
+ output: r.output
125
+ }));
126
+ return JSON.stringify(alpacaRecords, null, 2);
127
+ }
128
+ };
129
+ var DEFAULT_MAX_PAIRS = 1;
130
+ var QUESTION_TEMPLATES = {
131
+ preferences: [
132
+ "What kind of {topic} do you like?",
133
+ "What's your preference for {topic}?",
134
+ "What are your favorite {topic}?"
135
+ ],
136
+ opinions: [
137
+ "What do you think about {topic}?",
138
+ "How do you feel about {topic}?",
139
+ "What's your opinion on {topic}?"
140
+ ],
141
+ expertise: [
142
+ "Tell me about {topic}.",
143
+ "What do you know about {topic}?",
144
+ "Can you explain {topic}?"
145
+ ],
146
+ personal: [
147
+ "Can you tell me about your {topic}?",
148
+ "Tell me about your {topic}.",
149
+ "What can you share about your {topic}?"
150
+ ]
151
+ };
152
+ var DEFAULT_TEMPLATES = [
153
+ "Tell me about {topic}.",
154
+ "What can you share about {topic}?"
155
+ ];
156
+ var CATEGORY_TO_TEMPLATE = {
157
+ preference: "preferences",
158
+ fact: "expertise",
159
+ entity: "expertise",
160
+ skill: "expertise",
161
+ correction: "opinions",
162
+ decision: "opinions",
163
+ principle: "opinions",
164
+ rule: "opinions",
165
+ personal: "personal",
166
+ relationship: "personal",
167
+ commitment: "personal",
168
+ moment: "personal"
169
+ };
170
+ function synthesizeTrainingPairs(records, options) {
171
+ const maxPairs = options?.maxPairsPerRecord ?? DEFAULT_MAX_PAIRS;
172
+ const style = options?.styleMarkers;
173
+ const result = [];
174
+ for (let i = 0; i < records.length; i++) {
175
+ const record = records[i];
176
+ const templateKey = resolveTemplateKey(record.category);
177
+ const topic = extractTopic(record.instruction);
178
+ const templates = QUESTION_TEMPLATES[templateKey] ?? DEFAULT_TEMPLATES;
179
+ const pairCount = Math.min(maxPairs, templates.length);
180
+ for (let j = 0; j < pairCount; j++) {
181
+ const templateIndex = (i + j) % templates.length;
182
+ const question = templates[templateIndex].replace("{topic}", topic);
183
+ let output = record.output;
184
+ if (style?.usesLowercase) {
185
+ output = output.toLowerCase();
186
+ }
187
+ result.push({
188
+ instruction: question,
189
+ input: "",
190
+ output,
191
+ category: record.category,
192
+ confidence: record.confidence,
193
+ sourceIds: record.sourceIds
194
+ });
195
+ }
196
+ }
197
+ return result;
198
+ }
199
+ function resolveTemplateKey(category) {
200
+ if (!category) return "";
201
+ return CATEGORY_TO_TEMPLATE[category.toLowerCase()] ?? "";
202
+ }
203
+ function extractTopic(instruction) {
204
+ const tagMatch = instruction.match(/\(([^()]+)\)/);
205
+ if (tagMatch) {
206
+ return tagMatch[1].trim().toLowerCase();
207
+ }
208
+ return "this";
209
+ }
210
+ var PII_PATTERNS = [
211
+ {
212
+ // Email: user@domain.tld
213
+ name: "email",
214
+ regex: /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g
215
+ },
216
+ {
217
+ // SSN: 123-45-6789 (exactly 3-2-4 digit groups)
218
+ name: "ssn",
219
+ regex: /\b\d{3}-\d{2}-\d{4}\b/g
220
+ },
221
+ {
222
+ // Credit card: 4 groups of 4 digits separated by dashes or spaces
223
+ name: "credit_card",
224
+ regex: /\b\d{4}[-\s]\d{4}[-\s]\d{4}[-\s]\d{4}\b/g
225
+ },
226
+ {
227
+ // IP address: four octets 0-255
228
+ name: "ip_address",
229
+ regex: /\b(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\b/g
230
+ },
231
+ {
232
+ // Phone: optional +1- prefix, then 3-3-4 with dashes, dots, or spaces
233
+ // Also matches (555) 123-4567 format
234
+ name: "phone",
235
+ regex: /(?:\+\d{1,3}[-.\s]?)?\(?\d{3}\)?[-.\s]\d{3}[-.\s]\d{4}\b/g
236
+ }
237
+ ];
238
+ var SCANNED_FIELDS = [
239
+ "instruction",
240
+ "input",
241
+ "output"
242
+ ];
243
+ function sweepPii(records) {
244
+ const redactionDetails = [];
245
+ const recordHasRedaction = /* @__PURE__ */ new Set();
246
+ const cleanRecords = records.map((record, idx) => {
247
+ const cleaned = { ...record };
248
+ for (const field of SCANNED_FIELDS) {
249
+ let value = record[field];
250
+ if (!value) continue;
251
+ for (const pattern of PII_PATTERNS) {
252
+ pattern.regex.lastIndex = 0;
253
+ if (pattern.regex.test(value)) {
254
+ pattern.regex.lastIndex = 0;
255
+ value = value.replace(pattern.regex, "[REDACTED]");
256
+ recordHasRedaction.add(idx);
257
+ redactionDetails.push({
258
+ index: idx,
259
+ field,
260
+ pattern: pattern.name
261
+ });
262
+ }
263
+ }
264
+ cleaned[field] = value;
265
+ }
266
+ return cleaned;
267
+ });
268
+ return {
269
+ cleanRecords,
270
+ redactedCount: recordHasRedaction.size,
271
+ redactionDetails
272
+ };
273
+ }
274
+ function ensureWecloneExportAdapterRegistered() {
275
+ if (getTrainingExportAdapter(wecloneExportAdapter.name) !== void 0) {
276
+ return false;
277
+ }
278
+ registerTrainingExportAdapter(wecloneExportAdapter);
279
+ return true;
280
+ }
281
+ try {
282
+ ensureWecloneExportAdapterRegistered();
283
+ } catch {
284
+ }
285
+
286
+ // src/service-candidates.ts
287
+ function firstSuccessfulResult(candidates, attempt) {
288
+ for (const candidate of candidates) {
289
+ try {
290
+ const result = attempt(candidate);
291
+ if (result !== void 0) return result;
292
+ } catch {
293
+ }
294
+ }
295
+ return void 0;
296
+ }
297
+ function firstSuccessfulCandidate(candidates, attempt) {
298
+ return firstSuccessfulResult(candidates, (candidate) => {
299
+ attempt(candidate);
300
+ return candidate;
301
+ });
302
+ }
303
+
304
+ // src/bench-args.ts
42
305
  import path from "path";
43
- var DEFAULT_BASELINE_PATH = path.join(
44
- process.cwd(),
45
- "benchmarks",
46
- "baseline.json"
47
- );
48
- var DEFAULT_REPORT_PATH = path.join(
49
- process.cwd(),
50
- "benchmarks",
51
- "report.json"
52
- );
53
- var BASELINE_VERSION = 1;
54
- var DEFAULT_QUERIES = [
55
- "What is the storage?",
56
- "How do I access storage?",
57
- "What categories exist?",
58
- "How is memory organized?",
59
- "What is the recall budget?",
60
- "What is the extraction pipeline?",
61
- "What facts are stored about the project?",
62
- "What is the architecture?"
306
+
307
+ // src/path-utils.ts
308
+ function resolveHomeDir() {
309
+ return process.env.HOME ?? process.env.USERPROFILE ?? "~";
310
+ }
311
+ function expandTilde(p) {
312
+ if (p === "~" || p.startsWith("~/") || p.startsWith("~\\")) {
313
+ return resolveHomeDir() + p.slice(1);
314
+ }
315
+ const home = resolveHomeDir();
316
+ if (p === "$HOME" || p.startsWith("$HOME/") || p.startsWith("$HOME\\")) {
317
+ return home + p.slice(5);
318
+ }
319
+ if (p === "${HOME}" || p.startsWith("${HOME}/") || p.startsWith("${HOME}\\")) {
320
+ return home + p.slice(7);
321
+ }
322
+ return p;
323
+ }
324
+
325
+ // src/bench-args.ts
326
+ function readBenchOptionValue(argv, flag) {
327
+ const index = argv.indexOf(flag);
328
+ if (index === -1) {
329
+ return void 0;
330
+ }
331
+ const value = argv[index + 1];
332
+ if (!value || value.startsWith("-")) {
333
+ throw new Error(`ERROR: ${flag} requires a value.`);
334
+ }
335
+ return value;
336
+ }
337
+ function collectBenchmarks(argv) {
338
+ const benchmarks = [];
339
+ for (let index = 0; index < argv.length; index += 1) {
340
+ const arg = argv[index];
341
+ if (arg === "--dataset-dir" || arg === "--results-dir" || arg === "--baselines-dir" || arg === "--threshold" || arg === "--custom" || arg === "--format" || arg === "--output" || arg === "--target") {
342
+ index += 1;
343
+ continue;
344
+ }
345
+ if (!arg.startsWith("-")) {
346
+ benchmarks.push(arg);
347
+ }
348
+ }
349
+ return benchmarks;
350
+ }
351
+ function parseBenchActionArgs(argv) {
352
+ const [first, ...rest] = argv;
353
+ const action = first === "list" || first === "run" || first === "datasets" || first === "runs" || first === "compare" || first === "ui" || first === "results" || first === "baseline" || first === "export" || first === "providers" || first === "publish" || first === "check" || first === "report" ? first : first === void 0 || first === "--help" || first === "-h" ? "help" : "run";
354
+ return {
355
+ action,
356
+ args: action === "run" && action !== first ? argv : rest
357
+ };
358
+ }
359
+ function parseBenchArgs(argv) {
360
+ const { action, args } = parseBenchActionArgs(argv);
361
+ const baselineAction = action === "baseline" ? args[0] === "save" || args[0] === "list" ? args[0] : void 0 : void 0;
362
+ const datasetAction = action === "datasets" ? args[0] === "download" || args[0] === "status" ? args[0] : void 0 : void 0;
363
+ const providerAction = action === "providers" ? args[0] === "discover" ? args[0] : void 0 : void 0;
364
+ const runAction = action === "runs" ? args[0] === "list" || args[0] === "show" || args[0] === "delete" ? args[0] : void 0 : void 0;
365
+ if (action === "baseline" && baselineAction === void 0) {
366
+ throw new Error("ERROR: baseline requires a subcommand: save or list.");
367
+ }
368
+ if (action === "datasets" && datasetAction === void 0) {
369
+ throw new Error("ERROR: datasets requires a subcommand: download or status.");
370
+ }
371
+ if (action === "providers" && providerAction === void 0) {
372
+ throw new Error("ERROR: providers requires a subcommand: discover.");
373
+ }
374
+ if (action === "runs" && runAction === void 0) {
375
+ throw new Error("ERROR: runs requires a subcommand: list, show, or delete.");
376
+ }
377
+ const benchmarkArgs = action === "baseline" || action === "datasets" || action === "providers" || action === "runs" ? args.slice(1) : args;
378
+ const benchmarks = collectBenchmarks(benchmarkArgs);
379
+ const datasetDir = readBenchOptionValue(args, "--dataset-dir");
380
+ const resultsDir = readBenchOptionValue(args, "--results-dir");
381
+ const baselinesDir = readBenchOptionValue(args, "--baselines-dir");
382
+ const thresholdRaw = readBenchOptionValue(args, "--threshold");
383
+ const customRaw = readBenchOptionValue(args, "--custom");
384
+ const formatRaw = readBenchOptionValue(args, "--format");
385
+ const output = readBenchOptionValue(args, "--output");
386
+ const targetRaw = readBenchOptionValue(args, "--target");
387
+ let threshold;
388
+ if (thresholdRaw !== void 0) {
389
+ threshold = Number(thresholdRaw);
390
+ if (!Number.isFinite(threshold) || threshold < 0) {
391
+ throw new Error("ERROR: --threshold must be a non-negative number.");
392
+ }
393
+ }
394
+ let format;
395
+ if (formatRaw !== void 0) {
396
+ if (formatRaw !== "json" && formatRaw !== "csv" && formatRaw !== "html") {
397
+ throw new Error('ERROR: --format must be "json", "csv", or "html".');
398
+ }
399
+ format = formatRaw;
400
+ }
401
+ let target;
402
+ if (targetRaw !== void 0) {
403
+ if (targetRaw !== "remnic-ai") {
404
+ throw new Error('ERROR: --target must be "remnic-ai".');
405
+ }
406
+ target = targetRaw;
407
+ }
408
+ return {
409
+ action,
410
+ benchmarks,
411
+ quick: args.includes("--quick"),
412
+ all: args.includes("--all"),
413
+ json: args.includes("--json"),
414
+ detail: args.includes("--detail"),
415
+ datasetDir: datasetDir ? path.resolve(expandTilde(datasetDir)) : void 0,
416
+ resultsDir: resultsDir ? path.resolve(expandTilde(resultsDir)) : void 0,
417
+ baselinesDir: baselinesDir ? path.resolve(expandTilde(baselinesDir)) : void 0,
418
+ threshold,
419
+ custom: customRaw ? path.resolve(expandTilde(customRaw)) : void 0,
420
+ baselineAction,
421
+ datasetAction,
422
+ providerAction,
423
+ runAction,
424
+ format,
425
+ output: output ? path.resolve(expandTilde(output)) : void 0,
426
+ target
427
+ };
428
+ }
429
+
430
+ // src/cli-args.ts
431
+ function resolveFlag(args, flag) {
432
+ const idx = args.indexOf(flag);
433
+ return idx !== -1 && idx + 1 < args.length ? args[idx + 1] : void 0;
434
+ }
435
+ function hasFlag(args, flag) {
436
+ return args.indexOf(flag) !== -1;
437
+ }
438
+ var TAXONOMY_RESOLVE_BOOLEAN_FLAGS = /* @__PURE__ */ new Set(["--json"]);
439
+ function stripResolveFlags(args, booleanFlags = TAXONOMY_RESOLVE_BOOLEAN_FLAGS) {
440
+ const textParts = [];
441
+ for (let i = 0; i < args.length; i++) {
442
+ if (args[i].startsWith("--")) {
443
+ if (!booleanFlags.has(args[i])) {
444
+ i++;
445
+ }
446
+ continue;
447
+ }
448
+ textParts.push(args[i]);
449
+ }
450
+ return textParts;
451
+ }
452
+
453
+ // src/parse-connector-config.ts
454
+ function parseConnectorConfig(args) {
455
+ const config = {};
456
+ for (let i = 0; i < args.length; i++) {
457
+ const arg = args[i];
458
+ if (arg.startsWith("--config=")) {
459
+ const rest = arg.slice("--config=".length);
460
+ const eqIdx = rest.indexOf("=");
461
+ if (eqIdx !== -1) {
462
+ const key = rest.slice(0, eqIdx);
463
+ const value = rest.slice(eqIdx + 1);
464
+ if (key) config[key] = value;
465
+ }
466
+ } else if (arg === "--config") {
467
+ const next = args[i + 1];
468
+ if (next !== void 0) {
469
+ const eqIdx = next.indexOf("=");
470
+ if (eqIdx !== -1) {
471
+ const key = next.slice(0, eqIdx);
472
+ const value = next.slice(eqIdx + 1);
473
+ if (key) {
474
+ config[key] = value;
475
+ i++;
476
+ }
477
+ }
478
+ }
479
+ }
480
+ }
481
+ return config;
482
+ }
483
+ function stripConfigArgv(args) {
484
+ const result = [];
485
+ for (let i = 0; i < args.length; i++) {
486
+ const arg = args[i];
487
+ if (arg.startsWith("--config=")) {
488
+ continue;
489
+ } else if (arg === "--config") {
490
+ const next = args[i + 1];
491
+ if (next !== void 0 && next.includes("=") && !next.startsWith("--")) {
492
+ i++;
493
+ }
494
+ continue;
495
+ }
496
+ result.push(arg);
497
+ }
498
+ return result;
499
+ }
500
+
501
+ // src/index.ts
502
+ registerPublisher("codex", () => new CodexMemoryExtensionPublisher());
503
+ registerPublisher("claude-code", () => new ClaudeCodeMemoryExtensionPublisher());
504
+ registerPublisher("hermes", () => new HermesMemoryExtensionPublisher());
505
+ function readCompatEnv(primary, legacy) {
506
+ return process.env[primary] ?? process.env[legacy];
507
+ }
508
+ var PID_DIR = path2.join(resolveHomeDir(), ".remnic");
509
+ var LEGACY_PID_DIR = path2.join(resolveHomeDir(), ".engram");
510
+ var PID_FILE = path2.join(PID_DIR, "server.pid");
511
+ var LEGACY_PID_FILE = path2.join(LEGACY_PID_DIR, "server.pid");
512
+ var LOG_FILE = path2.join(PID_DIR, "server.log");
513
+ var LEGACY_LOG_FILE = path2.join(LEGACY_PID_DIR, "server.log");
514
+ var CLI_MODULE_DIR = path2.dirname(fileURLToPath(import.meta.url));
515
+ var CLI_REPO_ROOT = path2.resolve(CLI_MODULE_DIR, "../../..");
516
+ var EVAL_RUNNER_PATH = path2.join(CLI_REPO_ROOT, "evals", "run.ts");
517
+ var BENCHMARK_CATALOG = [
518
+ {
519
+ id: "ama-bench",
520
+ title: "AMA-Bench",
521
+ category: "agentic",
522
+ summary: "Agent Memory Abilities benchmark for long-horizon agent workflows."
523
+ },
524
+ {
525
+ id: "memory-arena",
526
+ title: "Memory Arena",
527
+ category: "agentic",
528
+ summary: "Interdependent multi-session tasks that stress operational recall."
529
+ },
530
+ {
531
+ id: "amemgym",
532
+ title: "AMemGym",
533
+ category: "agentic",
534
+ summary: "Interactive personalization benchmark for agent memory adaptation."
535
+ },
536
+ {
537
+ id: "longmemeval",
538
+ title: "LongMemEval",
539
+ category: "retrieval",
540
+ summary: "Long-term memory retrieval benchmark across core memory abilities."
541
+ },
542
+ {
543
+ id: "locomo",
544
+ title: "LoCoMo",
545
+ category: "conversational",
546
+ summary: "Long-conversation memory benchmark for persistent dialogue context."
547
+ }
63
548
  ];
64
- var DEFAULT_TOLERANCE = 10;
65
- function hrTimeMs() {
66
- const [s, ns] = process.hrtime();
67
- return s * 1e3 + Math.round(ns / 1e6);
68
- }
69
- function loadBaseline(baselinePath) {
70
- const p = baselinePath ?? DEFAULT_BASELINE_PATH;
71
- if (!fs.existsSync(p)) return void 0;
549
+ var BENCHMARK_IDS = new Set(BENCHMARK_CATALOG.map((entry) => entry.id));
550
+ function getBenchUsageText() {
551
+ return `Usage: remnic bench <list|run|datasets|runs|compare|results|baseline|export|publish|ui|providers> [options] [benchmark...]
552
+ remnic benchmark <list|run|datasets|runs|compare|results|baseline|export|publish|ui|providers|check|report> [options] [benchmark...]
553
+
554
+ Commands:
555
+ list List published benchmark packs
556
+ run [benchmark...] Run one or more benchmark packs
557
+ datasets download [benchmark...]
558
+ Download local datasets for supported published benchmarks
559
+ datasets status Show local dataset availability for supported benchmarks
560
+ runs list List stored benchmark runs
561
+ runs show <run> Show one stored benchmark run
562
+ runs delete <run...> Delete one or more stored benchmark runs
563
+ compare <base> <cand> Compare two stored benchmark runs by id or file path
564
+ results [run] List stored runs or inspect a stored run
565
+ baseline save <name> [run]
566
+ Save a stored run as a named baseline
567
+ baseline list List saved baselines
568
+ export <run> --format <json|csv|html>
569
+ Export one stored run as JSON, aggregate-metrics CSV, or static HTML
570
+ publish --target remnic-ai
571
+ Generate the Remnic.ai benchmark feed from stored runs
572
+ ui Launch the local benchmark overview UI
573
+ providers discover Auto-detect available local provider backends
574
+ check Legacy latency regression gate (compatibility)
575
+ report Legacy latency report generator (compatibility)
576
+
577
+ Options:
578
+ --quick Run a lightweight quick pass (maps to --lightweight --limit 1)
579
+ --all Run every published benchmark
580
+ --dataset-dir <path> Override the benchmark dataset directory for full runs
581
+ --custom <path> Run a YAML-defined custom benchmark file
582
+ --results-dir <path> Override the stored benchmark results directory
583
+ --baselines-dir <path> Override the named baseline directory
584
+ --threshold <value> Regression threshold for compare (default: 0.05)
585
+ --detail Include per-task details for bench results
586
+ --format <json|csv|html> Output format for bench export
587
+ --output <path> Write bench export output to a file
588
+ --target <name> Publish target for bench publish (remnic-ai)
589
+ --json Output JSON for \`list\`
590
+
591
+ Examples:
592
+ remnic bench list
593
+ remnic bench datasets status
594
+ remnic bench datasets download longmemeval
595
+ remnic bench datasets download --all
596
+ remnic bench runs list
597
+ remnic bench runs show candidate-run --detail
598
+ remnic bench runs delete candidate-run
599
+ remnic bench run --quick longmemeval
600
+ remnic bench run longmemeval --dataset-dir ~/datasets/longmemeval
601
+ remnic bench compare base-run candidate-run
602
+ remnic bench results
603
+ remnic bench results candidate-run --detail
604
+ remnic bench baseline save main candidate-run
605
+ remnic bench baseline list
606
+ remnic bench export candidate-run --format csv --output ./candidate.csv
607
+ remnic bench export candidate-run --format html --output ./report.html
608
+ remnic bench publish --target remnic-ai
609
+ remnic bench providers discover
610
+ remnic bench run --custom ./my-bench.yaml
611
+ remnic benchmark run --quick longmemeval`;
612
+ }
613
+ function buildBenchRunnerArgs(parsed, benchmarkId) {
614
+ const args = [EVAL_RUNNER_PATH, "--benchmark", benchmarkId];
615
+ if (parsed.quick) {
616
+ args.push("--lightweight", "--limit", "1");
617
+ }
618
+ if (parsed.datasetDir) {
619
+ args.push("--dataset-dir", parsed.datasetDir);
620
+ }
621
+ return args;
622
+ }
623
+ function coerceBenchCategory(benchmarkId, category) {
624
+ if (category === "agentic" || category === "retrieval" || category === "conversational") {
625
+ return category;
626
+ }
627
+ return BENCHMARK_CATALOG.find((entry) => entry.id === benchmarkId)?.category ?? "retrieval";
628
+ }
629
+ async function listBenchmarksFromPackage() {
630
+ const result = await loadBenchDefinitionsFromPackage();
631
+ if (!result) {
632
+ return void 0;
633
+ }
634
+ return result.map((entry) => ({
635
+ id: entry.id,
636
+ title: entry.title ?? entry.id,
637
+ category: coerceBenchCategory(entry.id, entry.meta?.category),
638
+ summary: entry.meta?.description ?? ""
639
+ }));
640
+ }
641
+ async function loadBenchDefinitionsFromPackage() {
72
642
  try {
73
- const raw = JSON.parse(fs.readFileSync(p, "utf8"));
74
- if (raw.version !== BASELINE_VERSION) {
75
- console.warn(
76
- `Baseline version mismatch: expected ${BASELINE_VERSION}, got ${raw.version}`
643
+ const benchModule = await import("./dist-7DCVQLUB.js");
644
+ if (!benchModule.listBenchmarks) return void 0;
645
+ const result = benchModule.listBenchmarks();
646
+ return Array.isArray(result) ? result : void 0;
647
+ } catch {
648
+ return void 0;
649
+ }
650
+ }
651
+ async function resolveAllBenchmarks() {
652
+ const packageBenchmarks = await loadBenchDefinitionsFromPackage();
653
+ if (packageBenchmarks) {
654
+ return packageBenchmarks.filter((entry) => entry.runnerAvailable).map((entry) => entry.id);
655
+ }
656
+ if (!fs.existsSync(EVAL_RUNNER_PATH)) {
657
+ return [];
658
+ }
659
+ return BENCHMARK_CATALOG.map((entry) => entry.id);
660
+ }
661
+ async function resolveKnownBenchmarkIds() {
662
+ const knownIds = new Set(BENCHMARK_IDS);
663
+ const packageBenchmarks = await loadBenchDefinitionsFromPackage();
664
+ if (packageBenchmarks) {
665
+ for (const benchmark of packageBenchmarks) {
666
+ knownIds.add(benchmark.id);
667
+ }
668
+ }
669
+ return knownIds;
670
+ }
671
+ async function runBenchViaFallback(parsed, benchmarkId) {
672
+ if (!fs.existsSync(EVAL_RUNNER_PATH)) {
673
+ console.error(
674
+ "Benchmark runner not found. Expected eval runner at evals/run.ts or a phase-1 @remnic/bench runtime export."
675
+ );
676
+ process.exit(1);
677
+ }
678
+ const tsxCandidates = [
679
+ path2.join(CLI_REPO_ROOT, "node_modules", ".bin", "tsx"),
680
+ path2.join(CLI_REPO_ROOT, "packages", "remnic-cli", "node_modules", ".bin", "tsx")
681
+ ];
682
+ const tsxCmd = tsxCandidates.find((candidate) => fs.existsSync(candidate)) ?? "tsx";
683
+ childProcess.execFileSync(tsxCmd, buildBenchRunnerArgs(parsed, benchmarkId), {
684
+ stdio: "inherit",
685
+ env: process.env
686
+ });
687
+ }
688
+ function resolveBenchOutputDir() {
689
+ return path2.join(resolveHomeDir(), ".remnic", "bench", "results");
690
+ }
691
+ var DOWNLOADABLE_BENCHMARK_DATASETS = [
692
+ "ama-bench",
693
+ "memory-arena",
694
+ "amemgym",
695
+ "longmemeval",
696
+ "locomo"
697
+ ];
698
+ var DOWNLOADED_DATASET_MARKERS = {
699
+ "ama-bench": { anyOf: ["open_end_qa_set.jsonl"] },
700
+ longmemeval: {
701
+ anyOf: ["longmemeval_oracle.json", "longmemeval_s_cleaned.json", "longmemeval.json"]
702
+ },
703
+ amemgym: {
704
+ anyOf: ["amemgym-v1-base.json", "amemgym-tasks.json", "data.json"]
705
+ },
706
+ locomo: { anyOf: ["locomo10.json", "locomo.json"] },
707
+ "memory-arena": { ext: ".jsonl" }
708
+ };
709
+ function isDatasetDownloaded(datasetPath, benchmarkId) {
710
+ let stats;
711
+ try {
712
+ stats = fs.statSync(datasetPath);
713
+ } catch {
714
+ return false;
715
+ }
716
+ if (!stats.isDirectory()) {
717
+ return false;
718
+ }
719
+ const marker = DOWNLOADED_DATASET_MARKERS[benchmarkId];
720
+ if (!marker) {
721
+ try {
722
+ return fs.readdirSync(datasetPath).length > 0;
723
+ } catch {
724
+ return false;
725
+ }
726
+ }
727
+ if (marker.anyOf) {
728
+ return marker.anyOf.some((name) => {
729
+ try {
730
+ return fs.statSync(path2.join(datasetPath, name)).isFile();
731
+ } catch {
732
+ return false;
733
+ }
734
+ });
735
+ }
736
+ if (marker.ext) {
737
+ try {
738
+ return fs.readdirSync(datasetPath).some((name) => name.endsWith(marker.ext));
739
+ } catch {
740
+ return false;
741
+ }
742
+ }
743
+ return false;
744
+ }
745
+ async function launchBenchUi(resultsDir) {
746
+ const benchUiDir = path2.join(CLI_REPO_ROOT, "packages", "bench-ui");
747
+ const pnpmCmd = process.platform === "win32" ? "pnpm.cmd" : "pnpm";
748
+ if (!fs.existsSync(path2.join(benchUiDir, "package.json"))) {
749
+ console.error("ERROR: @remnic/bench-ui is not available in this checkout.");
750
+ process.exit(1);
751
+ }
752
+ console.log(`Launching bench UI with results from ${resultsDir}`);
753
+ console.log("Press Ctrl+C to stop the local server.");
754
+ const child = childProcess.spawn(pnpmCmd, ["exec", "vite", "--host", "127.0.0.1"], {
755
+ cwd: benchUiDir,
756
+ stdio: "inherit",
757
+ shell: process.platform === "win32",
758
+ env: {
759
+ ...process.env,
760
+ REMNIC_BENCH_RESULTS_DIR: resultsDir
761
+ }
762
+ });
763
+ await new Promise((resolve, reject) => {
764
+ child.on("error", reject);
765
+ child.on("close", (code, signal) => {
766
+ if (code === 0 || signal === "SIGINT" || signal === "SIGTERM") {
767
+ resolve();
768
+ return;
769
+ }
770
+ reject(new Error(`bench UI exited with code ${code ?? "unknown"}`));
771
+ });
772
+ });
773
+ }
774
+ function resolveBenchBaselineDir() {
775
+ return defaultBenchmarkBaselineDir();
776
+ }
777
+ function resolveRepoDatasetRoot() {
778
+ const repoCandidate = path2.join(CLI_REPO_ROOT, "evals", "datasets");
779
+ if (isRepoCheckout()) {
780
+ return repoCandidate;
781
+ }
782
+ return path2.join(resolveHomeDir(), ".remnic", "bench", "datasets");
783
+ }
784
+ function listDownloadableBenchmarks() {
785
+ return [...DOWNLOADABLE_BENCHMARK_DATASETS];
786
+ }
787
+ function resolveDatasetDownloadScriptPath() {
788
+ const bundled = path2.join(CLI_MODULE_DIR, "assets", "download-datasets.sh");
789
+ if (fs.existsSync(bundled)) {
790
+ return bundled;
791
+ }
792
+ return path2.join(CLI_REPO_ROOT, "evals", "scripts", "download-datasets.sh");
793
+ }
794
+ function isRepoCheckout() {
795
+ return fs.existsSync(path2.join(CLI_REPO_ROOT, "pnpm-workspace.yaml")) && fs.existsSync(path2.join(CLI_REPO_ROOT, "evals", "scripts", "download-datasets.sh"));
796
+ }
797
+ function runDatasetDownloadScript(scriptPath, benchmarkId, datasetRoot, jsonMode) {
798
+ const stdio = jsonMode ? ["inherit", process.stderr, "inherit"] : "inherit";
799
+ const env = { ...process.env, DATASETS_DIR: datasetRoot };
800
+ const options = {
801
+ cwd: CLI_REPO_ROOT,
802
+ stdio,
803
+ env
804
+ };
805
+ const args = ["--benchmark", benchmarkId];
806
+ if (process.platform !== "win32") {
807
+ childProcess.execFileSync(scriptPath, args, options);
808
+ return;
809
+ }
810
+ const bashProbe = childProcess.spawnSync("bash", ["--version"], { stdio: "ignore" });
811
+ if (bashProbe.error || bashProbe.status !== 0) {
812
+ throw new Error(
813
+ "bench datasets download requires bash on Windows (Git Bash or WSL). Install bash or run this command from a Unix shell."
814
+ );
815
+ }
816
+ childProcess.execFileSync("bash", [scriptPath, ...args], options);
817
+ }
818
+ function resolveSelectedDatasetDownloads(parsed) {
819
+ const supported = listDownloadableBenchmarks();
820
+ if (parsed.all) {
821
+ return supported;
822
+ }
823
+ if (parsed.benchmarks.length === 0) {
824
+ console.error(
825
+ "ERROR: datasets download requires at least one benchmark id or --all. Usage: remnic bench datasets download <benchmark...> [--all] [--json]"
826
+ );
827
+ process.exit(1);
828
+ }
829
+ const selected = [...new Set(parsed.benchmarks)];
830
+ const unsupported = selected.filter((benchmarkId) => !supported.includes(benchmarkId));
831
+ if (unsupported.length > 0) {
832
+ console.error(
833
+ `ERROR: unsupported downloadable benchmark dataset(s): ${unsupported.join(", ")}. Supported datasets: ${supported.join(", ")}.`
834
+ );
835
+ process.exit(1);
836
+ }
837
+ return selected;
838
+ }
839
+ function resolveBenchDatasetDir(benchmarkId, quick, datasetDirOverride) {
840
+ if (datasetDirOverride) {
841
+ return datasetDirOverride;
842
+ }
843
+ if (quick) {
844
+ return void 0;
845
+ }
846
+ const datasetDir = path2.join(resolveRepoDatasetRoot(), benchmarkId);
847
+ if (isDatasetDownloaded(datasetDir, benchmarkId)) {
848
+ return datasetDir;
849
+ }
850
+ return void 0;
851
+ }
852
+ function printBenchPackageSummary(result, outputPath, outputLabel = "Results saved") {
853
+ console.log(`Benchmark: ${result.meta.benchmark}`);
854
+ console.log(`Mode: ${result.meta.mode}`);
855
+ console.log(`Tasks: ${result.results.tasks.length}`);
856
+ console.log(`Mean query latency: ${result.cost.meanQueryLatencyMs.toFixed(1)}ms`);
857
+ for (const [metric, aggregate] of Object.entries(result.results.aggregates).sort()) {
858
+ console.log(` ${metric.padEnd(20)} ${aggregate.mean.toFixed(4)}`);
859
+ }
860
+ console.log(`${outputLabel}: ${outputPath}`);
861
+ }
862
+ function printStoredBenchResultSummary(result, summary) {
863
+ printBenchPackageSummary(result, summary.path, "Stored result");
864
+ console.log(`Run id: ${summary.id}`);
865
+ }
866
+ function printStoredBenchResultDetails(result, summary) {
867
+ printStoredBenchResultSummary(result, summary);
868
+ if (result.results.tasks.length === 0) {
869
+ console.log("Tasks: none");
870
+ return;
871
+ }
872
+ console.log("Task breakdown:");
873
+ for (const task of result.results.tasks) {
874
+ const scores = Object.entries(task.scores).sort(([left], [right]) => left.localeCompare(right)).map(([metric, value]) => `${metric}=${value.toFixed(4)}`).join(", ");
875
+ console.log(
876
+ ` ${task.taskId}: ${task.latencyMs.toFixed(1)}ms${scores.length > 0 ? ` [${scores}]` : ""}`
877
+ );
878
+ }
879
+ }
880
+ function printBenchComparisonSummary(comparison, baseline, candidate) {
881
+ console.log(`Benchmark: ${comparison.benchmark}`);
882
+ console.log(`Baseline: ${baseline.id} (${baseline.path})`);
883
+ console.log(`Candidate: ${candidate.id} (${candidate.path})`);
884
+ console.log(`Verdict: ${comparison.verdict}`);
885
+ const metrics = Object.entries(comparison.metricDeltas).sort(
886
+ ([left], [right]) => left.localeCompare(right)
887
+ );
888
+ if (metrics.length === 0) {
889
+ console.log("No overlapping metrics were found between the two results.");
890
+ return;
891
+ }
892
+ console.log("Metrics:");
893
+ for (const [metric, delta] of metrics) {
894
+ const percent = Number.isFinite(delta.percentChange) ? `${(delta.percentChange * 100).toFixed(2)}%` : delta.percentChange > 0 ? "+Infinity%" : "-Infinity%";
895
+ const direction = delta.delta >= 0 ? "+" : "";
896
+ console.log(
897
+ ` ${metric.padEnd(18)} ${delta.baseline.toFixed(4)} -> ${delta.candidate.toFixed(4)} (${direction}${delta.delta.toFixed(4)}, ${percent}, d=${delta.effectSize.cohensD.toFixed(3)} ${delta.effectSize.interpretation})`
898
+ );
899
+ if (delta.ciOnDelta) {
900
+ console.log(
901
+ ` CI95 delta: [${delta.ciOnDelta.lower.toFixed(4)}, ${delta.ciOnDelta.upper.toFixed(4)}]`
77
902
  );
78
903
  }
79
- return raw;
80
- } catch {
81
- return void 0;
82
904
  }
83
905
  }
84
- function saveBaseline(baselinePath, baseline) {
85
- fs.mkdirSync(path.dirname(baselinePath), { recursive: true });
86
- fs.writeFileSync(baselinePath, JSON.stringify(baseline, null, 2) + "\n");
87
- }
88
- async function recallWithTiers(service, query) {
89
- const tiers = [];
90
- const tierDetails = [];
91
- const t0 = hrTimeMs();
92
- const r0 = await service.recall({ query, mode: "auto" });
93
- const d0 = hrTimeMs() - t0;
94
- if (r0.results && r0.results.length > 0) {
95
- const hasExactMatch = r0.results.some(
96
- (m) => m.preview.toLowerCase().includes(query.toLowerCase())
906
+ async function compareBenchPackageResults(parsed) {
907
+ const refs = parsed.benchmarks;
908
+ if (refs.length !== 2) {
909
+ console.error(
910
+ "ERROR: compare requires exactly two stored result references. Usage: remnic bench compare <baseline> <candidate> [--results-dir <path>] [--threshold <value>] [--json]"
911
+ );
912
+ process.exit(1);
913
+ }
914
+ const resultsDir = parsed.resultsDir ?? resolveBenchOutputDir();
915
+ const [baselineRef, candidateRef] = refs;
916
+ const baselineSummary = await resolveBenchmarkResultReference(resultsDir, baselineRef);
917
+ const candidateSummary = await resolveBenchmarkResultReference(resultsDir, candidateRef);
918
+ if (!baselineSummary) {
919
+ console.error(`ERROR: benchmark result not found: ${baselineRef}`);
920
+ process.exit(1);
921
+ }
922
+ if (!candidateSummary) {
923
+ console.error(`ERROR: benchmark result not found: ${candidateRef}`);
924
+ process.exit(1);
925
+ }
926
+ const baseline = await loadBenchmarkResult(baselineSummary.path);
927
+ const candidate = await loadBenchmarkResult(candidateSummary.path);
928
+ if (baseline.meta.benchmark !== candidate.meta.benchmark) {
929
+ console.error(
930
+ `ERROR: benchmark mismatch: ${baseline.meta.benchmark} vs ${candidate.meta.benchmark}. Compare runs from the same benchmark.`
931
+ );
932
+ process.exit(1);
933
+ }
934
+ const comparison = compareResults(
935
+ baseline,
936
+ candidate,
937
+ parsed.threshold ?? 0.05,
938
+ getBenchmarkLowerIsBetter(candidate.meta.benchmark)
939
+ );
940
+ if (parsed.json) {
941
+ console.log(JSON.stringify({
942
+ benchmark: comparison.benchmark,
943
+ baseline: baselineSummary,
944
+ candidate: candidateSummary,
945
+ comparison
946
+ }, null, 2));
947
+ } else {
948
+ printBenchComparisonSummary(comparison, baselineSummary, candidateSummary);
949
+ }
950
+ if (comparison.verdict === "regression") {
951
+ process.exit(1);
952
+ }
953
+ }
954
+ async function showBenchPackageResults(parsed) {
955
+ const resultsDir = parsed.resultsDir ?? resolveBenchOutputDir();
956
+ if (parsed.benchmarks.length === 0) {
957
+ const summaries = await listBenchmarkResults(resultsDir);
958
+ if (parsed.json) {
959
+ console.log(JSON.stringify(summaries, null, 2));
960
+ return;
961
+ }
962
+ if (summaries.length === 0) {
963
+ console.log(`No stored benchmark runs found in ${resultsDir}`);
964
+ return;
965
+ }
966
+ console.log("Stored benchmark runs:");
967
+ for (const summary2 of summaries) {
968
+ console.log(
969
+ ` ${summary2.id.padEnd(24)} ${summary2.benchmark.padEnd(16)} ${summary2.mode.padEnd(5)} ${summary2.timestamp}`
970
+ );
971
+ }
972
+ return;
973
+ }
974
+ if (parsed.benchmarks.length !== 1) {
975
+ console.error(
976
+ "ERROR: results accepts at most one stored result reference. Usage: remnic bench results [run] [--detail] [--results-dir <path>] [--json]"
977
+ );
978
+ process.exit(1);
979
+ }
980
+ const reference = parsed.benchmarks[0];
981
+ const summary = await resolveBenchmarkResultReference(resultsDir, reference);
982
+ if (!summary) {
983
+ console.error(`ERROR: benchmark result not found: ${reference}`);
984
+ process.exit(1);
985
+ }
986
+ const result = await loadBenchmarkResult(summary.path);
987
+ if (parsed.json) {
988
+ console.log(JSON.stringify(result, null, 2));
989
+ return;
990
+ }
991
+ if (parsed.detail) {
992
+ printStoredBenchResultDetails(result, summary);
993
+ } else {
994
+ printStoredBenchResultSummary(result, summary);
995
+ }
996
+ }
997
+ async function manageBenchBaselines(parsed) {
998
+ const baselineDir = parsed.baselinesDir ?? resolveBenchBaselineDir();
999
+ if (parsed.baselineAction === "list") {
1000
+ const baselines = await listBenchmarkBaselines(baselineDir);
1001
+ if (parsed.json) {
1002
+ console.log(JSON.stringify(baselines, null, 2));
1003
+ return;
1004
+ }
1005
+ if (baselines.length === 0) {
1006
+ console.log(`No saved baselines found in ${baselineDir}`);
1007
+ return;
1008
+ }
1009
+ console.log("Saved baselines:");
1010
+ for (const baseline of baselines) {
1011
+ console.log(
1012
+ ` ${baseline.name.padEnd(20)} ${baseline.benchmark.padEnd(16)} ${baseline.mode.padEnd(5)} ${baseline.timestamp}`
1013
+ );
1014
+ }
1015
+ return;
1016
+ }
1017
+ if (parsed.baselineAction !== "save") {
1018
+ console.error("ERROR: baseline requires a subcommand: save or list.");
1019
+ process.exit(1);
1020
+ }
1021
+ if (parsed.benchmarks.length < 1 || parsed.benchmarks.length > 2) {
1022
+ console.error(
1023
+ "ERROR: baseline save requires a name and optionally one stored result reference. Usage: remnic bench baseline save <name> [run] [--results-dir <path>] [--baselines-dir <path>] [--json]"
1024
+ );
1025
+ process.exit(1);
1026
+ }
1027
+ const [name, explicitReference] = parsed.benchmarks;
1028
+ const resultsDir = parsed.resultsDir ?? resolveBenchOutputDir();
1029
+ const sourceSummary = explicitReference ? await resolveBenchmarkResultReference(resultsDir, explicitReference) : (await listBenchmarkResults(resultsDir))[0];
1030
+ if (!sourceSummary) {
1031
+ console.error(
1032
+ explicitReference ? `ERROR: benchmark result not found: ${explicitReference}` : `ERROR: no stored benchmark runs found in ${resultsDir}`
1033
+ );
1034
+ process.exit(1);
1035
+ }
1036
+ const result = await loadBenchmarkResult(sourceSummary.path);
1037
+ let writtenPath;
1038
+ try {
1039
+ writtenPath = await saveBenchmarkBaseline(
1040
+ baselineDir,
1041
+ name,
1042
+ result,
1043
+ { id: sourceSummary.id, path: sourceSummary.path }
1044
+ );
1045
+ } catch (error) {
1046
+ console.error(error instanceof Error ? error.message : String(error));
1047
+ process.exit(1);
1048
+ }
1049
+ if (parsed.json) {
1050
+ const baseline = await loadBenchmarkBaseline(writtenPath);
1051
+ console.log(JSON.stringify({
1052
+ name: baseline.name,
1053
+ path: writtenPath,
1054
+ source: baseline.source,
1055
+ benchmark: baseline.result.meta.benchmark,
1056
+ timestamp: baseline.savedAt
1057
+ }, null, 2));
1058
+ return;
1059
+ }
1060
+ console.log(`Saved baseline "${name}" to ${writtenPath}`);
1061
+ console.log(` Source run: ${sourceSummary.id}`);
1062
+ console.log(` Benchmark: ${result.meta.benchmark}`);
1063
+ }
1064
+ async function exportBenchPackageResult(parsed) {
1065
+ if (parsed.benchmarks.length !== 1) {
1066
+ console.error(
1067
+ "ERROR: export requires exactly one stored result reference. Usage: remnic bench export <run> --format <json|csv|html> [--output <path>] [--results-dir <path>]"
1068
+ );
1069
+ process.exit(1);
1070
+ }
1071
+ if (!parsed.format) {
1072
+ console.error("ERROR: export requires --format json, csv, or html.");
1073
+ process.exit(1);
1074
+ }
1075
+ const resultsDir = parsed.resultsDir ?? resolveBenchOutputDir();
1076
+ const reference = parsed.benchmarks[0];
1077
+ const summary = await resolveBenchmarkResultReference(resultsDir, reference);
1078
+ if (!summary) {
1079
+ console.error(`ERROR: benchmark result not found: ${reference}`);
1080
+ process.exit(1);
1081
+ }
1082
+ const result = await loadBenchmarkResult(summary.path);
1083
+ const rendered = renderBenchmarkResultExport(result, parsed.format);
1084
+ if (parsed.output) {
1085
+ fs.mkdirSync(path2.dirname(parsed.output), { recursive: true });
1086
+ fs.writeFileSync(parsed.output, rendered);
1087
+ console.log(`Exported ${summary.id} as ${parsed.format} to ${parsed.output}`);
1088
+ return;
1089
+ }
1090
+ process.stdout.write(rendered);
1091
+ }
1092
+ async function manageBenchDatasets(parsed) {
1093
+ const datasetRoot = resolveRepoDatasetRoot();
1094
+ const supported = listDownloadableBenchmarks();
1095
+ if (parsed.datasetAction === "status") {
1096
+ if (parsed.benchmarks.length > 0 || parsed.all) {
1097
+ console.error(
1098
+ "ERROR: datasets status does not accept benchmark names or --all. Usage: remnic bench datasets status [--json]"
1099
+ );
1100
+ process.exit(1);
1101
+ }
1102
+ const status = supported.map((benchmarkId) => {
1103
+ const datasetPath = path2.join(datasetRoot, benchmarkId);
1104
+ return {
1105
+ benchmark: benchmarkId,
1106
+ downloaded: isDatasetDownloaded(datasetPath, benchmarkId),
1107
+ path: datasetPath
1108
+ };
1109
+ });
1110
+ if (parsed.json) {
1111
+ console.log(JSON.stringify(status, null, 2));
1112
+ return;
1113
+ }
1114
+ console.log("Downloadable benchmark datasets:");
1115
+ for (const entry of status) {
1116
+ console.log(
1117
+ ` ${entry.benchmark.padEnd(16)} ${entry.downloaded ? "downloaded" : "missing"} ${entry.path}`
1118
+ );
1119
+ }
1120
+ console.log("");
1121
+ console.log(
1122
+ "Only the script-backed published datasets are managed here. Other benchmark fixtures remain repo-managed or manual."
1123
+ );
1124
+ return;
1125
+ }
1126
+ if (parsed.datasetAction !== "download") {
1127
+ console.error("ERROR: datasets requires a subcommand: download or status.");
1128
+ process.exit(1);
1129
+ }
1130
+ const scriptPath = resolveDatasetDownloadScriptPath();
1131
+ if (!fs.existsSync(scriptPath)) {
1132
+ console.error(`ERROR: dataset download script not found: ${scriptPath}`);
1133
+ process.exit(1);
1134
+ }
1135
+ const selected = resolveSelectedDatasetDownloads(parsed);
1136
+ const downloaded = [];
1137
+ for (const benchmarkId of selected) {
1138
+ runDatasetDownloadScript(scriptPath, benchmarkId, datasetRoot, parsed.json === true);
1139
+ downloaded.push({
1140
+ benchmark: benchmarkId,
1141
+ path: path2.join(datasetRoot, benchmarkId)
1142
+ });
1143
+ }
1144
+ if (parsed.json) {
1145
+ console.log(JSON.stringify(downloaded, null, 2));
1146
+ return;
1147
+ }
1148
+ console.log("Downloaded benchmark datasets:");
1149
+ for (const entry of downloaded) {
1150
+ console.log(` ${entry.benchmark} ${entry.path}`);
1151
+ }
1152
+ }
1153
+ async function manageBenchRuns(parsed) {
1154
+ const resultsDir = parsed.resultsDir ?? resolveBenchOutputDir();
1155
+ if (parsed.runAction === "list") {
1156
+ if (parsed.benchmarks.length > 0 || parsed.all) {
1157
+ console.error(
1158
+ "ERROR: runs list does not accept benchmark names or --all. Usage: remnic bench runs list [--results-dir <path>] [--json]"
1159
+ );
1160
+ process.exit(1);
1161
+ }
1162
+ await showBenchPackageResults({ ...parsed, action: "results", benchmarks: [] });
1163
+ return;
1164
+ }
1165
+ if (parsed.runAction === "show") {
1166
+ if (parsed.benchmarks.length !== 1 || parsed.all) {
1167
+ console.error(
1168
+ "ERROR: runs show requires exactly one stored result reference. Usage: remnic bench runs show <run> [--detail] [--results-dir <path>] [--json]"
1169
+ );
1170
+ process.exit(1);
1171
+ }
1172
+ await showBenchPackageResults(parsed);
1173
+ return;
1174
+ }
1175
+ if (parsed.runAction === "delete") {
1176
+ if (parsed.benchmarks.length === 0 || parsed.all) {
1177
+ console.error(
1178
+ "ERROR: runs delete requires at least one stored result reference. Usage: remnic bench runs delete <run...> [--results-dir <path>] [--json]"
1179
+ );
1180
+ process.exit(1);
1181
+ }
1182
+ const deleted = await deleteBenchmarkResults(resultsDir, parsed.benchmarks);
1183
+ if (parsed.json) {
1184
+ console.log(JSON.stringify(deleted, null, 2));
1185
+ } else {
1186
+ if (deleted.deleted.length === 0) {
1187
+ console.log("No benchmark runs were deleted.");
1188
+ } else {
1189
+ console.log("Deleted benchmark runs:");
1190
+ for (const summary of deleted.deleted) {
1191
+ console.log(` ${summary.id} ${summary.path}`);
1192
+ }
1193
+ }
1194
+ if (deleted.missing.length > 0) {
1195
+ console.log("Missing benchmark runs:");
1196
+ for (const reference of deleted.missing) {
1197
+ console.log(` ${reference}`);
1198
+ }
1199
+ }
1200
+ }
1201
+ if (deleted.missing.length > 0) {
1202
+ process.exit(1);
1203
+ }
1204
+ return;
1205
+ }
1206
+ console.error("ERROR: runs requires a subcommand: list, show, or delete.");
1207
+ process.exit(1);
1208
+ }
1209
+ async function discoverBenchProviders(parsed) {
1210
+ if (parsed.benchmarks.length > 0) {
1211
+ console.error(
1212
+ "ERROR: providers discover does not accept positional arguments. Usage: remnic bench providers discover [--json]"
97
1213
  );
98
- if (hasExactMatch) {
99
- tiers.push("exact_match");
100
- tierDetails.push({
101
- tier: "exact_match",
102
- latencyMs: d0,
103
- resultsCount: r0.results.length
104
- });
105
- return { tiers, tierDetails };
106
- }
1214
+ process.exit(1);
107
1215
  }
108
- const t1 = hrTimeMs();
109
- const r1 = await service.recall({ query, mode: "auto" });
110
- const d1 = hrTimeMs() - t1;
111
- const queryWords = query.toLowerCase().split(/\s+/).filter((w) => w.length > 2);
112
- const hasKeywordMatch = (r1.results ?? []).some(
113
- (m) => queryWords.some((kw) => m.preview.toLowerCase().includes(kw))
114
- );
115
- if (hasKeywordMatch) {
116
- tiers.push("category_match");
117
- tierDetails.push({
118
- tier: "category_match",
119
- latencyMs: d1,
120
- resultsCount: r1.results.length
121
- });
122
- return { tiers, tierDetails };
123
- }
124
- const t2 = hrTimeMs();
125
- const r2 = await service.recall({ query, mode: "auto" });
126
- const d2 = hrTimeMs() - t2;
127
- const tagged = (r2.results ?? []).filter((m) => m.tags && m.tags.length > 0);
128
- if (tagged.length > 0) {
129
- tiers.push("high_confidence");
130
- tierDetails.push({
131
- tier: "high_confidence",
132
- latencyMs: d2,
133
- resultsCount: tagged.length
134
- });
135
- return { tiers, tierDetails };
136
- }
137
- const t3 = hrTimeMs();
138
- const r3 = await service.recall({ query, mode: "auto" });
139
- const d3 = hrTimeMs() - t3;
140
- if (r3.results && r3.results.length > 0) {
141
- tiers.push("semantic_search");
142
- tierDetails.push({
143
- tier: "semantic_search",
144
- latencyMs: d3,
145
- resultsCount: r3.results.length
146
- });
147
- return { tiers, tierDetails };
148
- }
149
- const t4 = hrTimeMs();
150
- const r4 = await service.recall({ query, mode: "full" });
151
- const d4 = hrTimeMs() - t4;
152
- if (r4.results && r4.results.length > 0) {
153
- tiers.push("full_search");
154
- tierDetails.push({
155
- tier: "full_search",
156
- latencyMs: d4,
157
- resultsCount: r4.results.length
158
- });
159
- return { tiers, tierDetails };
1216
+ const discovered = await discoverAllProviders();
1217
+ if (parsed.json) {
1218
+ console.log(JSON.stringify(discovered, null, 2));
1219
+ return;
1220
+ }
1221
+ if (discovered.length === 0) {
1222
+ console.log("No local bench providers were discovered.");
1223
+ return;
1224
+ }
1225
+ console.log("Discovered bench providers:");
1226
+ for (const entry of discovered) {
1227
+ console.log(` ${entry.provider}`);
1228
+ for (const model of entry.models) {
1229
+ const capabilities = model.capabilities.join(", ");
1230
+ const details = [
1231
+ model.contextLength > 0 ? `context=${model.contextLength}` : void 0,
1232
+ model.parameterCount ? `params=${model.parameterCount}` : void 0,
1233
+ model.quantization ? `quant=${model.quantization}` : void 0,
1234
+ capabilities.length > 0 ? `caps=${capabilities}` : void 0
1235
+ ].filter((value) => Boolean(value));
1236
+ console.log(
1237
+ ` - ${model.id}${details.length > 0 ? ` (${details.join(", ")})` : ""}`
1238
+ );
1239
+ }
160
1240
  }
161
- tiers.push("no_results");
162
- tierDetails.push({
163
- tier: "no_results",
164
- latencyMs: d0 + d1 + d2 + d3 + d4,
165
- resultsCount: 0
166
- });
167
- return { tiers, tierDetails };
168
- }
169
- async function runExplain(service, query) {
170
- const start = hrTimeMs();
171
- const { tiers, tierDetails } = await recallWithTiers(service, query);
172
- const totalDuration = hrTimeMs() - start;
173
- return {
174
- query,
175
- tiersUsed: tiers,
176
- tierResults: tierDetails,
177
- durationMs: tierDetails[0]?.latencyMs ?? totalDuration,
178
- totalDurationMs: totalDuration
179
- };
180
1241
  }
181
- async function runSingle(service, queryText) {
182
- const start = hrTimeMs();
183
- const { tiers, tierDetails } = await recallWithTiers(service, queryText);
184
- const duration = hrTimeMs() - start;
185
- return {
186
- query: queryText,
187
- latencyMs: duration,
188
- tiersUsed: tiers,
189
- throughput: duration > 0 ? 1 / (duration / 1e3) : 0,
190
- resultsCount: tierDetails.reduce((sum, t) => sum + t.resultsCount, 0),
191
- totalDurationMs: duration,
192
- tierDetails
193
- };
1242
+ async function publishBenchPackageResults(parsed) {
1243
+ if (parsed.benchmarks.length > 0) {
1244
+ console.error(
1245
+ "ERROR: publish does not accept positional result references. Usage: remnic bench publish --target remnic-ai [--results-dir <path>] [--output <path>] [--json]"
1246
+ );
1247
+ process.exit(1);
1248
+ }
1249
+ if (parsed.target !== "remnic-ai") {
1250
+ console.error("ERROR: publish requires --target remnic-ai.");
1251
+ process.exit(1);
1252
+ }
1253
+ const resultsDir = parsed.resultsDir ?? resolveBenchOutputDir();
1254
+ const feed = await buildBenchmarkPublishFeed(resultsDir, parsed.target);
1255
+ if (feed.benchmarks.length === 0) {
1256
+ console.error(
1257
+ `ERROR: no publishable benchmark results found in ${resultsDir}. remnic-ai requires stored full runs for published benchmarks.`
1258
+ );
1259
+ process.exit(1);
1260
+ }
1261
+ const outputPath = parsed.output ?? defaultBenchmarkPublishPath(parsed.target);
1262
+ const writtenPath = await writeBenchmarkPublishFeed(feed, outputPath);
1263
+ if (parsed.json) {
1264
+ console.log(JSON.stringify({
1265
+ target: parsed.target,
1266
+ outputPath: writtenPath,
1267
+ benchmarkCount: feed.benchmarks.length,
1268
+ feed
1269
+ }, null, 2));
1270
+ return;
1271
+ }
1272
+ console.log(
1273
+ `Published ${feed.benchmarks.length} benchmark entries for ${parsed.target} to ${writtenPath}`
1274
+ );
194
1275
  }
195
- async function runBenchSuite(service, config = {}) {
196
- const queries = config.queries ?? DEFAULT_QUERIES;
197
- const regressionTolerance = config.regressionTolerance ?? DEFAULT_TOLERANCE;
198
- const baselinePath = config.baselinePath ?? DEFAULT_BASELINE_PATH;
199
- const reportPath = config.reportPath ?? DEFAULT_REPORT_PATH;
200
- const explain = config.explain ?? false;
201
- const results = [];
202
- const suiteStart = hrTimeMs();
203
- for (const q of queries) {
204
- if (explain) {
205
- const ex = await runExplain(service, q);
206
- results.push({
207
- query: ex.query,
208
- latencyMs: ex.durationMs,
209
- tiersUsed: ex.tiersUsed,
210
- throughput: ex.totalDurationMs > 0 ? 1 / (ex.totalDurationMs / 1e3) : 0,
211
- resultsCount: ex.tierResults.reduce(
212
- (sum, t) => sum + t.resultsCount,
213
- 0
214
- ),
215
- totalDurationMs: ex.totalDurationMs,
216
- tierDetails: ex.tierResults
217
- });
218
- } else {
219
- results.push(await runSingle(service, q));
220
- }
1276
+ async function runBenchViaPackage(parsed, benchmarkId) {
1277
+ let benchModule;
1278
+ try {
1279
+ benchModule = await import("./dist-7DCVQLUB.js");
1280
+ } catch {
1281
+ return false;
221
1282
  }
222
- const totalDuration = hrTimeMs() - suiteStart;
223
- const metrics = {};
224
- for (const r of results) {
225
- metrics[r.query] = r.latencyMs;
1283
+ const definition = benchModule.getBenchmark?.(benchmarkId);
1284
+ if (!definition?.runnerAvailable || !benchModule.runBenchmark || !benchModule.writeBenchmarkResult) {
1285
+ return false;
226
1286
  }
227
- const report = generateReport(results, reportPath);
228
- const baseline = loadBaseline(baselinePath);
229
- const regressionResult = checkRegression(
230
- metrics,
231
- baseline,
232
- regressionTolerance
1287
+ if (definition.meta?.category === "ingestion") {
1288
+ throw new Error(
1289
+ `Benchmark "${benchmarkId}" requires an ingestion adapter which is not yet available via the CLI. Run ingestion benchmarks programmatically by passing an ingestionAdapter to runBenchmark().`
1290
+ );
1291
+ }
1292
+ const createAdapter = parsed.quick ? benchModule.createLightweightAdapter : benchModule.createRemnicAdapter;
1293
+ if (!createAdapter) {
1294
+ return false;
1295
+ }
1296
+ const outputDir = resolveBenchOutputDir();
1297
+ const datasetDir = resolveBenchDatasetDir(
1298
+ benchmarkId,
1299
+ parsed.quick,
1300
+ parsed.datasetDir
233
1301
  );
234
- if (!baseline) {
235
- saveBaseline(baselinePath, {
236
- version: BASELINE_VERSION,
237
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
238
- metrics
239
- });
1302
+ if (!parsed.quick && !datasetDir) {
1303
+ throw new Error(
1304
+ `full benchmark runs for "${benchmarkId}" require dataset files. Run "remnic bench datasets download ${benchmarkId}" or pass --dataset-dir <path>.`
1305
+ );
240
1306
  }
241
- return {
242
- results,
243
- report,
244
- totalDurationMs: totalDuration,
245
- regressions: regressionResult.regressions
246
- };
247
- }
248
- function checkRegression(metrics, baseline, tolerance) {
249
- if (!baseline) return { passed: true, regressions: [] };
250
- const regressions = [];
251
- for (const [metricName, currentValue] of Object.entries(metrics)) {
252
- const baselineValue = baseline.metrics[metricName];
253
- if (baselineValue === void 0) continue;
254
- const changePercent = baselineValue > 0 ? (currentValue - baselineValue) / baselineValue * 100 : 0;
255
- regressions.push({
256
- metric: metricName,
257
- currentValue,
258
- baselineValue,
259
- tolerance,
260
- passed: changePercent <= tolerance
1307
+ const system = await createAdapter();
1308
+ try {
1309
+ const result = await benchModule.runBenchmark(benchmarkId, {
1310
+ mode: parsed.quick ? "quick" : "full",
1311
+ datasetDir,
1312
+ outputDir,
1313
+ limit: parsed.quick ? 1 : void 0,
1314
+ adapterMode: parsed.quick ? "lightweight" : "direct",
1315
+ system
261
1316
  });
1317
+ const writtenPath = await benchModule.writeBenchmarkResult(result, outputDir);
1318
+ if (parsed.json) {
1319
+ console.log(JSON.stringify(result, null, 2));
1320
+ } else {
1321
+ printBenchPackageSummary(result, writtenPath);
1322
+ }
1323
+ return true;
1324
+ } finally {
1325
+ await system.destroy();
262
1326
  }
263
- return {
264
- passed: regressions.every((r) => r.passed),
265
- regressions
266
- };
267
1327
  }
268
- function generateReport(results, reportPath) {
269
- const report = {
270
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
271
- queries: results.map((r) => ({
272
- query: r.query,
273
- tiersUsed: r.tiersUsed,
274
- durationMs: r.latencyMs,
275
- resultsCount: r.resultsCount,
276
- throughput: r.throughput,
277
- tierDetails: r.tierDetails
278
- })),
279
- totalDurationMs: results.reduce(
280
- (sum, r) => sum + r.totalDurationMs,
281
- 0
282
- )
283
- };
284
- if (reportPath) {
285
- fs.mkdirSync(path.dirname(reportPath), { recursive: true });
286
- fs.writeFileSync(reportPath, JSON.stringify(report, null, 2) + "\n");
1328
+ async function runCustomBenchViaPackage(parsed) {
1329
+ let benchModule;
1330
+ try {
1331
+ benchModule = await import("./dist-7DCVQLUB.js");
1332
+ } catch {
1333
+ return false;
287
1334
  }
288
- return report;
289
- }
290
-
291
- // src/service-candidates.ts
292
- function firstSuccessfulResult(candidates, attempt) {
293
- for (const candidate of candidates) {
294
- try {
295
- const result = attempt(candidate);
296
- if (result !== void 0) return result;
297
- } catch {
1335
+ if (!benchModule.runCustomBenchmarkFile || !benchModule.writeBenchmarkResult) {
1336
+ return false;
1337
+ }
1338
+ const createAdapter = parsed.quick ? benchModule.createLightweightAdapter : benchModule.createRemnicAdapter;
1339
+ if (!createAdapter) {
1340
+ return false;
1341
+ }
1342
+ const outputDir = resolveBenchOutputDir();
1343
+ const system = await createAdapter();
1344
+ try {
1345
+ const result = await benchModule.runCustomBenchmarkFile(parsed.custom, {
1346
+ mode: parsed.quick ? "quick" : "full",
1347
+ outputDir,
1348
+ limit: parsed.quick ? 1 : void 0,
1349
+ adapterMode: parsed.quick ? "lightweight" : "direct",
1350
+ system
1351
+ });
1352
+ const writtenPath = await benchModule.writeBenchmarkResult(result, outputDir);
1353
+ if (parsed.json) {
1354
+ console.log(JSON.stringify(result, null, 2));
1355
+ } else {
1356
+ printBenchPackageSummary(result, writtenPath);
298
1357
  }
1358
+ return true;
1359
+ } finally {
1360
+ await system.destroy();
299
1361
  }
300
- return void 0;
301
- }
302
- function firstSuccessfulCandidate(candidates, attempt) {
303
- return firstSuccessfulResult(candidates, (candidate) => {
304
- attempt(candidate);
305
- return candidate;
306
- });
307
- }
308
-
309
- // src/index.ts
310
- function readCompatEnv(primary, legacy) {
311
- return process.env[primary] ?? process.env[legacy];
312
- }
313
- function resolveHomeDir() {
314
- return process.env.HOME ?? process.env.USERPROFILE ?? "~";
315
1362
  }
316
- var PID_DIR = path2.join(resolveHomeDir(), ".remnic");
317
- var LEGACY_PID_DIR = path2.join(resolveHomeDir(), ".engram");
318
- var PID_FILE = path2.join(PID_DIR, "server.pid");
319
- var LEGACY_PID_FILE = path2.join(LEGACY_PID_DIR, "server.pid");
320
- var LOG_FILE = path2.join(PID_DIR, "server.log");
321
- var LEGACY_LOG_FILE = path2.join(LEGACY_PID_DIR, "server.log");
322
1363
  function resolveConfigPath(cliPath) {
323
1364
  if (cliPath) return path2.resolve(cliPath);
324
1365
  const envPath = readCompatEnv("REMNIC_CONFIG_PATH", "ENGRAM_CONFIG_PATH");
@@ -330,7 +1371,7 @@ function resolveConfigPath(cliPath) {
330
1371
  path2.join(resolveHomeDir(), ".config", "engram", "config.json")
331
1372
  ];
332
1373
  for (const candidate of candidates) {
333
- if (fs2.existsSync(candidate)) return candidate;
1374
+ if (fs.existsSync(candidate)) return candidate;
334
1375
  }
335
1376
  return path2.join(resolveHomeDir(), ".config", "remnic", "config.json");
336
1377
  }
@@ -339,24 +1380,24 @@ function resolveMemoryDir() {
339
1380
  const envMemoryDir = readCompatEnv("REMNIC_MEMORY_DIR", "ENGRAM_MEMORY_DIR");
340
1381
  if (envMemoryDir) return envMemoryDir;
341
1382
  const configPath = resolveConfigPath();
342
- const raw = fs2.existsSync(configPath) ? JSON.parse(fs2.readFileSync(configPath, "utf8")) : {};
1383
+ const raw = fs.existsSync(configPath) ? JSON.parse(fs.readFileSync(configPath, "utf8")) : {};
343
1384
  const remnicCfg = raw.remnic ?? raw.engram ?? raw;
344
1385
  if (remnicCfg.memoryDir) return remnicCfg.memoryDir;
345
1386
  const home = resolveHomeDir();
346
1387
  const standalonePath = path2.join(home, ".remnic", "memory");
347
1388
  const legacyStandalonePath = path2.join(home, ".engram", "memory");
348
1389
  const openclawPath = path2.join(home, ".openclaw", "workspace", "memory", "local");
349
- if (fs2.existsSync(standalonePath)) return standalonePath;
350
- if (fs2.existsSync(legacyStandalonePath)) return legacyStandalonePath;
1390
+ if (fs.existsSync(standalonePath)) return standalonePath;
1391
+ if (fs.existsSync(legacyStandalonePath)) return legacyStandalonePath;
351
1392
  return openclawPath;
352
1393
  })();
353
1394
  const manifestPath = getManifestPath();
354
- if (fs2.existsSync(manifestPath)) {
1395
+ if (fs.existsSync(manifestPath)) {
355
1396
  try {
356
1397
  const active = getActiveSpace();
357
1398
  if (active?.memoryDir) {
358
- if (!fs2.existsSync(active.memoryDir)) {
359
- fs2.mkdirSync(active.memoryDir, { recursive: true });
1399
+ if (!fs.existsSync(active.memoryDir)) {
1400
+ fs.mkdirSync(active.memoryDir, { recursive: true });
360
1401
  }
361
1402
  return active.memoryDir;
362
1403
  }
@@ -370,23 +1411,51 @@ function resolveMemoryDir() {
370
1411
  }
371
1412
  return configMemoryDir;
372
1413
  }
373
- function resolveFlag(args, flag) {
1414
+ function resolveFlagStrict(args, flag) {
374
1415
  const idx = args.indexOf(flag);
375
- return idx !== -1 && idx + 1 < args.length ? args[idx + 1] : void 0;
1416
+ if (idx === -1 || idx + 1 >= args.length) return void 0;
1417
+ const next = args[idx + 1];
1418
+ return next.startsWith("-") ? void 0 : next;
376
1419
  }
377
- function parseConnectorConfig(args) {
378
- const config = {};
379
- for (const arg of args) {
380
- if (arg.startsWith("--config=")) {
381
- const [key, value] = arg.slice("--config=".length).split("=");
382
- if (key && value) config[key] = value;
383
- }
1420
+ var REMNIC_OPENCLAW_PLUGIN_ID = "openclaw-remnic";
1421
+ var REMNIC_OPENCLAW_LEGACY_PLUGIN_ID = "openclaw-engram";
1422
+ var DEFAULT_OPENCLAW_CONFIG_PATHS_FOR_DOCTOR = [
1423
+ process.env.OPENCLAW_CONFIG_PATH,
1424
+ process.env.OPENCLAW_ENGRAM_CONFIG_PATH,
1425
+ path2.join(resolveHomeDir(), ".openclaw", "openclaw.json")
1426
+ ].filter(Boolean);
1427
+ function resolveOpenclawConfigPath(cliPath) {
1428
+ if (cliPath) return path2.resolve(expandTilde(cliPath));
1429
+ const envPath = process.env.OPENCLAW_CONFIG_PATH || process.env.OPENCLAW_ENGRAM_CONFIG_PATH;
1430
+ if (envPath) return path2.resolve(expandTilde(envPath));
1431
+ for (const candidate of DEFAULT_OPENCLAW_CONFIG_PATHS_FOR_DOCTOR) {
1432
+ if (fs.existsSync(candidate)) return candidate;
384
1433
  }
385
- return config;
1434
+ return path2.join(resolveHomeDir(), ".openclaw", "openclaw.json");
1435
+ }
1436
+ function readOpenclawConfig(configPath) {
1437
+ if (!fs.existsSync(configPath)) return {};
1438
+ const raw = fs.readFileSync(configPath, "utf-8");
1439
+ let parsed;
1440
+ try {
1441
+ parsed = JSON.parse(raw);
1442
+ } catch (err) {
1443
+ throw new Error(
1444
+ `OpenClaw config at ${configPath} contains invalid JSON \u2014 refusing to overwrite.
1445
+ Fix the file manually, then re-run.
1446
+ Parse error: ${err.message}`
1447
+ );
1448
+ }
1449
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
1450
+ throw new Error(
1451
+ `OpenClaw config at ${configPath} is not a JSON object (got ${Array.isArray(parsed) ? "array" : typeof parsed}) \u2014 refusing to overwrite.`
1452
+ );
1453
+ }
1454
+ return parsed;
386
1455
  }
387
1456
  function cmdInit() {
388
1457
  const configPath = path2.join(process.cwd(), "remnic.config.json");
389
- if (fs2.existsSync(configPath)) {
1458
+ if (fs.existsSync(configPath)) {
390
1459
  console.log(`Config already exists: ${configPath}`);
391
1460
  return;
392
1461
  }
@@ -402,7 +1471,7 @@ function cmdInit() {
402
1471
  authToken: "${REMNIC_AUTH_TOKEN}"
403
1472
  }
404
1473
  };
405
- fs2.writeFileSync(configPath, JSON.stringify(template, null, 2) + "\n");
1474
+ fs.writeFileSync(configPath, JSON.stringify(template, null, 2) + "\n");
406
1475
  console.log(`Created ${configPath}`);
407
1476
  console.log("\nSet these environment variables:");
408
1477
  console.log(" export OPENAI_API_KEY=sk-...");
@@ -417,68 +1486,568 @@ async function cmdStatus(json) {
417
1486
  console.log(JSON.stringify({ running, pid: pid ?? null, pidFile: PID_FILE, logFile: LOG_FILE }));
418
1487
  return;
419
1488
  }
420
- if (!running) {
421
- console.log("Remnic server: stopped");
1489
+ if (!running) {
1490
+ console.log("Remnic server: stopped");
1491
+ return;
1492
+ }
1493
+ console.log(`Remnic server: running${pid ? ` (pid ${pid})` : ""}`);
1494
+ const port = inferPort();
1495
+ const controller = new AbortController();
1496
+ const timeoutId = setTimeout(() => controller.abort(), 3e3);
1497
+ try {
1498
+ const response = await fetch(`http://127.0.0.1:${port}/engram/v1/health`, {
1499
+ signal: controller.signal
1500
+ });
1501
+ if (!response.ok) {
1502
+ console.log(`Health: server responded with ${response.status} ${response.statusText}`);
1503
+ } else {
1504
+ const health = await response.json();
1505
+ console.log(`Health: ${health.status ?? "ok"}`);
1506
+ }
1507
+ } catch {
1508
+ console.log("Health: unable to reach server");
1509
+ } finally {
1510
+ clearTimeout(timeoutId);
1511
+ }
1512
+ }
1513
+ async function cmdQuery(queryText, json, explain) {
1514
+ if (!queryText) {
1515
+ console.error("Usage: remnic query <text>");
1516
+ process.exit(1);
1517
+ }
1518
+ initLogger();
1519
+ const configPath = resolveConfigPath();
1520
+ const raw = fs.existsSync(configPath) ? JSON.parse(fs.readFileSync(configPath, "utf8")) : {};
1521
+ const remnicCfg = raw.remnic ?? raw.engram ?? raw;
1522
+ const config = parseConfig(remnicCfg);
1523
+ const orchestrator = new Orchestrator(config);
1524
+ await orchestrator.initialize();
1525
+ await orchestrator.deferredReady;
1526
+ const service = new EngramAccessService(orchestrator);
1527
+ if (explain) {
1528
+ const result2 = await runExplain(service, queryText);
1529
+ if (json) {
1530
+ console.log(JSON.stringify(result2, null, 2));
1531
+ } else {
1532
+ console.log(`Query: ${result2.query}`);
1533
+ console.log(`Tiers used: ${result2.tiersUsed.join(" \u2192 ")}`);
1534
+ console.log(`Total duration: ${result2.totalDurationMs}ms`);
1535
+ for (const t of result2.tierResults) {
1536
+ console.log(` ${t.tier}: ${t.latencyMs}ms (${t.resultsCount} results)`);
1537
+ }
1538
+ }
1539
+ return;
1540
+ }
1541
+ const result = await service.recall({ query: queryText, mode: "auto" });
1542
+ if (json) {
1543
+ console.log(JSON.stringify(result, null, 2));
1544
+ } else {
1545
+ const memories = result.memories ?? [];
1546
+ if (memories.length === 0) {
1547
+ console.log("No results.");
1548
+ return;
1549
+ }
1550
+ for (const m of memories) {
1551
+ console.log(`- ${m.content}`);
1552
+ }
1553
+ }
1554
+ }
1555
+ async function cmdVersions(rest) {
1556
+ initLogger();
1557
+ const configPath = resolveConfigPath();
1558
+ const raw = fs.existsSync(configPath) ? JSON.parse(fs.readFileSync(configPath, "utf8")) : {};
1559
+ const remnicCfg = raw.remnic ?? raw.engram ?? raw;
1560
+ const config = parseConfig(remnicCfg);
1561
+ if (!config.versioningEnabled) {
1562
+ console.error("Page versioning is disabled (versioningEnabled = false).");
1563
+ process.exit(1);
1564
+ }
1565
+ const versioningConfig = {
1566
+ enabled: config.versioningEnabled,
1567
+ maxVersionsPerPage: config.versioningMaxPerPage,
1568
+ sidecarDir: config.versioningSidecarDir
1569
+ };
1570
+ const memDir = resolveMemoryDir();
1571
+ const action = rest[0] ?? "help";
1572
+ const json = rest.includes("--json");
1573
+ switch (action) {
1574
+ case "list": {
1575
+ const pagePath = rest[1];
1576
+ if (!pagePath) {
1577
+ console.error("Usage: remnic versions list <page-path>");
1578
+ process.exit(1);
1579
+ }
1580
+ const absPath = path2.resolve(pagePath);
1581
+ const history = await listVersions(absPath, versioningConfig, memDir);
1582
+ if (json) {
1583
+ console.log(JSON.stringify(history, null, 2));
1584
+ } else {
1585
+ if (history.versions.length === 0) {
1586
+ console.log(`No versions found for ${pagePath}`);
1587
+ } else {
1588
+ console.log(`Versions for ${pagePath} (current: v${history.currentVersion}):
1589
+ `);
1590
+ for (const v of history.versions) {
1591
+ const note = v.note ? ` \u2014 ${v.note}` : "";
1592
+ console.log(` v${v.versionId} ${v.timestamp} ${v.trigger} ${v.sizeBytes} bytes${note}`);
1593
+ }
1594
+ }
1595
+ }
1596
+ break;
1597
+ }
1598
+ case "show": {
1599
+ const pagePath = rest[1];
1600
+ const versionId = rest[2];
1601
+ if (!pagePath || !versionId) {
1602
+ console.error("Usage: remnic versions show <page-path> <version-id>");
1603
+ process.exit(1);
1604
+ }
1605
+ const absPath = path2.resolve(pagePath);
1606
+ try {
1607
+ const content = await getVersion(absPath, versionId, versioningConfig, memDir);
1608
+ console.log(content);
1609
+ } catch (err) {
1610
+ console.error(err instanceof Error ? err.message : String(err));
1611
+ process.exit(1);
1612
+ }
1613
+ break;
1614
+ }
1615
+ case "diff": {
1616
+ const pagePath = rest[1];
1617
+ const v1 = rest[2];
1618
+ const v2 = rest[3];
1619
+ if (!pagePath || !v1 || !v2) {
1620
+ console.error("Usage: remnic versions diff <page-path> <v1> <v2>");
1621
+ process.exit(1);
1622
+ }
1623
+ const absPath = path2.resolve(pagePath);
1624
+ try {
1625
+ const diffOutput = await diffVersions(absPath, v1, v2, versioningConfig, memDir);
1626
+ console.log(diffOutput);
1627
+ } catch (err) {
1628
+ console.error(err instanceof Error ? err.message : String(err));
1629
+ process.exit(1);
1630
+ }
1631
+ break;
1632
+ }
1633
+ case "revert": {
1634
+ const pagePath = rest[1];
1635
+ const versionId = rest[2];
1636
+ if (!pagePath || !versionId) {
1637
+ console.error("Usage: remnic versions revert <page-path> <version-id>");
1638
+ process.exit(1);
1639
+ }
1640
+ const absPath = path2.resolve(pagePath);
1641
+ try {
1642
+ const version = await revertToVersion(absPath, versionId, versioningConfig, void 0, memDir);
1643
+ if (json) {
1644
+ console.log(JSON.stringify(version, null, 2));
1645
+ } else {
1646
+ console.log(`Reverted ${pagePath} to version ${versionId}.`);
1647
+ console.log(`Created snapshot v${version.versionId} of previous content.`);
1648
+ }
1649
+ } catch (err) {
1650
+ console.error(err instanceof Error ? err.message : String(err));
1651
+ process.exit(1);
1652
+ }
1653
+ break;
1654
+ }
1655
+ default:
1656
+ console.log(`
1657
+ remnic versions \u2014 Page-level versioning
1658
+
1659
+ Usage:
1660
+ remnic versions list <page-path> List all versions of a page
1661
+ remnic versions show <page-path> <id> Print content of a specific version
1662
+ remnic versions diff <page-path> <v1> <v2> Show diff between two versions
1663
+ remnic versions revert <page-path> <id> Revert page to a specific version
1664
+
1665
+ Options:
1666
+ --json Output in JSON format
1667
+ `);
1668
+ break;
1669
+ }
1670
+ }
1671
+ async function cmdEnrich(rest) {
1672
+ initLogger();
1673
+ const configPath = resolveConfigPath();
1674
+ const raw = fs.existsSync(configPath) ? JSON.parse(fs.readFileSync(configPath, "utf8")) : {};
1675
+ const remnicCfg = raw.remnic ?? raw.engram ?? raw;
1676
+ const config = parseConfig(remnicCfg);
1677
+ const subcommand = rest[0];
1678
+ if (subcommand === "audit") {
1679
+ const memoryDir2 = expandTilde(config.memoryDir);
1680
+ const auditDir2 = path2.join(memoryDir2, "enrichment");
1681
+ const sinceFlag = resolveFlag(rest.slice(1), "--since");
1682
+ const entries = await readAuditLog(auditDir2, sinceFlag ?? void 0);
1683
+ if (entries.length === 0) {
1684
+ console.log("No enrichment audit entries found.");
1685
+ return;
1686
+ }
1687
+ for (const entry of entries) {
1688
+ const status = entry.accepted ? "ACCEPTED" : "REJECTED";
1689
+ const url = entry.sourceUrl ? ` (${entry.sourceUrl})` : "";
1690
+ console.log(
1691
+ `[${entry.timestamp}] ${status} ${entry.entityName} via ${entry.provider}: ${entry.candidateText}${url}`
1692
+ );
1693
+ }
1694
+ return;
1695
+ }
1696
+ if (subcommand === "providers") {
1697
+ const pipelineConfig2 = defaultEnrichmentPipelineConfig();
1698
+ pipelineConfig2.enabled = config.enrichmentEnabled;
1699
+ pipelineConfig2.maxCandidatesPerEntity = config.enrichmentMaxCandidatesPerEntity;
1700
+ pipelineConfig2.autoEnrichOnCreate = config.enrichmentAutoOnCreate;
1701
+ pipelineConfig2.providers = [
1702
+ { id: "web-search", enabled: true, costTier: "cheap" }
1703
+ ];
1704
+ const orchestrator2 = new Orchestrator(config);
1705
+ await orchestrator2.initialize();
1706
+ await orchestrator2.deferredReady;
1707
+ const searchBackend2 = orchestrator2.qmd;
1708
+ const searchFn2 = searchBackend2.isAvailable() ? async (query) => {
1709
+ const results2 = await searchBackend2.search(query, void 0, 10);
1710
+ return results2.map((r) => r.snippet);
1711
+ } : void 0;
1712
+ const registry2 = new EnrichmentProviderRegistry();
1713
+ registry2.register(new WebSearchProvider({ searchFn: searchFn2 }));
1714
+ const allEnabled = registry2.listEnabled(pipelineConfig2);
1715
+ console.log(`Pipeline enabled: ${pipelineConfig2.enabled}`);
1716
+ console.log(`Auto-enrich on create: ${pipelineConfig2.autoEnrichOnCreate}`);
1717
+ console.log(`Max candidates per entity: ${pipelineConfig2.maxCandidatesPerEntity}`);
1718
+ console.log(`
1719
+ Registered providers:`);
1720
+ const webSearch = registry2.get("web-search");
1721
+ if (webSearch) {
1722
+ const available = await webSearch.isAvailable();
1723
+ console.log(` - web-search (${webSearch.costTier}) \u2014 ${available ? "available" : "unavailable (no searchFn configured)"}`);
1724
+ }
1725
+ if (allEnabled.length === 0) {
1726
+ console.log("\n No providers are currently enabled in config.");
1727
+ }
1728
+ return;
1729
+ }
1730
+ if (!config.enrichmentEnabled) {
1731
+ console.error("Enrichment pipeline is disabled (enrichmentEnabled = false).");
1732
+ process.exit(1);
1733
+ }
1734
+ const dryRun = rest.includes("--dry-run");
1735
+ const all = rest.includes("--all");
1736
+ if (!all && (!subcommand || subcommand.startsWith("--"))) {
1737
+ console.error("Usage: remnic enrich <entity-name> | --all | --dry-run | audit | providers");
1738
+ process.exit(1);
1739
+ }
1740
+ const orchestrator = new Orchestrator(config);
1741
+ await orchestrator.initialize();
1742
+ await orchestrator.deferredReady;
1743
+ const storage = await orchestrator.getStorage(config.defaultNamespace);
1744
+ const entityFiles = await storage.readAllEntityFiles();
1745
+ let targets = entityFiles;
1746
+ if (!all && subcommand && !subcommand.startsWith("--")) {
1747
+ const match = entityFiles.find(
1748
+ (e) => e.name.toLowerCase() === subcommand.toLowerCase()
1749
+ );
1750
+ if (!match) {
1751
+ console.error(`Entity not found: ${subcommand}`);
1752
+ process.exit(1);
1753
+ }
1754
+ targets = [match];
1755
+ }
1756
+ if (targets.length === 0) {
1757
+ console.log("No entities to enrich.");
1758
+ return;
1759
+ }
1760
+ const pipelineConfig = defaultEnrichmentPipelineConfig();
1761
+ pipelineConfig.enabled = true;
1762
+ pipelineConfig.maxCandidatesPerEntity = config.enrichmentMaxCandidatesPerEntity;
1763
+ pipelineConfig.providers = [
1764
+ { id: "web-search", enabled: true, costTier: "cheap" }
1765
+ ];
1766
+ pipelineConfig.importanceThresholds = {
1767
+ critical: ["web-search"],
1768
+ high: ["web-search"],
1769
+ normal: ["web-search"],
1770
+ low: []
1771
+ };
1772
+ const searchBackend = orchestrator.qmd;
1773
+ const searchFn = searchBackend.isAvailable() ? async (query) => {
1774
+ const results2 = await searchBackend.search(query, void 0, 10);
1775
+ return results2.map((r) => r.snippet);
1776
+ } : void 0;
1777
+ const registry = new EnrichmentProviderRegistry();
1778
+ registry.register(new WebSearchProvider({ searchFn }));
1779
+ const inputs = targets.map((ef) => ({
1780
+ name: ef.name,
1781
+ type: ef.type,
1782
+ knownFacts: ef.facts,
1783
+ importanceLevel: "normal"
1784
+ }));
1785
+ if (dryRun) {
1786
+ console.log(`Dry run: would enrich ${inputs.length} entity(ies):`);
1787
+ for (const input of inputs) {
1788
+ const providers = registry.getForImportance(input.importanceLevel, pipelineConfig);
1789
+ console.log(` - ${input.name} (${input.type}) \u2014 ${providers.length} provider(s)`);
1790
+ }
1791
+ return;
1792
+ }
1793
+ console.log(`Enriching ${inputs.length} entity(ies)...`);
1794
+ const noopLog = { info() {
1795
+ }, warn() {
1796
+ }, error() {
1797
+ }, debug() {
1798
+ } };
1799
+ const results = await runEnrichmentPipeline(inputs, registry, pipelineConfig, noopLog);
1800
+ if (results.length === 0) {
1801
+ console.log("No enrichment results (no providers matched).");
422
1802
  return;
423
1803
  }
424
- console.log(`Remnic server: running${pid ? ` (pid ${pid})` : ""}`);
425
- const port = inferPort();
426
- const controller = new AbortController();
427
- const timeoutId = setTimeout(() => controller.abort(), 3e3);
428
- try {
429
- const response = await fetch(`http://127.0.0.1:${port}/engram/v1/health`, {
430
- signal: controller.signal
431
- });
432
- if (!response.ok) {
433
- console.log(`Health: server responded with ${response.status} ${response.statusText}`);
434
- } else {
435
- const health = await response.json();
436
- console.log(`Health: ${health.status ?? "ok"}`);
1804
+ const memoryDir = expandTilde(config.memoryDir);
1805
+ const auditDir = path2.join(memoryDir, "enrichment");
1806
+ let totalPersisted = 0;
1807
+ for (const result of results) {
1808
+ for (const candidate of result.acceptedCandidates) {
1809
+ let persisted = false;
1810
+ try {
1811
+ await storage.writeMemory(candidate.category, candidate.text, {
1812
+ confidence: candidate.confidence,
1813
+ tags: [...candidate.tags ?? [], "enrichment", candidate.source],
1814
+ entityRef: result.entityName,
1815
+ source: `enrichment:${candidate.source}`
1816
+ });
1817
+ persisted = true;
1818
+ totalPersisted++;
1819
+ } catch (err) {
1820
+ console.error(
1821
+ ` Failed to persist candidate for ${result.entityName}: ${err instanceof Error ? err.message : String(err)}`
1822
+ );
1823
+ try {
1824
+ await appendAuditEntry(auditDir, {
1825
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1826
+ entityName: result.entityName,
1827
+ provider: result.provider,
1828
+ candidateText: candidate.text,
1829
+ sourceUrl: candidate.sourceUrl,
1830
+ accepted: false,
1831
+ reason: `persist failed: ${err instanceof Error ? err.message : String(err)}`
1832
+ });
1833
+ } catch {
1834
+ }
1835
+ }
1836
+ if (persisted) {
1837
+ try {
1838
+ await appendAuditEntry(auditDir, {
1839
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1840
+ entityName: result.entityName,
1841
+ provider: result.provider,
1842
+ candidateText: candidate.text,
1843
+ sourceUrl: candidate.sourceUrl,
1844
+ accepted: true
1845
+ });
1846
+ } catch (auditErr) {
1847
+ console.warn(
1848
+ ` Warning: audit write failed for ${result.entityName} (memory was persisted): ${auditErr instanceof Error ? auditErr.message : String(auditErr)}`
1849
+ );
1850
+ }
1851
+ }
437
1852
  }
438
- } catch {
439
- console.log("Health: unable to reach server");
440
- } finally {
441
- clearTimeout(timeoutId);
442
1853
  }
443
- }
444
- async function cmdQuery(queryText, json, explain) {
445
- if (!queryText) {
446
- console.error("Usage: remnic query <text>");
447
- process.exit(1);
1854
+ if (totalPersisted > 0 && searchBackend.isAvailable()) {
1855
+ try {
1856
+ await searchBackend.update();
1857
+ } catch {
1858
+ }
1859
+ }
1860
+ for (const result of results) {
1861
+ console.log(
1862
+ ` ${result.entityName} via ${result.provider}: ${result.candidatesAccepted} accepted, ${result.candidatesRejected} rejected (${result.elapsed}ms)`
1863
+ );
1864
+ }
1865
+ if (totalPersisted > 0) {
1866
+ console.log(`
1867
+ ${totalPersisted} candidate(s) persisted to memory store.`);
448
1868
  }
1869
+ }
1870
+ async function cmdExtensions(action, rest) {
449
1871
  initLogger();
450
1872
  const configPath = resolveConfigPath();
451
- const raw = fs2.existsSync(configPath) ? JSON.parse(fs2.readFileSync(configPath, "utf8")) : {};
1873
+ const raw = fs.existsSync(configPath) ? JSON.parse(fs.readFileSync(configPath, "utf8")) : {};
452
1874
  const remnicCfg = raw.remnic ?? raw.engram ?? raw;
453
1875
  const config = parseConfig(remnicCfg);
454
- const orchestrator = new Orchestrator(config);
455
- await orchestrator.initialize();
456
- const service = new EngramAccessService(orchestrator);
457
- if (explain) {
458
- const result2 = await runExplain(service, queryText);
459
- if (json) {
460
- console.log(JSON.stringify(result2, null, 2));
461
- } else {
462
- console.log(`Query: ${result2.query}`);
463
- console.log(`Tiers used: ${result2.tiersUsed.join(" \u2192 ")}`);
464
- console.log(`Total duration: ${result2.totalDurationMs}ms`);
465
- for (const t of result2.tierResults) {
466
- console.log(` ${t.tier}: ${t.latencyMs}ms (${t.resultsCount} results)`);
1876
+ const root = resolveExtensionsRoot(config);
1877
+ const noopLog = { warn: () => {
1878
+ }, debug: () => {
1879
+ } };
1880
+ const warnLog = {
1881
+ warn: (msg) => console.warn(msg),
1882
+ debug: () => {
1883
+ }
1884
+ };
1885
+ switch (action) {
1886
+ case "list": {
1887
+ const extensions = await discoverMemoryExtensions(root, noopLog);
1888
+ if (extensions.length === 0) {
1889
+ console.log("No memory extensions found.");
1890
+ console.log(` Scanned: ${root}`);
1891
+ return;
467
1892
  }
1893
+ console.log(`Memory extensions (${extensions.length}):`);
1894
+ for (const ext of extensions) {
1895
+ const schemaInfo = ext.schema?.version ? ` v${ext.schema.version}` : "";
1896
+ const types = ext.schema?.memoryTypes?.join(", ") ?? "any";
1897
+ console.log(` ${ext.name}${schemaInfo} (types: ${types})`);
1898
+ }
1899
+ console.log(`
1900
+ Root: ${root}`);
1901
+ break;
468
1902
  }
469
- return;
470
- }
471
- const result = await service.recall({ query: queryText, mode: "auto" });
472
- if (json) {
473
- console.log(JSON.stringify(result, null, 2));
474
- } else {
475
- const memories = result.memories ?? [];
476
- if (memories.length === 0) {
477
- console.log("No results.");
478
- return;
1903
+ case "show": {
1904
+ const name = rest[0];
1905
+ if (!name) {
1906
+ console.error("Usage: remnic extensions show <name>");
1907
+ process.exitCode = 1;
1908
+ return;
1909
+ }
1910
+ const extensions = await discoverMemoryExtensions(root, noopLog);
1911
+ const ext = extensions.find((e) => e.name === name);
1912
+ if (!ext) {
1913
+ console.error(`Extension "${name}" not found in ${root}`);
1914
+ process.exitCode = 1;
1915
+ return;
1916
+ }
1917
+ console.log(ext.instructions);
1918
+ break;
479
1919
  }
480
- for (const m of memories) {
481
- console.log(`- ${m.content}`);
1920
+ case "validate": {
1921
+ const extensions = await discoverMemoryExtensions(root, warnLog);
1922
+ let entries = [];
1923
+ try {
1924
+ entries = fs.readdirSync(root);
1925
+ } catch {
1926
+ console.log(`Extensions root does not exist: ${root}`);
1927
+ process.exitCode = 0;
1928
+ return;
1929
+ }
1930
+ const validNames = new Set(extensions.map((e) => e.name));
1931
+ let errors = 0;
1932
+ for (const entry of entries) {
1933
+ const entryPath = path2.join(root, entry);
1934
+ try {
1935
+ if (!fs.statSync(entryPath).isDirectory()) continue;
1936
+ } catch {
1937
+ continue;
1938
+ }
1939
+ if (!validNames.has(entry)) {
1940
+ errors++;
1941
+ }
1942
+ }
1943
+ console.log(`Validated: ${extensions.length} valid, ${errors} skipped`);
1944
+ if (errors > 0) {
1945
+ process.exitCode = 1;
1946
+ }
1947
+ break;
1948
+ }
1949
+ case "reload": {
1950
+ console.log("Extension cache reloaded (no-op: caching not yet implemented).");
1951
+ break;
1952
+ }
1953
+ default:
1954
+ console.log(`Usage: remnic extensions <list|show|validate|reload>
1955
+
1956
+ list List discovered extensions
1957
+ show <name> Print instructions.md content
1958
+ validate Validate all extensions, exit non-zero on errors
1959
+ reload Reserved for future caching (no-op)
1960
+ `);
1961
+ break;
1962
+ }
1963
+ }
1964
+ async function cmdBriefing(rest) {
1965
+ initLogger();
1966
+ const configPath = resolveConfigPath();
1967
+ const raw = fs.existsSync(configPath) ? JSON.parse(fs.readFileSync(configPath, "utf8")) : {};
1968
+ const remnicCfg = raw.remnic ?? raw.engram ?? raw;
1969
+ const config = parseConfig(remnicCfg);
1970
+ if (!config.briefing.enabled) {
1971
+ console.error("Briefing is disabled in config (briefing.enabled = false).");
1972
+ process.exit(1);
1973
+ }
1974
+ const sinceFlag = resolveFlag(rest, "--since");
1975
+ const focusFlag = resolveFlag(rest, "--focus");
1976
+ const formatFlag = resolveFlag(rest, "--format");
1977
+ const save = rest.includes("--save") || config.briefing.saveByDefault;
1978
+ if (hasFlag(rest, "--since") && sinceFlag === void 0) {
1979
+ console.error("Missing value for --since. Accepted: yesterday, today, NNh, NNd, NNw.");
1980
+ process.exit(1);
1981
+ }
1982
+ if (hasFlag(rest, "--format") && formatFlag === void 0) {
1983
+ console.error("Missing value for --format. Accepted: markdown, json.");
1984
+ process.exit(1);
1985
+ }
1986
+ if (hasFlag(rest, "--focus") && (focusFlag === void 0 || focusFlag.startsWith("--"))) {
1987
+ console.error(
1988
+ "Missing value for --focus. Expected: project:<id>, topic:<name>, or person:<id>."
1989
+ );
1990
+ process.exit(1);
1991
+ }
1992
+ const token = sinceFlag ?? config.briefing.defaultWindow;
1993
+ const window = parseBriefingWindow(token);
1994
+ if (!window) {
1995
+ console.error(
1996
+ `Invalid --since value: ${token}. Accepted: yesterday, today, NNh, NNd, NNw.`
1997
+ );
1998
+ process.exit(1);
1999
+ }
2000
+ const rawFocus = typeof focusFlag === "string" ? focusFlag.trim() : "";
2001
+ const focus = rawFocus.length > 0 ? parseBriefingFocus(rawFocus) : null;
2002
+ if (rawFocus.length > 0 && !focus) {
2003
+ console.error(
2004
+ `Invalid --focus value: expected project:<id>, topic:<name>, or person:<id>, got: ${focusFlag}`
2005
+ );
2006
+ process.exit(1);
2007
+ }
2008
+ const jsonFlag = rest.includes("--json");
2009
+ if (jsonFlag && formatFlag !== void 0 && formatFlag !== "json") {
2010
+ console.error(
2011
+ `Conflicting flags: --json and --format ${formatFlag}. Use one or the other.`
2012
+ );
2013
+ process.exit(1);
2014
+ }
2015
+ const effectiveFormatFlag = jsonFlag ? "json" : formatFlag;
2016
+ const formatError = validateBriefingFormat(effectiveFormatFlag);
2017
+ if (formatError) {
2018
+ console.error(formatError);
2019
+ process.exit(1);
2020
+ }
2021
+ const format = effectiveFormatFlag === "json" ? "json" : effectiveFormatFlag === "markdown" ? "markdown" : config.briefing.defaultFormat;
2022
+ const orchestrator = new Orchestrator(config);
2023
+ await orchestrator.initialize();
2024
+ const storage = await orchestrator.getStorage(config.defaultNamespace);
2025
+ const calendarSource = config.briefing.calendarSource ? new FileCalendarSource(config.briefing.calendarSource) : void 0;
2026
+ const result = await buildBriefing({
2027
+ storage,
2028
+ window,
2029
+ focus,
2030
+ namespace: config.defaultNamespace,
2031
+ calendarSource,
2032
+ maxFollowups: config.briefing.maxFollowups,
2033
+ allowLlm: config.briefing.llmFollowups,
2034
+ openaiApiKey: config.openaiApiKey,
2035
+ openaiBaseUrl: config.openaiBaseUrl,
2036
+ model: config.model
2037
+ });
2038
+ const payload = format === "json" ? JSON.stringify(result.json, null, 2) : result.markdown;
2039
+ console.log(payload);
2040
+ if (save) {
2041
+ try {
2042
+ const saveDir = resolveBriefingSaveDir(config.briefing.saveDir);
2043
+ fs.mkdirSync(saveDir, { recursive: true });
2044
+ const filename = briefingFilename(new Date(result.window.to), format);
2045
+ const filePath = path2.join(saveDir, filename);
2046
+ fs.writeFileSync(filePath, payload + (payload.endsWith("\n") ? "" : "\n"));
2047
+ console.error(`Saved briefing: ${filePath}`);
2048
+ } catch (err) {
2049
+ console.error(`Failed to save briefing: ${err instanceof Error ? err.message : String(err)}`);
2050
+ process.exit(1);
482
2051
  }
483
2052
  }
484
2053
  }
@@ -492,7 +2061,7 @@ function cmdDoctor() {
492
2061
  detail: `${nodeVersion} (requires >= 22.12.0)`
493
2062
  });
494
2063
  const configPath = resolveConfigPath();
495
- const configExists = fs2.existsSync(configPath);
2064
+ const configExists = fs.existsSync(configPath);
496
2065
  checks.push({ name: "Config file", ok: configExists, detail: configPath });
497
2066
  const hasApiKey = !!process.env.OPENAI_API_KEY;
498
2067
  checks.push({
@@ -502,7 +2071,7 @@ function cmdDoctor() {
502
2071
  });
503
2072
  const memoryDir = resolveMemoryDir();
504
2073
  try {
505
- fs2.mkdirSync(memoryDir, { recursive: true });
2074
+ fs.mkdirSync(memoryDir, { recursive: true });
506
2075
  checks.push({ name: "Memory directory", ok: true, detail: memoryDir });
507
2076
  } catch {
508
2077
  checks.push({ name: "Memory directory", ok: false, detail: `cannot create ${memoryDir}` });
@@ -513,19 +2082,128 @@ function cmdDoctor() {
513
2082
  ok: svcState.running,
514
2083
  detail: svcState.running ? `running${svcState.pid ? ` (pid ${svcState.pid})` : ""}` : "stopped"
515
2084
  });
2085
+ const openclawConfigPath = resolveOpenclawConfigPath();
2086
+ const openclawConfigExists = fs.existsSync(openclawConfigPath);
2087
+ let openclawConfig = {};
2088
+ let openclawConfigValid = false;
2089
+ if (openclawConfigExists) {
2090
+ try {
2091
+ const parsed = JSON.parse(fs.readFileSync(openclawConfigPath, "utf-8"));
2092
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
2093
+ openclawConfig = parsed;
2094
+ openclawConfigValid = true;
2095
+ } else {
2096
+ openclawConfigValid = false;
2097
+ }
2098
+ } catch {
2099
+ openclawConfigValid = false;
2100
+ }
2101
+ }
2102
+ checks.push({
2103
+ name: "OpenClaw config file",
2104
+ ok: openclawConfigExists && openclawConfigValid,
2105
+ warn: openclawConfigExists && !openclawConfigValid,
2106
+ detail: openclawConfigExists ? openclawConfigValid ? openclawConfigPath : `${openclawConfigPath} (invalid JSON)` : `${openclawConfigPath} (not found)`,
2107
+ remediation: openclawConfigExists && !openclawConfigValid ? "Fix the JSON syntax in your OpenClaw config file." : !openclawConfigExists ? "Run `remnic openclaw install` to create the OpenClaw config with the Remnic entry." : void 0
2108
+ });
2109
+ if (openclawConfigValid) {
2110
+ const rawPlugins = openclawConfig.plugins;
2111
+ const pluginsIsObject = rawPlugins && typeof rawPlugins === "object" && !Array.isArray(rawPlugins);
2112
+ if (!pluginsIsObject && rawPlugins !== void 0) {
2113
+ checks.push({
2114
+ name: "OpenClaw plugins",
2115
+ ok: false,
2116
+ detail: `plugins is ${typeof rawPlugins}, expected object`,
2117
+ remediation: "Run `remnic openclaw install` to recreate the plugins section."
2118
+ });
2119
+ }
2120
+ const plugins = pluginsIsObject ? rawPlugins : {};
2121
+ const entries = plugins.entries && typeof plugins.entries === "object" && !Array.isArray(plugins.entries) ? plugins.entries : null;
2122
+ const slots = plugins.slots && typeof plugins.slots === "object" && !Array.isArray(plugins.slots) ? plugins.slots : null;
2123
+ const entriesIsArray = Array.isArray(plugins.entries);
2124
+ checks.push({
2125
+ name: "OpenClaw plugins.entries",
2126
+ ok: !!entries,
2127
+ detail: entries ? "present" : entriesIsArray ? "invalid (array)" : "missing",
2128
+ remediation: !entries ? "Run `remnic openclaw install` to add the Remnic plugin entry." : void 0
2129
+ });
2130
+ if (entries) {
2131
+ const isValidEntry = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
2132
+ const hasNew = REMNIC_OPENCLAW_PLUGIN_ID in entries && isValidEntry(entries[REMNIC_OPENCLAW_PLUGIN_ID]);
2133
+ const hasLegacy = REMNIC_OPENCLAW_LEGACY_PLUGIN_ID in entries && isValidEntry(entries[REMNIC_OPENCLAW_LEGACY_PLUGIN_ID]);
2134
+ const keyExistsButMalformed = REMNIC_OPENCLAW_PLUGIN_ID in entries && !hasNew || REMNIC_OPENCLAW_LEGACY_PLUGIN_ID in entries && !hasLegacy;
2135
+ checks.push({
2136
+ name: "OpenClaw plugin entry",
2137
+ ok: hasNew,
2138
+ warn: !hasNew && hasLegacy || keyExistsButMalformed,
2139
+ detail: hasNew ? `${REMNIC_OPENCLAW_PLUGIN_ID} entry found` : hasLegacy ? `only legacy ${REMNIC_OPENCLAW_LEGACY_PLUGIN_ID} entry found (upgrade recommended)` : keyExistsButMalformed ? "entry key exists but value is not a valid object" : "no Remnic entry found",
2140
+ remediation: keyExistsButMalformed ? "Run `remnic openclaw install` to recreate the Remnic plugin entry with correct structure." : !hasNew && hasLegacy ? `Run \`remnic openclaw install\` to migrate from the legacy ${REMNIC_OPENCLAW_LEGACY_PLUGIN_ID} to ${REMNIC_OPENCLAW_PLUGIN_ID}.` : !hasNew ? "Run `remnic openclaw install` to add the Remnic plugin entry." : void 0
2141
+ });
2142
+ const slotValue = slots?.memory;
2143
+ const validEntryIds = Object.keys(entries);
2144
+ const slotMissing = !slotValue;
2145
+ const slotMismatch = !slotMissing && !validEntryIds.includes(slotValue);
2146
+ const slotMatchesEntry = !slotMissing && !slotMismatch;
2147
+ const slotIsLegacy = slotMatchesEntry && slotValue === REMNIC_OPENCLAW_LEGACY_PLUGIN_ID;
2148
+ const slotIsPreferred = slotMatchesEntry && slotValue === REMNIC_OPENCLAW_PLUGIN_ID;
2149
+ checks.push({
2150
+ name: "OpenClaw plugins.slots.memory",
2151
+ ok: slotMatchesEntry,
2152
+ warn: slotMatchesEntry && !slotIsPreferred,
2153
+ detail: slotMissing ? "(unset)" : slotMismatch ? `"${slotValue}" (not found in entries: ${validEntryIds.join(", ")})` : `"${slotValue}"`,
2154
+ remediation: slotMissing ? `Run \`remnic openclaw install\` to set plugins.slots.memory = "${REMNIC_OPENCLAW_PLUGIN_ID}". Without this, hooks never fire.` : slotMismatch ? `plugins.slots.memory = "${slotValue}" but no matching entry exists. Run \`remnic openclaw install\` to fix.` : slotIsLegacy ? `Slot is set to the legacy id "${REMNIC_OPENCLAW_LEGACY_PLUGIN_ID}". Run \`remnic openclaw install\` to migrate to "${REMNIC_OPENCLAW_PLUGIN_ID}" (optional \u2014 hooks fire with either id while the legacy entry is present).` : slotMatchesEntry && !slotIsPreferred && !slotIsLegacy ? `plugins.slots.memory = "${slotValue}" points to another plugin. Run \`remnic openclaw install\` to set it to "${REMNIC_OPENCLAW_PLUGIN_ID}".` : void 0
2155
+ });
2156
+ const activeSlotEntry = slotValue ? entries[slotValue] : void 0;
2157
+ const entryToCheck = activeSlotEntry ?? entries[REMNIC_OPENCLAW_PLUGIN_ID] ?? entries[REMNIC_OPENCLAW_LEGACY_PLUGIN_ID];
2158
+ const entryConfig = entryToCheck?.config && typeof entryToCheck.config === "object" ? entryToCheck.config : null;
2159
+ const rawMemoryDir = entryConfig?.memoryDir;
2160
+ const configuredMemoryDir = typeof rawMemoryDir === "string" ? rawMemoryDir : void 0;
2161
+ if (configuredMemoryDir) {
2162
+ const resolvedMemDir = path2.resolve(expandTilde(configuredMemoryDir));
2163
+ let memDirOk = false;
2164
+ let memDirDetail = `${resolvedMemDir} (not found)`;
2165
+ let memDirRemediation = `Run \`remnic openclaw install --memory-dir "${resolvedMemDir}"\` to create the directory.`;
2166
+ if (fs.existsSync(resolvedMemDir)) {
2167
+ try {
2168
+ const stat = fs.statSync(resolvedMemDir);
2169
+ if (stat.isDirectory()) {
2170
+ memDirOk = true;
2171
+ memDirDetail = resolvedMemDir;
2172
+ memDirRemediation = void 0;
2173
+ } else {
2174
+ memDirDetail = `${resolvedMemDir} (exists but is not a directory)`;
2175
+ memDirRemediation = `Remove the file at ${resolvedMemDir} and run \`remnic openclaw install --memory-dir "${resolvedMemDir}"\` to create it as a directory.`;
2176
+ }
2177
+ } catch {
2178
+ memDirDetail = `${resolvedMemDir} (cannot stat)`;
2179
+ }
2180
+ }
2181
+ checks.push({
2182
+ name: "OpenClaw memoryDir",
2183
+ ok: memDirOk,
2184
+ warn: !memDirOk,
2185
+ detail: memDirDetail,
2186
+ remediation: memDirRemediation
2187
+ });
2188
+ }
2189
+ }
2190
+ }
516
2191
  for (const check of checks) {
517
- const icon = check.ok ? "\u2713" : "\u2717";
2192
+ const icon = check.ok ? check.warn ? "\u26A0" : "\u2713" : check.warn ? "\u26A0" : "\u2717";
518
2193
  console.log(` ${icon} ${check.name}: ${check.detail}`);
2194
+ if ((!check.ok || check.warn) && check.remediation) {
2195
+ console.log(` \u2192 ${check.remediation}`);
2196
+ }
519
2197
  }
520
2198
  }
521
2199
  function cmdConfig() {
522
2200
  const configPath = resolveConfigPath();
523
- if (!fs2.existsSync(configPath)) {
2201
+ if (!fs.existsSync(configPath)) {
524
2202
  console.log("No config file found. Run `remnic init` to create one.");
525
2203
  return;
526
2204
  }
527
2205
  console.log(`Config: ${configPath}`);
528
- const rawConfig = fs2.readFileSync(configPath, "utf8");
2206
+ const rawConfig = fs.readFileSync(configPath, "utf8");
529
2207
  const redacted = rawConfig.replace(
530
2208
  /("(?:openaiApiKey|localLlmApiKey|authToken|apiKey|remoteSearchApiKey|meilisearchApiKey|opikApiKey)"\s*:\s*")([^"]*)(")/g,
531
2209
  "$1[REDACTED]$3"
@@ -689,7 +2367,8 @@ function cmdDedup(json) {
689
2367
  console.log(`Duration: ${result.durationMs}ms`);
690
2368
  }
691
2369
  async function cmdConnectors(action, rest, json) {
692
- const nonFlagArgs = rest.filter((a) => !a.startsWith("--"));
2370
+ const strippedRest = stripConfigArgv(rest);
2371
+ const nonFlagArgs = strippedRest.filter((a) => !a.startsWith("--"));
693
2372
  const connectorId = nonFlagArgs[0];
694
2373
  if (action === "list") {
695
2374
  const { installed, available } = listConnectors();
@@ -707,40 +2386,211 @@ async function cmdConnectors(action, rest, json) {
707
2386
  console.error("Usage: remnic connectors install <id>");
708
2387
  process.exit(1);
709
2388
  }
2389
+ const connectorConfig = parseConnectorConfig(rest);
710
2390
  const result = installConnector({
711
2391
  connectorId,
712
- config: parseConnectorConfig(rest),
2392
+ config: connectorConfig,
713
2393
  force: rest.includes("--force")
714
2394
  });
2395
+ if (result.status === "error") {
2396
+ console.error(result.message);
2397
+ process.exit(1);
2398
+ }
715
2399
  console.log(result.message);
716
2400
  if (result.configPath) console.log(` Config: ${result.configPath}`);
717
2401
  if (result.status === "already_installed") console.log("Use --force to reinstall.");
718
2402
  if (result.status === "config_required") console.log("Set config with --config <key>=<value>");
719
- if (result.status === "error") console.error(`Error: ${result.message}`);
2403
+ if (result.status === "installed") {
2404
+ const pub = publisherForConnector(connectorId);
2405
+ if (pub) {
2406
+ try {
2407
+ const available = await pub.isHostAvailable();
2408
+ if (available) {
2409
+ const memoryDir = resolveMemoryDir();
2410
+ const connectorNamespace = typeof connectorConfig?.namespace === "string" && connectorConfig.namespace.length > 0 ? connectorConfig.namespace : void 0;
2411
+ const pubResult = await pub.publish({
2412
+ config: { memoryDir, namespace: connectorNamespace },
2413
+ skillsRoot: path2.join(memoryDir, "skills"),
2414
+ log: { info: console.log, warn: console.warn, error: console.error }
2415
+ });
2416
+ if (pubResult.filesWritten.length > 0) {
2417
+ console.log(` Published memory extension to ${pubResult.extensionRoot}`);
2418
+ }
2419
+ }
2420
+ } catch (err) {
2421
+ const msg = err instanceof Error ? err.message : String(err);
2422
+ console.warn(` Warning: memory extension publish failed: ${msg}`);
2423
+ }
2424
+ }
2425
+ }
720
2426
  } else if (action === "remove") {
721
2427
  if (!connectorId) {
722
2428
  console.error("Usage: remnic connectors remove <id>");
723
2429
  process.exit(1);
724
2430
  }
725
2431
  const result = removeConnector(connectorId);
2432
+ if (result.status === "error") {
2433
+ console.error(result.message);
2434
+ process.exit(1);
2435
+ }
726
2436
  console.log(result.message);
2437
+ if (result.status === "skipped" && result.reason === "config-parse-failed") {
2438
+ console.error(
2439
+ `Error: removal skipped because the connector config could not be parsed. Fix or delete the config file at ${result.configPath} manually and retry.`
2440
+ );
2441
+ process.exit(1);
2442
+ }
727
2443
  } else if (action === "doctor") {
728
2444
  if (!connectorId) {
729
2445
  console.error("Usage: remnic connectors doctor <id>");
730
2446
  process.exit(1);
731
2447
  }
732
2448
  const result = await doctorConnector(connectorId);
2449
+ const publisherChecks = [];
2450
+ const targetHostId = hostIdForConnector(connectorId);
2451
+ const factory = PUBLISHERS[targetHostId];
2452
+ const connectorInstance = listConnectors().installed.find(
2453
+ (c) => c.connectorId === connectorId
2454
+ );
2455
+ const savedInstallExt = connectorInstance ? coerceInstallExtension(connectorInstance.config.installExtension) : void 0;
2456
+ const extensionOptedOut = savedInstallExt === false;
2457
+ if (factory) {
2458
+ if (extensionOptedOut) {
2459
+ publisherChecks.push({
2460
+ name: `Publisher: ${targetHostId}`,
2461
+ ok: true,
2462
+ detail: "skipped (installExtension=false)"
2463
+ });
2464
+ } else {
2465
+ try {
2466
+ const pub = factory();
2467
+ const available = await pub.isHostAvailable();
2468
+ const extRoot = available ? await pub.resolveExtensionRoot() : "(host not installed)";
2469
+ const extensionExists = available && extRoot ? fs.existsSync(extRoot) : false;
2470
+ publisherChecks.push({
2471
+ name: `Publisher: ${targetHostId}`,
2472
+ ok: !available || extensionExists,
2473
+ detail: !available ? "host not installed (skip)" : extensionExists ? `extension at ${extRoot}` : `extension missing at ${extRoot} \u2014 run \`remnic connectors install ${connectorId}\``
2474
+ });
2475
+ } catch (err) {
2476
+ const msg = err instanceof Error ? err.message : String(err);
2477
+ publisherChecks.push({
2478
+ name: `Publisher: ${targetHostId}`,
2479
+ ok: false,
2480
+ detail: `error: ${msg}`
2481
+ });
2482
+ }
2483
+ }
2484
+ }
2485
+ const allChecks = [...result.checks, ...publisherChecks];
2486
+ const healthy = allChecks.every((c) => c.ok);
733
2487
  if (json) {
734
- console.log(JSON.stringify(result, null, 2));
2488
+ console.log(JSON.stringify({ ...result, checks: allChecks, healthy }, null, 2));
735
2489
  } else {
736
- for (const check of result.checks) {
2490
+ for (const check of allChecks) {
737
2491
  const icon = check.ok ? "\u2713" : "\u2717";
738
2492
  console.log(` ${icon} ${check.name}: ${check.detail}`);
739
2493
  }
740
- console.log(result.healthy ? "\nConnector healthy" : "\nConnector has issues");
2494
+ console.log(healthy ? "\nConnector healthy" : "\nConnector has issues");
2495
+ }
2496
+ } else if (action === "marketplace") {
2497
+ const subAction = nonFlagArgs[0];
2498
+ let subActionRemoved = false;
2499
+ const marketplaceRest = rest.filter((a) => {
2500
+ if (!subActionRemoved && a === subAction) {
2501
+ subActionRemoved = true;
2502
+ return false;
2503
+ }
2504
+ return true;
2505
+ });
2506
+ await cmdConnectorsMarketplace(subAction, marketplaceRest, json);
2507
+ } else {
2508
+ console.log("Usage: remnic connectors <list|install|remove|doctor|marketplace> [id]");
2509
+ process.exit(1);
2510
+ }
2511
+ }
2512
+ async function cmdConnectorsMarketplace(subAction, rest, json) {
2513
+ const configPath = resolveConfigPath(resolveFlagStrict(rest, "--config"));
2514
+ const rawConfig = fs.existsSync(configPath) ? JSON.parse(fs.readFileSync(configPath, "utf8")) : {};
2515
+ const pluginConfig = rawConfig.remnic ?? rawConfig.engram ?? rawConfig;
2516
+ const config = parseConfig(pluginConfig);
2517
+ if (subAction === "generate") {
2518
+ const outputDir = resolveFlagStrict(rest, "--output") ?? process.cwd();
2519
+ const manifest = generateMarketplaceManifest();
2520
+ await writeMarketplaceManifest(outputDir, manifest);
2521
+ const outPath = path2.join(outputDir, "marketplace.json");
2522
+ if (json) {
2523
+ console.log(JSON.stringify({ status: "generated", path: outPath }, null, 2));
2524
+ } else {
2525
+ console.log(`Generated marketplace.json at ${outPath}`);
2526
+ }
2527
+ } else if (subAction === "validate") {
2528
+ const targetPath = rest.filter((a) => !a.startsWith("--"))[0] ?? path2.join(process.cwd(), "marketplace.json");
2529
+ const resolved = path2.resolve(targetPath);
2530
+ if (!fs.existsSync(resolved)) {
2531
+ console.error(`File not found: ${resolved}`);
2532
+ process.exit(1);
2533
+ }
2534
+ let parsed;
2535
+ try {
2536
+ parsed = JSON.parse(fs.readFileSync(resolved, "utf8"));
2537
+ } catch {
2538
+ console.error(`Invalid JSON in ${resolved}`);
2539
+ process.exit(1);
2540
+ }
2541
+ const validation = checkMarketplaceManifest(parsed);
2542
+ if (json) {
2543
+ console.log(JSON.stringify(validation, null, 2));
2544
+ }
2545
+ if (validation.valid) {
2546
+ if (!json) console.log(`Valid marketplace manifest: ${resolved}`);
2547
+ } else {
2548
+ if (!json) {
2549
+ console.error(`Invalid marketplace manifest: ${resolved}`);
2550
+ for (const err of validation.errors) {
2551
+ console.error(` - ${err}`);
2552
+ }
2553
+ }
2554
+ process.exit(1);
2555
+ }
2556
+ } else if (subAction === "install") {
2557
+ const source = rest.filter((a) => !a.startsWith("--"))[0];
2558
+ if (!source) {
2559
+ console.error("Usage: remnic connectors marketplace install <source> [--type github|git|local|url]");
2560
+ process.exit(1);
2561
+ }
2562
+ const validTypes = /* @__PURE__ */ new Set(["github", "git", "local", "url"]);
2563
+ const hasTypeFlag = rest.includes("--type");
2564
+ const typeFlag = resolveFlagStrict(rest, "--type") ?? (hasTypeFlag ? void 0 : "github");
2565
+ if (typeFlag === void 0) {
2566
+ console.error(`--type requires a value. Must be one of: ${[...validTypes].join(", ")}`);
2567
+ process.exit(1);
2568
+ }
2569
+ if (!validTypes.has(typeFlag)) {
2570
+ console.error(`Invalid --type: "${typeFlag}". Must be one of: ${[...validTypes].join(", ")}`);
2571
+ process.exit(1);
2572
+ }
2573
+ const result = await installFromMarketplace(
2574
+ source,
2575
+ typeFlag,
2576
+ config
2577
+ );
2578
+ if (json) {
2579
+ console.log(JSON.stringify(result, null, 2));
2580
+ } else {
2581
+ console.log(result.message);
2582
+ if (result.pluginsFound.length > 0) {
2583
+ console.log(` Plugins: ${result.pluginsFound.join(", ")}`);
2584
+ }
741
2585
  }
2586
+ if (!result.ok) process.exit(1);
742
2587
  } else {
743
- console.log("Usage: remnic connectors <list|install|remove|doctor> [id]");
2588
+ console.log(`Usage: remnic connectors marketplace <generate|validate|install> [args]
2589
+
2590
+ generate [--output <dir>] Generate marketplace.json
2591
+ validate [path] Validate a marketplace.json file
2592
+ install <source> [--type <type>] Install from marketplace source
2593
+ Types: github, git, local, url (default: github)`);
744
2594
  process.exit(1);
745
2595
  }
746
2596
  }
@@ -880,10 +2730,10 @@ async function cmdSpace(action, rest, json) {
880
2730
  process.exit(1);
881
2731
  }
882
2732
  }
883
- async function cmdBenchmark(action, rest, json) {
2733
+ async function cmdLegacyBenchmark(action, rest, json) {
884
2734
  initLogger();
885
2735
  const configPath = resolveConfigPath();
886
- const raw = fs2.existsSync(configPath) ? JSON.parse(fs2.readFileSync(configPath, "utf8")) : {};
2736
+ const raw = fs.existsSync(configPath) ? JSON.parse(fs.readFileSync(configPath, "utf8")) : {};
887
2737
  const remnicCfg = raw.remnic ?? raw.engram ?? raw;
888
2738
  const config = parseConfig(remnicCfg);
889
2739
  const orchestrator = new Orchestrator(config);
@@ -919,41 +2769,140 @@ async function cmdBenchmark(action, rest, json) {
919
2769
  console.log("No baseline found. Run `remnic benchmark run` first.");
920
2770
  return;
921
2771
  }
922
- const suite = await runBenchSuite(service, benchConfig);
923
- const metrics = {};
924
- for (const r of suite.results) {
925
- metrics[r.query] = r.latencyMs;
926
- }
927
- const tolerance = benchConfig.regressionTolerance ?? 10;
928
- const result = checkRegression(metrics, baseline, tolerance);
929
- if (json) {
930
- console.log(JSON.stringify(result, null, 2));
931
- } else {
932
- if (result.passed) {
933
- console.log("No regressions detected.");
934
- } else {
935
- console.log("Regressions detected:");
936
- for (const reg of result.regressions) {
937
- if (!reg.passed) {
938
- console.log(` \u2717 ${reg.metric}: ${reg.currentValue}ms vs ${reg.baselineValue}ms baseline (+${((reg.currentValue - reg.baselineValue) / reg.baselineValue * 100).toFixed(1)}%)`);
939
- }
940
- }
941
- }
2772
+ const suite = await runBenchSuite(service, benchConfig);
2773
+ const metrics = {};
2774
+ for (const r of suite.results) {
2775
+ metrics[r.query] = r.latencyMs;
2776
+ }
2777
+ const tolerance = benchConfig.regressionTolerance ?? 10;
2778
+ const result = checkRegression(metrics, baseline, tolerance);
2779
+ if (json) {
2780
+ console.log(JSON.stringify(result, null, 2));
2781
+ } else {
2782
+ if (result.passed) {
2783
+ console.log("No regressions detected.");
2784
+ } else {
2785
+ console.log("Regressions detected:");
2786
+ for (const reg of result.regressions) {
2787
+ if (!reg.passed) {
2788
+ console.log(` \u2717 ${reg.metric}: ${reg.currentValue}ms vs ${reg.baselineValue}ms baseline (+${((reg.currentValue - reg.baselineValue) / reg.baselineValue * 100).toFixed(1)}%)`);
2789
+ }
2790
+ }
2791
+ }
2792
+ }
2793
+ if (!result.passed) {
2794
+ process.exit(1);
2795
+ }
2796
+ } else if (action === "report") {
2797
+ const reportPath = benchConfig.reportPath;
2798
+ const suite = await runBenchSuite(service, { ...benchConfig, reportPath });
2799
+ console.log(`Report saved to ${reportPath ?? "benchmarks/report.json"}`);
2800
+ if (json) {
2801
+ console.log(JSON.stringify(suite.report, null, 2));
2802
+ }
2803
+ } else {
2804
+ console.log("Usage: remnic benchmark <run|check|report> [queries...] [--explain] [--baseline=<path>] [--report=<path>]");
2805
+ process.exit(1);
2806
+ }
2807
+ }
2808
+ async function cmdBench(rest) {
2809
+ const benchAction = parseBenchActionArgs(rest);
2810
+ let parsed;
2811
+ try {
2812
+ parsed = parseBenchArgs(rest);
2813
+ } catch (error) {
2814
+ console.error(error instanceof Error ? error.message : String(error));
2815
+ process.exit(1);
2816
+ }
2817
+ if (parsed.action === "help") {
2818
+ console.log(getBenchUsageText());
2819
+ return;
2820
+ }
2821
+ if (parsed.action === "check" || parsed.action === "report") {
2822
+ await cmdLegacyBenchmark(parsed.action, benchAction.args, parsed.json);
2823
+ return;
2824
+ }
2825
+ if (parsed.action === "compare") {
2826
+ await compareBenchPackageResults(parsed);
2827
+ return;
2828
+ }
2829
+ if (parsed.action === "results") {
2830
+ await showBenchPackageResults(parsed);
2831
+ return;
2832
+ }
2833
+ if (parsed.action === "baseline") {
2834
+ await manageBenchBaselines(parsed);
2835
+ return;
2836
+ }
2837
+ if (parsed.action === "export") {
2838
+ await exportBenchPackageResult(parsed);
2839
+ return;
2840
+ }
2841
+ if (parsed.action === "datasets") {
2842
+ await manageBenchDatasets(parsed);
2843
+ return;
2844
+ }
2845
+ if (parsed.action === "runs") {
2846
+ await manageBenchRuns(parsed);
2847
+ return;
2848
+ }
2849
+ if (parsed.action === "publish") {
2850
+ await publishBenchPackageResults(parsed);
2851
+ return;
2852
+ }
2853
+ if (parsed.action === "ui") {
2854
+ await launchBenchUi(parsed.resultsDir ?? resolveBenchOutputDir());
2855
+ return;
2856
+ }
2857
+ if (parsed.action === "providers") {
2858
+ await discoverBenchProviders(parsed);
2859
+ return;
2860
+ }
2861
+ if (parsed.action === "list") {
2862
+ const catalog = await listBenchmarksFromPackage() ?? BENCHMARK_CATALOG;
2863
+ if (parsed.json) {
2864
+ console.log(JSON.stringify(catalog, null, 2));
2865
+ return;
2866
+ }
2867
+ console.log("Published benchmarks:");
2868
+ for (const entry of catalog) {
2869
+ console.log(` ${entry.id.padEnd(14)} ${entry.category.padEnd(14)} ${entry.summary}`);
942
2870
  }
943
- if (!result.passed) {
2871
+ return;
2872
+ }
2873
+ if (parsed.custom) {
2874
+ if (parsed.all || parsed.benchmarks.length > 0) {
2875
+ console.error("ERROR: --custom cannot be combined with benchmark names or --all.");
944
2876
  process.exit(1);
945
2877
  }
946
- } else if (action === "report") {
947
- const reportPath = benchConfig.reportPath;
948
- const suite = await runBenchSuite(service, { ...benchConfig, reportPath });
949
- console.log(`Report saved to ${reportPath ?? "benchmarks/report.json"}`);
950
- if (json) {
951
- console.log(JSON.stringify(suite.report, null, 2));
2878
+ const handledByPackage = await runCustomBenchViaPackage(parsed);
2879
+ if (!handledByPackage) {
2880
+ console.error(
2881
+ "Benchmark runner not found. Expected a phase-1 @remnic/bench runtime export for custom benchmarks."
2882
+ );
2883
+ process.exit(1);
952
2884
  }
953
- } else {
954
- console.log("Usage: remnic benchmark <run|check|report> [queries...] [--explain] [--baseline=<path>] [--report=<path>]");
2885
+ return;
2886
+ }
2887
+ const selectedBenchmarks = parsed.all ? await resolveAllBenchmarks() : parsed.benchmarks;
2888
+ if (selectedBenchmarks.length === 0) {
2889
+ console.error(
2890
+ parsed.all ? "ERROR: no runnable benchmarks are available for --all in this install. Use 'remnic bench list' to inspect the catalog." : "ERROR: specify benchmark name(s) or --all. Use 'remnic bench list' to see available."
2891
+ );
2892
+ process.exit(1);
2893
+ }
2894
+ const knownBenchmarkIds = await resolveKnownBenchmarkIds();
2895
+ const unknown = selectedBenchmarks.filter((benchmarkId) => !knownBenchmarkIds.has(benchmarkId));
2896
+ if (unknown.length > 0) {
2897
+ console.error(`ERROR: unknown benchmark(s): ${unknown.join(", ")}. Use 'remnic bench list' to see available.`);
955
2898
  process.exit(1);
956
2899
  }
2900
+ for (const benchmarkId of selectedBenchmarks) {
2901
+ const handledByPackage = await runBenchViaPackage(parsed, benchmarkId);
2902
+ if (!handledByPackage) {
2903
+ await runBenchViaFallback(parsed, benchmarkId);
2904
+ }
2905
+ }
957
2906
  }
958
2907
  var LOGS_DIR = path2.join(PID_DIR, "logs");
959
2908
  var LAUNCHD_LABEL = "ai.remnic.daemon";
@@ -989,7 +2938,7 @@ var LEGACY_SYSTEMD_UNIT_PATH = path2.join(
989
2938
  function readPid() {
990
2939
  for (const file of [PID_FILE, LEGACY_PID_FILE]) {
991
2940
  try {
992
- return parseInt(fs2.readFileSync(file, "utf8").trim(), 10);
2941
+ return parseInt(fs.readFileSync(file, "utf8").trim(), 10);
993
2942
  } catch {
994
2943
  }
995
2944
  }
@@ -998,7 +2947,7 @@ function readPid() {
998
2947
  function inferPort() {
999
2948
  try {
1000
2949
  const configPath = resolveConfigPath();
1001
- const raw = JSON.parse(fs2.readFileSync(configPath, "utf8"));
2950
+ const raw = JSON.parse(fs.readFileSync(configPath, "utf8"));
1002
2951
  return raw.server?.port ?? 4318;
1003
2952
  } catch {
1004
2953
  return 4318;
@@ -1009,7 +2958,7 @@ function resolveNodePath() {
1009
2958
  }
1010
2959
  function resolveServerBin() {
1011
2960
  const distPath = path2.resolve(import.meta.dirname, "../../remnic-server/dist/index.js");
1012
- if (fs2.existsSync(distPath)) return distPath;
2961
+ if (fs.existsSync(distPath)) return distPath;
1013
2962
  const srcPath = path2.resolve(import.meta.dirname, "../../remnic-server/src/index.ts");
1014
2963
  return srcPath;
1015
2964
  }
@@ -1037,13 +2986,13 @@ function daemonInstall() {
1037
2986
  process.exit(1);
1038
2987
  }
1039
2988
  const vars = { HOME: home, NODE_PATH: nodePath, REMNIC_SERVER_BIN: serverBin };
1040
- fs2.mkdirSync(LOGS_DIR, { recursive: true });
2989
+ fs.mkdirSync(LOGS_DIR, { recursive: true });
1041
2990
  if (isMacOS()) {
1042
2991
  const templatePath = path2.resolve(import.meta.dirname, "../templates/launchd/ai.remnic.daemon.plist");
1043
- const template = fs2.readFileSync(templatePath, "utf8");
2992
+ const template = fs.readFileSync(templatePath, "utf8");
1044
2993
  const plist = renderTemplate(template, vars);
1045
- fs2.mkdirSync(path2.dirname(LAUNCHD_PLIST_PATH), { recursive: true });
1046
- fs2.writeFileSync(LAUNCHD_PLIST_PATH, plist);
2994
+ fs.mkdirSync(path2.dirname(LAUNCHD_PLIST_PATH), { recursive: true });
2995
+ fs.writeFileSync(LAUNCHD_PLIST_PATH, plist);
1047
2996
  try {
1048
2997
  childProcess.execSync(`launchctl load -w "${LAUNCHD_PLIST_PATH}"`, { stdio: "pipe" });
1049
2998
  } catch {
@@ -1054,10 +3003,10 @@ function daemonInstall() {
1054
3003
  console.log(` Logs: ${LOGS_DIR}/daemon.log`);
1055
3004
  } else if (isLinux()) {
1056
3005
  const templatePath = path2.resolve(import.meta.dirname, "../templates/systemd/remnic.service");
1057
- const template = fs2.readFileSync(templatePath, "utf8");
3006
+ const template = fs.readFileSync(templatePath, "utf8");
1058
3007
  const unit = renderTemplate(template, vars);
1059
- fs2.mkdirSync(path2.dirname(SYSTEMD_UNIT_PATH), { recursive: true });
1060
- fs2.writeFileSync(SYSTEMD_UNIT_PATH, unit);
3008
+ fs.mkdirSync(path2.dirname(SYSTEMD_UNIT_PATH), { recursive: true });
3009
+ fs.writeFileSync(SYSTEMD_UNIT_PATH, unit);
1061
3010
  try {
1062
3011
  childProcess.execSync("systemctl --user daemon-reload", { stdio: "pipe" });
1063
3012
  childProcess.execSync(`systemctl --user enable ${SYSTEMD_SERVICE}`, { stdio: "pipe" });
@@ -1081,7 +3030,7 @@ function daemonUninstall() {
1081
3030
  } catch {
1082
3031
  }
1083
3032
  try {
1084
- fs2.unlinkSync(plistPath);
3033
+ fs.unlinkSync(plistPath);
1085
3034
  removed = true;
1086
3035
  console.log(`Removed launchd service: ${plistPath}`);
1087
3036
  } catch {
@@ -1101,7 +3050,7 @@ function daemonUninstall() {
1101
3050
  let removed = false;
1102
3051
  for (const unitPath of [SYSTEMD_UNIT_PATH, LEGACY_SYSTEMD_UNIT_PATH]) {
1103
3052
  try {
1104
- fs2.unlinkSync(unitPath);
3053
+ fs.unlinkSync(unitPath);
1105
3054
  removed = true;
1106
3055
  console.log(`Removed systemd service: ${unitPath}`);
1107
3056
  } catch {
@@ -1159,17 +3108,36 @@ function isServiceRunning() {
1159
3108
  }
1160
3109
  return { running: false };
1161
3110
  }
1162
- function daemonStatus() {
3111
+ async function daemonStatus() {
1163
3112
  const { running, pid } = isServiceRunning();
1164
3113
  const port = inferPort();
1165
- const serviceInstalled = isMacOS() ? fs2.existsSync(LAUNCHD_PLIST_PATH) || fs2.existsSync(LEGACY_LAUNCHD_PLIST_PATH) : isLinux() ? fs2.existsSync(SYSTEMD_UNIT_PATH) || fs2.existsSync(LEGACY_SYSTEMD_UNIT_PATH) : false;
3114
+ const serviceInstalled = isMacOS() ? fs.existsSync(LAUNCHD_PLIST_PATH) || fs.existsSync(LEGACY_LAUNCHD_PLIST_PATH) : isLinux() ? fs.existsSync(SYSTEMD_UNIT_PATH) || fs.existsSync(LEGACY_SYSTEMD_UNIT_PATH) : false;
1166
3115
  console.log(`Remnic daemon status:`);
1167
3116
  console.log(` Running: ${running ? `yes${pid ? ` (pid ${pid})` : ""}` : "no"}`);
1168
3117
  console.log(` Port: ${port}`);
1169
3118
  console.log(` Service: ${serviceInstalled ? "installed" : "not installed"}`);
1170
3119
  console.log(` Platform: ${process.platform}`);
1171
- console.log(` PID file: ${fs2.existsSync(PID_FILE) ? PID_FILE : LEGACY_PID_FILE}`);
1172
- console.log(` Log file: ${fs2.existsSync(LOG_FILE) ? LOG_FILE : LEGACY_LOG_FILE}`);
3120
+ console.log(` PID file: ${fs.existsSync(PID_FILE) ? PID_FILE : LEGACY_PID_FILE}`);
3121
+ console.log(` Log file: ${fs.existsSync(LOG_FILE) ? LOG_FILE : LEGACY_LOG_FILE}`);
3122
+ try {
3123
+ const configPath = resolveConfigPath();
3124
+ const raw = fs.existsSync(configPath) ? JSON.parse(fs.readFileSync(configPath, "utf8")) : {};
3125
+ const remnicCfg = raw.remnic ?? raw.engram ?? raw;
3126
+ const config = parseConfig(remnicCfg);
3127
+ const extRoot = resolveExtensionsRoot(config);
3128
+ const noopLog = { warn: () => {
3129
+ }, debug: () => {
3130
+ } };
3131
+ const exts = await discoverMemoryExtensions(extRoot, noopLog);
3132
+ if (exts.length > 0) {
3133
+ const names = exts.map((e) => e.name).join(", ");
3134
+ console.log(` Memory extensions: ${exts.length} active (${names})`);
3135
+ } else {
3136
+ console.log(` Memory extensions: none`);
3137
+ }
3138
+ } catch {
3139
+ console.log(` Memory extensions: unknown (config error)`);
3140
+ }
1173
3141
  }
1174
3142
  function daemonStart() {
1175
3143
  const svc = isServiceRunning();
@@ -1177,7 +3145,7 @@ function daemonStart() {
1177
3145
  console.log(`Already running${svc.pid ? ` (pid ${svc.pid})` : " (via service manager)"}`);
1178
3146
  return;
1179
3147
  }
1180
- if (isMacOS() && (fs2.existsSync(LAUNCHD_PLIST_PATH) || fs2.existsSync(LEGACY_LAUNCHD_PLIST_PATH))) {
3148
+ if (isMacOS() && (fs.existsSync(LAUNCHD_PLIST_PATH) || fs.existsSync(LEGACY_LAUNCHD_PLIST_PATH))) {
1181
3149
  const label = firstSuccessfulCandidate([LAUNCHD_LABEL, LEGACY_LAUNCHD_LABEL], (candidate) => {
1182
3150
  childProcess.execSync(`launchctl start ${candidate} 2>/dev/null`, { stdio: "pipe" });
1183
3151
  });
@@ -1185,7 +3153,7 @@ function daemonStart() {
1185
3153
  console.log(`Started remnic daemon via launchd (${label})`);
1186
3154
  return;
1187
3155
  }
1188
- } else if (isLinux() && (fs2.existsSync(SYSTEMD_UNIT_PATH) || fs2.existsSync(LEGACY_SYSTEMD_UNIT_PATH))) {
3156
+ } else if (isLinux() && (fs.existsSync(SYSTEMD_UNIT_PATH) || fs.existsSync(LEGACY_SYSTEMD_UNIT_PATH))) {
1189
3157
  const serviceName = firstSuccessfulCandidate([SYSTEMD_SERVICE, LEGACY_SYSTEMD_SERVICE], (candidate) => {
1190
3158
  childProcess.execSync(`systemctl --user start ${candidate}`, { stdio: "pipe" });
1191
3159
  });
@@ -1194,9 +3162,9 @@ function daemonStart() {
1194
3162
  return;
1195
3163
  }
1196
3164
  }
1197
- fs2.mkdirSync(PID_DIR, { recursive: true });
1198
- fs2.mkdirSync(LOGS_DIR, { recursive: true });
1199
- const logStream = fs2.openSync(LOG_FILE, "a");
3165
+ fs.mkdirSync(PID_DIR, { recursive: true });
3166
+ fs.mkdirSync(LOGS_DIR, { recursive: true });
3167
+ const logStream = fs.openSync(LOG_FILE, "a");
1200
3168
  const serverBin = resolveServerBin();
1201
3169
  const isSource = serverBin.endsWith(".ts");
1202
3170
  let cmd;
@@ -1218,12 +3186,12 @@ function daemonStart() {
1218
3186
  }
1219
3187
  });
1220
3188
  child.unref();
1221
- fs2.writeFileSync(PID_FILE, String(child.pid));
3189
+ fs.writeFileSync(PID_FILE, String(child.pid));
1222
3190
  console.log(`Started remnic server (pid ${child.pid})`);
1223
3191
  console.log(` Log: ${LOG_FILE}`);
1224
3192
  }
1225
3193
  function daemonStop() {
1226
- if (isMacOS() && (fs2.existsSync(LAUNCHD_PLIST_PATH) || fs2.existsSync(LEGACY_LAUNCHD_PLIST_PATH))) {
3194
+ if (isMacOS() && (fs.existsSync(LAUNCHD_PLIST_PATH) || fs.existsSync(LEGACY_LAUNCHD_PLIST_PATH))) {
1227
3195
  const label = firstSuccessfulCandidate([LAUNCHD_LABEL, LEGACY_LAUNCHD_LABEL], (candidate) => {
1228
3196
  childProcess.execSync(`launchctl stop ${candidate} 2>/dev/null`, { stdio: "pipe" });
1229
3197
  });
@@ -1231,7 +3199,7 @@ function daemonStop() {
1231
3199
  console.log(`Stopped remnic daemon via launchd (${label})`);
1232
3200
  return;
1233
3201
  }
1234
- } else if (isLinux() && (fs2.existsSync(SYSTEMD_UNIT_PATH) || fs2.existsSync(LEGACY_SYSTEMD_UNIT_PATH))) {
3202
+ } else if (isLinux() && (fs.existsSync(SYSTEMD_UNIT_PATH) || fs.existsSync(LEGACY_SYSTEMD_UNIT_PATH))) {
1235
3203
  const serviceName = firstSuccessfulCandidate([SYSTEMD_SERVICE, LEGACY_SYSTEMD_SERVICE], (candidate) => {
1236
3204
  childProcess.execSync(`systemctl --user stop ${candidate}`, { stdio: "pipe" });
1237
3205
  });
@@ -1252,11 +3220,11 @@ function daemonStop() {
1252
3220
  console.log("Process not found (cleaning up PID file)");
1253
3221
  }
1254
3222
  try {
1255
- fs2.unlinkSync(PID_FILE);
3223
+ fs.unlinkSync(PID_FILE);
1256
3224
  } catch {
1257
3225
  }
1258
3226
  try {
1259
- fs2.unlinkSync(LEGACY_PID_FILE);
3227
+ fs.unlinkSync(LEGACY_PID_FILE);
1260
3228
  } catch {
1261
3229
  }
1262
3230
  }
@@ -1304,6 +3272,644 @@ function cmdTokenRevoke(connector) {
1304
3272
  console.log(`No token found for ${connector}`);
1305
3273
  }
1306
3274
  }
3275
+ async function promptYesNo(question, defaultYes = true) {
3276
+ if (!process.stdin.isTTY) return defaultYes;
3277
+ process.stdout.write(question + " ");
3278
+ return new Promise((resolve) => {
3279
+ let buf = "";
3280
+ const cleanup = () => {
3281
+ process.stdin.removeListener("data", onData);
3282
+ process.stdin.removeListener("end", onEnd);
3283
+ process.stdin.removeListener("close", onEnd);
3284
+ process.stdin.pause();
3285
+ };
3286
+ const onEnd = () => {
3287
+ cleanup();
3288
+ resolve(defaultYes);
3289
+ };
3290
+ const onData = (chunk) => {
3291
+ buf += chunk.toString();
3292
+ const nl = buf.indexOf("\n");
3293
+ if (nl >= 0) {
3294
+ cleanup();
3295
+ const answer = buf.slice(0, nl).trim().toLowerCase();
3296
+ if (answer === "" || answer === "y" || answer === "yes") {
3297
+ resolve(defaultYes || answer !== "");
3298
+ } else if (answer === "n" || answer === "no") {
3299
+ resolve(false);
3300
+ } else {
3301
+ resolve(defaultYes);
3302
+ }
3303
+ }
3304
+ };
3305
+ process.stdin.resume();
3306
+ process.stdin.on("data", onData);
3307
+ process.stdin.on("end", onEnd);
3308
+ process.stdin.on("close", onEnd);
3309
+ });
3310
+ }
3311
+ async function cmdBinary(rest) {
3312
+ initLogger();
3313
+ const configPath = resolveConfigPath();
3314
+ const raw = fs.existsSync(configPath) ? JSON.parse(fs.readFileSync(configPath, "utf8")) : {};
3315
+ const remnicCfg = raw.remnic ?? raw.engram ?? raw;
3316
+ const config = parseConfig(remnicCfg);
3317
+ const memoryDir = resolveMemoryDir();
3318
+ const blConfig = {
3319
+ enabled: config.binaryLifecycleEnabled,
3320
+ gracePeriodDays: config.binaryLifecycleGracePeriodDays,
3321
+ maxBinarySizeBytes: DEFAULT_MAX_BINARY_SIZE_BYTES,
3322
+ scanPatterns: DEFAULT_SCAN_PATTERNS,
3323
+ backend: {
3324
+ type: config.binaryLifecycleBackendType,
3325
+ basePath: config.binaryLifecycleBackendPath ? expandTilde(config.binaryLifecycleBackendPath) : void 0
3326
+ }
3327
+ };
3328
+ const action = rest[0] ?? "help";
3329
+ switch (action) {
3330
+ case "scan": {
3331
+ const manifest = await readManifest(memoryDir);
3332
+ const { scanForBinaries } = await import("@remnic/core");
3333
+ const found = await scanForBinaries(memoryDir, blConfig, manifest);
3334
+ if (found.length === 0) {
3335
+ console.log("No untracked binary files found.");
3336
+ } else {
3337
+ console.log(`Found ${found.length} untracked binary file(s):`);
3338
+ for (const p of found) {
3339
+ console.log(` ${p}`);
3340
+ }
3341
+ }
3342
+ break;
3343
+ }
3344
+ case "status": {
3345
+ const manifest = await readManifest(memoryDir);
3346
+ const counts = {
3347
+ total: manifest.assets.length,
3348
+ pending: manifest.assets.filter((a) => a.status === "pending").length,
3349
+ mirrored: manifest.assets.filter((a) => a.status === "mirrored").length,
3350
+ redirected: manifest.assets.filter((a) => a.status === "redirected").length,
3351
+ cleaned: manifest.assets.filter((a) => a.status === "cleaned").length,
3352
+ error: manifest.assets.filter((a) => a.status === "error").length
3353
+ };
3354
+ const totalBytes = manifest.assets.reduce((sum, a) => sum + a.sizeBytes, 0);
3355
+ console.log(`Binary lifecycle manifest (${memoryDir}):`);
3356
+ console.log(` Total assets: ${counts.total}`);
3357
+ console.log(` Pending: ${counts.pending}`);
3358
+ console.log(` Mirrored: ${counts.mirrored}`);
3359
+ console.log(` Redirected: ${counts.redirected}`);
3360
+ console.log(` Cleaned: ${counts.cleaned}`);
3361
+ console.log(` Errors: ${counts.error}`);
3362
+ console.log(` Total size: ${(totalBytes / 1024).toFixed(1)} KB`);
3363
+ if (manifest.lastScanAt) {
3364
+ console.log(` Last scan: ${manifest.lastScanAt}`);
3365
+ }
3366
+ break;
3367
+ }
3368
+ case "run": {
3369
+ const dryRun = rest.includes("--dry-run");
3370
+ const backend = createBackend(blConfig.backend);
3371
+ const log = {
3372
+ info: (msg) => console.log(msg),
3373
+ warn: (msg) => console.warn(msg),
3374
+ error: (msg) => console.error(msg)
3375
+ };
3376
+ const result = await runBinaryLifecyclePipeline(
3377
+ memoryDir,
3378
+ blConfig,
3379
+ backend,
3380
+ log,
3381
+ { dryRun }
3382
+ );
3383
+ console.log(
3384
+ `
3385
+ Pipeline complete${dryRun ? " (dry-run)" : ""}: scanned=${result.scanned}, mirrored=${result.mirrored}, redirected=${result.redirected}, cleaned=${result.cleaned}`
3386
+ );
3387
+ if (result.errors.length > 0) {
3388
+ console.error(`Errors (${result.errors.length}):`);
3389
+ for (const e of result.errors) console.error(` ${e}`);
3390
+ }
3391
+ break;
3392
+ }
3393
+ case "clean": {
3394
+ const force = rest.includes("--force");
3395
+ if (!force) {
3396
+ console.error("Use --force to confirm cleanup of local binary copies.");
3397
+ process.exit(1);
3398
+ }
3399
+ const backend = createBackend(blConfig.backend);
3400
+ const log = {
3401
+ info: (msg) => console.log(msg),
3402
+ warn: (msg) => console.warn(msg),
3403
+ error: (msg) => console.error(msg)
3404
+ };
3405
+ const result = await runBinaryLifecyclePipeline(
3406
+ memoryDir,
3407
+ blConfig,
3408
+ backend,
3409
+ log,
3410
+ { forceClean: true }
3411
+ );
3412
+ console.log(
3413
+ `
3414
+ Clean complete: cleaned=${result.cleaned}`
3415
+ );
3416
+ if (result.errors.length > 0) {
3417
+ console.error(`Errors (${result.errors.length}):`);
3418
+ for (const e of result.errors) console.error(` ${e}`);
3419
+ }
3420
+ break;
3421
+ }
3422
+ default:
3423
+ console.log(`Usage: remnic binary <scan|status|run|clean>
3424
+
3425
+ scan Scan for untracked binary files
3426
+ status Show binary lifecycle manifest summary
3427
+ run [--dry-run] Run full binary lifecycle pipeline
3428
+ clean --force Force-clean local copies past grace period`);
3429
+ break;
3430
+ }
3431
+ }
3432
+ async function cmdOpenclawInstall(opts) {
3433
+ const configPath = resolveOpenclawConfigPath(opts.configPath);
3434
+ const fallbackMemoryDir = path2.join(resolveHomeDir(), ".openclaw", "workspace", "memory", "local");
3435
+ console.log(`OpenClaw config: ${configPath}`);
3436
+ const existingConfig = readOpenclawConfig(configPath);
3437
+ const rawPlugins = existingConfig.plugins;
3438
+ if (rawPlugins !== void 0 && (typeof rawPlugins !== "object" || rawPlugins === null || Array.isArray(rawPlugins))) {
3439
+ throw new Error(
3440
+ `OpenClaw config at ${configPath} has an invalid plugins field (expected an object, got ${Array.isArray(rawPlugins) ? "array" : typeof rawPlugins}). Fix the file manually and re-run.`
3441
+ );
3442
+ }
3443
+ const plugins = rawPlugins ?? {};
3444
+ const rawEntries = plugins.entries;
3445
+ if (rawEntries !== void 0 && (typeof rawEntries !== "object" || rawEntries === null || Array.isArray(rawEntries))) {
3446
+ throw new Error(
3447
+ `OpenClaw config at ${configPath} has an invalid plugins.entries field (expected an object, got ${Array.isArray(rawEntries) ? "array" : typeof rawEntries}). Fix the file manually and re-run.`
3448
+ );
3449
+ }
3450
+ const entries = rawEntries ?? {};
3451
+ const rawSlots = plugins.slots;
3452
+ if (rawSlots !== void 0 && (typeof rawSlots !== "object" || rawSlots === null || Array.isArray(rawSlots))) {
3453
+ throw new Error(
3454
+ `OpenClaw config at ${configPath} has an invalid plugins.slots field (expected an object, got ${Array.isArray(rawSlots) ? "array" : typeof rawSlots}). Fix the file manually and re-run.`
3455
+ );
3456
+ }
3457
+ const slots = rawSlots ?? {};
3458
+ const hasLegacy = REMNIC_OPENCLAW_LEGACY_PLUGIN_ID in entries;
3459
+ const hasNew = REMNIC_OPENCLAW_PLUGIN_ID in entries;
3460
+ const currentSlot = slots.memory;
3461
+ let migrateLegacy = false;
3462
+ if (hasLegacy && !opts.yes) {
3463
+ migrateLegacy = await promptYesNo(
3464
+ `Found legacy '${REMNIC_OPENCLAW_LEGACY_PLUGIN_ID}' entry. Migrate to '${REMNIC_OPENCLAW_PLUGIN_ID}'? [Y/n]`,
3465
+ true
3466
+ );
3467
+ } else if (hasLegacy) {
3468
+ migrateLegacy = true;
3469
+ }
3470
+ const legacyEntry = entries[REMNIC_OPENCLAW_LEGACY_PLUGIN_ID];
3471
+ const existingNewEntry = entries[REMNIC_OPENCLAW_PLUGIN_ID];
3472
+ const legacyConfigToMerge = migrateLegacy && legacyEntry?.config && typeof legacyEntry.config === "object" ? legacyEntry.config : {};
3473
+ const existingNewEntryConfig = existingNewEntry?.config && typeof existingNewEntry.config === "object" ? existingNewEntry.config : {};
3474
+ const existingMemoryDir = (typeof existingNewEntryConfig.memoryDir === "string" ? existingNewEntryConfig.memoryDir : void 0) || (migrateLegacy && typeof legacyConfigToMerge.memoryDir === "string" ? legacyConfigToMerge.memoryDir : void 0);
3475
+ const memoryDir = opts.memoryDir ? path2.resolve(expandTilde(opts.memoryDir)) : existingMemoryDir ? path2.resolve(expandTilde(existingMemoryDir)) : fallbackMemoryDir;
3476
+ console.log(`Memory dir: ${memoryDir}`);
3477
+ const legacyNonConfigFields = {};
3478
+ if (migrateLegacy && legacyEntry && typeof legacyEntry === "object" && !Array.isArray(legacyEntry)) {
3479
+ for (const [k, v] of Object.entries(legacyEntry)) {
3480
+ if (k !== "config") legacyNonConfigFields[k] = v;
3481
+ }
3482
+ }
3483
+ const existingNewEntryFields = existingNewEntry && typeof existingNewEntry === "object" && !Array.isArray(existingNewEntry) ? existingNewEntry : {};
3484
+ const newEntry = {
3485
+ ...legacyNonConfigFields,
3486
+ ...existingNewEntryFields,
3487
+ config: {
3488
+ ...legacyConfigToMerge,
3489
+ ...existingNewEntryConfig,
3490
+ memoryDir
3491
+ }
3492
+ };
3493
+ const updatedEntries = { ...entries };
3494
+ updatedEntries[REMNIC_OPENCLAW_PLUGIN_ID] = newEntry;
3495
+ const slotIsActiveLegacy = hasLegacy && !migrateLegacy && currentSlot === REMNIC_OPENCLAW_LEGACY_PLUGIN_ID;
3496
+ const updatedSlots = slotIsActiveLegacy ? { ...slots } : { ...slots, memory: REMNIC_OPENCLAW_PLUGIN_ID };
3497
+ const updatedConfig = {
3498
+ ...existingConfig,
3499
+ plugins: {
3500
+ ...plugins,
3501
+ entries: updatedEntries,
3502
+ slots: updatedSlots
3503
+ }
3504
+ };
3505
+ const changes = [];
3506
+ if (!hasNew) changes.push(`+ Added plugins.entries["${REMNIC_OPENCLAW_PLUGIN_ID}"]`);
3507
+ else changes.push(`~ Updated plugins.entries["${REMNIC_OPENCLAW_PLUGIN_ID}"].config.memoryDir`);
3508
+ if (!slotIsActiveLegacy && currentSlot !== REMNIC_OPENCLAW_PLUGIN_ID) {
3509
+ changes.push(`~ Set plugins.slots.memory = "${REMNIC_OPENCLAW_PLUGIN_ID}" (was: ${currentSlot ?? "(unset)"})`);
3510
+ } else if (slotIsActiveLegacy) {
3511
+ changes.push(` Slot left as "${REMNIC_OPENCLAW_LEGACY_PLUGIN_ID}" \u2014 re-run with --yes to activate the new entry`);
3512
+ }
3513
+ if (!fs.existsSync(memoryDir)) changes.push(`+ Will create memory directory: ${memoryDir}`);
3514
+ if (hasLegacy && migrateLegacy) {
3515
+ changes.push(`~ Legacy '${REMNIC_OPENCLAW_LEGACY_PLUGIN_ID}' entry retained (safe to remove after verifying hooks fire)`);
3516
+ }
3517
+ if (opts.dryRun) {
3518
+ console.log("\n--- DRY RUN \u2014 no changes written ---");
3519
+ for (const c of changes) console.log(" " + c);
3520
+ const dryRunPlugins = updatedConfig.plugins;
3521
+ const dryRunEntries = dryRunPlugins.entries;
3522
+ const entrySummary = dryRunEntries ? Object.keys(dryRunEntries).map((k) => {
3523
+ const cfg = dryRunEntries[k]?.config;
3524
+ return ` ${k}: { config: { memoryDir: ${cfg?.memoryDir ?? "(unset)"}, ... } }`;
3525
+ }).join("\n") : " (none)";
3526
+ console.log("\nResulting plugins.entries:");
3527
+ console.log(entrySummary);
3528
+ console.log(`
3529
+ Resulting plugins.slots.memory: ${dryRunPlugins.slots?.memory ?? "(unset)"}`);
3530
+ return;
3531
+ }
3532
+ if (fs.existsSync(memoryDir)) {
3533
+ const st = fs.statSync(memoryDir);
3534
+ if (!st.isDirectory()) {
3535
+ throw new Error(
3536
+ `Cannot use ${memoryDir} as the memory directory \u2014 a file already exists at that path.
3537
+ Remove it first and re-run, or choose a different path with --memory-dir.`
3538
+ );
3539
+ }
3540
+ } else {
3541
+ fs.mkdirSync(memoryDir, { recursive: true });
3542
+ console.log(`Created memory directory: ${memoryDir}`);
3543
+ }
3544
+ const configDir = path2.dirname(configPath);
3545
+ if (!fs.existsSync(configDir)) {
3546
+ fs.mkdirSync(configDir, { recursive: true });
3547
+ }
3548
+ fs.writeFileSync(configPath, JSON.stringify(updatedConfig, null, 2) + "\n");
3549
+ console.log("\nDone! Summary of changes:");
3550
+ for (const c of changes) console.log(" " + c);
3551
+ if (hasLegacy && migrateLegacy) {
3552
+ console.log(
3553
+ `
3554
+ Note: The legacy '${REMNIC_OPENCLAW_LEGACY_PLUGIN_ID}' entry has been kept alongside '${REMNIC_OPENCLAW_PLUGIN_ID}'.`
3555
+ );
3556
+ console.log(
3557
+ "Once you verify that [remnic] gateway_start fired appears in your gateway log,"
3558
+ );
3559
+ console.log(`you can safely remove the '${REMNIC_OPENCLAW_LEGACY_PLUGIN_ID}' entry from openclaw.json.`);
3560
+ }
3561
+ console.log("\nNext steps:");
3562
+ console.log(" 1. Restart the OpenClaw gateway:");
3563
+ console.log(" launchctl kickstart -k gui/$(id -u)/ai.openclaw.gateway");
3564
+ console.log(" 2. Start a conversation \u2014 check your gateway log for:");
3565
+ console.log(" [remnic] gateway_start fired \u2014 Remnic memory plugin is active");
3566
+ console.log(" 3. Run `remnic doctor` to verify the full configuration.");
3567
+ }
3568
+ async function cmdTaxonomy(rest) {
3569
+ initLogger();
3570
+ const configPath = resolveConfigPath();
3571
+ const raw = fs.existsSync(configPath) ? JSON.parse(fs.readFileSync(configPath, "utf8")) : {};
3572
+ const remnicCfg = raw.remnic ?? raw.engram ?? raw;
3573
+ const config = parseConfig(remnicCfg);
3574
+ if (!config.taxonomyEnabled) {
3575
+ console.error(
3576
+ "Taxonomy is disabled in config (taxonomyEnabled = false). Enable it to use taxonomy commands."
3577
+ );
3578
+ process.exit(1);
3579
+ }
3580
+ const subCommand = rest[0];
3581
+ switch (subCommand) {
3582
+ case "show": {
3583
+ const taxonomy = await loadTaxonomy(config.memoryDir);
3584
+ const json = rest.includes("--json");
3585
+ if (json) {
3586
+ console.log(JSON.stringify(taxonomy, null, 2));
3587
+ } else {
3588
+ console.log(`Taxonomy v${taxonomy.version} \u2014 ${taxonomy.categories.length} categories
3589
+ `);
3590
+ const idWidth = Math.max(4, ...taxonomy.categories.map((c) => c.id.length));
3591
+ const nameWidth = Math.max(6, ...taxonomy.categories.map((c) => c.name.length));
3592
+ const header = `${"ID".padEnd(idWidth)} ${"Name".padEnd(nameWidth)} ${"Pri".padStart(3)} Memory Categories`;
3593
+ console.log(header);
3594
+ console.log("-".repeat(header.length + 10));
3595
+ const sorted = [...taxonomy.categories].sort((a, b) => a.priority - b.priority);
3596
+ for (const cat of sorted) {
3597
+ const line = `${cat.id.padEnd(idWidth)} ${cat.name.padEnd(nameWidth)} ${String(cat.priority).padStart(3)} ${cat.memoryCategories.join(", ")}`;
3598
+ console.log(line);
3599
+ }
3600
+ }
3601
+ break;
3602
+ }
3603
+ case "resolver": {
3604
+ const taxonomy = await loadTaxonomy(config.memoryDir);
3605
+ const doc = generateResolverDocument(taxonomy);
3606
+ console.log(doc);
3607
+ if (config.taxonomyAutoGenResolver) {
3608
+ const resolverPath = path2.join(config.memoryDir, ".taxonomy", "RESOLVER.md");
3609
+ fs.mkdirSync(path2.dirname(resolverPath), { recursive: true });
3610
+ fs.writeFileSync(resolverPath, doc);
3611
+ console.error(`Written: ${resolverPath}`);
3612
+ }
3613
+ break;
3614
+ }
3615
+ case "add": {
3616
+ const id = rest[1];
3617
+ const name = rest[2];
3618
+ if (!id || !name) {
3619
+ console.error("Usage: remnic taxonomy add <id> <name>");
3620
+ process.exit(1);
3621
+ }
3622
+ try {
3623
+ validateSlug(id);
3624
+ } catch (err) {
3625
+ console.error(err instanceof Error ? err.message : String(err));
3626
+ process.exit(1);
3627
+ }
3628
+ const taxonomy = await loadTaxonomy(config.memoryDir);
3629
+ if (taxonomy.categories.some((c) => c.id === id)) {
3630
+ console.error(`Category "${id}" already exists.`);
3631
+ process.exit(1);
3632
+ }
3633
+ const descriptionFlag = resolveFlag(rest, "--description");
3634
+ const priorityFlag = resolveFlag(rest, "--priority");
3635
+ const memoryCategoriesFlag = resolveFlag(rest, "--memory-categories");
3636
+ const newCat = {
3637
+ id,
3638
+ name,
3639
+ description: descriptionFlag ?? `Custom category: ${name}`,
3640
+ filingRules: [`Content belonging to ${name}`],
3641
+ priority: priorityFlag ? Number(priorityFlag) : 100,
3642
+ memoryCategories: memoryCategoriesFlag ? memoryCategoriesFlag.split(",").map((s) => s.trim()) : []
3643
+ };
3644
+ taxonomy.categories.push(newCat);
3645
+ try {
3646
+ validateTaxonomy(taxonomy);
3647
+ } catch (err) {
3648
+ console.error(`Invalid taxonomy: ${err instanceof Error ? err.message : String(err)}`);
3649
+ process.exit(1);
3650
+ }
3651
+ await saveTaxonomy(config.memoryDir, taxonomy);
3652
+ console.log(`Added category "${id}" (${name}).`);
3653
+ if (config.taxonomyAutoGenResolver) {
3654
+ const doc = generateResolverDocument(taxonomy);
3655
+ const resolverPath = path2.join(config.memoryDir, ".taxonomy", "RESOLVER.md");
3656
+ fs.writeFileSync(resolverPath, doc);
3657
+ console.error(`Regenerated: ${resolverPath}`);
3658
+ }
3659
+ break;
3660
+ }
3661
+ case "remove": {
3662
+ const id = rest[1];
3663
+ if (!id) {
3664
+ console.error("Usage: remnic taxonomy remove <id>");
3665
+ process.exit(1);
3666
+ }
3667
+ const taxonomy = await loadTaxonomy(config.memoryDir);
3668
+ const idx = taxonomy.categories.findIndex((c) => c.id === id);
3669
+ if (idx === -1) {
3670
+ console.error(`Category "${id}" not found.`);
3671
+ process.exit(1);
3672
+ }
3673
+ const target = taxonomy.categories[idx];
3674
+ const isDefault = DEFAULT_TAXONOMY.categories.some((c) => c.id === id);
3675
+ if (isDefault && target.memoryCategories.length > 0) {
3676
+ console.error(
3677
+ `Cannot remove default category "${id}" that maps MemoryCategory values: ${target.memoryCategories.join(", ")}. Reassign them first.`
3678
+ );
3679
+ process.exit(1);
3680
+ }
3681
+ taxonomy.categories.splice(idx, 1);
3682
+ await saveTaxonomy(config.memoryDir, taxonomy);
3683
+ console.log(`Removed category "${id}".`);
3684
+ if (config.taxonomyAutoGenResolver) {
3685
+ const doc = generateResolverDocument(taxonomy);
3686
+ const resolverPath = path2.join(config.memoryDir, ".taxonomy", "RESOLVER.md");
3687
+ fs.writeFileSync(resolverPath, doc);
3688
+ console.error(`Regenerated: ${resolverPath}`);
3689
+ }
3690
+ break;
3691
+ }
3692
+ case "resolve": {
3693
+ const resolveArgs = rest.slice(1);
3694
+ const textParts = stripResolveFlags(resolveArgs, TAXONOMY_RESOLVE_BOOLEAN_FLAGS);
3695
+ const text = textParts.join(" ");
3696
+ if (!text) {
3697
+ console.error("Usage: remnic taxonomy resolve <text>");
3698
+ process.exit(1);
3699
+ }
3700
+ const categoryFlag = resolveFlag(rest, "--category");
3701
+ const memoryCategory = categoryFlag ?? "fact";
3702
+ const taxonomy = await loadTaxonomy(config.memoryDir);
3703
+ const decision = resolveCategory(text, memoryCategory, taxonomy);
3704
+ const json = rest.includes("--json");
3705
+ if (json) {
3706
+ console.log(JSON.stringify(decision, null, 2));
3707
+ } else {
3708
+ console.log(`Category: ${decision.categoryId}`);
3709
+ console.log(`Confidence: ${decision.confidence.toFixed(2)}`);
3710
+ console.log(`Reason: ${decision.reason}`);
3711
+ if (decision.alternatives.length > 0) {
3712
+ console.log(`
3713
+ Alternatives:`);
3714
+ for (const alt of decision.alternatives.slice(0, 3)) {
3715
+ console.log(` - ${alt.categoryId}: ${alt.reason}`);
3716
+ }
3717
+ }
3718
+ }
3719
+ break;
3720
+ }
3721
+ default:
3722
+ console.log(`
3723
+ remnic taxonomy \u2014 MECE knowledge directory
3724
+
3725
+ Usage:
3726
+ remnic taxonomy show [--json] Show current taxonomy
3727
+ remnic taxonomy resolver Print/regenerate RESOLVER.md
3728
+ remnic taxonomy add <id> <name> [options] Add a custom category
3729
+ --description <text> Category description
3730
+ --priority <number> Priority (lower wins, default 100)
3731
+ --memory-categories <list> Comma-separated MemoryCategory values
3732
+ remnic taxonomy remove <id> Remove a custom category
3733
+ remnic taxonomy resolve <text> [--category <cat>] Test: resolve text to a category
3734
+ --json JSON output
3735
+ `);
3736
+ break;
3737
+ }
3738
+ }
3739
+ function resolveRequiredValueFlag(args, flag) {
3740
+ if (!hasFlag(args, flag)) return void 0;
3741
+ const value = resolveFlagStrict(args, flag);
3742
+ if (value === void 0) {
3743
+ throw new Error(
3744
+ `${flag} requires a value. Provide it as \`${flag} <value>\`, not as a bare flag.`
3745
+ );
3746
+ }
3747
+ return value;
3748
+ }
3749
+ function parseTrainingExportArgs(rest, defaultMemoryDir) {
3750
+ const format = resolveRequiredValueFlag(rest, "--format");
3751
+ if (!format) {
3752
+ throw new Error(
3753
+ "--format <name> is required. Run `remnic training:export --help` for the list of registered adapters."
3754
+ );
3755
+ }
3756
+ const dryRun = hasFlag(rest, "--dry-run");
3757
+ const outputRaw = resolveRequiredValueFlag(rest, "--output") ?? resolveRequiredValueFlag(rest, "--out");
3758
+ if (!outputRaw && !dryRun) {
3759
+ throw new Error(
3760
+ "--output <path> (or --out <path>) is required for training:export. Use --dry-run to print statistics without writing a file."
3761
+ );
3762
+ }
3763
+ const output = outputRaw ? expandTilde(outputRaw) : "";
3764
+ const memoryDirFlag = resolveRequiredValueFlag(rest, "--memory-dir");
3765
+ const memoryDir = expandTilde(memoryDirFlag ?? defaultMemoryDir);
3766
+ const since = resolveRequiredValueFlag(rest, "--since");
3767
+ const until = resolveRequiredValueFlag(rest, "--until");
3768
+ const minConfidenceRaw = resolveRequiredValueFlag(rest, "--min-confidence");
3769
+ let minConfidence;
3770
+ if (minConfidenceRaw !== void 0) {
3771
+ const n = Number(minConfidenceRaw);
3772
+ if (!Number.isFinite(n) || n < 0 || n > 1) {
3773
+ throw new Error(
3774
+ `Invalid --min-confidence value "${minConfidenceRaw}": expected a number in [0, 1].`
3775
+ );
3776
+ }
3777
+ minConfidence = n;
3778
+ }
3779
+ const categoriesRaw = resolveRequiredValueFlag(rest, "--categories");
3780
+ const categories = categoriesRaw ? categoriesRaw.split(",").map((c) => c.trim()).filter((c) => c.length > 0) : void 0;
3781
+ const maxPairsRaw = resolveRequiredValueFlag(rest, "--max-pairs-per-record");
3782
+ let maxPairsPerRecord;
3783
+ if (maxPairsRaw !== void 0) {
3784
+ const n = Number(maxPairsRaw);
3785
+ if (!Number.isInteger(n) || n < 1) {
3786
+ throw new Error(
3787
+ `Invalid --max-pairs-per-record value "${maxPairsRaw}": expected a positive integer.`
3788
+ );
3789
+ }
3790
+ maxPairsPerRecord = n;
3791
+ }
3792
+ const includeEntities = hasFlag(rest, "--include-entities");
3793
+ const synthesize = hasFlag(rest, "--synthesize");
3794
+ const privacySweep = !hasFlag(rest, "--no-privacy-sweep");
3795
+ return {
3796
+ format,
3797
+ output,
3798
+ memoryDir,
3799
+ since,
3800
+ until,
3801
+ minConfidence,
3802
+ categories,
3803
+ includeEntities,
3804
+ synthesize,
3805
+ maxPairsPerRecord,
3806
+ privacySweep,
3807
+ dryRun
3808
+ };
3809
+ }
3810
+ async function runTrainingExport(args, stdout = process.stdout) {
3811
+ ensureWecloneExportAdapterRegistered();
3812
+ const adapter = getTrainingExportAdapter2(args.format);
3813
+ if (!adapter) {
3814
+ const registered = listTrainingExportAdapters();
3815
+ const validList = registered.length > 0 ? `Valid formats: [${registered.join(", ")}]` : "No adapters are currently registered.";
3816
+ throw new Error(
3817
+ `Unknown training-export format "${args.format}". ${validList}`
3818
+ );
3819
+ }
3820
+ if (!fs.existsSync(args.memoryDir)) {
3821
+ throw new Error(
3822
+ `--memory-dir "${args.memoryDir}" does not exist. Provide the path to an existing memory directory.`
3823
+ );
3824
+ }
3825
+ if (!fs.statSync(args.memoryDir).isDirectory()) {
3826
+ throw new Error(
3827
+ `--memory-dir "${args.memoryDir}" is not a directory. Provide the path to a memory directory, not a file.`
3828
+ );
3829
+ }
3830
+ let since;
3831
+ if (args.since) since = parseStrictCliDate(args.since, "--since");
3832
+ let until;
3833
+ if (args.until) until = parseStrictCliDate(args.until, "--until");
3834
+ const convertOptions = {
3835
+ memoryDir: args.memoryDir,
3836
+ since,
3837
+ until,
3838
+ minConfidence: args.minConfidence,
3839
+ categories: args.categories,
3840
+ includeEntities: args.includeEntities
3841
+ };
3842
+ let records = await convertMemoriesToRecords(convertOptions);
3843
+ const recordsRead = records.length;
3844
+ if (args.synthesize) {
3845
+ records = synthesizeTrainingPairs(records, {
3846
+ maxPairsPerRecord: args.maxPairsPerRecord
3847
+ });
3848
+ }
3849
+ let redactedCount = 0;
3850
+ if (args.privacySweep) {
3851
+ const swept = sweepPii(records);
3852
+ records = swept.cleanRecords;
3853
+ redactedCount = swept.redactedCount;
3854
+ }
3855
+ if (args.dryRun) {
3856
+ stdout.write(`Training export dry run
3857
+ `);
3858
+ stdout.write(`Format: ${adapter.name}
3859
+ `);
3860
+ stdout.write(`Records read: ${recordsRead}
3861
+ `);
3862
+ stdout.write(`Records to write: ${records.length}
3863
+ `);
3864
+ if (args.privacySweep) {
3865
+ stdout.write(`Redacted records: ${redactedCount}
3866
+ `);
3867
+ }
3868
+ const cats = /* @__PURE__ */ new Map();
3869
+ for (const r of records) {
3870
+ const c = r.category ?? "unknown";
3871
+ cats.set(c, (cats.get(c) ?? 0) + 1);
3872
+ }
3873
+ const sortedCats = [...cats.entries()].sort(
3874
+ (a, b) => a[0].localeCompare(b[0])
3875
+ );
3876
+ for (const [cat, count] of sortedCats) {
3877
+ stdout.write(` ${cat}: ${count}
3878
+ `);
3879
+ }
3880
+ return {
3881
+ recordsRead,
3882
+ recordsWritten: 0,
3883
+ redactedCount,
3884
+ outputPath: null
3885
+ };
3886
+ }
3887
+ if (!args.output) {
3888
+ throw new Error(
3889
+ "runTrainingExport: `output` is required when dryRun is false. Pass dryRun: true to skip file I/O."
3890
+ );
3891
+ }
3892
+ const formatted = adapter.formatRecords(records);
3893
+ const outDir = path2.dirname(args.output);
3894
+ fs.mkdirSync(outDir, { recursive: true });
3895
+ const tmpPath = `${args.output}.tmp-${process.pid}-${Date.now()}`;
3896
+ fs.writeFileSync(tmpPath, formatted, "utf-8");
3897
+ fs.renameSync(tmpPath, args.output);
3898
+ stdout.write(
3899
+ `Exported ${records.length} records to ${args.output} (${adapter.name} format)
3900
+ `
3901
+ );
3902
+ if (args.privacySweep && redactedCount > 0) {
3903
+ stdout.write(`Privacy sweep redacted PII in ${redactedCount} record(s).
3904
+ `);
3905
+ }
3906
+ return {
3907
+ recordsRead,
3908
+ recordsWritten: records.length,
3909
+ redactedCount,
3910
+ outputPath: args.output
3911
+ };
3912
+ }
1307
3913
  async function main(argv = process.argv.slice(2)) {
1308
3914
  const [command, ...rest] = argv;
1309
3915
  if (command !== "migrate") {
@@ -1356,7 +3962,7 @@ async function main(argv = process.argv.slice(2)) {
1356
3962
  daemonUninstall();
1357
3963
  break;
1358
3964
  case "status":
1359
- daemonStatus();
3965
+ await daemonStatus();
1360
3966
  break;
1361
3967
  default:
1362
3968
  console.log("Usage: remnic daemon <start|stop|restart|install|uninstall|status>");
@@ -1451,7 +4057,7 @@ async function main(argv = process.argv.slice(2)) {
1451
4057
  }
1452
4058
  }, 500);
1453
4059
  };
1454
- fs2.watch(memoryDir, { recursive: true }, (_event, filename) => {
4060
+ fs.watch(memoryDir, { recursive: true }, (_event, filename) => {
1455
4061
  if (filename && filename.startsWith(".")) return;
1456
4062
  rebuild();
1457
4063
  });
@@ -1459,12 +4065,12 @@ async function main(argv = process.argv.slice(2)) {
1459
4065
  });
1460
4066
  } else if (subAction === "validate") {
1461
4067
  const treeDir = outputDir;
1462
- if (!fs2.existsSync(treeDir)) {
4068
+ if (!fs.existsSync(treeDir)) {
1463
4069
  console.error(`Context tree not found at ${treeDir}. Run 'remnic tree generate' first.`);
1464
4070
  process.exit(1);
1465
4071
  }
1466
4072
  const indexPath = path2.join(treeDir, "INDEX.md");
1467
- if (!fs2.existsSync(indexPath)) {
4073
+ if (!fs.existsSync(indexPath)) {
1468
4074
  console.error(`INDEX.md missing in ${treeDir}. Tree may be corrupt \u2014 regenerate.`);
1469
4075
  process.exit(1);
1470
4076
  }
@@ -1529,10 +4135,106 @@ Options:
1529
4135
  await cmdSpace(action, rest.slice(1), json);
1530
4136
  break;
1531
4137
  }
4138
+ case "bench": {
4139
+ await cmdBench(rest);
4140
+ break;
4141
+ }
1532
4142
  case "benchmark": {
1533
- const action = rest[0] ?? "run";
1534
- const json = rest.includes("--json");
1535
- await cmdBenchmark(action, rest.slice(1), json);
4143
+ await cmdBench(rest);
4144
+ break;
4145
+ }
4146
+ case "briefing": {
4147
+ await cmdBriefing(rest);
4148
+ break;
4149
+ }
4150
+ case "versions": {
4151
+ await cmdVersions(rest);
4152
+ break;
4153
+ }
4154
+ case "binary": {
4155
+ await cmdBinary(rest);
4156
+ break;
4157
+ }
4158
+ case "taxonomy": {
4159
+ await cmdTaxonomy(rest);
4160
+ break;
4161
+ }
4162
+ case "enrich": {
4163
+ await cmdEnrich(rest);
4164
+ break;
4165
+ }
4166
+ case "extensions": {
4167
+ const action = rest[0] ?? "help";
4168
+ await cmdExtensions(action, rest.slice(1));
4169
+ break;
4170
+ }
4171
+ case "training:export": {
4172
+ if (rest.includes("--help") || rest.includes("-h")) {
4173
+ console.log(`
4174
+ remnic training:export \u2014 Export Remnic memories as fine-tuning datasets (issue #459)
4175
+
4176
+ Usage:
4177
+ remnic training:export --format <name> --output <path> [options]
4178
+
4179
+ Required:
4180
+ --format <name> Registered adapter name (e.g. weclone)
4181
+ --output <path> | --out Path to write the dataset file
4182
+
4183
+ Filters:
4184
+ --memory-dir <path> Memory directory (defaults to resolved memoryDir)
4185
+ --since <YYYY-MM-DD[T...]> Only include memories created at or after this date
4186
+ --until <YYYY-MM-DD[T...]> Only include memories created before this date (exclusive)
4187
+ --min-confidence <0..1> Inclusive lower bound on memory confidence
4188
+ --categories <list> Comma-separated category filter (fact,preference,...)
4189
+ --include-entities Also read from entities/ (off by default)
4190
+
4191
+ Adapter options:
4192
+ --synthesize Generate conversational Q/A pairs (WeClone-optimised)
4193
+ --max-pairs-per-record <n> When --synthesize, max pairs emitted per memory
4194
+ --no-privacy-sweep Skip the final PII redaction pass (default: on)
4195
+
4196
+ Other:
4197
+ --dry-run Print statistics only; do not write the file
4198
+ `);
4199
+ break;
4200
+ }
4201
+ let parsed;
4202
+ try {
4203
+ parsed = parseTrainingExportArgs(rest, resolveMemoryDir());
4204
+ } catch (err) {
4205
+ console.error(err instanceof Error ? err.message : String(err));
4206
+ process.exit(1);
4207
+ }
4208
+ try {
4209
+ await runTrainingExport(parsed);
4210
+ } catch (err) {
4211
+ console.error(err instanceof Error ? err.message : String(err));
4212
+ process.exit(1);
4213
+ }
4214
+ break;
4215
+ }
4216
+ case "openclaw": {
4217
+ const subAction = rest[0] ?? "help";
4218
+ if (subAction === "install") {
4219
+ const yes = rest.includes("--yes") || rest.includes("-y") || rest.includes("--force");
4220
+ const dryRun = rest.includes("--dry-run");
4221
+ const memoryDir = resolveFlagStrict(rest, "--memory-dir");
4222
+ const configOverride = resolveFlagStrict(rest, "--config");
4223
+ await cmdOpenclawInstall({ yes, dryRun, memoryDir, configPath: configOverride });
4224
+ } else {
4225
+ console.log(`Usage: remnic openclaw <install>
4226
+
4227
+ install Configure OpenClaw to use Remnic as the memory plugin.
4228
+
4229
+ Sets plugins.entries["${REMNIC_OPENCLAW_PLUGIN_ID}"] and plugins.slots.memory
4230
+ in ~/.openclaw/openclaw.json (or $OPENCLAW_CONFIG_PATH).
4231
+
4232
+ Options:
4233
+ --yes / -y / --force Skip interactive prompts, assume Y
4234
+ --dry-run Print resulting config diff without writing
4235
+ --memory-dir <path> Override default memory dir (~/.openclaw/workspace/memory/local)
4236
+ --config <path> Override OpenClaw config path`);
4237
+ }
1536
4238
  break;
1537
4239
  }
1538
4240
  default:
@@ -1547,6 +4249,11 @@ Usage:
1547
4249
 
1548
4250
  remnic doctor Run diagnostics
1549
4251
  remnic config Show current config
4252
+ remnic openclaw install Configure OpenClaw to use Remnic memory (sets slot + entry)
4253
+ --yes / -y / --force Skip prompts
4254
+ --dry-run Preview changes without writing
4255
+ --memory-dir <path> Custom memory directory
4256
+ --config <path> Custom OpenClaw config path
1550
4257
  remnic daemon <start|stop|restart|install|uninstall|status> Manage background server
1551
4258
  remnic token <generate|list|revoke> [connector-id] Manage auth tokens
1552
4259
  remnic tree <generate|watch|validate> Generate context tree
@@ -1555,10 +4262,39 @@ Usage:
1555
4262
  remnic review <list|approve|dismiss|flag> [id] Review inbox
1556
4263
  remnic sync <run|watch> [--source <dir>] Diff-aware sync
1557
4264
  remnic dedup [--json] Find duplicate memories
1558
- remnic connectors <list|install|remove|doctor> [id] Manage connectors
4265
+ remnic connectors <list|install|remove|doctor|marketplace> [id] Manage connectors
4266
+ marketplace generate Generate marketplace.json for Codex
4267
+ marketplace validate Validate a marketplace.json file
4268
+ marketplace install Install from a marketplace source
4269
+ remnic extensions <list|show|validate|reload> Manage memory extensions
1559
4270
  remnic space <list|switch|create|delete|push|pull|share|promote|audit> Manage spaces
1560
4271
  create accepts --parent <id> to set parent-child relationship
1561
- remnic benchmark <run|check|report> [queries...] [--explain] [--baseline=<path>] [--report=<path>]
4272
+ remnic bench <list|run|datasets|runs|compare|results|baseline|export|publish|ui|providers> [benchmark...] [--quick] [--all] [--dataset-dir <path>] [--results-dir <path>] [--baselines-dir <path>] [--threshold <value>] [--detail] [--format <json|csv|html>] [--output <path>] [--target remnic-ai] [--json]
4273
+ benchmark is kept as a compatibility alias. check/report remain under that alias.
4274
+ remnic benchmark <list|run|datasets|runs|compare|results|baseline|export|publish|ui|providers|check|report> [queries...] [--explain] [--baseline=<path>] [--report=<path>]
4275
+ remnic briefing [--since <window>] [--focus <filter>] [--save] [--format markdown|json]
4276
+ Daily context briefing. Windows: yesterday, today, NNh, NNd, NNw.
4277
+ Focus: person:<name>, project:<name>, topic:<name>.
4278
+ remnic versions <list|show|diff|revert> <page-path> [id] [--json]
4279
+ Page-level versioning: list, show, diff, or revert page snapshots.
4280
+ remnic binary scan Scan for untracked binary files
4281
+ remnic binary status Show binary lifecycle manifest summary
4282
+ remnic binary run [--dry-run] Run full binary lifecycle pipeline
4283
+ remnic binary clean --force Force-clean binaries past grace period
4284
+ remnic taxonomy <show|resolver|add|remove|resolve> MECE knowledge directory
4285
+ show [--json] Show current taxonomy
4286
+ resolver Print/regenerate RESOLVER.md
4287
+ add <id> <name> [--priority N] Add custom category
4288
+ remove <id> Remove custom category
4289
+ resolve <text> [--category <cat>] Test resolver on sample text
4290
+ remnic enrich <entity-name> Manually enrich a specific entity
4291
+ remnic enrich --all Enrich all entities
4292
+ remnic enrich --dry-run Preview what would be enriched
4293
+ remnic enrich audit Show recent enrichment audit log
4294
+ remnic enrich providers List registered providers and their status
4295
+ remnic training:export --format <name> --output <path> [options]
4296
+ Export memories as a fine-tuning dataset (issue #459). Run
4297
+ 'remnic training:export --help' for the full option list.
1562
4298
 
1563
4299
  Options:
1564
4300
  --json Output in JSON format
@@ -1577,5 +4313,17 @@ if (argv1Base.endsWith("remnic.ts") || argv1Base.endsWith("remnic.js") || argv1B
1577
4313
  });
1578
4314
  }
1579
4315
  export {
1580
- main
4316
+ BENCHMARK_CATALOG,
4317
+ TAXONOMY_RESOLVE_BOOLEAN_FLAGS,
4318
+ buildBenchRunnerArgs,
4319
+ getBenchUsageText,
4320
+ hasFlag,
4321
+ main,
4322
+ parseBenchArgs,
4323
+ parseConnectorConfig,
4324
+ parseTrainingExportArgs,
4325
+ resolveFlag,
4326
+ runTrainingExport,
4327
+ stripConfigArgv,
4328
+ stripResolveFlags
1581
4329
  };