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