@remnic/cli 1.0.2 → 1.0.4

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,15 @@
1
+ import {
2
+ checkRegression,
3
+ loadBaseline,
4
+ runBenchSuite,
5
+ runExplain
6
+ } from "./chunk-U4MQO3IF.js";
7
+
1
8
  // src/index.ts
2
- import fs2 from "fs";
9
+ import fs from "fs";
3
10
  import path2 from "path";
4
11
  import * as childProcess from "child_process";
12
+ import { fileURLToPath } from "url";
5
13
  import {
6
14
  parseConfig,
7
15
  Orchestrator,
@@ -34,291 +42,430 @@ import {
34
42
  getManifestPath,
35
43
  generateContextTree,
36
44
  migrateFromEngram,
37
- rollbackFromEngramMigration
45
+ rollbackFromEngramMigration,
46
+ buildBriefing,
47
+ parseBriefingWindow,
48
+ parseBriefingFocus,
49
+ validateBriefingFormat,
50
+ resolveBriefingSaveDir,
51
+ briefingFilename,
52
+ FileCalendarSource,
53
+ listVersions,
54
+ getVersion,
55
+ revertToVersion,
56
+ diffVersions,
57
+ readManifest,
58
+ createBackend,
59
+ runBinaryLifecyclePipeline,
60
+ DEFAULT_SCAN_PATTERNS,
61
+ DEFAULT_MAX_BINARY_SIZE_BYTES,
62
+ publisherForConnector,
63
+ hostIdForConnector,
64
+ registerPublisher,
65
+ PUBLISHERS,
66
+ CodexMemoryExtensionPublisher,
67
+ ClaudeCodeMemoryExtensionPublisher,
68
+ HermesMemoryExtensionPublisher,
69
+ DEFAULT_TAXONOMY,
70
+ resolveCategory,
71
+ generateResolverDocument,
72
+ loadTaxonomy,
73
+ saveTaxonomy,
74
+ validateSlug,
75
+ validateTaxonomy,
76
+ generateMarketplaceManifest,
77
+ checkMarketplaceManifest,
78
+ writeMarketplaceManifest,
79
+ installFromMarketplace,
80
+ EnrichmentProviderRegistry,
81
+ WebSearchProvider,
82
+ runEnrichmentPipeline,
83
+ appendAuditEntry,
84
+ readAuditLog,
85
+ defaultEnrichmentPipelineConfig,
86
+ discoverMemoryExtensions,
87
+ resolveExtensionsRoot,
88
+ coerceInstallExtension
38
89
  } from "@remnic/core";
39
90
 
40
- // ../bench/src/benchmark.ts
41
- import fs from "fs";
42
- 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?"
63
- ];
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;
72
- 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}`
77
- );
91
+ // src/service-candidates.ts
92
+ function firstSuccessfulResult(candidates, attempt) {
93
+ for (const candidate of candidates) {
94
+ try {
95
+ const result = attempt(candidate);
96
+ if (result !== void 0) return result;
97
+ } catch {
78
98
  }
79
- return raw;
80
- } catch {
81
- return void 0;
82
99
  }
100
+ return void 0;
83
101
  }
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())
97
- );
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
- }
107
- }
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 };
160
- }
161
- tiers.push("no_results");
162
- tierDetails.push({
163
- tier: "no_results",
164
- latencyMs: d0 + d1 + d2 + d3 + d4,
165
- resultsCount: 0
102
+ function firstSuccessfulCandidate(candidates, attempt) {
103
+ return firstSuccessfulResult(candidates, (candidate) => {
104
+ attempt(candidate);
105
+ return candidate;
166
106
  });
167
- return { tiers, tierDetails };
168
107
  }
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
- };
108
+
109
+ // src/bench-args.ts
110
+ import path from "path";
111
+
112
+ // src/path-utils.ts
113
+ function resolveHomeDir() {
114
+ return process.env.HOME ?? process.env.USERPROFILE ?? "~";
180
115
  }
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
- };
116
+ function expandTilde(p) {
117
+ if (p === "~" || p.startsWith("~/") || p.startsWith("~\\")) {
118
+ return resolveHomeDir() + p.slice(1);
119
+ }
120
+ const home = resolveHomeDir();
121
+ if (p === "$HOME" || p.startsWith("$HOME/") || p.startsWith("$HOME\\")) {
122
+ return home + p.slice(5);
123
+ }
124
+ if (p === "${HOME}" || p.startsWith("${HOME}/") || p.startsWith("${HOME}\\")) {
125
+ return home + p.slice(7);
126
+ }
127
+ return p;
194
128
  }
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
- }
129
+
130
+ // src/bench-args.ts
131
+ function readBenchOptionValue(argv, flag) {
132
+ const index = argv.indexOf(flag);
133
+ if (index === -1) {
134
+ return void 0;
221
135
  }
222
- const totalDuration = hrTimeMs() - suiteStart;
223
- const metrics = {};
224
- for (const r of results) {
225
- metrics[r.query] = r.latencyMs;
136
+ const value = argv[index + 1];
137
+ if (!value || value.startsWith("-")) {
138
+ throw new Error(`ERROR: ${flag} requires a value.`);
226
139
  }
227
- const report = generateReport(results, reportPath);
228
- const baseline = loadBaseline(baselinePath);
229
- const regressionResult = checkRegression(
230
- metrics,
231
- baseline,
232
- regressionTolerance
233
- );
234
- if (!baseline) {
235
- saveBaseline(baselinePath, {
236
- version: BASELINE_VERSION,
237
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
238
- metrics
239
- });
140
+ return value;
141
+ }
142
+ function collectBenchmarks(argv) {
143
+ const benchmarks = [];
144
+ for (let index = 0; index < argv.length; index += 1) {
145
+ const arg = argv[index];
146
+ if (arg === "--dataset-dir") {
147
+ index += 1;
148
+ continue;
149
+ }
150
+ if (!arg.startsWith("-")) {
151
+ benchmarks.push(arg);
152
+ }
240
153
  }
154
+ return benchmarks;
155
+ }
156
+ function parseBenchActionArgs(argv) {
157
+ const [first, ...rest] = argv;
158
+ const action = first === "list" || first === "run" || first === "check" || first === "report" ? first : first === void 0 || first === "--help" || first === "-h" ? "help" : "run";
241
159
  return {
242
- results,
243
- report,
244
- totalDurationMs: totalDuration,
245
- regressions: regressionResult.regressions
160
+ action,
161
+ args: action === "run" && action !== first ? argv : rest
246
162
  };
247
163
  }
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
261
- });
262
- }
164
+ function parseBenchArgs(argv) {
165
+ const { action, args } = parseBenchActionArgs(argv);
166
+ const benchmarks = collectBenchmarks(args);
167
+ const datasetDir = readBenchOptionValue(args, "--dataset-dir");
263
168
  return {
264
- passed: regressions.every((r) => r.passed),
265
- regressions
169
+ action,
170
+ benchmarks,
171
+ quick: args.includes("--quick"),
172
+ all: args.includes("--all"),
173
+ json: args.includes("--json"),
174
+ datasetDir: datasetDir ? path.resolve(expandTilde(datasetDir)) : void 0
266
175
  };
267
176
  }
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");
177
+
178
+ // src/cli-args.ts
179
+ function resolveFlag(args, flag) {
180
+ const idx = args.indexOf(flag);
181
+ return idx !== -1 && idx + 1 < args.length ? args[idx + 1] : void 0;
182
+ }
183
+ function hasFlag(args, flag) {
184
+ return args.indexOf(flag) !== -1;
185
+ }
186
+ var TAXONOMY_RESOLVE_BOOLEAN_FLAGS = /* @__PURE__ */ new Set(["--json"]);
187
+ function stripResolveFlags(args, booleanFlags = TAXONOMY_RESOLVE_BOOLEAN_FLAGS) {
188
+ const textParts = [];
189
+ for (let i = 0; i < args.length; i++) {
190
+ if (args[i].startsWith("--")) {
191
+ if (!booleanFlags.has(args[i])) {
192
+ i++;
193
+ }
194
+ continue;
195
+ }
196
+ textParts.push(args[i]);
287
197
  }
288
- return report;
198
+ return textParts;
289
199
  }
290
200
 
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 {
201
+ // src/parse-connector-config.ts
202
+ function parseConnectorConfig(args) {
203
+ const config = {};
204
+ for (let i = 0; i < args.length; i++) {
205
+ const arg = args[i];
206
+ if (arg.startsWith("--config=")) {
207
+ const rest = arg.slice("--config=".length);
208
+ const eqIdx = rest.indexOf("=");
209
+ if (eqIdx !== -1) {
210
+ const key = rest.slice(0, eqIdx);
211
+ const value = rest.slice(eqIdx + 1);
212
+ if (key) config[key] = value;
213
+ }
214
+ } else if (arg === "--config") {
215
+ const next = args[i + 1];
216
+ if (next !== void 0) {
217
+ const eqIdx = next.indexOf("=");
218
+ if (eqIdx !== -1) {
219
+ const key = next.slice(0, eqIdx);
220
+ const value = next.slice(eqIdx + 1);
221
+ if (key) {
222
+ config[key] = value;
223
+ i++;
224
+ }
225
+ }
226
+ }
298
227
  }
299
228
  }
300
- return void 0;
229
+ return config;
301
230
  }
302
- function firstSuccessfulCandidate(candidates, attempt) {
303
- return firstSuccessfulResult(candidates, (candidate) => {
304
- attempt(candidate);
305
- return candidate;
306
- });
231
+ function stripConfigArgv(args) {
232
+ const result = [];
233
+ for (let i = 0; i < args.length; i++) {
234
+ const arg = args[i];
235
+ if (arg.startsWith("--config=")) {
236
+ continue;
237
+ } else if (arg === "--config") {
238
+ const next = args[i + 1];
239
+ if (next !== void 0 && next.includes("=") && !next.startsWith("--")) {
240
+ i++;
241
+ }
242
+ continue;
243
+ }
244
+ result.push(arg);
245
+ }
246
+ return result;
307
247
  }
308
248
 
309
249
  // src/index.ts
250
+ registerPublisher("codex", () => new CodexMemoryExtensionPublisher());
251
+ registerPublisher("claude-code", () => new ClaudeCodeMemoryExtensionPublisher());
252
+ registerPublisher("hermes", () => new HermesMemoryExtensionPublisher());
310
253
  function readCompatEnv(primary, legacy) {
311
254
  return process.env[primary] ?? process.env[legacy];
312
255
  }
313
- function resolveHomeDir() {
314
- return process.env.HOME ?? process.env.USERPROFILE ?? "~";
315
- }
316
256
  var PID_DIR = path2.join(resolveHomeDir(), ".remnic");
317
257
  var LEGACY_PID_DIR = path2.join(resolveHomeDir(), ".engram");
318
258
  var PID_FILE = path2.join(PID_DIR, "server.pid");
319
259
  var LEGACY_PID_FILE = path2.join(LEGACY_PID_DIR, "server.pid");
320
260
  var LOG_FILE = path2.join(PID_DIR, "server.log");
321
261
  var LEGACY_LOG_FILE = path2.join(LEGACY_PID_DIR, "server.log");
262
+ var CLI_MODULE_DIR = path2.dirname(fileURLToPath(import.meta.url));
263
+ var CLI_REPO_ROOT = path2.resolve(CLI_MODULE_DIR, "../../..");
264
+ var EVAL_RUNNER_PATH = path2.join(CLI_REPO_ROOT, "evals", "run.ts");
265
+ var BENCHMARK_CATALOG = [
266
+ {
267
+ id: "ama-bench",
268
+ title: "AMA-Bench",
269
+ category: "agentic",
270
+ summary: "Agent Memory Abilities benchmark for long-horizon agent workflows."
271
+ },
272
+ {
273
+ id: "memory-arena",
274
+ title: "Memory Arena",
275
+ category: "agentic",
276
+ summary: "Interdependent multi-session tasks that stress operational recall."
277
+ },
278
+ {
279
+ id: "amemgym",
280
+ title: "AMemGym",
281
+ category: "agentic",
282
+ summary: "Interactive personalization benchmark for agent memory adaptation."
283
+ },
284
+ {
285
+ id: "longmemeval",
286
+ title: "LongMemEval",
287
+ category: "retrieval",
288
+ summary: "Long-term memory retrieval benchmark across core memory abilities."
289
+ },
290
+ {
291
+ id: "locomo",
292
+ title: "LoCoMo",
293
+ category: "conversational",
294
+ summary: "Long-conversation memory benchmark for persistent dialogue context."
295
+ }
296
+ ];
297
+ var BENCHMARK_IDS = new Set(BENCHMARK_CATALOG.map((entry) => entry.id));
298
+ function getBenchUsageText() {
299
+ return `Usage: remnic bench <list|run> [options] [benchmark...]
300
+ remnic benchmark <list|run|check|report> [options] [benchmark...]
301
+
302
+ Commands:
303
+ list List published benchmark packs
304
+ run [benchmark...] Run one or more benchmark packs
305
+ check Legacy latency regression gate (compatibility)
306
+ report Legacy latency report generator (compatibility)
307
+
308
+ Options:
309
+ --quick Run a lightweight quick pass (maps to --lightweight --limit 1)
310
+ --all Run every published benchmark
311
+ --dataset-dir <path> Override the benchmark dataset directory for full runs
312
+ --json Output JSON for \`list\`
313
+
314
+ Examples:
315
+ remnic bench list
316
+ remnic bench run --quick longmemeval
317
+ remnic bench run longmemeval --dataset-dir ~/datasets/longmemeval
318
+ remnic benchmark run --quick longmemeval`;
319
+ }
320
+ function buildBenchRunnerArgs(parsed, benchmarkId) {
321
+ const args = [EVAL_RUNNER_PATH, "--benchmark", benchmarkId];
322
+ if (parsed.quick) {
323
+ args.push("--lightweight", "--limit", "1");
324
+ }
325
+ if (parsed.datasetDir) {
326
+ args.push("--dataset-dir", parsed.datasetDir);
327
+ }
328
+ return args;
329
+ }
330
+ function coerceBenchCategory(benchmarkId, category) {
331
+ if (category === "agentic" || category === "retrieval" || category === "conversational") {
332
+ return category;
333
+ }
334
+ return BENCHMARK_CATALOG.find((entry) => entry.id === benchmarkId)?.category ?? "retrieval";
335
+ }
336
+ async function listBenchmarksFromPackage() {
337
+ const result = await loadBenchDefinitionsFromPackage();
338
+ if (!result) {
339
+ return void 0;
340
+ }
341
+ return result.map((entry) => ({
342
+ id: entry.id,
343
+ title: entry.title ?? entry.id,
344
+ category: coerceBenchCategory(entry.id, entry.meta?.category),
345
+ summary: entry.meta?.description ?? ""
346
+ }));
347
+ }
348
+ async function loadBenchDefinitionsFromPackage() {
349
+ try {
350
+ const benchModule = await import("./dist-B67STFFX.js");
351
+ if (!benchModule.listBenchmarks) return void 0;
352
+ const result = benchModule.listBenchmarks();
353
+ return Array.isArray(result) ? result : void 0;
354
+ } catch {
355
+ return void 0;
356
+ }
357
+ }
358
+ async function resolveAllBenchmarks() {
359
+ const packageBenchmarks = await loadBenchDefinitionsFromPackage();
360
+ if (packageBenchmarks) {
361
+ return packageBenchmarks.filter((entry) => entry.runnerAvailable).map((entry) => entry.id);
362
+ }
363
+ if (!fs.existsSync(EVAL_RUNNER_PATH)) {
364
+ return [];
365
+ }
366
+ return BENCHMARK_CATALOG.map((entry) => entry.id);
367
+ }
368
+ async function resolveKnownBenchmarkIds() {
369
+ const knownIds = new Set(BENCHMARK_IDS);
370
+ const packageBenchmarks = await loadBenchDefinitionsFromPackage();
371
+ if (packageBenchmarks) {
372
+ for (const benchmark of packageBenchmarks) {
373
+ knownIds.add(benchmark.id);
374
+ }
375
+ }
376
+ return knownIds;
377
+ }
378
+ async function runBenchViaFallback(parsed, benchmarkId) {
379
+ if (!fs.existsSync(EVAL_RUNNER_PATH)) {
380
+ console.error(
381
+ "Benchmark runner not found. Expected eval runner at evals/run.ts or a phase-1 @remnic/bench runtime export."
382
+ );
383
+ process.exit(1);
384
+ }
385
+ const tsxCandidates = [
386
+ path2.join(CLI_REPO_ROOT, "node_modules", ".bin", "tsx"),
387
+ path2.join(CLI_REPO_ROOT, "packages", "remnic-cli", "node_modules", ".bin", "tsx")
388
+ ];
389
+ const tsxCmd = tsxCandidates.find((candidate) => fs.existsSync(candidate)) ?? "tsx";
390
+ childProcess.execFileSync(tsxCmd, buildBenchRunnerArgs(parsed, benchmarkId), {
391
+ stdio: "inherit",
392
+ env: process.env
393
+ });
394
+ }
395
+ function resolveBenchOutputDir() {
396
+ return path2.join(resolveHomeDir(), ".remnic", "bench", "results");
397
+ }
398
+ function resolveBenchDatasetDir(benchmarkId, quick, datasetDirOverride) {
399
+ if (datasetDirOverride) {
400
+ return datasetDirOverride;
401
+ }
402
+ if (quick) {
403
+ return void 0;
404
+ }
405
+ const repoDatasetDir = path2.join(CLI_REPO_ROOT, "evals", "datasets", benchmarkId);
406
+ try {
407
+ return fs.statSync(repoDatasetDir).isDirectory() ? repoDatasetDir : void 0;
408
+ } catch {
409
+ return void 0;
410
+ }
411
+ }
412
+ function printBenchPackageSummary(result, outputPath) {
413
+ console.log(`Benchmark: ${result.meta.benchmark}`);
414
+ console.log(`Mode: ${result.meta.mode}`);
415
+ console.log(`Tasks: ${result.results.tasks.length}`);
416
+ console.log(`Mean query latency: ${result.cost.meanQueryLatencyMs.toFixed(1)}ms`);
417
+ for (const [metric, aggregate] of Object.entries(result.results.aggregates).sort()) {
418
+ console.log(` ${metric.padEnd(20)} ${aggregate.mean.toFixed(4)}`);
419
+ }
420
+ console.log(`Results saved: ${outputPath}`);
421
+ }
422
+ async function runBenchViaPackage(parsed, benchmarkId) {
423
+ let benchModule;
424
+ try {
425
+ benchModule = await import("./dist-B67STFFX.js");
426
+ } catch {
427
+ return false;
428
+ }
429
+ const definition = benchModule.getBenchmark?.(benchmarkId);
430
+ if (!definition?.runnerAvailable || !benchModule.runBenchmark || !benchModule.writeBenchmarkResult) {
431
+ return false;
432
+ }
433
+ const createAdapter = parsed.quick ? benchModule.createLightweightAdapter : benchModule.createRemnicAdapter;
434
+ if (!createAdapter) {
435
+ return false;
436
+ }
437
+ const outputDir = resolveBenchOutputDir();
438
+ const datasetDir = resolveBenchDatasetDir(
439
+ benchmarkId,
440
+ parsed.quick,
441
+ parsed.datasetDir
442
+ );
443
+ if (!parsed.quick && !datasetDir) {
444
+ throw new Error(
445
+ `full benchmark runs for "${benchmarkId}" require dataset files. Pass --dataset-dir <path> or run from a Remnic repo checkout with evals/datasets/${benchmarkId}.`
446
+ );
447
+ }
448
+ const system = await createAdapter();
449
+ try {
450
+ const result = await benchModule.runBenchmark(benchmarkId, {
451
+ mode: parsed.quick ? "quick" : "full",
452
+ datasetDir,
453
+ outputDir,
454
+ limit: parsed.quick ? 1 : void 0,
455
+ adapterMode: parsed.quick ? "lightweight" : "direct",
456
+ system
457
+ });
458
+ const writtenPath = await benchModule.writeBenchmarkResult(result, outputDir);
459
+ if (parsed.json) {
460
+ console.log(JSON.stringify(result, null, 2));
461
+ } else {
462
+ printBenchPackageSummary(result, writtenPath);
463
+ }
464
+ return true;
465
+ } finally {
466
+ await system.destroy();
467
+ }
468
+ }
322
469
  function resolveConfigPath(cliPath) {
323
470
  if (cliPath) return path2.resolve(cliPath);
324
471
  const envPath = readCompatEnv("REMNIC_CONFIG_PATH", "ENGRAM_CONFIG_PATH");
@@ -330,7 +477,7 @@ function resolveConfigPath(cliPath) {
330
477
  path2.join(resolveHomeDir(), ".config", "engram", "config.json")
331
478
  ];
332
479
  for (const candidate of candidates) {
333
- if (fs2.existsSync(candidate)) return candidate;
480
+ if (fs.existsSync(candidate)) return candidate;
334
481
  }
335
482
  return path2.join(resolveHomeDir(), ".config", "remnic", "config.json");
336
483
  }
@@ -339,24 +486,24 @@ function resolveMemoryDir() {
339
486
  const envMemoryDir = readCompatEnv("REMNIC_MEMORY_DIR", "ENGRAM_MEMORY_DIR");
340
487
  if (envMemoryDir) return envMemoryDir;
341
488
  const configPath = resolveConfigPath();
342
- const raw = fs2.existsSync(configPath) ? JSON.parse(fs2.readFileSync(configPath, "utf8")) : {};
489
+ const raw = fs.existsSync(configPath) ? JSON.parse(fs.readFileSync(configPath, "utf8")) : {};
343
490
  const remnicCfg = raw.remnic ?? raw.engram ?? raw;
344
491
  if (remnicCfg.memoryDir) return remnicCfg.memoryDir;
345
492
  const home = resolveHomeDir();
346
493
  const standalonePath = path2.join(home, ".remnic", "memory");
347
494
  const legacyStandalonePath = path2.join(home, ".engram", "memory");
348
495
  const openclawPath = path2.join(home, ".openclaw", "workspace", "memory", "local");
349
- if (fs2.existsSync(standalonePath)) return standalonePath;
350
- if (fs2.existsSync(legacyStandalonePath)) return legacyStandalonePath;
496
+ if (fs.existsSync(standalonePath)) return standalonePath;
497
+ if (fs.existsSync(legacyStandalonePath)) return legacyStandalonePath;
351
498
  return openclawPath;
352
499
  })();
353
500
  const manifestPath = getManifestPath();
354
- if (fs2.existsSync(manifestPath)) {
501
+ if (fs.existsSync(manifestPath)) {
355
502
  try {
356
503
  const active = getActiveSpace();
357
504
  if (active?.memoryDir) {
358
- if (!fs2.existsSync(active.memoryDir)) {
359
- fs2.mkdirSync(active.memoryDir, { recursive: true });
505
+ if (!fs.existsSync(active.memoryDir)) {
506
+ fs.mkdirSync(active.memoryDir, { recursive: true });
360
507
  }
361
508
  return active.memoryDir;
362
509
  }
@@ -370,23 +517,51 @@ function resolveMemoryDir() {
370
517
  }
371
518
  return configMemoryDir;
372
519
  }
373
- function resolveFlag(args, flag) {
520
+ function resolveFlagStrict(args, flag) {
374
521
  const idx = args.indexOf(flag);
375
- return idx !== -1 && idx + 1 < args.length ? args[idx + 1] : void 0;
522
+ if (idx === -1 || idx + 1 >= args.length) return void 0;
523
+ const next = args[idx + 1];
524
+ return next.startsWith("-") ? void 0 : next;
376
525
  }
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
- }
526
+ var REMNIC_OPENCLAW_PLUGIN_ID = "openclaw-remnic";
527
+ var REMNIC_OPENCLAW_LEGACY_PLUGIN_ID = "openclaw-engram";
528
+ var DEFAULT_OPENCLAW_CONFIG_PATHS_FOR_DOCTOR = [
529
+ process.env.OPENCLAW_CONFIG_PATH,
530
+ process.env.OPENCLAW_ENGRAM_CONFIG_PATH,
531
+ path2.join(resolveHomeDir(), ".openclaw", "openclaw.json")
532
+ ].filter(Boolean);
533
+ function resolveOpenclawConfigPath(cliPath) {
534
+ if (cliPath) return path2.resolve(expandTilde(cliPath));
535
+ const envPath = process.env.OPENCLAW_CONFIG_PATH || process.env.OPENCLAW_ENGRAM_CONFIG_PATH;
536
+ if (envPath) return path2.resolve(expandTilde(envPath));
537
+ for (const candidate of DEFAULT_OPENCLAW_CONFIG_PATHS_FOR_DOCTOR) {
538
+ if (fs.existsSync(candidate)) return candidate;
384
539
  }
385
- return config;
540
+ return path2.join(resolveHomeDir(), ".openclaw", "openclaw.json");
541
+ }
542
+ function readOpenclawConfig(configPath) {
543
+ if (!fs.existsSync(configPath)) return {};
544
+ const raw = fs.readFileSync(configPath, "utf-8");
545
+ let parsed;
546
+ try {
547
+ parsed = JSON.parse(raw);
548
+ } catch (err) {
549
+ throw new Error(
550
+ `OpenClaw config at ${configPath} contains invalid JSON \u2014 refusing to overwrite.
551
+ Fix the file manually, then re-run.
552
+ Parse error: ${err.message}`
553
+ );
554
+ }
555
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
556
+ throw new Error(
557
+ `OpenClaw config at ${configPath} is not a JSON object (got ${Array.isArray(parsed) ? "array" : typeof parsed}) \u2014 refusing to overwrite.`
558
+ );
559
+ }
560
+ return parsed;
386
561
  }
387
562
  function cmdInit() {
388
563
  const configPath = path2.join(process.cwd(), "remnic.config.json");
389
- if (fs2.existsSync(configPath)) {
564
+ if (fs.existsSync(configPath)) {
390
565
  console.log(`Config already exists: ${configPath}`);
391
566
  return;
392
567
  }
@@ -402,7 +577,7 @@ function cmdInit() {
402
577
  authToken: "${REMNIC_AUTH_TOKEN}"
403
578
  }
404
579
  };
405
- fs2.writeFileSync(configPath, JSON.stringify(template, null, 2) + "\n");
580
+ fs.writeFileSync(configPath, JSON.stringify(template, null, 2) + "\n");
406
581
  console.log(`Created ${configPath}`);
407
582
  console.log("\nSet these environment variables:");
408
583
  console.log(" export OPENAI_API_KEY=sk-...");
@@ -448,7 +623,7 @@ async function cmdQuery(queryText, json, explain) {
448
623
  }
449
624
  initLogger();
450
625
  const configPath = resolveConfigPath();
451
- const raw = fs2.existsSync(configPath) ? JSON.parse(fs2.readFileSync(configPath, "utf8")) : {};
626
+ const raw = fs.existsSync(configPath) ? JSON.parse(fs.readFileSync(configPath, "utf8")) : {};
452
627
  const remnicCfg = raw.remnic ?? raw.engram ?? raw;
453
628
  const config = parseConfig(remnicCfg);
454
629
  const orchestrator = new Orchestrator(config);
@@ -482,6 +657,503 @@ async function cmdQuery(queryText, json, explain) {
482
657
  }
483
658
  }
484
659
  }
660
+ async function cmdVersions(rest) {
661
+ initLogger();
662
+ const configPath = resolveConfigPath();
663
+ const raw = fs.existsSync(configPath) ? JSON.parse(fs.readFileSync(configPath, "utf8")) : {};
664
+ const remnicCfg = raw.remnic ?? raw.engram ?? raw;
665
+ const config = parseConfig(remnicCfg);
666
+ if (!config.versioningEnabled) {
667
+ console.error("Page versioning is disabled (versioningEnabled = false).");
668
+ process.exit(1);
669
+ }
670
+ const versioningConfig = {
671
+ enabled: config.versioningEnabled,
672
+ maxVersionsPerPage: config.versioningMaxPerPage,
673
+ sidecarDir: config.versioningSidecarDir
674
+ };
675
+ const memDir = resolveMemoryDir();
676
+ const action = rest[0] ?? "help";
677
+ const json = rest.includes("--json");
678
+ switch (action) {
679
+ case "list": {
680
+ const pagePath = rest[1];
681
+ if (!pagePath) {
682
+ console.error("Usage: remnic versions list <page-path>");
683
+ process.exit(1);
684
+ }
685
+ const absPath = path2.resolve(pagePath);
686
+ const history = await listVersions(absPath, versioningConfig, memDir);
687
+ if (json) {
688
+ console.log(JSON.stringify(history, null, 2));
689
+ } else {
690
+ if (history.versions.length === 0) {
691
+ console.log(`No versions found for ${pagePath}`);
692
+ } else {
693
+ console.log(`Versions for ${pagePath} (current: v${history.currentVersion}):
694
+ `);
695
+ for (const v of history.versions) {
696
+ const note = v.note ? ` \u2014 ${v.note}` : "";
697
+ console.log(` v${v.versionId} ${v.timestamp} ${v.trigger} ${v.sizeBytes} bytes${note}`);
698
+ }
699
+ }
700
+ }
701
+ break;
702
+ }
703
+ case "show": {
704
+ const pagePath = rest[1];
705
+ const versionId = rest[2];
706
+ if (!pagePath || !versionId) {
707
+ console.error("Usage: remnic versions show <page-path> <version-id>");
708
+ process.exit(1);
709
+ }
710
+ const absPath = path2.resolve(pagePath);
711
+ try {
712
+ const content = await getVersion(absPath, versionId, versioningConfig, memDir);
713
+ console.log(content);
714
+ } catch (err) {
715
+ console.error(err instanceof Error ? err.message : String(err));
716
+ process.exit(1);
717
+ }
718
+ break;
719
+ }
720
+ case "diff": {
721
+ const pagePath = rest[1];
722
+ const v1 = rest[2];
723
+ const v2 = rest[3];
724
+ if (!pagePath || !v1 || !v2) {
725
+ console.error("Usage: remnic versions diff <page-path> <v1> <v2>");
726
+ process.exit(1);
727
+ }
728
+ const absPath = path2.resolve(pagePath);
729
+ try {
730
+ const diffOutput = await diffVersions(absPath, v1, v2, versioningConfig, memDir);
731
+ console.log(diffOutput);
732
+ } catch (err) {
733
+ console.error(err instanceof Error ? err.message : String(err));
734
+ process.exit(1);
735
+ }
736
+ break;
737
+ }
738
+ case "revert": {
739
+ const pagePath = rest[1];
740
+ const versionId = rest[2];
741
+ if (!pagePath || !versionId) {
742
+ console.error("Usage: remnic versions revert <page-path> <version-id>");
743
+ process.exit(1);
744
+ }
745
+ const absPath = path2.resolve(pagePath);
746
+ try {
747
+ const version = await revertToVersion(absPath, versionId, versioningConfig, void 0, memDir);
748
+ if (json) {
749
+ console.log(JSON.stringify(version, null, 2));
750
+ } else {
751
+ console.log(`Reverted ${pagePath} to version ${versionId}.`);
752
+ console.log(`Created snapshot v${version.versionId} of previous content.`);
753
+ }
754
+ } catch (err) {
755
+ console.error(err instanceof Error ? err.message : String(err));
756
+ process.exit(1);
757
+ }
758
+ break;
759
+ }
760
+ default:
761
+ console.log(`
762
+ remnic versions \u2014 Page-level versioning
763
+
764
+ Usage:
765
+ remnic versions list <page-path> List all versions of a page
766
+ remnic versions show <page-path> <id> Print content of a specific version
767
+ remnic versions diff <page-path> <v1> <v2> Show diff between two versions
768
+ remnic versions revert <page-path> <id> Revert page to a specific version
769
+
770
+ Options:
771
+ --json Output in JSON format
772
+ `);
773
+ break;
774
+ }
775
+ }
776
+ async function cmdEnrich(rest) {
777
+ initLogger();
778
+ const configPath = resolveConfigPath();
779
+ const raw = fs.existsSync(configPath) ? JSON.parse(fs.readFileSync(configPath, "utf8")) : {};
780
+ const remnicCfg = raw.remnic ?? raw.engram ?? raw;
781
+ const config = parseConfig(remnicCfg);
782
+ const subcommand = rest[0];
783
+ if (subcommand === "audit") {
784
+ const memoryDir2 = expandTilde(config.memoryDir);
785
+ const auditDir2 = path2.join(memoryDir2, "enrichment");
786
+ const sinceFlag = resolveFlag(rest.slice(1), "--since");
787
+ const entries = await readAuditLog(auditDir2, sinceFlag ?? void 0);
788
+ if (entries.length === 0) {
789
+ console.log("No enrichment audit entries found.");
790
+ return;
791
+ }
792
+ for (const entry of entries) {
793
+ const status = entry.accepted ? "ACCEPTED" : "REJECTED";
794
+ const url = entry.sourceUrl ? ` (${entry.sourceUrl})` : "";
795
+ console.log(
796
+ `[${entry.timestamp}] ${status} ${entry.entityName} via ${entry.provider}: ${entry.candidateText}${url}`
797
+ );
798
+ }
799
+ return;
800
+ }
801
+ if (subcommand === "providers") {
802
+ const pipelineConfig2 = defaultEnrichmentPipelineConfig();
803
+ pipelineConfig2.enabled = config.enrichmentEnabled;
804
+ pipelineConfig2.maxCandidatesPerEntity = config.enrichmentMaxCandidatesPerEntity;
805
+ pipelineConfig2.autoEnrichOnCreate = config.enrichmentAutoOnCreate;
806
+ pipelineConfig2.providers = [
807
+ { id: "web-search", enabled: true, costTier: "cheap" }
808
+ ];
809
+ const orchestrator2 = new Orchestrator(config);
810
+ await orchestrator2.initialize();
811
+ const searchBackend2 = orchestrator2.qmd;
812
+ const searchFn2 = searchBackend2.isAvailable() ? async (query) => {
813
+ const results2 = await searchBackend2.search(query, void 0, 10);
814
+ return results2.map((r) => r.snippet);
815
+ } : void 0;
816
+ const registry2 = new EnrichmentProviderRegistry();
817
+ registry2.register(new WebSearchProvider({ searchFn: searchFn2 }));
818
+ const allEnabled = registry2.listEnabled(pipelineConfig2);
819
+ console.log(`Pipeline enabled: ${pipelineConfig2.enabled}`);
820
+ console.log(`Auto-enrich on create: ${pipelineConfig2.autoEnrichOnCreate}`);
821
+ console.log(`Max candidates per entity: ${pipelineConfig2.maxCandidatesPerEntity}`);
822
+ console.log(`
823
+ Registered providers:`);
824
+ const webSearch = registry2.get("web-search");
825
+ if (webSearch) {
826
+ const available = await webSearch.isAvailable();
827
+ console.log(` - web-search (${webSearch.costTier}) \u2014 ${available ? "available" : "unavailable (no searchFn configured)"}`);
828
+ }
829
+ if (allEnabled.length === 0) {
830
+ console.log("\n No providers are currently enabled in config.");
831
+ }
832
+ return;
833
+ }
834
+ if (!config.enrichmentEnabled) {
835
+ console.error("Enrichment pipeline is disabled (enrichmentEnabled = false).");
836
+ process.exit(1);
837
+ }
838
+ const dryRun = rest.includes("--dry-run");
839
+ const all = rest.includes("--all");
840
+ if (!all && (!subcommand || subcommand.startsWith("--"))) {
841
+ console.error("Usage: remnic enrich <entity-name> | --all | --dry-run | audit | providers");
842
+ process.exit(1);
843
+ }
844
+ const orchestrator = new Orchestrator(config);
845
+ await orchestrator.initialize();
846
+ const storage = await orchestrator.getStorage(config.defaultNamespace);
847
+ const entityFiles = await storage.readAllEntityFiles();
848
+ let targets = entityFiles;
849
+ if (!all && subcommand && !subcommand.startsWith("--")) {
850
+ const match = entityFiles.find(
851
+ (e) => e.name.toLowerCase() === subcommand.toLowerCase()
852
+ );
853
+ if (!match) {
854
+ console.error(`Entity not found: ${subcommand}`);
855
+ process.exit(1);
856
+ }
857
+ targets = [match];
858
+ }
859
+ if (targets.length === 0) {
860
+ console.log("No entities to enrich.");
861
+ return;
862
+ }
863
+ const pipelineConfig = defaultEnrichmentPipelineConfig();
864
+ pipelineConfig.enabled = true;
865
+ pipelineConfig.maxCandidatesPerEntity = config.enrichmentMaxCandidatesPerEntity;
866
+ pipelineConfig.providers = [
867
+ { id: "web-search", enabled: true, costTier: "cheap" }
868
+ ];
869
+ pipelineConfig.importanceThresholds = {
870
+ critical: ["web-search"],
871
+ high: ["web-search"],
872
+ normal: ["web-search"],
873
+ low: []
874
+ };
875
+ const searchBackend = orchestrator.qmd;
876
+ const searchFn = searchBackend.isAvailable() ? async (query) => {
877
+ const results2 = await searchBackend.search(query, void 0, 10);
878
+ return results2.map((r) => r.snippet);
879
+ } : void 0;
880
+ const registry = new EnrichmentProviderRegistry();
881
+ registry.register(new WebSearchProvider({ searchFn }));
882
+ const inputs = targets.map((ef) => ({
883
+ name: ef.name,
884
+ type: ef.type,
885
+ knownFacts: ef.facts,
886
+ importanceLevel: "normal"
887
+ }));
888
+ if (dryRun) {
889
+ console.log(`Dry run: would enrich ${inputs.length} entity(ies):`);
890
+ for (const input of inputs) {
891
+ const providers = registry.getForImportance(input.importanceLevel, pipelineConfig);
892
+ console.log(` - ${input.name} (${input.type}) \u2014 ${providers.length} provider(s)`);
893
+ }
894
+ return;
895
+ }
896
+ console.log(`Enriching ${inputs.length} entity(ies)...`);
897
+ const noopLog = { info() {
898
+ }, warn() {
899
+ }, error() {
900
+ }, debug() {
901
+ } };
902
+ const results = await runEnrichmentPipeline(inputs, registry, pipelineConfig, noopLog);
903
+ if (results.length === 0) {
904
+ console.log("No enrichment results (no providers matched).");
905
+ return;
906
+ }
907
+ const memoryDir = expandTilde(config.memoryDir);
908
+ const auditDir = path2.join(memoryDir, "enrichment");
909
+ let totalPersisted = 0;
910
+ for (const result of results) {
911
+ for (const candidate of result.acceptedCandidates) {
912
+ let persisted = false;
913
+ try {
914
+ await storage.writeMemory(candidate.category, candidate.text, {
915
+ confidence: candidate.confidence,
916
+ tags: [...candidate.tags ?? [], "enrichment", candidate.source],
917
+ entityRef: result.entityName,
918
+ source: `enrichment:${candidate.source}`
919
+ });
920
+ persisted = true;
921
+ totalPersisted++;
922
+ } catch (err) {
923
+ console.error(
924
+ ` Failed to persist candidate for ${result.entityName}: ${err instanceof Error ? err.message : String(err)}`
925
+ );
926
+ try {
927
+ await appendAuditEntry(auditDir, {
928
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
929
+ entityName: result.entityName,
930
+ provider: result.provider,
931
+ candidateText: candidate.text,
932
+ sourceUrl: candidate.sourceUrl,
933
+ accepted: false,
934
+ reason: `persist failed: ${err instanceof Error ? err.message : String(err)}`
935
+ });
936
+ } catch {
937
+ }
938
+ }
939
+ if (persisted) {
940
+ try {
941
+ await appendAuditEntry(auditDir, {
942
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
943
+ entityName: result.entityName,
944
+ provider: result.provider,
945
+ candidateText: candidate.text,
946
+ sourceUrl: candidate.sourceUrl,
947
+ accepted: true
948
+ });
949
+ } catch (auditErr) {
950
+ console.warn(
951
+ ` Warning: audit write failed for ${result.entityName} (memory was persisted): ${auditErr instanceof Error ? auditErr.message : String(auditErr)}`
952
+ );
953
+ }
954
+ }
955
+ }
956
+ }
957
+ if (totalPersisted > 0 && searchBackend.isAvailable()) {
958
+ try {
959
+ await searchBackend.update();
960
+ } catch {
961
+ }
962
+ }
963
+ for (const result of results) {
964
+ console.log(
965
+ ` ${result.entityName} via ${result.provider}: ${result.candidatesAccepted} accepted, ${result.candidatesRejected} rejected (${result.elapsed}ms)`
966
+ );
967
+ }
968
+ if (totalPersisted > 0) {
969
+ console.log(`
970
+ ${totalPersisted} candidate(s) persisted to memory store.`);
971
+ }
972
+ }
973
+ async function cmdExtensions(action, rest) {
974
+ initLogger();
975
+ const configPath = resolveConfigPath();
976
+ const raw = fs.existsSync(configPath) ? JSON.parse(fs.readFileSync(configPath, "utf8")) : {};
977
+ const remnicCfg = raw.remnic ?? raw.engram ?? raw;
978
+ const config = parseConfig(remnicCfg);
979
+ const root = resolveExtensionsRoot(config);
980
+ const noopLog = { warn: () => {
981
+ }, debug: () => {
982
+ } };
983
+ const warnLog = {
984
+ warn: (msg) => console.warn(msg),
985
+ debug: () => {
986
+ }
987
+ };
988
+ switch (action) {
989
+ case "list": {
990
+ const extensions = await discoverMemoryExtensions(root, noopLog);
991
+ if (extensions.length === 0) {
992
+ console.log("No memory extensions found.");
993
+ console.log(` Scanned: ${root}`);
994
+ return;
995
+ }
996
+ console.log(`Memory extensions (${extensions.length}):`);
997
+ for (const ext of extensions) {
998
+ const schemaInfo = ext.schema?.version ? ` v${ext.schema.version}` : "";
999
+ const types = ext.schema?.memoryTypes?.join(", ") ?? "any";
1000
+ console.log(` ${ext.name}${schemaInfo} (types: ${types})`);
1001
+ }
1002
+ console.log(`
1003
+ Root: ${root}`);
1004
+ break;
1005
+ }
1006
+ case "show": {
1007
+ const name = rest[0];
1008
+ if (!name) {
1009
+ console.error("Usage: remnic extensions show <name>");
1010
+ process.exitCode = 1;
1011
+ return;
1012
+ }
1013
+ const extensions = await discoverMemoryExtensions(root, noopLog);
1014
+ const ext = extensions.find((e) => e.name === name);
1015
+ if (!ext) {
1016
+ console.error(`Extension "${name}" not found in ${root}`);
1017
+ process.exitCode = 1;
1018
+ return;
1019
+ }
1020
+ console.log(ext.instructions);
1021
+ break;
1022
+ }
1023
+ case "validate": {
1024
+ const extensions = await discoverMemoryExtensions(root, warnLog);
1025
+ let entries = [];
1026
+ try {
1027
+ entries = fs.readdirSync(root);
1028
+ } catch {
1029
+ console.log(`Extensions root does not exist: ${root}`);
1030
+ process.exitCode = 0;
1031
+ return;
1032
+ }
1033
+ const validNames = new Set(extensions.map((e) => e.name));
1034
+ let errors = 0;
1035
+ for (const entry of entries) {
1036
+ const entryPath = path2.join(root, entry);
1037
+ try {
1038
+ if (!fs.statSync(entryPath).isDirectory()) continue;
1039
+ } catch {
1040
+ continue;
1041
+ }
1042
+ if (!validNames.has(entry)) {
1043
+ errors++;
1044
+ }
1045
+ }
1046
+ console.log(`Validated: ${extensions.length} valid, ${errors} skipped`);
1047
+ if (errors > 0) {
1048
+ process.exitCode = 1;
1049
+ }
1050
+ break;
1051
+ }
1052
+ case "reload": {
1053
+ console.log("Extension cache reloaded (no-op: caching not yet implemented).");
1054
+ break;
1055
+ }
1056
+ default:
1057
+ console.log(`Usage: remnic extensions <list|show|validate|reload>
1058
+
1059
+ list List discovered extensions
1060
+ show <name> Print instructions.md content
1061
+ validate Validate all extensions, exit non-zero on errors
1062
+ reload Reserved for future caching (no-op)
1063
+ `);
1064
+ break;
1065
+ }
1066
+ }
1067
+ async function cmdBriefing(rest) {
1068
+ initLogger();
1069
+ const configPath = resolveConfigPath();
1070
+ const raw = fs.existsSync(configPath) ? JSON.parse(fs.readFileSync(configPath, "utf8")) : {};
1071
+ const remnicCfg = raw.remnic ?? raw.engram ?? raw;
1072
+ const config = parseConfig(remnicCfg);
1073
+ if (!config.briefing.enabled) {
1074
+ console.error("Briefing is disabled in config (briefing.enabled = false).");
1075
+ process.exit(1);
1076
+ }
1077
+ const sinceFlag = resolveFlag(rest, "--since");
1078
+ const focusFlag = resolveFlag(rest, "--focus");
1079
+ const formatFlag = resolveFlag(rest, "--format");
1080
+ const save = rest.includes("--save") || config.briefing.saveByDefault;
1081
+ if (hasFlag(rest, "--since") && sinceFlag === void 0) {
1082
+ console.error("Missing value for --since. Accepted: yesterday, today, NNh, NNd, NNw.");
1083
+ process.exit(1);
1084
+ }
1085
+ if (hasFlag(rest, "--format") && formatFlag === void 0) {
1086
+ console.error("Missing value for --format. Accepted: markdown, json.");
1087
+ process.exit(1);
1088
+ }
1089
+ if (hasFlag(rest, "--focus") && (focusFlag === void 0 || focusFlag.startsWith("--"))) {
1090
+ console.error(
1091
+ "Missing value for --focus. Expected: project:<id>, topic:<name>, or person:<id>."
1092
+ );
1093
+ process.exit(1);
1094
+ }
1095
+ const token = sinceFlag ?? config.briefing.defaultWindow;
1096
+ const window = parseBriefingWindow(token);
1097
+ if (!window) {
1098
+ console.error(
1099
+ `Invalid --since value: ${token}. Accepted: yesterday, today, NNh, NNd, NNw.`
1100
+ );
1101
+ process.exit(1);
1102
+ }
1103
+ const rawFocus = typeof focusFlag === "string" ? focusFlag.trim() : "";
1104
+ const focus = rawFocus.length > 0 ? parseBriefingFocus(rawFocus) : null;
1105
+ if (rawFocus.length > 0 && !focus) {
1106
+ console.error(
1107
+ `Invalid --focus value: expected project:<id>, topic:<name>, or person:<id>, got: ${focusFlag}`
1108
+ );
1109
+ process.exit(1);
1110
+ }
1111
+ const jsonFlag = rest.includes("--json");
1112
+ if (jsonFlag && formatFlag !== void 0 && formatFlag !== "json") {
1113
+ console.error(
1114
+ `Conflicting flags: --json and --format ${formatFlag}. Use one or the other.`
1115
+ );
1116
+ process.exit(1);
1117
+ }
1118
+ const effectiveFormatFlag = jsonFlag ? "json" : formatFlag;
1119
+ const formatError = validateBriefingFormat(effectiveFormatFlag);
1120
+ if (formatError) {
1121
+ console.error(formatError);
1122
+ process.exit(1);
1123
+ }
1124
+ const format = effectiveFormatFlag === "json" ? "json" : effectiveFormatFlag === "markdown" ? "markdown" : config.briefing.defaultFormat;
1125
+ const orchestrator = new Orchestrator(config);
1126
+ await orchestrator.initialize();
1127
+ const storage = await orchestrator.getStorage(config.defaultNamespace);
1128
+ const calendarSource = config.briefing.calendarSource ? new FileCalendarSource(config.briefing.calendarSource) : void 0;
1129
+ const result = await buildBriefing({
1130
+ storage,
1131
+ window,
1132
+ focus,
1133
+ namespace: config.defaultNamespace,
1134
+ calendarSource,
1135
+ maxFollowups: config.briefing.maxFollowups,
1136
+ allowLlm: config.briefing.llmFollowups,
1137
+ openaiApiKey: config.openaiApiKey,
1138
+ openaiBaseUrl: config.openaiBaseUrl,
1139
+ model: config.model
1140
+ });
1141
+ const payload = format === "json" ? JSON.stringify(result.json, null, 2) : result.markdown;
1142
+ console.log(payload);
1143
+ if (save) {
1144
+ try {
1145
+ const saveDir = resolveBriefingSaveDir(config.briefing.saveDir);
1146
+ fs.mkdirSync(saveDir, { recursive: true });
1147
+ const filename = briefingFilename(new Date(result.window.to), format);
1148
+ const filePath = path2.join(saveDir, filename);
1149
+ fs.writeFileSync(filePath, payload + (payload.endsWith("\n") ? "" : "\n"));
1150
+ console.error(`Saved briefing: ${filePath}`);
1151
+ } catch (err) {
1152
+ console.error(`Failed to save briefing: ${err instanceof Error ? err.message : String(err)}`);
1153
+ process.exit(1);
1154
+ }
1155
+ }
1156
+ }
485
1157
  function cmdDoctor() {
486
1158
  const checks = [];
487
1159
  const nodeVersion = process.version;
@@ -492,7 +1164,7 @@ function cmdDoctor() {
492
1164
  detail: `${nodeVersion} (requires >= 22.12.0)`
493
1165
  });
494
1166
  const configPath = resolveConfigPath();
495
- const configExists = fs2.existsSync(configPath);
1167
+ const configExists = fs.existsSync(configPath);
496
1168
  checks.push({ name: "Config file", ok: configExists, detail: configPath });
497
1169
  const hasApiKey = !!process.env.OPENAI_API_KEY;
498
1170
  checks.push({
@@ -502,7 +1174,7 @@ function cmdDoctor() {
502
1174
  });
503
1175
  const memoryDir = resolveMemoryDir();
504
1176
  try {
505
- fs2.mkdirSync(memoryDir, { recursive: true });
1177
+ fs.mkdirSync(memoryDir, { recursive: true });
506
1178
  checks.push({ name: "Memory directory", ok: true, detail: memoryDir });
507
1179
  } catch {
508
1180
  checks.push({ name: "Memory directory", ok: false, detail: `cannot create ${memoryDir}` });
@@ -513,19 +1185,128 @@ function cmdDoctor() {
513
1185
  ok: svcState.running,
514
1186
  detail: svcState.running ? `running${svcState.pid ? ` (pid ${svcState.pid})` : ""}` : "stopped"
515
1187
  });
1188
+ const openclawConfigPath = resolveOpenclawConfigPath();
1189
+ const openclawConfigExists = fs.existsSync(openclawConfigPath);
1190
+ let openclawConfig = {};
1191
+ let openclawConfigValid = false;
1192
+ if (openclawConfigExists) {
1193
+ try {
1194
+ const parsed = JSON.parse(fs.readFileSync(openclawConfigPath, "utf-8"));
1195
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
1196
+ openclawConfig = parsed;
1197
+ openclawConfigValid = true;
1198
+ } else {
1199
+ openclawConfigValid = false;
1200
+ }
1201
+ } catch {
1202
+ openclawConfigValid = false;
1203
+ }
1204
+ }
1205
+ checks.push({
1206
+ name: "OpenClaw config file",
1207
+ ok: openclawConfigExists && openclawConfigValid,
1208
+ warn: openclawConfigExists && !openclawConfigValid,
1209
+ detail: openclawConfigExists ? openclawConfigValid ? openclawConfigPath : `${openclawConfigPath} (invalid JSON)` : `${openclawConfigPath} (not found)`,
1210
+ 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
1211
+ });
1212
+ if (openclawConfigValid) {
1213
+ const rawPlugins = openclawConfig.plugins;
1214
+ const pluginsIsObject = rawPlugins && typeof rawPlugins === "object" && !Array.isArray(rawPlugins);
1215
+ if (!pluginsIsObject && rawPlugins !== void 0) {
1216
+ checks.push({
1217
+ name: "OpenClaw plugins",
1218
+ ok: false,
1219
+ detail: `plugins is ${typeof rawPlugins}, expected object`,
1220
+ remediation: "Run `remnic openclaw install` to recreate the plugins section."
1221
+ });
1222
+ }
1223
+ const plugins = pluginsIsObject ? rawPlugins : {};
1224
+ const entries = plugins.entries && typeof plugins.entries === "object" && !Array.isArray(plugins.entries) ? plugins.entries : null;
1225
+ const slots = plugins.slots && typeof plugins.slots === "object" && !Array.isArray(plugins.slots) ? plugins.slots : null;
1226
+ const entriesIsArray = Array.isArray(plugins.entries);
1227
+ checks.push({
1228
+ name: "OpenClaw plugins.entries",
1229
+ ok: !!entries,
1230
+ detail: entries ? "present" : entriesIsArray ? "invalid (array)" : "missing",
1231
+ remediation: !entries ? "Run `remnic openclaw install` to add the Remnic plugin entry." : void 0
1232
+ });
1233
+ if (entries) {
1234
+ const isValidEntry = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
1235
+ const hasNew = REMNIC_OPENCLAW_PLUGIN_ID in entries && isValidEntry(entries[REMNIC_OPENCLAW_PLUGIN_ID]);
1236
+ const hasLegacy = REMNIC_OPENCLAW_LEGACY_PLUGIN_ID in entries && isValidEntry(entries[REMNIC_OPENCLAW_LEGACY_PLUGIN_ID]);
1237
+ const keyExistsButMalformed = REMNIC_OPENCLAW_PLUGIN_ID in entries && !hasNew || REMNIC_OPENCLAW_LEGACY_PLUGIN_ID in entries && !hasLegacy;
1238
+ checks.push({
1239
+ name: "OpenClaw plugin entry",
1240
+ ok: hasNew,
1241
+ warn: !hasNew && hasLegacy || keyExistsButMalformed,
1242
+ 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",
1243
+ 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
1244
+ });
1245
+ const slotValue = slots?.memory;
1246
+ const validEntryIds = Object.keys(entries);
1247
+ const slotMissing = !slotValue;
1248
+ const slotMismatch = !slotMissing && !validEntryIds.includes(slotValue);
1249
+ const slotMatchesEntry = !slotMissing && !slotMismatch;
1250
+ const slotIsLegacy = slotMatchesEntry && slotValue === REMNIC_OPENCLAW_LEGACY_PLUGIN_ID;
1251
+ const slotIsPreferred = slotMatchesEntry && slotValue === REMNIC_OPENCLAW_PLUGIN_ID;
1252
+ checks.push({
1253
+ name: "OpenClaw plugins.slots.memory",
1254
+ ok: slotMatchesEntry,
1255
+ warn: slotMatchesEntry && !slotIsPreferred,
1256
+ detail: slotMissing ? "(unset)" : slotMismatch ? `"${slotValue}" (not found in entries: ${validEntryIds.join(", ")})` : `"${slotValue}"`,
1257
+ 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
1258
+ });
1259
+ const activeSlotEntry = slotValue ? entries[slotValue] : void 0;
1260
+ const entryToCheck = activeSlotEntry ?? entries[REMNIC_OPENCLAW_PLUGIN_ID] ?? entries[REMNIC_OPENCLAW_LEGACY_PLUGIN_ID];
1261
+ const entryConfig = entryToCheck?.config && typeof entryToCheck.config === "object" ? entryToCheck.config : null;
1262
+ const rawMemoryDir = entryConfig?.memoryDir;
1263
+ const configuredMemoryDir = typeof rawMemoryDir === "string" ? rawMemoryDir : void 0;
1264
+ if (configuredMemoryDir) {
1265
+ const resolvedMemDir = path2.resolve(expandTilde(configuredMemoryDir));
1266
+ let memDirOk = false;
1267
+ let memDirDetail = `${resolvedMemDir} (not found)`;
1268
+ let memDirRemediation = `Run \`remnic openclaw install --memory-dir "${resolvedMemDir}"\` to create the directory.`;
1269
+ if (fs.existsSync(resolvedMemDir)) {
1270
+ try {
1271
+ const stat = fs.statSync(resolvedMemDir);
1272
+ if (stat.isDirectory()) {
1273
+ memDirOk = true;
1274
+ memDirDetail = resolvedMemDir;
1275
+ memDirRemediation = void 0;
1276
+ } else {
1277
+ memDirDetail = `${resolvedMemDir} (exists but is not a directory)`;
1278
+ memDirRemediation = `Remove the file at ${resolvedMemDir} and run \`remnic openclaw install --memory-dir "${resolvedMemDir}"\` to create it as a directory.`;
1279
+ }
1280
+ } catch {
1281
+ memDirDetail = `${resolvedMemDir} (cannot stat)`;
1282
+ }
1283
+ }
1284
+ checks.push({
1285
+ name: "OpenClaw memoryDir",
1286
+ ok: memDirOk,
1287
+ warn: !memDirOk,
1288
+ detail: memDirDetail,
1289
+ remediation: memDirRemediation
1290
+ });
1291
+ }
1292
+ }
1293
+ }
516
1294
  for (const check of checks) {
517
- const icon = check.ok ? "\u2713" : "\u2717";
1295
+ const icon = check.ok ? check.warn ? "\u26A0" : "\u2713" : check.warn ? "\u26A0" : "\u2717";
518
1296
  console.log(` ${icon} ${check.name}: ${check.detail}`);
1297
+ if ((!check.ok || check.warn) && check.remediation) {
1298
+ console.log(` \u2192 ${check.remediation}`);
1299
+ }
519
1300
  }
520
1301
  }
521
1302
  function cmdConfig() {
522
1303
  const configPath = resolveConfigPath();
523
- if (!fs2.existsSync(configPath)) {
1304
+ if (!fs.existsSync(configPath)) {
524
1305
  console.log("No config file found. Run `remnic init` to create one.");
525
1306
  return;
526
1307
  }
527
1308
  console.log(`Config: ${configPath}`);
528
- const rawConfig = fs2.readFileSync(configPath, "utf8");
1309
+ const rawConfig = fs.readFileSync(configPath, "utf8");
529
1310
  const redacted = rawConfig.replace(
530
1311
  /("(?:openaiApiKey|localLlmApiKey|authToken|apiKey|remoteSearchApiKey|meilisearchApiKey|opikApiKey)"\s*:\s*")([^"]*)(")/g,
531
1312
  "$1[REDACTED]$3"
@@ -689,7 +1470,8 @@ function cmdDedup(json) {
689
1470
  console.log(`Duration: ${result.durationMs}ms`);
690
1471
  }
691
1472
  async function cmdConnectors(action, rest, json) {
692
- const nonFlagArgs = rest.filter((a) => !a.startsWith("--"));
1473
+ const strippedRest = stripConfigArgv(rest);
1474
+ const nonFlagArgs = strippedRest.filter((a) => !a.startsWith("--"));
693
1475
  const connectorId = nonFlagArgs[0];
694
1476
  if (action === "list") {
695
1477
  const { installed, available } = listConnectors();
@@ -707,40 +1489,211 @@ async function cmdConnectors(action, rest, json) {
707
1489
  console.error("Usage: remnic connectors install <id>");
708
1490
  process.exit(1);
709
1491
  }
1492
+ const connectorConfig = parseConnectorConfig(rest);
710
1493
  const result = installConnector({
711
1494
  connectorId,
712
- config: parseConnectorConfig(rest),
1495
+ config: connectorConfig,
713
1496
  force: rest.includes("--force")
714
1497
  });
1498
+ if (result.status === "error") {
1499
+ console.error(result.message);
1500
+ process.exit(1);
1501
+ }
715
1502
  console.log(result.message);
716
1503
  if (result.configPath) console.log(` Config: ${result.configPath}`);
717
1504
  if (result.status === "already_installed") console.log("Use --force to reinstall.");
718
1505
  if (result.status === "config_required") console.log("Set config with --config <key>=<value>");
719
- if (result.status === "error") console.error(`Error: ${result.message}`);
1506
+ if (result.status === "installed") {
1507
+ const pub = publisherForConnector(connectorId);
1508
+ if (pub) {
1509
+ try {
1510
+ const available = await pub.isHostAvailable();
1511
+ if (available) {
1512
+ const memoryDir = resolveMemoryDir();
1513
+ const connectorNamespace = typeof connectorConfig?.namespace === "string" && connectorConfig.namespace.length > 0 ? connectorConfig.namespace : void 0;
1514
+ const pubResult = await pub.publish({
1515
+ config: { memoryDir, namespace: connectorNamespace },
1516
+ skillsRoot: path2.join(memoryDir, "skills"),
1517
+ log: { info: console.log, warn: console.warn, error: console.error }
1518
+ });
1519
+ if (pubResult.filesWritten.length > 0) {
1520
+ console.log(` Published memory extension to ${pubResult.extensionRoot}`);
1521
+ }
1522
+ }
1523
+ } catch (err) {
1524
+ const msg = err instanceof Error ? err.message : String(err);
1525
+ console.warn(` Warning: memory extension publish failed: ${msg}`);
1526
+ }
1527
+ }
1528
+ }
720
1529
  } else if (action === "remove") {
721
1530
  if (!connectorId) {
722
1531
  console.error("Usage: remnic connectors remove <id>");
723
1532
  process.exit(1);
724
1533
  }
725
1534
  const result = removeConnector(connectorId);
1535
+ if (result.status === "error") {
1536
+ console.error(result.message);
1537
+ process.exit(1);
1538
+ }
726
1539
  console.log(result.message);
1540
+ if (result.status === "skipped" && result.reason === "config-parse-failed") {
1541
+ console.error(
1542
+ `Error: removal skipped because the connector config could not be parsed. Fix or delete the config file at ${result.configPath} manually and retry.`
1543
+ );
1544
+ process.exit(1);
1545
+ }
727
1546
  } else if (action === "doctor") {
728
1547
  if (!connectorId) {
729
1548
  console.error("Usage: remnic connectors doctor <id>");
730
1549
  process.exit(1);
731
1550
  }
732
1551
  const result = await doctorConnector(connectorId);
1552
+ const publisherChecks = [];
1553
+ const targetHostId = hostIdForConnector(connectorId);
1554
+ const factory = PUBLISHERS[targetHostId];
1555
+ const connectorInstance = listConnectors().installed.find(
1556
+ (c) => c.connectorId === connectorId
1557
+ );
1558
+ const savedInstallExt = connectorInstance ? coerceInstallExtension(connectorInstance.config.installExtension) : void 0;
1559
+ const extensionOptedOut = savedInstallExt === false;
1560
+ if (factory) {
1561
+ if (extensionOptedOut) {
1562
+ publisherChecks.push({
1563
+ name: `Publisher: ${targetHostId}`,
1564
+ ok: true,
1565
+ detail: "skipped (installExtension=false)"
1566
+ });
1567
+ } else {
1568
+ try {
1569
+ const pub = factory();
1570
+ const available = await pub.isHostAvailable();
1571
+ const extRoot = available ? await pub.resolveExtensionRoot() : "(host not installed)";
1572
+ const extensionExists = available && extRoot ? fs.existsSync(extRoot) : false;
1573
+ publisherChecks.push({
1574
+ name: `Publisher: ${targetHostId}`,
1575
+ ok: !available || extensionExists,
1576
+ detail: !available ? "host not installed (skip)" : extensionExists ? `extension at ${extRoot}` : `extension missing at ${extRoot} \u2014 run \`remnic connectors install ${connectorId}\``
1577
+ });
1578
+ } catch (err) {
1579
+ const msg = err instanceof Error ? err.message : String(err);
1580
+ publisherChecks.push({
1581
+ name: `Publisher: ${targetHostId}`,
1582
+ ok: false,
1583
+ detail: `error: ${msg}`
1584
+ });
1585
+ }
1586
+ }
1587
+ }
1588
+ const allChecks = [...result.checks, ...publisherChecks];
1589
+ const healthy = allChecks.every((c) => c.ok);
733
1590
  if (json) {
734
- console.log(JSON.stringify(result, null, 2));
1591
+ console.log(JSON.stringify({ ...result, checks: allChecks, healthy }, null, 2));
735
1592
  } else {
736
- for (const check of result.checks) {
1593
+ for (const check of allChecks) {
737
1594
  const icon = check.ok ? "\u2713" : "\u2717";
738
1595
  console.log(` ${icon} ${check.name}: ${check.detail}`);
739
1596
  }
740
- console.log(result.healthy ? "\nConnector healthy" : "\nConnector has issues");
1597
+ console.log(healthy ? "\nConnector healthy" : "\nConnector has issues");
1598
+ }
1599
+ } else if (action === "marketplace") {
1600
+ const subAction = nonFlagArgs[0];
1601
+ let subActionRemoved = false;
1602
+ const marketplaceRest = rest.filter((a) => {
1603
+ if (!subActionRemoved && a === subAction) {
1604
+ subActionRemoved = true;
1605
+ return false;
1606
+ }
1607
+ return true;
1608
+ });
1609
+ await cmdConnectorsMarketplace(subAction, marketplaceRest, json);
1610
+ } else {
1611
+ console.log("Usage: remnic connectors <list|install|remove|doctor|marketplace> [id]");
1612
+ process.exit(1);
1613
+ }
1614
+ }
1615
+ async function cmdConnectorsMarketplace(subAction, rest, json) {
1616
+ const configPath = resolveConfigPath(resolveFlagStrict(rest, "--config"));
1617
+ const rawConfig = fs.existsSync(configPath) ? JSON.parse(fs.readFileSync(configPath, "utf8")) : {};
1618
+ const pluginConfig = rawConfig.remnic ?? rawConfig.engram ?? rawConfig;
1619
+ const config = parseConfig(pluginConfig);
1620
+ if (subAction === "generate") {
1621
+ const outputDir = resolveFlagStrict(rest, "--output") ?? process.cwd();
1622
+ const manifest = generateMarketplaceManifest();
1623
+ await writeMarketplaceManifest(outputDir, manifest);
1624
+ const outPath = path2.join(outputDir, "marketplace.json");
1625
+ if (json) {
1626
+ console.log(JSON.stringify({ status: "generated", path: outPath }, null, 2));
1627
+ } else {
1628
+ console.log(`Generated marketplace.json at ${outPath}`);
1629
+ }
1630
+ } else if (subAction === "validate") {
1631
+ const targetPath = rest.filter((a) => !a.startsWith("--"))[0] ?? path2.join(process.cwd(), "marketplace.json");
1632
+ const resolved = path2.resolve(targetPath);
1633
+ if (!fs.existsSync(resolved)) {
1634
+ console.error(`File not found: ${resolved}`);
1635
+ process.exit(1);
1636
+ }
1637
+ let parsed;
1638
+ try {
1639
+ parsed = JSON.parse(fs.readFileSync(resolved, "utf8"));
1640
+ } catch {
1641
+ console.error(`Invalid JSON in ${resolved}`);
1642
+ process.exit(1);
1643
+ }
1644
+ const validation = checkMarketplaceManifest(parsed);
1645
+ if (json) {
1646
+ console.log(JSON.stringify(validation, null, 2));
1647
+ }
1648
+ if (validation.valid) {
1649
+ if (!json) console.log(`Valid marketplace manifest: ${resolved}`);
1650
+ } else {
1651
+ if (!json) {
1652
+ console.error(`Invalid marketplace manifest: ${resolved}`);
1653
+ for (const err of validation.errors) {
1654
+ console.error(` - ${err}`);
1655
+ }
1656
+ }
1657
+ process.exit(1);
1658
+ }
1659
+ } else if (subAction === "install") {
1660
+ const source = rest.filter((a) => !a.startsWith("--"))[0];
1661
+ if (!source) {
1662
+ console.error("Usage: remnic connectors marketplace install <source> [--type github|git|local|url]");
1663
+ process.exit(1);
1664
+ }
1665
+ const validTypes = /* @__PURE__ */ new Set(["github", "git", "local", "url"]);
1666
+ const hasTypeFlag = rest.includes("--type");
1667
+ const typeFlag = resolveFlagStrict(rest, "--type") ?? (hasTypeFlag ? void 0 : "github");
1668
+ if (typeFlag === void 0) {
1669
+ console.error(`--type requires a value. Must be one of: ${[...validTypes].join(", ")}`);
1670
+ process.exit(1);
1671
+ }
1672
+ if (!validTypes.has(typeFlag)) {
1673
+ console.error(`Invalid --type: "${typeFlag}". Must be one of: ${[...validTypes].join(", ")}`);
1674
+ process.exit(1);
1675
+ }
1676
+ const result = await installFromMarketplace(
1677
+ source,
1678
+ typeFlag,
1679
+ config
1680
+ );
1681
+ if (json) {
1682
+ console.log(JSON.stringify(result, null, 2));
1683
+ } else {
1684
+ console.log(result.message);
1685
+ if (result.pluginsFound.length > 0) {
1686
+ console.log(` Plugins: ${result.pluginsFound.join(", ")}`);
1687
+ }
741
1688
  }
1689
+ if (!result.ok) process.exit(1);
742
1690
  } else {
743
- console.log("Usage: remnic connectors <list|install|remove|doctor> [id]");
1691
+ console.log(`Usage: remnic connectors marketplace <generate|validate|install> [args]
1692
+
1693
+ generate [--output <dir>] Generate marketplace.json
1694
+ validate [path] Validate a marketplace.json file
1695
+ install <source> [--type <type>] Install from marketplace source
1696
+ Types: github, git, local, url (default: github)`);
744
1697
  process.exit(1);
745
1698
  }
746
1699
  }
@@ -880,10 +1833,10 @@ async function cmdSpace(action, rest, json) {
880
1833
  process.exit(1);
881
1834
  }
882
1835
  }
883
- async function cmdBenchmark(action, rest, json) {
1836
+ async function cmdLegacyBenchmark(action, rest, json) {
884
1837
  initLogger();
885
1838
  const configPath = resolveConfigPath();
886
- const raw = fs2.existsSync(configPath) ? JSON.parse(fs2.readFileSync(configPath, "utf8")) : {};
1839
+ const raw = fs.existsSync(configPath) ? JSON.parse(fs.readFileSync(configPath, "utf8")) : {};
887
1840
  const remnicCfg = raw.remnic ?? raw.engram ?? raw;
888
1841
  const config = parseConfig(remnicCfg);
889
1842
  const orchestrator = new Orchestrator(config);
@@ -955,6 +1908,55 @@ async function cmdBenchmark(action, rest, json) {
955
1908
  process.exit(1);
956
1909
  }
957
1910
  }
1911
+ async function cmdBench(rest) {
1912
+ const benchAction = parseBenchActionArgs(rest);
1913
+ let parsed;
1914
+ try {
1915
+ parsed = parseBenchArgs(rest);
1916
+ } catch (error) {
1917
+ console.error(error instanceof Error ? error.message : String(error));
1918
+ process.exit(1);
1919
+ }
1920
+ if (parsed.action === "help") {
1921
+ console.log(getBenchUsageText());
1922
+ return;
1923
+ }
1924
+ if (parsed.action === "check" || parsed.action === "report") {
1925
+ await cmdLegacyBenchmark(parsed.action, benchAction.args, parsed.json);
1926
+ return;
1927
+ }
1928
+ if (parsed.action === "list") {
1929
+ const catalog = await listBenchmarksFromPackage() ?? BENCHMARK_CATALOG;
1930
+ if (parsed.json) {
1931
+ console.log(JSON.stringify(catalog, null, 2));
1932
+ return;
1933
+ }
1934
+ console.log("Published benchmarks:");
1935
+ for (const entry of catalog) {
1936
+ console.log(` ${entry.id.padEnd(14)} ${entry.category.padEnd(14)} ${entry.summary}`);
1937
+ }
1938
+ return;
1939
+ }
1940
+ const selectedBenchmarks = parsed.all ? await resolveAllBenchmarks() : parsed.benchmarks;
1941
+ if (selectedBenchmarks.length === 0) {
1942
+ console.error(
1943
+ 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."
1944
+ );
1945
+ process.exit(1);
1946
+ }
1947
+ const knownBenchmarkIds = await resolveKnownBenchmarkIds();
1948
+ const unknown = selectedBenchmarks.filter((benchmarkId) => !knownBenchmarkIds.has(benchmarkId));
1949
+ if (unknown.length > 0) {
1950
+ console.error(`ERROR: unknown benchmark(s): ${unknown.join(", ")}. Use 'remnic bench list' to see available.`);
1951
+ process.exit(1);
1952
+ }
1953
+ for (const benchmarkId of selectedBenchmarks) {
1954
+ const handledByPackage = await runBenchViaPackage(parsed, benchmarkId);
1955
+ if (!handledByPackage) {
1956
+ await runBenchViaFallback(parsed, benchmarkId);
1957
+ }
1958
+ }
1959
+ }
958
1960
  var LOGS_DIR = path2.join(PID_DIR, "logs");
959
1961
  var LAUNCHD_LABEL = "ai.remnic.daemon";
960
1962
  var LEGACY_LAUNCHD_LABEL = "ai.engram.daemon";
@@ -989,7 +1991,7 @@ var LEGACY_SYSTEMD_UNIT_PATH = path2.join(
989
1991
  function readPid() {
990
1992
  for (const file of [PID_FILE, LEGACY_PID_FILE]) {
991
1993
  try {
992
- return parseInt(fs2.readFileSync(file, "utf8").trim(), 10);
1994
+ return parseInt(fs.readFileSync(file, "utf8").trim(), 10);
993
1995
  } catch {
994
1996
  }
995
1997
  }
@@ -998,7 +2000,7 @@ function readPid() {
998
2000
  function inferPort() {
999
2001
  try {
1000
2002
  const configPath = resolveConfigPath();
1001
- const raw = JSON.parse(fs2.readFileSync(configPath, "utf8"));
2003
+ const raw = JSON.parse(fs.readFileSync(configPath, "utf8"));
1002
2004
  return raw.server?.port ?? 4318;
1003
2005
  } catch {
1004
2006
  return 4318;
@@ -1009,7 +2011,7 @@ function resolveNodePath() {
1009
2011
  }
1010
2012
  function resolveServerBin() {
1011
2013
  const distPath = path2.resolve(import.meta.dirname, "../../remnic-server/dist/index.js");
1012
- if (fs2.existsSync(distPath)) return distPath;
2014
+ if (fs.existsSync(distPath)) return distPath;
1013
2015
  const srcPath = path2.resolve(import.meta.dirname, "../../remnic-server/src/index.ts");
1014
2016
  return srcPath;
1015
2017
  }
@@ -1037,13 +2039,13 @@ function daemonInstall() {
1037
2039
  process.exit(1);
1038
2040
  }
1039
2041
  const vars = { HOME: home, NODE_PATH: nodePath, REMNIC_SERVER_BIN: serverBin };
1040
- fs2.mkdirSync(LOGS_DIR, { recursive: true });
2042
+ fs.mkdirSync(LOGS_DIR, { recursive: true });
1041
2043
  if (isMacOS()) {
1042
2044
  const templatePath = path2.resolve(import.meta.dirname, "../templates/launchd/ai.remnic.daemon.plist");
1043
- const template = fs2.readFileSync(templatePath, "utf8");
2045
+ const template = fs.readFileSync(templatePath, "utf8");
1044
2046
  const plist = renderTemplate(template, vars);
1045
- fs2.mkdirSync(path2.dirname(LAUNCHD_PLIST_PATH), { recursive: true });
1046
- fs2.writeFileSync(LAUNCHD_PLIST_PATH, plist);
2047
+ fs.mkdirSync(path2.dirname(LAUNCHD_PLIST_PATH), { recursive: true });
2048
+ fs.writeFileSync(LAUNCHD_PLIST_PATH, plist);
1047
2049
  try {
1048
2050
  childProcess.execSync(`launchctl load -w "${LAUNCHD_PLIST_PATH}"`, { stdio: "pipe" });
1049
2051
  } catch {
@@ -1054,10 +2056,10 @@ function daemonInstall() {
1054
2056
  console.log(` Logs: ${LOGS_DIR}/daemon.log`);
1055
2057
  } else if (isLinux()) {
1056
2058
  const templatePath = path2.resolve(import.meta.dirname, "../templates/systemd/remnic.service");
1057
- const template = fs2.readFileSync(templatePath, "utf8");
2059
+ const template = fs.readFileSync(templatePath, "utf8");
1058
2060
  const unit = renderTemplate(template, vars);
1059
- fs2.mkdirSync(path2.dirname(SYSTEMD_UNIT_PATH), { recursive: true });
1060
- fs2.writeFileSync(SYSTEMD_UNIT_PATH, unit);
2061
+ fs.mkdirSync(path2.dirname(SYSTEMD_UNIT_PATH), { recursive: true });
2062
+ fs.writeFileSync(SYSTEMD_UNIT_PATH, unit);
1061
2063
  try {
1062
2064
  childProcess.execSync("systemctl --user daemon-reload", { stdio: "pipe" });
1063
2065
  childProcess.execSync(`systemctl --user enable ${SYSTEMD_SERVICE}`, { stdio: "pipe" });
@@ -1081,7 +2083,7 @@ function daemonUninstall() {
1081
2083
  } catch {
1082
2084
  }
1083
2085
  try {
1084
- fs2.unlinkSync(plistPath);
2086
+ fs.unlinkSync(plistPath);
1085
2087
  removed = true;
1086
2088
  console.log(`Removed launchd service: ${plistPath}`);
1087
2089
  } catch {
@@ -1101,7 +2103,7 @@ function daemonUninstall() {
1101
2103
  let removed = false;
1102
2104
  for (const unitPath of [SYSTEMD_UNIT_PATH, LEGACY_SYSTEMD_UNIT_PATH]) {
1103
2105
  try {
1104
- fs2.unlinkSync(unitPath);
2106
+ fs.unlinkSync(unitPath);
1105
2107
  removed = true;
1106
2108
  console.log(`Removed systemd service: ${unitPath}`);
1107
2109
  } catch {
@@ -1159,17 +2161,36 @@ function isServiceRunning() {
1159
2161
  }
1160
2162
  return { running: false };
1161
2163
  }
1162
- function daemonStatus() {
2164
+ async function daemonStatus() {
1163
2165
  const { running, pid } = isServiceRunning();
1164
2166
  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;
2167
+ 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
2168
  console.log(`Remnic daemon status:`);
1167
2169
  console.log(` Running: ${running ? `yes${pid ? ` (pid ${pid})` : ""}` : "no"}`);
1168
2170
  console.log(` Port: ${port}`);
1169
2171
  console.log(` Service: ${serviceInstalled ? "installed" : "not installed"}`);
1170
2172
  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}`);
2173
+ console.log(` PID file: ${fs.existsSync(PID_FILE) ? PID_FILE : LEGACY_PID_FILE}`);
2174
+ console.log(` Log file: ${fs.existsSync(LOG_FILE) ? LOG_FILE : LEGACY_LOG_FILE}`);
2175
+ try {
2176
+ const configPath = resolveConfigPath();
2177
+ const raw = fs.existsSync(configPath) ? JSON.parse(fs.readFileSync(configPath, "utf8")) : {};
2178
+ const remnicCfg = raw.remnic ?? raw.engram ?? raw;
2179
+ const config = parseConfig(remnicCfg);
2180
+ const extRoot = resolveExtensionsRoot(config);
2181
+ const noopLog = { warn: () => {
2182
+ }, debug: () => {
2183
+ } };
2184
+ const exts = await discoverMemoryExtensions(extRoot, noopLog);
2185
+ if (exts.length > 0) {
2186
+ const names = exts.map((e) => e.name).join(", ");
2187
+ console.log(` Memory extensions: ${exts.length} active (${names})`);
2188
+ } else {
2189
+ console.log(` Memory extensions: none`);
2190
+ }
2191
+ } catch {
2192
+ console.log(` Memory extensions: unknown (config error)`);
2193
+ }
1173
2194
  }
1174
2195
  function daemonStart() {
1175
2196
  const svc = isServiceRunning();
@@ -1177,7 +2198,7 @@ function daemonStart() {
1177
2198
  console.log(`Already running${svc.pid ? ` (pid ${svc.pid})` : " (via service manager)"}`);
1178
2199
  return;
1179
2200
  }
1180
- if (isMacOS() && (fs2.existsSync(LAUNCHD_PLIST_PATH) || fs2.existsSync(LEGACY_LAUNCHD_PLIST_PATH))) {
2201
+ if (isMacOS() && (fs.existsSync(LAUNCHD_PLIST_PATH) || fs.existsSync(LEGACY_LAUNCHD_PLIST_PATH))) {
1181
2202
  const label = firstSuccessfulCandidate([LAUNCHD_LABEL, LEGACY_LAUNCHD_LABEL], (candidate) => {
1182
2203
  childProcess.execSync(`launchctl start ${candidate} 2>/dev/null`, { stdio: "pipe" });
1183
2204
  });
@@ -1185,7 +2206,7 @@ function daemonStart() {
1185
2206
  console.log(`Started remnic daemon via launchd (${label})`);
1186
2207
  return;
1187
2208
  }
1188
- } else if (isLinux() && (fs2.existsSync(SYSTEMD_UNIT_PATH) || fs2.existsSync(LEGACY_SYSTEMD_UNIT_PATH))) {
2209
+ } else if (isLinux() && (fs.existsSync(SYSTEMD_UNIT_PATH) || fs.existsSync(LEGACY_SYSTEMD_UNIT_PATH))) {
1189
2210
  const serviceName = firstSuccessfulCandidate([SYSTEMD_SERVICE, LEGACY_SYSTEMD_SERVICE], (candidate) => {
1190
2211
  childProcess.execSync(`systemctl --user start ${candidate}`, { stdio: "pipe" });
1191
2212
  });
@@ -1194,9 +2215,9 @@ function daemonStart() {
1194
2215
  return;
1195
2216
  }
1196
2217
  }
1197
- fs2.mkdirSync(PID_DIR, { recursive: true });
1198
- fs2.mkdirSync(LOGS_DIR, { recursive: true });
1199
- const logStream = fs2.openSync(LOG_FILE, "a");
2218
+ fs.mkdirSync(PID_DIR, { recursive: true });
2219
+ fs.mkdirSync(LOGS_DIR, { recursive: true });
2220
+ const logStream = fs.openSync(LOG_FILE, "a");
1200
2221
  const serverBin = resolveServerBin();
1201
2222
  const isSource = serverBin.endsWith(".ts");
1202
2223
  let cmd;
@@ -1218,12 +2239,12 @@ function daemonStart() {
1218
2239
  }
1219
2240
  });
1220
2241
  child.unref();
1221
- fs2.writeFileSync(PID_FILE, String(child.pid));
2242
+ fs.writeFileSync(PID_FILE, String(child.pid));
1222
2243
  console.log(`Started remnic server (pid ${child.pid})`);
1223
2244
  console.log(` Log: ${LOG_FILE}`);
1224
2245
  }
1225
2246
  function daemonStop() {
1226
- if (isMacOS() && (fs2.existsSync(LAUNCHD_PLIST_PATH) || fs2.existsSync(LEGACY_LAUNCHD_PLIST_PATH))) {
2247
+ if (isMacOS() && (fs.existsSync(LAUNCHD_PLIST_PATH) || fs.existsSync(LEGACY_LAUNCHD_PLIST_PATH))) {
1227
2248
  const label = firstSuccessfulCandidate([LAUNCHD_LABEL, LEGACY_LAUNCHD_LABEL], (candidate) => {
1228
2249
  childProcess.execSync(`launchctl stop ${candidate} 2>/dev/null`, { stdio: "pipe" });
1229
2250
  });
@@ -1231,7 +2252,7 @@ function daemonStop() {
1231
2252
  console.log(`Stopped remnic daemon via launchd (${label})`);
1232
2253
  return;
1233
2254
  }
1234
- } else if (isLinux() && (fs2.existsSync(SYSTEMD_UNIT_PATH) || fs2.existsSync(LEGACY_SYSTEMD_UNIT_PATH))) {
2255
+ } else if (isLinux() && (fs.existsSync(SYSTEMD_UNIT_PATH) || fs.existsSync(LEGACY_SYSTEMD_UNIT_PATH))) {
1235
2256
  const serviceName = firstSuccessfulCandidate([SYSTEMD_SERVICE, LEGACY_SYSTEMD_SERVICE], (candidate) => {
1236
2257
  childProcess.execSync(`systemctl --user stop ${candidate}`, { stdio: "pipe" });
1237
2258
  });
@@ -1252,11 +2273,11 @@ function daemonStop() {
1252
2273
  console.log("Process not found (cleaning up PID file)");
1253
2274
  }
1254
2275
  try {
1255
- fs2.unlinkSync(PID_FILE);
2276
+ fs.unlinkSync(PID_FILE);
1256
2277
  } catch {
1257
2278
  }
1258
2279
  try {
1259
- fs2.unlinkSync(LEGACY_PID_FILE);
2280
+ fs.unlinkSync(LEGACY_PID_FILE);
1260
2281
  } catch {
1261
2282
  }
1262
2283
  }
@@ -1304,6 +2325,470 @@ function cmdTokenRevoke(connector) {
1304
2325
  console.log(`No token found for ${connector}`);
1305
2326
  }
1306
2327
  }
2328
+ async function promptYesNo(question, defaultYes = true) {
2329
+ if (!process.stdin.isTTY) return defaultYes;
2330
+ process.stdout.write(question + " ");
2331
+ return new Promise((resolve) => {
2332
+ let buf = "";
2333
+ const cleanup = () => {
2334
+ process.stdin.removeListener("data", onData);
2335
+ process.stdin.removeListener("end", onEnd);
2336
+ process.stdin.removeListener("close", onEnd);
2337
+ process.stdin.pause();
2338
+ };
2339
+ const onEnd = () => {
2340
+ cleanup();
2341
+ resolve(defaultYes);
2342
+ };
2343
+ const onData = (chunk) => {
2344
+ buf += chunk.toString();
2345
+ const nl = buf.indexOf("\n");
2346
+ if (nl >= 0) {
2347
+ cleanup();
2348
+ const answer = buf.slice(0, nl).trim().toLowerCase();
2349
+ if (answer === "" || answer === "y" || answer === "yes") {
2350
+ resolve(defaultYes || answer !== "");
2351
+ } else if (answer === "n" || answer === "no") {
2352
+ resolve(false);
2353
+ } else {
2354
+ resolve(defaultYes);
2355
+ }
2356
+ }
2357
+ };
2358
+ process.stdin.resume();
2359
+ process.stdin.on("data", onData);
2360
+ process.stdin.on("end", onEnd);
2361
+ process.stdin.on("close", onEnd);
2362
+ });
2363
+ }
2364
+ async function cmdBinary(rest) {
2365
+ initLogger();
2366
+ const configPath = resolveConfigPath();
2367
+ const raw = fs.existsSync(configPath) ? JSON.parse(fs.readFileSync(configPath, "utf8")) : {};
2368
+ const remnicCfg = raw.remnic ?? raw.engram ?? raw;
2369
+ const config = parseConfig(remnicCfg);
2370
+ const memoryDir = resolveMemoryDir();
2371
+ const blConfig = {
2372
+ enabled: config.binaryLifecycleEnabled,
2373
+ gracePeriodDays: config.binaryLifecycleGracePeriodDays,
2374
+ maxBinarySizeBytes: DEFAULT_MAX_BINARY_SIZE_BYTES,
2375
+ scanPatterns: DEFAULT_SCAN_PATTERNS,
2376
+ backend: {
2377
+ type: config.binaryLifecycleBackendType,
2378
+ basePath: config.binaryLifecycleBackendPath ? expandTilde(config.binaryLifecycleBackendPath) : void 0
2379
+ }
2380
+ };
2381
+ const action = rest[0] ?? "help";
2382
+ switch (action) {
2383
+ case "scan": {
2384
+ const manifest = await readManifest(memoryDir);
2385
+ const { scanForBinaries } = await import("@remnic/core");
2386
+ const found = await scanForBinaries(memoryDir, blConfig, manifest);
2387
+ if (found.length === 0) {
2388
+ console.log("No untracked binary files found.");
2389
+ } else {
2390
+ console.log(`Found ${found.length} untracked binary file(s):`);
2391
+ for (const p of found) {
2392
+ console.log(` ${p}`);
2393
+ }
2394
+ }
2395
+ break;
2396
+ }
2397
+ case "status": {
2398
+ const manifest = await readManifest(memoryDir);
2399
+ const counts = {
2400
+ total: manifest.assets.length,
2401
+ pending: manifest.assets.filter((a) => a.status === "pending").length,
2402
+ mirrored: manifest.assets.filter((a) => a.status === "mirrored").length,
2403
+ redirected: manifest.assets.filter((a) => a.status === "redirected").length,
2404
+ cleaned: manifest.assets.filter((a) => a.status === "cleaned").length,
2405
+ error: manifest.assets.filter((a) => a.status === "error").length
2406
+ };
2407
+ const totalBytes = manifest.assets.reduce((sum, a) => sum + a.sizeBytes, 0);
2408
+ console.log(`Binary lifecycle manifest (${memoryDir}):`);
2409
+ console.log(` Total assets: ${counts.total}`);
2410
+ console.log(` Pending: ${counts.pending}`);
2411
+ console.log(` Mirrored: ${counts.mirrored}`);
2412
+ console.log(` Redirected: ${counts.redirected}`);
2413
+ console.log(` Cleaned: ${counts.cleaned}`);
2414
+ console.log(` Errors: ${counts.error}`);
2415
+ console.log(` Total size: ${(totalBytes / 1024).toFixed(1)} KB`);
2416
+ if (manifest.lastScanAt) {
2417
+ console.log(` Last scan: ${manifest.lastScanAt}`);
2418
+ }
2419
+ break;
2420
+ }
2421
+ case "run": {
2422
+ const dryRun = rest.includes("--dry-run");
2423
+ const backend = createBackend(blConfig.backend);
2424
+ const log = {
2425
+ info: (msg) => console.log(msg),
2426
+ warn: (msg) => console.warn(msg),
2427
+ error: (msg) => console.error(msg)
2428
+ };
2429
+ const result = await runBinaryLifecyclePipeline(
2430
+ memoryDir,
2431
+ blConfig,
2432
+ backend,
2433
+ log,
2434
+ { dryRun }
2435
+ );
2436
+ console.log(
2437
+ `
2438
+ Pipeline complete${dryRun ? " (dry-run)" : ""}: scanned=${result.scanned}, mirrored=${result.mirrored}, redirected=${result.redirected}, cleaned=${result.cleaned}`
2439
+ );
2440
+ if (result.errors.length > 0) {
2441
+ console.error(`Errors (${result.errors.length}):`);
2442
+ for (const e of result.errors) console.error(` ${e}`);
2443
+ }
2444
+ break;
2445
+ }
2446
+ case "clean": {
2447
+ const force = rest.includes("--force");
2448
+ if (!force) {
2449
+ console.error("Use --force to confirm cleanup of local binary copies.");
2450
+ process.exit(1);
2451
+ }
2452
+ const backend = createBackend(blConfig.backend);
2453
+ const log = {
2454
+ info: (msg) => console.log(msg),
2455
+ warn: (msg) => console.warn(msg),
2456
+ error: (msg) => console.error(msg)
2457
+ };
2458
+ const result = await runBinaryLifecyclePipeline(
2459
+ memoryDir,
2460
+ blConfig,
2461
+ backend,
2462
+ log,
2463
+ { forceClean: true }
2464
+ );
2465
+ console.log(
2466
+ `
2467
+ Clean complete: cleaned=${result.cleaned}`
2468
+ );
2469
+ if (result.errors.length > 0) {
2470
+ console.error(`Errors (${result.errors.length}):`);
2471
+ for (const e of result.errors) console.error(` ${e}`);
2472
+ }
2473
+ break;
2474
+ }
2475
+ default:
2476
+ console.log(`Usage: remnic binary <scan|status|run|clean>
2477
+
2478
+ scan Scan for untracked binary files
2479
+ status Show binary lifecycle manifest summary
2480
+ run [--dry-run] Run full binary lifecycle pipeline
2481
+ clean --force Force-clean local copies past grace period`);
2482
+ break;
2483
+ }
2484
+ }
2485
+ async function cmdOpenclawInstall(opts) {
2486
+ const configPath = resolveOpenclawConfigPath(opts.configPath);
2487
+ const fallbackMemoryDir = path2.join(resolveHomeDir(), ".openclaw", "workspace", "memory", "local");
2488
+ console.log(`OpenClaw config: ${configPath}`);
2489
+ const existingConfig = readOpenclawConfig(configPath);
2490
+ const rawPlugins = existingConfig.plugins;
2491
+ if (rawPlugins !== void 0 && (typeof rawPlugins !== "object" || rawPlugins === null || Array.isArray(rawPlugins))) {
2492
+ throw new Error(
2493
+ `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.`
2494
+ );
2495
+ }
2496
+ const plugins = rawPlugins ?? {};
2497
+ const rawEntries = plugins.entries;
2498
+ if (rawEntries !== void 0 && (typeof rawEntries !== "object" || rawEntries === null || Array.isArray(rawEntries))) {
2499
+ throw new Error(
2500
+ `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.`
2501
+ );
2502
+ }
2503
+ const entries = rawEntries ?? {};
2504
+ const rawSlots = plugins.slots;
2505
+ if (rawSlots !== void 0 && (typeof rawSlots !== "object" || rawSlots === null || Array.isArray(rawSlots))) {
2506
+ throw new Error(
2507
+ `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.`
2508
+ );
2509
+ }
2510
+ const slots = rawSlots ?? {};
2511
+ const hasLegacy = REMNIC_OPENCLAW_LEGACY_PLUGIN_ID in entries;
2512
+ const hasNew = REMNIC_OPENCLAW_PLUGIN_ID in entries;
2513
+ const currentSlot = slots.memory;
2514
+ let migrateLegacy = false;
2515
+ if (hasLegacy && !opts.yes) {
2516
+ migrateLegacy = await promptYesNo(
2517
+ `Found legacy '${REMNIC_OPENCLAW_LEGACY_PLUGIN_ID}' entry. Migrate to '${REMNIC_OPENCLAW_PLUGIN_ID}'? [Y/n]`,
2518
+ true
2519
+ );
2520
+ } else if (hasLegacy) {
2521
+ migrateLegacy = true;
2522
+ }
2523
+ const legacyEntry = entries[REMNIC_OPENCLAW_LEGACY_PLUGIN_ID];
2524
+ const existingNewEntry = entries[REMNIC_OPENCLAW_PLUGIN_ID];
2525
+ const legacyConfigToMerge = migrateLegacy && legacyEntry?.config && typeof legacyEntry.config === "object" ? legacyEntry.config : {};
2526
+ const existingNewEntryConfig = existingNewEntry?.config && typeof existingNewEntry.config === "object" ? existingNewEntry.config : {};
2527
+ const existingMemoryDir = (typeof existingNewEntryConfig.memoryDir === "string" ? existingNewEntryConfig.memoryDir : void 0) || (migrateLegacy && typeof legacyConfigToMerge.memoryDir === "string" ? legacyConfigToMerge.memoryDir : void 0);
2528
+ const memoryDir = opts.memoryDir ? path2.resolve(expandTilde(opts.memoryDir)) : existingMemoryDir ? path2.resolve(expandTilde(existingMemoryDir)) : fallbackMemoryDir;
2529
+ console.log(`Memory dir: ${memoryDir}`);
2530
+ const legacyNonConfigFields = {};
2531
+ if (migrateLegacy && legacyEntry && typeof legacyEntry === "object" && !Array.isArray(legacyEntry)) {
2532
+ for (const [k, v] of Object.entries(legacyEntry)) {
2533
+ if (k !== "config") legacyNonConfigFields[k] = v;
2534
+ }
2535
+ }
2536
+ const existingNewEntryFields = existingNewEntry && typeof existingNewEntry === "object" && !Array.isArray(existingNewEntry) ? existingNewEntry : {};
2537
+ const newEntry = {
2538
+ ...legacyNonConfigFields,
2539
+ ...existingNewEntryFields,
2540
+ config: {
2541
+ ...legacyConfigToMerge,
2542
+ ...existingNewEntryConfig,
2543
+ memoryDir
2544
+ }
2545
+ };
2546
+ const updatedEntries = { ...entries };
2547
+ updatedEntries[REMNIC_OPENCLAW_PLUGIN_ID] = newEntry;
2548
+ const slotIsActiveLegacy = hasLegacy && !migrateLegacy && currentSlot === REMNIC_OPENCLAW_LEGACY_PLUGIN_ID;
2549
+ const updatedSlots = slotIsActiveLegacy ? { ...slots } : { ...slots, memory: REMNIC_OPENCLAW_PLUGIN_ID };
2550
+ const updatedConfig = {
2551
+ ...existingConfig,
2552
+ plugins: {
2553
+ ...plugins,
2554
+ entries: updatedEntries,
2555
+ slots: updatedSlots
2556
+ }
2557
+ };
2558
+ const changes = [];
2559
+ if (!hasNew) changes.push(`+ Added plugins.entries["${REMNIC_OPENCLAW_PLUGIN_ID}"]`);
2560
+ else changes.push(`~ Updated plugins.entries["${REMNIC_OPENCLAW_PLUGIN_ID}"].config.memoryDir`);
2561
+ if (!slotIsActiveLegacy && currentSlot !== REMNIC_OPENCLAW_PLUGIN_ID) {
2562
+ changes.push(`~ Set plugins.slots.memory = "${REMNIC_OPENCLAW_PLUGIN_ID}" (was: ${currentSlot ?? "(unset)"})`);
2563
+ } else if (slotIsActiveLegacy) {
2564
+ changes.push(` Slot left as "${REMNIC_OPENCLAW_LEGACY_PLUGIN_ID}" \u2014 re-run with --yes to activate the new entry`);
2565
+ }
2566
+ if (!fs.existsSync(memoryDir)) changes.push(`+ Will create memory directory: ${memoryDir}`);
2567
+ if (hasLegacy && migrateLegacy) {
2568
+ changes.push(`~ Legacy '${REMNIC_OPENCLAW_LEGACY_PLUGIN_ID}' entry retained (safe to remove after verifying hooks fire)`);
2569
+ }
2570
+ if (opts.dryRun) {
2571
+ console.log("\n--- DRY RUN \u2014 no changes written ---");
2572
+ for (const c of changes) console.log(" " + c);
2573
+ const dryRunPlugins = updatedConfig.plugins;
2574
+ const dryRunEntries = dryRunPlugins.entries;
2575
+ const entrySummary = dryRunEntries ? Object.keys(dryRunEntries).map((k) => {
2576
+ const cfg = dryRunEntries[k]?.config;
2577
+ return ` ${k}: { config: { memoryDir: ${cfg?.memoryDir ?? "(unset)"}, ... } }`;
2578
+ }).join("\n") : " (none)";
2579
+ console.log("\nResulting plugins.entries:");
2580
+ console.log(entrySummary);
2581
+ console.log(`
2582
+ Resulting plugins.slots.memory: ${dryRunPlugins.slots?.memory ?? "(unset)"}`);
2583
+ return;
2584
+ }
2585
+ if (fs.existsSync(memoryDir)) {
2586
+ const st = fs.statSync(memoryDir);
2587
+ if (!st.isDirectory()) {
2588
+ throw new Error(
2589
+ `Cannot use ${memoryDir} as the memory directory \u2014 a file already exists at that path.
2590
+ Remove it first and re-run, or choose a different path with --memory-dir.`
2591
+ );
2592
+ }
2593
+ } else {
2594
+ fs.mkdirSync(memoryDir, { recursive: true });
2595
+ console.log(`Created memory directory: ${memoryDir}`);
2596
+ }
2597
+ const configDir = path2.dirname(configPath);
2598
+ if (!fs.existsSync(configDir)) {
2599
+ fs.mkdirSync(configDir, { recursive: true });
2600
+ }
2601
+ fs.writeFileSync(configPath, JSON.stringify(updatedConfig, null, 2) + "\n");
2602
+ console.log("\nDone! Summary of changes:");
2603
+ for (const c of changes) console.log(" " + c);
2604
+ if (hasLegacy && migrateLegacy) {
2605
+ console.log(
2606
+ `
2607
+ Note: The legacy '${REMNIC_OPENCLAW_LEGACY_PLUGIN_ID}' entry has been kept alongside '${REMNIC_OPENCLAW_PLUGIN_ID}'.`
2608
+ );
2609
+ console.log(
2610
+ "Once you verify that [remnic] gateway_start fired appears in your gateway log,"
2611
+ );
2612
+ console.log(`you can safely remove the '${REMNIC_OPENCLAW_LEGACY_PLUGIN_ID}' entry from openclaw.json.`);
2613
+ }
2614
+ console.log("\nNext steps:");
2615
+ console.log(" 1. Restart the OpenClaw gateway:");
2616
+ console.log(" launchctl kickstart -k gui/$(id -u)/ai.openclaw.gateway");
2617
+ console.log(" 2. Start a conversation \u2014 check your gateway log for:");
2618
+ console.log(" [remnic] gateway_start fired \u2014 Remnic memory plugin is active");
2619
+ console.log(" 3. Run `remnic doctor` to verify the full configuration.");
2620
+ }
2621
+ async function cmdTaxonomy(rest) {
2622
+ initLogger();
2623
+ const configPath = resolveConfigPath();
2624
+ const raw = fs.existsSync(configPath) ? JSON.parse(fs.readFileSync(configPath, "utf8")) : {};
2625
+ const remnicCfg = raw.remnic ?? raw.engram ?? raw;
2626
+ const config = parseConfig(remnicCfg);
2627
+ if (!config.taxonomyEnabled) {
2628
+ console.error(
2629
+ "Taxonomy is disabled in config (taxonomyEnabled = false). Enable it to use taxonomy commands."
2630
+ );
2631
+ process.exit(1);
2632
+ }
2633
+ const subCommand = rest[0];
2634
+ switch (subCommand) {
2635
+ case "show": {
2636
+ const taxonomy = await loadTaxonomy(config.memoryDir);
2637
+ const json = rest.includes("--json");
2638
+ if (json) {
2639
+ console.log(JSON.stringify(taxonomy, null, 2));
2640
+ } else {
2641
+ console.log(`Taxonomy v${taxonomy.version} \u2014 ${taxonomy.categories.length} categories
2642
+ `);
2643
+ const idWidth = Math.max(4, ...taxonomy.categories.map((c) => c.id.length));
2644
+ const nameWidth = Math.max(6, ...taxonomy.categories.map((c) => c.name.length));
2645
+ const header = `${"ID".padEnd(idWidth)} ${"Name".padEnd(nameWidth)} ${"Pri".padStart(3)} Memory Categories`;
2646
+ console.log(header);
2647
+ console.log("-".repeat(header.length + 10));
2648
+ const sorted = [...taxonomy.categories].sort((a, b) => a.priority - b.priority);
2649
+ for (const cat of sorted) {
2650
+ const line = `${cat.id.padEnd(idWidth)} ${cat.name.padEnd(nameWidth)} ${String(cat.priority).padStart(3)} ${cat.memoryCategories.join(", ")}`;
2651
+ console.log(line);
2652
+ }
2653
+ }
2654
+ break;
2655
+ }
2656
+ case "resolver": {
2657
+ const taxonomy = await loadTaxonomy(config.memoryDir);
2658
+ const doc = generateResolverDocument(taxonomy);
2659
+ console.log(doc);
2660
+ if (config.taxonomyAutoGenResolver) {
2661
+ const resolverPath = path2.join(config.memoryDir, ".taxonomy", "RESOLVER.md");
2662
+ fs.mkdirSync(path2.dirname(resolverPath), { recursive: true });
2663
+ fs.writeFileSync(resolverPath, doc);
2664
+ console.error(`Written: ${resolverPath}`);
2665
+ }
2666
+ break;
2667
+ }
2668
+ case "add": {
2669
+ const id = rest[1];
2670
+ const name = rest[2];
2671
+ if (!id || !name) {
2672
+ console.error("Usage: remnic taxonomy add <id> <name>");
2673
+ process.exit(1);
2674
+ }
2675
+ try {
2676
+ validateSlug(id);
2677
+ } catch (err) {
2678
+ console.error(err instanceof Error ? err.message : String(err));
2679
+ process.exit(1);
2680
+ }
2681
+ const taxonomy = await loadTaxonomy(config.memoryDir);
2682
+ if (taxonomy.categories.some((c) => c.id === id)) {
2683
+ console.error(`Category "${id}" already exists.`);
2684
+ process.exit(1);
2685
+ }
2686
+ const descriptionFlag = resolveFlag(rest, "--description");
2687
+ const priorityFlag = resolveFlag(rest, "--priority");
2688
+ const memoryCategoriesFlag = resolveFlag(rest, "--memory-categories");
2689
+ const newCat = {
2690
+ id,
2691
+ name,
2692
+ description: descriptionFlag ?? `Custom category: ${name}`,
2693
+ filingRules: [`Content belonging to ${name}`],
2694
+ priority: priorityFlag ? Number(priorityFlag) : 100,
2695
+ memoryCategories: memoryCategoriesFlag ? memoryCategoriesFlag.split(",").map((s) => s.trim()) : []
2696
+ };
2697
+ taxonomy.categories.push(newCat);
2698
+ try {
2699
+ validateTaxonomy(taxonomy);
2700
+ } catch (err) {
2701
+ console.error(`Invalid taxonomy: ${err instanceof Error ? err.message : String(err)}`);
2702
+ process.exit(1);
2703
+ }
2704
+ await saveTaxonomy(config.memoryDir, taxonomy);
2705
+ console.log(`Added category "${id}" (${name}).`);
2706
+ if (config.taxonomyAutoGenResolver) {
2707
+ const doc = generateResolverDocument(taxonomy);
2708
+ const resolverPath = path2.join(config.memoryDir, ".taxonomy", "RESOLVER.md");
2709
+ fs.writeFileSync(resolverPath, doc);
2710
+ console.error(`Regenerated: ${resolverPath}`);
2711
+ }
2712
+ break;
2713
+ }
2714
+ case "remove": {
2715
+ const id = rest[1];
2716
+ if (!id) {
2717
+ console.error("Usage: remnic taxonomy remove <id>");
2718
+ process.exit(1);
2719
+ }
2720
+ const taxonomy = await loadTaxonomy(config.memoryDir);
2721
+ const idx = taxonomy.categories.findIndex((c) => c.id === id);
2722
+ if (idx === -1) {
2723
+ console.error(`Category "${id}" not found.`);
2724
+ process.exit(1);
2725
+ }
2726
+ const target = taxonomy.categories[idx];
2727
+ const isDefault = DEFAULT_TAXONOMY.categories.some((c) => c.id === id);
2728
+ if (isDefault && target.memoryCategories.length > 0) {
2729
+ console.error(
2730
+ `Cannot remove default category "${id}" that maps MemoryCategory values: ${target.memoryCategories.join(", ")}. Reassign them first.`
2731
+ );
2732
+ process.exit(1);
2733
+ }
2734
+ taxonomy.categories.splice(idx, 1);
2735
+ await saveTaxonomy(config.memoryDir, taxonomy);
2736
+ console.log(`Removed category "${id}".`);
2737
+ if (config.taxonomyAutoGenResolver) {
2738
+ const doc = generateResolverDocument(taxonomy);
2739
+ const resolverPath = path2.join(config.memoryDir, ".taxonomy", "RESOLVER.md");
2740
+ fs.writeFileSync(resolverPath, doc);
2741
+ console.error(`Regenerated: ${resolverPath}`);
2742
+ }
2743
+ break;
2744
+ }
2745
+ case "resolve": {
2746
+ const resolveArgs = rest.slice(1);
2747
+ const textParts = stripResolveFlags(resolveArgs, TAXONOMY_RESOLVE_BOOLEAN_FLAGS);
2748
+ const text = textParts.join(" ");
2749
+ if (!text) {
2750
+ console.error("Usage: remnic taxonomy resolve <text>");
2751
+ process.exit(1);
2752
+ }
2753
+ const categoryFlag = resolveFlag(rest, "--category");
2754
+ const memoryCategory = categoryFlag ?? "fact";
2755
+ const taxonomy = await loadTaxonomy(config.memoryDir);
2756
+ const decision = resolveCategory(text, memoryCategory, taxonomy);
2757
+ const json = rest.includes("--json");
2758
+ if (json) {
2759
+ console.log(JSON.stringify(decision, null, 2));
2760
+ } else {
2761
+ console.log(`Category: ${decision.categoryId}`);
2762
+ console.log(`Confidence: ${decision.confidence.toFixed(2)}`);
2763
+ console.log(`Reason: ${decision.reason}`);
2764
+ if (decision.alternatives.length > 0) {
2765
+ console.log(`
2766
+ Alternatives:`);
2767
+ for (const alt of decision.alternatives.slice(0, 3)) {
2768
+ console.log(` - ${alt.categoryId}: ${alt.reason}`);
2769
+ }
2770
+ }
2771
+ }
2772
+ break;
2773
+ }
2774
+ default:
2775
+ console.log(`
2776
+ remnic taxonomy \u2014 MECE knowledge directory
2777
+
2778
+ Usage:
2779
+ remnic taxonomy show [--json] Show current taxonomy
2780
+ remnic taxonomy resolver Print/regenerate RESOLVER.md
2781
+ remnic taxonomy add <id> <name> [options] Add a custom category
2782
+ --description <text> Category description
2783
+ --priority <number> Priority (lower wins, default 100)
2784
+ --memory-categories <list> Comma-separated MemoryCategory values
2785
+ remnic taxonomy remove <id> Remove a custom category
2786
+ remnic taxonomy resolve <text> [--category <cat>] Test: resolve text to a category
2787
+ --json JSON output
2788
+ `);
2789
+ break;
2790
+ }
2791
+ }
1307
2792
  async function main(argv = process.argv.slice(2)) {
1308
2793
  const [command, ...rest] = argv;
1309
2794
  if (command !== "migrate") {
@@ -1356,7 +2841,7 @@ async function main(argv = process.argv.slice(2)) {
1356
2841
  daemonUninstall();
1357
2842
  break;
1358
2843
  case "status":
1359
- daemonStatus();
2844
+ await daemonStatus();
1360
2845
  break;
1361
2846
  default:
1362
2847
  console.log("Usage: remnic daemon <start|stop|restart|install|uninstall|status>");
@@ -1451,7 +2936,7 @@ async function main(argv = process.argv.slice(2)) {
1451
2936
  }
1452
2937
  }, 500);
1453
2938
  };
1454
- fs2.watch(memoryDir, { recursive: true }, (_event, filename) => {
2939
+ fs.watch(memoryDir, { recursive: true }, (_event, filename) => {
1455
2940
  if (filename && filename.startsWith(".")) return;
1456
2941
  rebuild();
1457
2942
  });
@@ -1459,12 +2944,12 @@ async function main(argv = process.argv.slice(2)) {
1459
2944
  });
1460
2945
  } else if (subAction === "validate") {
1461
2946
  const treeDir = outputDir;
1462
- if (!fs2.existsSync(treeDir)) {
2947
+ if (!fs.existsSync(treeDir)) {
1463
2948
  console.error(`Context tree not found at ${treeDir}. Run 'remnic tree generate' first.`);
1464
2949
  process.exit(1);
1465
2950
  }
1466
2951
  const indexPath = path2.join(treeDir, "INDEX.md");
1467
- if (!fs2.existsSync(indexPath)) {
2952
+ if (!fs.existsSync(indexPath)) {
1468
2953
  console.error(`INDEX.md missing in ${treeDir}. Tree may be corrupt \u2014 regenerate.`);
1469
2954
  process.exit(1);
1470
2955
  }
@@ -1529,10 +3014,61 @@ Options:
1529
3014
  await cmdSpace(action, rest.slice(1), json);
1530
3015
  break;
1531
3016
  }
3017
+ case "bench": {
3018
+ await cmdBench(rest);
3019
+ break;
3020
+ }
1532
3021
  case "benchmark": {
1533
- const action = rest[0] ?? "run";
1534
- const json = rest.includes("--json");
1535
- await cmdBenchmark(action, rest.slice(1), json);
3022
+ await cmdBench(rest);
3023
+ break;
3024
+ }
3025
+ case "briefing": {
3026
+ await cmdBriefing(rest);
3027
+ break;
3028
+ }
3029
+ case "versions": {
3030
+ await cmdVersions(rest);
3031
+ break;
3032
+ }
3033
+ case "binary": {
3034
+ await cmdBinary(rest);
3035
+ break;
3036
+ }
3037
+ case "taxonomy": {
3038
+ await cmdTaxonomy(rest);
3039
+ break;
3040
+ }
3041
+ case "enrich": {
3042
+ await cmdEnrich(rest);
3043
+ break;
3044
+ }
3045
+ case "extensions": {
3046
+ const action = rest[0] ?? "help";
3047
+ await cmdExtensions(action, rest.slice(1));
3048
+ break;
3049
+ }
3050
+ case "openclaw": {
3051
+ const subAction = rest[0] ?? "help";
3052
+ if (subAction === "install") {
3053
+ const yes = rest.includes("--yes") || rest.includes("-y") || rest.includes("--force");
3054
+ const dryRun = rest.includes("--dry-run");
3055
+ const memoryDir = resolveFlagStrict(rest, "--memory-dir");
3056
+ const configOverride = resolveFlagStrict(rest, "--config");
3057
+ await cmdOpenclawInstall({ yes, dryRun, memoryDir, configPath: configOverride });
3058
+ } else {
3059
+ console.log(`Usage: remnic openclaw <install>
3060
+
3061
+ install Configure OpenClaw to use Remnic as the memory plugin.
3062
+
3063
+ Sets plugins.entries["${REMNIC_OPENCLAW_PLUGIN_ID}"] and plugins.slots.memory
3064
+ in ~/.openclaw/openclaw.json (or $OPENCLAW_CONFIG_PATH).
3065
+
3066
+ Options:
3067
+ --yes / -y / --force Skip interactive prompts, assume Y
3068
+ --dry-run Print resulting config diff without writing
3069
+ --memory-dir <path> Override default memory dir (~/.openclaw/workspace/memory/local)
3070
+ --config <path> Override OpenClaw config path`);
3071
+ }
1536
3072
  break;
1537
3073
  }
1538
3074
  default:
@@ -1547,6 +3083,11 @@ Usage:
1547
3083
 
1548
3084
  remnic doctor Run diagnostics
1549
3085
  remnic config Show current config
3086
+ remnic openclaw install Configure OpenClaw to use Remnic memory (sets slot + entry)
3087
+ --yes / -y / --force Skip prompts
3088
+ --dry-run Preview changes without writing
3089
+ --memory-dir <path> Custom memory directory
3090
+ --config <path> Custom OpenClaw config path
1550
3091
  remnic daemon <start|stop|restart|install|uninstall|status> Manage background server
1551
3092
  remnic token <generate|list|revoke> [connector-id] Manage auth tokens
1552
3093
  remnic tree <generate|watch|validate> Generate context tree
@@ -1555,10 +3096,36 @@ Usage:
1555
3096
  remnic review <list|approve|dismiss|flag> [id] Review inbox
1556
3097
  remnic sync <run|watch> [--source <dir>] Diff-aware sync
1557
3098
  remnic dedup [--json] Find duplicate memories
1558
- remnic connectors <list|install|remove|doctor> [id] Manage connectors
3099
+ remnic connectors <list|install|remove|doctor|marketplace> [id] Manage connectors
3100
+ marketplace generate Generate marketplace.json for Codex
3101
+ marketplace validate Validate a marketplace.json file
3102
+ marketplace install Install from a marketplace source
3103
+ remnic extensions <list|show|validate|reload> Manage memory extensions
1559
3104
  remnic space <list|switch|create|delete|push|pull|share|promote|audit> Manage spaces
1560
3105
  create accepts --parent <id> to set parent-child relationship
1561
- remnic benchmark <run|check|report> [queries...] [--explain] [--baseline=<path>] [--report=<path>]
3106
+ remnic bench <list|run> [benchmark...] [--quick] [--all] [--dataset-dir <path>] [--json]
3107
+ benchmark is kept as a compatibility alias. check/report remain under that alias.
3108
+ remnic benchmark <list|run|check|report> [queries...] [--explain] [--baseline=<path>] [--report=<path>]
3109
+ remnic briefing [--since <window>] [--focus <filter>] [--save] [--format markdown|json]
3110
+ Daily context briefing. Windows: yesterday, today, NNh, NNd, NNw.
3111
+ Focus: person:<name>, project:<name>, topic:<name>.
3112
+ remnic versions <list|show|diff|revert> <page-path> [id] [--json]
3113
+ Page-level versioning: list, show, diff, or revert page snapshots.
3114
+ remnic binary scan Scan for untracked binary files
3115
+ remnic binary status Show binary lifecycle manifest summary
3116
+ remnic binary run [--dry-run] Run full binary lifecycle pipeline
3117
+ remnic binary clean --force Force-clean binaries past grace period
3118
+ remnic taxonomy <show|resolver|add|remove|resolve> MECE knowledge directory
3119
+ show [--json] Show current taxonomy
3120
+ resolver Print/regenerate RESOLVER.md
3121
+ add <id> <name> [--priority N] Add custom category
3122
+ remove <id> Remove custom category
3123
+ resolve <text> [--category <cat>] Test resolver on sample text
3124
+ remnic enrich <entity-name> Manually enrich a specific entity
3125
+ remnic enrich --all Enrich all entities
3126
+ remnic enrich --dry-run Preview what would be enriched
3127
+ remnic enrich audit Show recent enrichment audit log
3128
+ remnic enrich providers List registered providers and their status
1562
3129
 
1563
3130
  Options:
1564
3131
  --json Output in JSON format
@@ -1577,5 +3144,15 @@ if (argv1Base.endsWith("remnic.ts") || argv1Base.endsWith("remnic.js") || argv1B
1577
3144
  });
1578
3145
  }
1579
3146
  export {
1580
- main
3147
+ BENCHMARK_CATALOG,
3148
+ TAXONOMY_RESOLVE_BOOLEAN_FLAGS,
3149
+ buildBenchRunnerArgs,
3150
+ getBenchUsageText,
3151
+ hasFlag,
3152
+ main,
3153
+ parseBenchArgs,
3154
+ parseConnectorConfig,
3155
+ resolveFlag,
3156
+ stripConfigArgv,
3157
+ stripResolveFlags
1581
3158
  };