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