@remnic/cli 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/engram.cjs +56 -0
- package/bin/remnic.cjs +44 -0
- package/dist/index.js +1581 -0
- package/package.json +45 -0
- package/templates/launchd/ai.remnic.daemon.plist +43 -0
- package/templates/systemd/remnic.service +17 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1581 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import fs2 from "fs";
|
|
3
|
+
import path2 from "path";
|
|
4
|
+
import * as childProcess from "child_process";
|
|
5
|
+
import {
|
|
6
|
+
parseConfig,
|
|
7
|
+
Orchestrator,
|
|
8
|
+
EngramAccessService,
|
|
9
|
+
initLogger,
|
|
10
|
+
onboard,
|
|
11
|
+
curate,
|
|
12
|
+
listReviewItems,
|
|
13
|
+
performReview,
|
|
14
|
+
syncChanges,
|
|
15
|
+
watchForChanges,
|
|
16
|
+
findDuplicates,
|
|
17
|
+
listConnectors,
|
|
18
|
+
installConnector,
|
|
19
|
+
removeConnector,
|
|
20
|
+
doctorConnector,
|
|
21
|
+
generateToken,
|
|
22
|
+
listTokens,
|
|
23
|
+
revokeToken,
|
|
24
|
+
listSpaces,
|
|
25
|
+
getActiveSpace,
|
|
26
|
+
createSpace,
|
|
27
|
+
deleteSpace,
|
|
28
|
+
switchSpace,
|
|
29
|
+
pushToSpace,
|
|
30
|
+
pullFromSpace,
|
|
31
|
+
shareSpace,
|
|
32
|
+
promoteSpace,
|
|
33
|
+
getAuditLog,
|
|
34
|
+
getManifestPath,
|
|
35
|
+
generateContextTree,
|
|
36
|
+
migrateFromEngram,
|
|
37
|
+
rollbackFromEngramMigration
|
|
38
|
+
} from "@remnic/core";
|
|
39
|
+
|
|
40
|
+
// ../bench/src/benchmark.ts
|
|
41
|
+
import fs from "fs";
|
|
42
|
+
import path from "path";
|
|
43
|
+
var DEFAULT_BASELINE_PATH = path.join(
|
|
44
|
+
process.cwd(),
|
|
45
|
+
"benchmarks",
|
|
46
|
+
"baseline.json"
|
|
47
|
+
);
|
|
48
|
+
var DEFAULT_REPORT_PATH = path.join(
|
|
49
|
+
process.cwd(),
|
|
50
|
+
"benchmarks",
|
|
51
|
+
"report.json"
|
|
52
|
+
);
|
|
53
|
+
var BASELINE_VERSION = 1;
|
|
54
|
+
var DEFAULT_QUERIES = [
|
|
55
|
+
"What is the storage?",
|
|
56
|
+
"How do I access storage?",
|
|
57
|
+
"What categories exist?",
|
|
58
|
+
"How is memory organized?",
|
|
59
|
+
"What is the recall budget?",
|
|
60
|
+
"What is the extraction pipeline?",
|
|
61
|
+
"What facts are stored about the project?",
|
|
62
|
+
"What is the architecture?"
|
|
63
|
+
];
|
|
64
|
+
var DEFAULT_TOLERANCE = 10;
|
|
65
|
+
function hrTimeMs() {
|
|
66
|
+
const [s, ns] = process.hrtime();
|
|
67
|
+
return s * 1e3 + Math.round(ns / 1e6);
|
|
68
|
+
}
|
|
69
|
+
function loadBaseline(baselinePath) {
|
|
70
|
+
const p = baselinePath ?? DEFAULT_BASELINE_PATH;
|
|
71
|
+
if (!fs.existsSync(p)) return void 0;
|
|
72
|
+
try {
|
|
73
|
+
const raw = JSON.parse(fs.readFileSync(p, "utf8"));
|
|
74
|
+
if (raw.version !== BASELINE_VERSION) {
|
|
75
|
+
console.warn(
|
|
76
|
+
`Baseline version mismatch: expected ${BASELINE_VERSION}, got ${raw.version}`
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
return raw;
|
|
80
|
+
} catch {
|
|
81
|
+
return void 0;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function saveBaseline(baselinePath, baseline) {
|
|
85
|
+
fs.mkdirSync(path.dirname(baselinePath), { recursive: true });
|
|
86
|
+
fs.writeFileSync(baselinePath, JSON.stringify(baseline, null, 2) + "\n");
|
|
87
|
+
}
|
|
88
|
+
async function recallWithTiers(service, query) {
|
|
89
|
+
const tiers = [];
|
|
90
|
+
const tierDetails = [];
|
|
91
|
+
const t0 = hrTimeMs();
|
|
92
|
+
const r0 = await service.recall({ query, mode: "auto" });
|
|
93
|
+
const d0 = hrTimeMs() - t0;
|
|
94
|
+
if (r0.results && r0.results.length > 0) {
|
|
95
|
+
const hasExactMatch = r0.results.some(
|
|
96
|
+
(m) => m.preview.toLowerCase().includes(query.toLowerCase())
|
|
97
|
+
);
|
|
98
|
+
if (hasExactMatch) {
|
|
99
|
+
tiers.push("exact_match");
|
|
100
|
+
tierDetails.push({
|
|
101
|
+
tier: "exact_match",
|
|
102
|
+
latencyMs: d0,
|
|
103
|
+
resultsCount: r0.results.length
|
|
104
|
+
});
|
|
105
|
+
return { tiers, tierDetails };
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const t1 = hrTimeMs();
|
|
109
|
+
const r1 = await service.recall({ query, mode: "auto" });
|
|
110
|
+
const d1 = hrTimeMs() - t1;
|
|
111
|
+
const queryWords = query.toLowerCase().split(/\s+/).filter((w) => w.length > 2);
|
|
112
|
+
const hasKeywordMatch = (r1.results ?? []).some(
|
|
113
|
+
(m) => queryWords.some((kw) => m.preview.toLowerCase().includes(kw))
|
|
114
|
+
);
|
|
115
|
+
if (hasKeywordMatch) {
|
|
116
|
+
tiers.push("category_match");
|
|
117
|
+
tierDetails.push({
|
|
118
|
+
tier: "category_match",
|
|
119
|
+
latencyMs: d1,
|
|
120
|
+
resultsCount: r1.results.length
|
|
121
|
+
});
|
|
122
|
+
return { tiers, tierDetails };
|
|
123
|
+
}
|
|
124
|
+
const t2 = hrTimeMs();
|
|
125
|
+
const r2 = await service.recall({ query, mode: "auto" });
|
|
126
|
+
const d2 = hrTimeMs() - t2;
|
|
127
|
+
const tagged = (r2.results ?? []).filter((m) => m.tags && m.tags.length > 0);
|
|
128
|
+
if (tagged.length > 0) {
|
|
129
|
+
tiers.push("high_confidence");
|
|
130
|
+
tierDetails.push({
|
|
131
|
+
tier: "high_confidence",
|
|
132
|
+
latencyMs: d2,
|
|
133
|
+
resultsCount: tagged.length
|
|
134
|
+
});
|
|
135
|
+
return { tiers, tierDetails };
|
|
136
|
+
}
|
|
137
|
+
const t3 = hrTimeMs();
|
|
138
|
+
const r3 = await service.recall({ query, mode: "auto" });
|
|
139
|
+
const d3 = hrTimeMs() - t3;
|
|
140
|
+
if (r3.results && r3.results.length > 0) {
|
|
141
|
+
tiers.push("semantic_search");
|
|
142
|
+
tierDetails.push({
|
|
143
|
+
tier: "semantic_search",
|
|
144
|
+
latencyMs: d3,
|
|
145
|
+
resultsCount: r3.results.length
|
|
146
|
+
});
|
|
147
|
+
return { tiers, tierDetails };
|
|
148
|
+
}
|
|
149
|
+
const t4 = hrTimeMs();
|
|
150
|
+
const r4 = await service.recall({ query, mode: "full" });
|
|
151
|
+
const d4 = hrTimeMs() - t4;
|
|
152
|
+
if (r4.results && r4.results.length > 0) {
|
|
153
|
+
tiers.push("full_search");
|
|
154
|
+
tierDetails.push({
|
|
155
|
+
tier: "full_search",
|
|
156
|
+
latencyMs: d4,
|
|
157
|
+
resultsCount: r4.results.length
|
|
158
|
+
});
|
|
159
|
+
return { tiers, tierDetails };
|
|
160
|
+
}
|
|
161
|
+
tiers.push("no_results");
|
|
162
|
+
tierDetails.push({
|
|
163
|
+
tier: "no_results",
|
|
164
|
+
latencyMs: d0 + d1 + d2 + d3 + d4,
|
|
165
|
+
resultsCount: 0
|
|
166
|
+
});
|
|
167
|
+
return { tiers, tierDetails };
|
|
168
|
+
}
|
|
169
|
+
async function runExplain(service, query) {
|
|
170
|
+
const start = hrTimeMs();
|
|
171
|
+
const { tiers, tierDetails } = await recallWithTiers(service, query);
|
|
172
|
+
const totalDuration = hrTimeMs() - start;
|
|
173
|
+
return {
|
|
174
|
+
query,
|
|
175
|
+
tiersUsed: tiers,
|
|
176
|
+
tierResults: tierDetails,
|
|
177
|
+
durationMs: tierDetails[0]?.latencyMs ?? totalDuration,
|
|
178
|
+
totalDurationMs: totalDuration
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
async function runSingle(service, queryText) {
|
|
182
|
+
const start = hrTimeMs();
|
|
183
|
+
const { tiers, tierDetails } = await recallWithTiers(service, queryText);
|
|
184
|
+
const duration = hrTimeMs() - start;
|
|
185
|
+
return {
|
|
186
|
+
query: queryText,
|
|
187
|
+
latencyMs: duration,
|
|
188
|
+
tiersUsed: tiers,
|
|
189
|
+
throughput: duration > 0 ? 1 / (duration / 1e3) : 0,
|
|
190
|
+
resultsCount: tierDetails.reduce((sum, t) => sum + t.resultsCount, 0),
|
|
191
|
+
totalDurationMs: duration,
|
|
192
|
+
tierDetails
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
async function runBenchSuite(service, config = {}) {
|
|
196
|
+
const queries = config.queries ?? DEFAULT_QUERIES;
|
|
197
|
+
const regressionTolerance = config.regressionTolerance ?? DEFAULT_TOLERANCE;
|
|
198
|
+
const baselinePath = config.baselinePath ?? DEFAULT_BASELINE_PATH;
|
|
199
|
+
const reportPath = config.reportPath ?? DEFAULT_REPORT_PATH;
|
|
200
|
+
const explain = config.explain ?? false;
|
|
201
|
+
const results = [];
|
|
202
|
+
const suiteStart = hrTimeMs();
|
|
203
|
+
for (const q of queries) {
|
|
204
|
+
if (explain) {
|
|
205
|
+
const ex = await runExplain(service, q);
|
|
206
|
+
results.push({
|
|
207
|
+
query: ex.query,
|
|
208
|
+
latencyMs: ex.durationMs,
|
|
209
|
+
tiersUsed: ex.tiersUsed,
|
|
210
|
+
throughput: ex.totalDurationMs > 0 ? 1 / (ex.totalDurationMs / 1e3) : 0,
|
|
211
|
+
resultsCount: ex.tierResults.reduce(
|
|
212
|
+
(sum, t) => sum + t.resultsCount,
|
|
213
|
+
0
|
|
214
|
+
),
|
|
215
|
+
totalDurationMs: ex.totalDurationMs,
|
|
216
|
+
tierDetails: ex.tierResults
|
|
217
|
+
});
|
|
218
|
+
} else {
|
|
219
|
+
results.push(await runSingle(service, q));
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
const totalDuration = hrTimeMs() - suiteStart;
|
|
223
|
+
const metrics = {};
|
|
224
|
+
for (const r of results) {
|
|
225
|
+
metrics[r.query] = r.latencyMs;
|
|
226
|
+
}
|
|
227
|
+
const report = generateReport(results, reportPath);
|
|
228
|
+
const baseline = loadBaseline(baselinePath);
|
|
229
|
+
const regressionResult = checkRegression(
|
|
230
|
+
metrics,
|
|
231
|
+
baseline,
|
|
232
|
+
regressionTolerance
|
|
233
|
+
);
|
|
234
|
+
if (!baseline) {
|
|
235
|
+
saveBaseline(baselinePath, {
|
|
236
|
+
version: BASELINE_VERSION,
|
|
237
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
238
|
+
metrics
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
return {
|
|
242
|
+
results,
|
|
243
|
+
report,
|
|
244
|
+
totalDurationMs: totalDuration,
|
|
245
|
+
regressions: regressionResult.regressions
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
function checkRegression(metrics, baseline, tolerance) {
|
|
249
|
+
if (!baseline) return { passed: true, regressions: [] };
|
|
250
|
+
const regressions = [];
|
|
251
|
+
for (const [metricName, currentValue] of Object.entries(metrics)) {
|
|
252
|
+
const baselineValue = baseline.metrics[metricName];
|
|
253
|
+
if (baselineValue === void 0) continue;
|
|
254
|
+
const changePercent = baselineValue > 0 ? (currentValue - baselineValue) / baselineValue * 100 : 0;
|
|
255
|
+
regressions.push({
|
|
256
|
+
metric: metricName,
|
|
257
|
+
currentValue,
|
|
258
|
+
baselineValue,
|
|
259
|
+
tolerance,
|
|
260
|
+
passed: changePercent <= tolerance
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
return {
|
|
264
|
+
passed: regressions.every((r) => r.passed),
|
|
265
|
+
regressions
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
function generateReport(results, reportPath) {
|
|
269
|
+
const report = {
|
|
270
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
271
|
+
queries: results.map((r) => ({
|
|
272
|
+
query: r.query,
|
|
273
|
+
tiersUsed: r.tiersUsed,
|
|
274
|
+
durationMs: r.latencyMs,
|
|
275
|
+
resultsCount: r.resultsCount,
|
|
276
|
+
throughput: r.throughput,
|
|
277
|
+
tierDetails: r.tierDetails
|
|
278
|
+
})),
|
|
279
|
+
totalDurationMs: results.reduce(
|
|
280
|
+
(sum, r) => sum + r.totalDurationMs,
|
|
281
|
+
0
|
|
282
|
+
)
|
|
283
|
+
};
|
|
284
|
+
if (reportPath) {
|
|
285
|
+
fs.mkdirSync(path.dirname(reportPath), { recursive: true });
|
|
286
|
+
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2) + "\n");
|
|
287
|
+
}
|
|
288
|
+
return report;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// src/service-candidates.ts
|
|
292
|
+
function firstSuccessfulResult(candidates, attempt) {
|
|
293
|
+
for (const candidate of candidates) {
|
|
294
|
+
try {
|
|
295
|
+
const result = attempt(candidate);
|
|
296
|
+
if (result !== void 0) return result;
|
|
297
|
+
} catch {
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return void 0;
|
|
301
|
+
}
|
|
302
|
+
function firstSuccessfulCandidate(candidates, attempt) {
|
|
303
|
+
return firstSuccessfulResult(candidates, (candidate) => {
|
|
304
|
+
attempt(candidate);
|
|
305
|
+
return candidate;
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// src/index.ts
|
|
310
|
+
function readCompatEnv(primary, legacy) {
|
|
311
|
+
return process.env[primary] ?? process.env[legacy];
|
|
312
|
+
}
|
|
313
|
+
function resolveHomeDir() {
|
|
314
|
+
return process.env.HOME ?? process.env.USERPROFILE ?? "~";
|
|
315
|
+
}
|
|
316
|
+
var PID_DIR = path2.join(resolveHomeDir(), ".remnic");
|
|
317
|
+
var LEGACY_PID_DIR = path2.join(resolveHomeDir(), ".engram");
|
|
318
|
+
var PID_FILE = path2.join(PID_DIR, "server.pid");
|
|
319
|
+
var LEGACY_PID_FILE = path2.join(LEGACY_PID_DIR, "server.pid");
|
|
320
|
+
var LOG_FILE = path2.join(PID_DIR, "server.log");
|
|
321
|
+
var LEGACY_LOG_FILE = path2.join(LEGACY_PID_DIR, "server.log");
|
|
322
|
+
function resolveConfigPath(cliPath) {
|
|
323
|
+
if (cliPath) return path2.resolve(cliPath);
|
|
324
|
+
const envPath = readCompatEnv("REMNIC_CONFIG_PATH", "ENGRAM_CONFIG_PATH");
|
|
325
|
+
if (envPath) return path2.resolve(envPath);
|
|
326
|
+
const candidates = [
|
|
327
|
+
path2.join(process.cwd(), "remnic.config.json"),
|
|
328
|
+
path2.join(process.cwd(), "engram.config.json"),
|
|
329
|
+
path2.join(resolveHomeDir(), ".config", "remnic", "config.json"),
|
|
330
|
+
path2.join(resolveHomeDir(), ".config", "engram", "config.json")
|
|
331
|
+
];
|
|
332
|
+
for (const candidate of candidates) {
|
|
333
|
+
if (fs2.existsSync(candidate)) return candidate;
|
|
334
|
+
}
|
|
335
|
+
return path2.join(resolveHomeDir(), ".config", "remnic", "config.json");
|
|
336
|
+
}
|
|
337
|
+
function resolveMemoryDir() {
|
|
338
|
+
const configMemoryDir = (() => {
|
|
339
|
+
const envMemoryDir = readCompatEnv("REMNIC_MEMORY_DIR", "ENGRAM_MEMORY_DIR");
|
|
340
|
+
if (envMemoryDir) return envMemoryDir;
|
|
341
|
+
const configPath = resolveConfigPath();
|
|
342
|
+
const raw = fs2.existsSync(configPath) ? JSON.parse(fs2.readFileSync(configPath, "utf8")) : {};
|
|
343
|
+
const remnicCfg = raw.remnic ?? raw.engram ?? raw;
|
|
344
|
+
if (remnicCfg.memoryDir) return remnicCfg.memoryDir;
|
|
345
|
+
const home = resolveHomeDir();
|
|
346
|
+
const standalonePath = path2.join(home, ".remnic", "memory");
|
|
347
|
+
const legacyStandalonePath = path2.join(home, ".engram", "memory");
|
|
348
|
+
const openclawPath = path2.join(home, ".openclaw", "workspace", "memory", "local");
|
|
349
|
+
if (fs2.existsSync(standalonePath)) return standalonePath;
|
|
350
|
+
if (fs2.existsSync(legacyStandalonePath)) return legacyStandalonePath;
|
|
351
|
+
return openclawPath;
|
|
352
|
+
})();
|
|
353
|
+
const manifestPath = getManifestPath();
|
|
354
|
+
if (fs2.existsSync(manifestPath)) {
|
|
355
|
+
try {
|
|
356
|
+
const active = getActiveSpace();
|
|
357
|
+
if (active?.memoryDir) {
|
|
358
|
+
if (!fs2.existsSync(active.memoryDir)) {
|
|
359
|
+
fs2.mkdirSync(active.memoryDir, { recursive: true });
|
|
360
|
+
}
|
|
361
|
+
return active.memoryDir;
|
|
362
|
+
}
|
|
363
|
+
} catch (err) {
|
|
364
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
365
|
+
if (!msg.includes("not found")) {
|
|
366
|
+
console.error(`Error: failed to resolve active space from ${manifestPath}: ${msg}`);
|
|
367
|
+
process.exit(1);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return configMemoryDir;
|
|
372
|
+
}
|
|
373
|
+
function resolveFlag(args, flag) {
|
|
374
|
+
const idx = args.indexOf(flag);
|
|
375
|
+
return idx !== -1 && idx + 1 < args.length ? args[idx + 1] : void 0;
|
|
376
|
+
}
|
|
377
|
+
function parseConnectorConfig(args) {
|
|
378
|
+
const config = {};
|
|
379
|
+
for (const arg of args) {
|
|
380
|
+
if (arg.startsWith("--config=")) {
|
|
381
|
+
const [key, value] = arg.slice("--config=".length).split("=");
|
|
382
|
+
if (key && value) config[key] = value;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
return config;
|
|
386
|
+
}
|
|
387
|
+
function cmdInit() {
|
|
388
|
+
const configPath = path2.join(process.cwd(), "remnic.config.json");
|
|
389
|
+
if (fs2.existsSync(configPath)) {
|
|
390
|
+
console.log(`Config already exists: ${configPath}`);
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
const template = {
|
|
394
|
+
remnic: {
|
|
395
|
+
openaiApiKey: "${OPENAI_API_KEY}",
|
|
396
|
+
memoryDir: path2.join(process.cwd(), ".remnic", "memory"),
|
|
397
|
+
memoryOsPreset: "balanced"
|
|
398
|
+
},
|
|
399
|
+
server: {
|
|
400
|
+
host: "127.0.0.1",
|
|
401
|
+
port: 4318,
|
|
402
|
+
authToken: "${REMNIC_AUTH_TOKEN}"
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
fs2.writeFileSync(configPath, JSON.stringify(template, null, 2) + "\n");
|
|
406
|
+
console.log(`Created ${configPath}`);
|
|
407
|
+
console.log("\nSet these environment variables:");
|
|
408
|
+
console.log(" export OPENAI_API_KEY=sk-...");
|
|
409
|
+
console.log(" export REMNIC_AUTH_TOKEN=$(openssl rand -hex 32)");
|
|
410
|
+
console.log(" # ENGRAM_AUTH_TOKEN is still accepted during v1.x");
|
|
411
|
+
console.log("\nThen start the server:");
|
|
412
|
+
console.log(" npx remnic-server");
|
|
413
|
+
}
|
|
414
|
+
async function cmdStatus(json) {
|
|
415
|
+
const { running, pid } = isServiceRunning();
|
|
416
|
+
if (json) {
|
|
417
|
+
console.log(JSON.stringify({ running, pid: pid ?? null, pidFile: PID_FILE, logFile: LOG_FILE }));
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
if (!running) {
|
|
421
|
+
console.log("Remnic server: stopped");
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
console.log(`Remnic server: running${pid ? ` (pid ${pid})` : ""}`);
|
|
425
|
+
const port = inferPort();
|
|
426
|
+
const controller = new AbortController();
|
|
427
|
+
const timeoutId = setTimeout(() => controller.abort(), 3e3);
|
|
428
|
+
try {
|
|
429
|
+
const response = await fetch(`http://127.0.0.1:${port}/engram/v1/health`, {
|
|
430
|
+
signal: controller.signal
|
|
431
|
+
});
|
|
432
|
+
if (!response.ok) {
|
|
433
|
+
console.log(`Health: server responded with ${response.status} ${response.statusText}`);
|
|
434
|
+
} else {
|
|
435
|
+
const health = await response.json();
|
|
436
|
+
console.log(`Health: ${health.status ?? "ok"}`);
|
|
437
|
+
}
|
|
438
|
+
} catch {
|
|
439
|
+
console.log("Health: unable to reach server");
|
|
440
|
+
} finally {
|
|
441
|
+
clearTimeout(timeoutId);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
async function cmdQuery(queryText, json, explain) {
|
|
445
|
+
if (!queryText) {
|
|
446
|
+
console.error("Usage: remnic query <text>");
|
|
447
|
+
process.exit(1);
|
|
448
|
+
}
|
|
449
|
+
initLogger();
|
|
450
|
+
const configPath = resolveConfigPath();
|
|
451
|
+
const raw = fs2.existsSync(configPath) ? JSON.parse(fs2.readFileSync(configPath, "utf8")) : {};
|
|
452
|
+
const remnicCfg = raw.remnic ?? raw.engram ?? raw;
|
|
453
|
+
const config = parseConfig(remnicCfg);
|
|
454
|
+
const orchestrator = new Orchestrator(config);
|
|
455
|
+
await orchestrator.initialize();
|
|
456
|
+
const service = new EngramAccessService(orchestrator);
|
|
457
|
+
if (explain) {
|
|
458
|
+
const result2 = await runExplain(service, queryText);
|
|
459
|
+
if (json) {
|
|
460
|
+
console.log(JSON.stringify(result2, null, 2));
|
|
461
|
+
} else {
|
|
462
|
+
console.log(`Query: ${result2.query}`);
|
|
463
|
+
console.log(`Tiers used: ${result2.tiersUsed.join(" \u2192 ")}`);
|
|
464
|
+
console.log(`Total duration: ${result2.totalDurationMs}ms`);
|
|
465
|
+
for (const t of result2.tierResults) {
|
|
466
|
+
console.log(` ${t.tier}: ${t.latencyMs}ms (${t.resultsCount} results)`);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
const result = await service.recall({ query: queryText, mode: "auto" });
|
|
472
|
+
if (json) {
|
|
473
|
+
console.log(JSON.stringify(result, null, 2));
|
|
474
|
+
} else {
|
|
475
|
+
const memories = result.memories ?? [];
|
|
476
|
+
if (memories.length === 0) {
|
|
477
|
+
console.log("No results.");
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
for (const m of memories) {
|
|
481
|
+
console.log(`- ${m.content}`);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
function cmdDoctor() {
|
|
486
|
+
const checks = [];
|
|
487
|
+
const nodeVersion = process.version;
|
|
488
|
+
const nodeMajor = parseInt(nodeVersion.slice(1).split(".")[0], 10);
|
|
489
|
+
checks.push({
|
|
490
|
+
name: "Node.js version",
|
|
491
|
+
ok: nodeMajor >= 22,
|
|
492
|
+
detail: `${nodeVersion} (requires >= 22.12.0)`
|
|
493
|
+
});
|
|
494
|
+
const configPath = resolveConfigPath();
|
|
495
|
+
const configExists = fs2.existsSync(configPath);
|
|
496
|
+
checks.push({ name: "Config file", ok: configExists, detail: configPath });
|
|
497
|
+
const hasApiKey = !!process.env.OPENAI_API_KEY;
|
|
498
|
+
checks.push({
|
|
499
|
+
name: "OPENAI_API_KEY",
|
|
500
|
+
ok: hasApiKey,
|
|
501
|
+
detail: hasApiKey ? "set" : "not set (extraction will not work)"
|
|
502
|
+
});
|
|
503
|
+
const memoryDir = resolveMemoryDir();
|
|
504
|
+
try {
|
|
505
|
+
fs2.mkdirSync(memoryDir, { recursive: true });
|
|
506
|
+
checks.push({ name: "Memory directory", ok: true, detail: memoryDir });
|
|
507
|
+
} catch {
|
|
508
|
+
checks.push({ name: "Memory directory", ok: false, detail: `cannot create ${memoryDir}` });
|
|
509
|
+
}
|
|
510
|
+
const svcState = isServiceRunning();
|
|
511
|
+
checks.push({
|
|
512
|
+
name: "Server daemon",
|
|
513
|
+
ok: svcState.running,
|
|
514
|
+
detail: svcState.running ? `running${svcState.pid ? ` (pid ${svcState.pid})` : ""}` : "stopped"
|
|
515
|
+
});
|
|
516
|
+
for (const check of checks) {
|
|
517
|
+
const icon = check.ok ? "\u2713" : "\u2717";
|
|
518
|
+
console.log(` ${icon} ${check.name}: ${check.detail}`);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
function cmdConfig() {
|
|
522
|
+
const configPath = resolveConfigPath();
|
|
523
|
+
if (!fs2.existsSync(configPath)) {
|
|
524
|
+
console.log("No config file found. Run `remnic init` to create one.");
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
console.log(`Config: ${configPath}`);
|
|
528
|
+
const rawConfig = fs2.readFileSync(configPath, "utf8");
|
|
529
|
+
const redacted = rawConfig.replace(
|
|
530
|
+
/("(?:openaiApiKey|localLlmApiKey|authToken|apiKey|remoteSearchApiKey|meilisearchApiKey|opikApiKey)"\s*:\s*")([^"]*)(")/g,
|
|
531
|
+
"$1[REDACTED]$3"
|
|
532
|
+
);
|
|
533
|
+
console.log(redacted);
|
|
534
|
+
}
|
|
535
|
+
async function cmdMigrate(json, rollback) {
|
|
536
|
+
if (rollback) {
|
|
537
|
+
const result2 = await rollbackFromEngramMigration({ quiet: json });
|
|
538
|
+
if (json) {
|
|
539
|
+
console.log(JSON.stringify(result2, null, 2));
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
if (result2.restored.length === 0 && result2.removed.length === 0) {
|
|
543
|
+
console.log("No migration rollback state found.");
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
console.log("Rollback complete.");
|
|
547
|
+
if (result2.restored.length > 0) {
|
|
548
|
+
console.log(` Restored: ${result2.restored.length}`);
|
|
549
|
+
}
|
|
550
|
+
if (result2.removed.length > 0) {
|
|
551
|
+
console.log(` Removed: ${result2.removed.length}`);
|
|
552
|
+
}
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
const result = await migrateFromEngram({ quiet: json });
|
|
556
|
+
if (json) {
|
|
557
|
+
console.log(JSON.stringify(result, null, 2));
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
if (result.status === "fresh-install") {
|
|
561
|
+
console.log("No Engram install found. Nothing to migrate.");
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
if (result.status === "already-migrated") {
|
|
565
|
+
console.log("Migration already completed.");
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
console.log("Migration complete.");
|
|
569
|
+
console.log(` Copied: ${result.copied.length}`);
|
|
570
|
+
console.log(` Tokens rewritten: ${result.tokensRegenerated}`);
|
|
571
|
+
console.log(` Services updated: ${result.servicesReinstalled.length}`);
|
|
572
|
+
console.log(` Rollback: ${result.rollbackCommand}`);
|
|
573
|
+
}
|
|
574
|
+
function cmdOnboard(dirPath, json) {
|
|
575
|
+
const directory = path2.resolve(dirPath || process.cwd());
|
|
576
|
+
const result = onboard({ directory });
|
|
577
|
+
if (json) {
|
|
578
|
+
console.log(JSON.stringify(result, null, 2));
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
console.log(`Shape: ${result.shape}`);
|
|
582
|
+
console.log(`Languages: ${result.languages.map((l) => `${l.language} (${(l.confidence * 100).toFixed(0)}%)`).join(", ")}`);
|
|
583
|
+
console.log(`Docs: ${result.docs.length} file(s)`);
|
|
584
|
+
console.log(result.docs.map((s) => ` ${s.kind} (${s.size} bytes)`).join("\n"));
|
|
585
|
+
console.log(`Plan: ${result.plan.priorityFiles.length} priority, ${result.plan.estimatedFiles} total files`);
|
|
586
|
+
console.log(`
|
|
587
|
+
Suggested namespace: ${result.plan.suggestedNamespace}`);
|
|
588
|
+
console.log(`Total files: ${result.totalFiles}`);
|
|
589
|
+
console.log(`Duration: ${result.durationMs}ms`);
|
|
590
|
+
}
|
|
591
|
+
async function cmdCurate(targetPath, json) {
|
|
592
|
+
const memoryDir = resolveMemoryDir();
|
|
593
|
+
const result = await curate({
|
|
594
|
+
targetPath: path2.resolve(targetPath),
|
|
595
|
+
memoryDir,
|
|
596
|
+
source: "curation",
|
|
597
|
+
checkDuplicates: true,
|
|
598
|
+
checkContradictions: true
|
|
599
|
+
});
|
|
600
|
+
if (json) {
|
|
601
|
+
console.log(JSON.stringify(result, null, 2));
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
console.log(`Files: ${result.filesProcessed} processed, ${result.filesSkipped} skipped`);
|
|
605
|
+
console.log(`Statements: ${result.statements.length}`);
|
|
606
|
+
if (result.duplicates.length > 0) console.log(`Duplicates: ${result.duplicates.length}`);
|
|
607
|
+
if (result.contradictions.length > 0) console.log(`Contradictions: ${result.contradictions.length}`);
|
|
608
|
+
console.log(`Written: ${result.written.length}`);
|
|
609
|
+
console.log(`Duration: ${result.durationMs}ms`);
|
|
610
|
+
}
|
|
611
|
+
function cmdReview(action, rest) {
|
|
612
|
+
const memoryDir = resolveMemoryDir();
|
|
613
|
+
if (action === "list") {
|
|
614
|
+
const result = listReviewItems({ memoryDir });
|
|
615
|
+
if (result.items.length === 0) {
|
|
616
|
+
console.log("No items pending review.");
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
for (const item of result.items) {
|
|
620
|
+
console.log(`[${item.reviewReason}] ${item.id} ${item.content.slice(0, 80)}${item.content.length > 80 ? "..." : ""}`);
|
|
621
|
+
console.log(` Confidence: ${item.confidence} | Category: ${item.category}`);
|
|
622
|
+
console.log(` Source: ${item.source} | Created: ${item.created}`);
|
|
623
|
+
}
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
if (action === "approve" || action === "dismiss" || action === "flag") {
|
|
627
|
+
const id = rest[0];
|
|
628
|
+
if (!id) {
|
|
629
|
+
console.error("Usage: remnic review <approve|dismiss|flag> <id>");
|
|
630
|
+
process.exit(1);
|
|
631
|
+
}
|
|
632
|
+
const result = performReview(memoryDir, id, action);
|
|
633
|
+
console.log(result.message);
|
|
634
|
+
} else {
|
|
635
|
+
console.log("Usage: remnic review <list|approve|dismiss|flag> [id]");
|
|
636
|
+
process.exit(1);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
function cmdSync(action, rest, json) {
|
|
640
|
+
const sourceIdx = rest.indexOf("--source");
|
|
641
|
+
const sourceDir = sourceIdx >= 0 && rest[sourceIdx + 1] ? rest[sourceIdx + 1] : ".";
|
|
642
|
+
const memoryDir = resolveMemoryDir();
|
|
643
|
+
if (action === "run") {
|
|
644
|
+
const result = syncChanges({ sourceDir, memoryDir });
|
|
645
|
+
if (json) {
|
|
646
|
+
console.log(JSON.stringify(result, null, 2));
|
|
647
|
+
} else {
|
|
648
|
+
console.log(`Scanned: ${result.scanned}`);
|
|
649
|
+
console.log(`Added: ${result.added.length}`);
|
|
650
|
+
console.log(`Modified: ${result.changed.filter((c) => c.type === "modified").length}`);
|
|
651
|
+
console.log(`Deleted: ${result.deleted.length}`);
|
|
652
|
+
console.log(`Unchanged: ${result.unchanged}`);
|
|
653
|
+
console.log(`Duration: ${result.durationMs}ms`);
|
|
654
|
+
}
|
|
655
|
+
} else if (action === "watch") {
|
|
656
|
+
const { stop } = watchForChanges(
|
|
657
|
+
{ sourceDir, memoryDir },
|
|
658
|
+
(changes) => {
|
|
659
|
+
console.log(`Changed: ${changes.length} file(s)`);
|
|
660
|
+
for (const c of changes) {
|
|
661
|
+
console.log(` [${c.type}] ${c.relativePath}`);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
);
|
|
665
|
+
console.log("Watching... (Ctrl+C to stop)");
|
|
666
|
+
process.on("SIGINT", () => {
|
|
667
|
+
stop();
|
|
668
|
+
console.log("Stopped watching.");
|
|
669
|
+
});
|
|
670
|
+
} else {
|
|
671
|
+
console.log("Usage: remnic sync <run|watch> [--source <dir>]");
|
|
672
|
+
process.exit(1);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
function cmdDedup(json) {
|
|
676
|
+
const memoryDir = resolveMemoryDir();
|
|
677
|
+
const result = findDuplicates({ memoryDir });
|
|
678
|
+
if (json) {
|
|
679
|
+
console.log(JSON.stringify(result, null, 2));
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
console.log(`Scanned: ${result.scanned} memories`);
|
|
683
|
+
console.log(`Found ${result.duplicates.length} duplicate pairs`);
|
|
684
|
+
for (const dup of result.duplicates) {
|
|
685
|
+
console.log(` [${dup.action}] ${dup.left.content.slice(0, 60)}...`);
|
|
686
|
+
console.log(` vs: ${dup.right.content.slice(0, 60)}...`);
|
|
687
|
+
console.log(` Similarity: ${(dup.similarity * 100).toFixed(2)}%`);
|
|
688
|
+
}
|
|
689
|
+
console.log(`Duration: ${result.durationMs}ms`);
|
|
690
|
+
}
|
|
691
|
+
async function cmdConnectors(action, rest, json) {
|
|
692
|
+
const nonFlagArgs = rest.filter((a) => !a.startsWith("--"));
|
|
693
|
+
const connectorId = nonFlagArgs[0];
|
|
694
|
+
if (action === "list") {
|
|
695
|
+
const { installed, available } = listConnectors();
|
|
696
|
+
if (json) {
|
|
697
|
+
console.log(JSON.stringify({ installed, available }, null, 2));
|
|
698
|
+
} else {
|
|
699
|
+
console.log("Available connectors:");
|
|
700
|
+
for (const c of available) {
|
|
701
|
+
const icon = c.installed ? "\u2713" : "\u25CB";
|
|
702
|
+
console.log(` ${icon} ${c.id.padEnd(22)} ${c.name} v${c.version} \u2014 ${c.description}`);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
} else if (action === "install") {
|
|
706
|
+
if (!connectorId) {
|
|
707
|
+
console.error("Usage: remnic connectors install <id>");
|
|
708
|
+
process.exit(1);
|
|
709
|
+
}
|
|
710
|
+
const result = installConnector({
|
|
711
|
+
connectorId,
|
|
712
|
+
config: parseConnectorConfig(rest),
|
|
713
|
+
force: rest.includes("--force")
|
|
714
|
+
});
|
|
715
|
+
console.log(result.message);
|
|
716
|
+
if (result.configPath) console.log(` Config: ${result.configPath}`);
|
|
717
|
+
if (result.status === "already_installed") console.log("Use --force to reinstall.");
|
|
718
|
+
if (result.status === "config_required") console.log("Set config with --config <key>=<value>");
|
|
719
|
+
if (result.status === "error") console.error(`Error: ${result.message}`);
|
|
720
|
+
} else if (action === "remove") {
|
|
721
|
+
if (!connectorId) {
|
|
722
|
+
console.error("Usage: remnic connectors remove <id>");
|
|
723
|
+
process.exit(1);
|
|
724
|
+
}
|
|
725
|
+
const result = removeConnector(connectorId);
|
|
726
|
+
console.log(result.message);
|
|
727
|
+
} else if (action === "doctor") {
|
|
728
|
+
if (!connectorId) {
|
|
729
|
+
console.error("Usage: remnic connectors doctor <id>");
|
|
730
|
+
process.exit(1);
|
|
731
|
+
}
|
|
732
|
+
const result = await doctorConnector(connectorId);
|
|
733
|
+
if (json) {
|
|
734
|
+
console.log(JSON.stringify(result, null, 2));
|
|
735
|
+
} else {
|
|
736
|
+
for (const check of result.checks) {
|
|
737
|
+
const icon = check.ok ? "\u2713" : "\u2717";
|
|
738
|
+
console.log(` ${icon} ${check.name}: ${check.detail}`);
|
|
739
|
+
}
|
|
740
|
+
console.log(result.healthy ? "\nConnector healthy" : "\nConnector has issues");
|
|
741
|
+
}
|
|
742
|
+
} else {
|
|
743
|
+
console.log("Usage: remnic connectors <list|install|remove|doctor> [id]");
|
|
744
|
+
process.exit(1);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
async function cmdSpace(action, rest, json) {
|
|
748
|
+
const nonFlagArgs = rest.filter((a) => !a.startsWith("--"));
|
|
749
|
+
if (action === "list") {
|
|
750
|
+
const spaces = listSpaces();
|
|
751
|
+
if (json) {
|
|
752
|
+
console.log(JSON.stringify(spaces, null, 2));
|
|
753
|
+
} else {
|
|
754
|
+
const active = getActiveSpace();
|
|
755
|
+
for (const s of spaces) {
|
|
756
|
+
const icon = s.id === active.id ? "\u25CF" : "\u25CB";
|
|
757
|
+
console.log(` ${icon} ${s.name} (${s.kind}) \u2014 ${s.memoryDir}`);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
} else if (action === "switch") {
|
|
761
|
+
const spaceId = nonFlagArgs[0];
|
|
762
|
+
if (!spaceId) {
|
|
763
|
+
console.error("Usage: remnic space switch <id>");
|
|
764
|
+
process.exit(1);
|
|
765
|
+
}
|
|
766
|
+
const result = switchSpace(spaceId);
|
|
767
|
+
console.log(result.message);
|
|
768
|
+
} else if (action === "create") {
|
|
769
|
+
const parentIdx = rest.indexOf("--parent");
|
|
770
|
+
const parentSpaceId = parentIdx >= 0 && rest[parentIdx + 1] ? rest[parentIdx + 1] : void 0;
|
|
771
|
+
const positionals = [];
|
|
772
|
+
for (let i = 0; i < rest.length; i++) {
|
|
773
|
+
if (rest[i] === "--parent") {
|
|
774
|
+
i++;
|
|
775
|
+
continue;
|
|
776
|
+
}
|
|
777
|
+
if (rest[i].startsWith("--")) continue;
|
|
778
|
+
positionals.push(rest[i]);
|
|
779
|
+
}
|
|
780
|
+
const name = positionals[0];
|
|
781
|
+
const rawKind = positionals[1] ?? "project";
|
|
782
|
+
const validKinds = ["personal", "project", "team"];
|
|
783
|
+
if (!validKinds.includes(rawKind)) {
|
|
784
|
+
console.error(`Invalid kind "${rawKind}". Must be one of: ${validKinds.join(", ")}`);
|
|
785
|
+
process.exit(1);
|
|
786
|
+
}
|
|
787
|
+
const kind = rawKind;
|
|
788
|
+
if (!name) {
|
|
789
|
+
console.error("Usage: remnic space create <name> [personal|project|team] [--parent <id>]");
|
|
790
|
+
process.exit(1);
|
|
791
|
+
}
|
|
792
|
+
const space = createSpace({ name, kind, parentSpaceId });
|
|
793
|
+
if (json) {
|
|
794
|
+
console.log(JSON.stringify(space, null, 2));
|
|
795
|
+
} else {
|
|
796
|
+
console.log(`Created space "${space.name}" (${space.id})`);
|
|
797
|
+
console.log(` Kind: ${space.kind}`);
|
|
798
|
+
console.log(` Dir: ${space.memoryDir}`);
|
|
799
|
+
}
|
|
800
|
+
} else if (action === "delete") {
|
|
801
|
+
const spaceId = nonFlagArgs[0];
|
|
802
|
+
if (!spaceId) {
|
|
803
|
+
console.error("Usage: remnic space delete <id>");
|
|
804
|
+
process.exit(1);
|
|
805
|
+
}
|
|
806
|
+
deleteSpace(spaceId);
|
|
807
|
+
console.log(`Deleted space "${spaceId}"`);
|
|
808
|
+
} else if (action === "push") {
|
|
809
|
+
const sourceId = nonFlagArgs[0];
|
|
810
|
+
const targetId = nonFlagArgs[1];
|
|
811
|
+
if (!sourceId || !targetId) {
|
|
812
|
+
console.error("Usage: remnic space push <source> <target>");
|
|
813
|
+
process.exit(1);
|
|
814
|
+
}
|
|
815
|
+
const result = pushToSpace(sourceId, targetId, { force: rest.includes("--force") });
|
|
816
|
+
if (json) {
|
|
817
|
+
console.log(JSON.stringify(result, null, 2));
|
|
818
|
+
} else {
|
|
819
|
+
console.log(`Pushed ${result.memoriesPushed} memories`);
|
|
820
|
+
if (result.conflicts.length > 0) console.log(`Conflicts: ${result.conflicts.length}`);
|
|
821
|
+
console.log(`Duration: ${result.durationMs}ms`);
|
|
822
|
+
}
|
|
823
|
+
} else if (action === "pull") {
|
|
824
|
+
const sourceId = nonFlagArgs[0];
|
|
825
|
+
const targetId = nonFlagArgs[1];
|
|
826
|
+
if (!sourceId || !targetId) {
|
|
827
|
+
console.error("Usage: remnic space pull <source> <target>");
|
|
828
|
+
process.exit(1);
|
|
829
|
+
}
|
|
830
|
+
const result = pullFromSpace(sourceId, targetId, { force: rest.includes("--force") });
|
|
831
|
+
if (json) {
|
|
832
|
+
console.log(JSON.stringify(result, null, 2));
|
|
833
|
+
} else {
|
|
834
|
+
console.log(`Pulled ${result.memoriesPulled} memories`);
|
|
835
|
+
if (result.conflicts.length > 0) console.log(`Conflicts: ${result.conflicts.length}`);
|
|
836
|
+
console.log(`Duration: ${result.durationMs}ms`);
|
|
837
|
+
}
|
|
838
|
+
} else if (action === "share") {
|
|
839
|
+
const spaceId = nonFlagArgs[0];
|
|
840
|
+
const members = nonFlagArgs.slice(1);
|
|
841
|
+
if (!spaceId || members.length === 0) {
|
|
842
|
+
console.error("Usage: remnic space share <id> <member1> [member2 ...]");
|
|
843
|
+
process.exit(1);
|
|
844
|
+
}
|
|
845
|
+
const result = shareSpace(spaceId, members);
|
|
846
|
+
console.log(result.message);
|
|
847
|
+
} else if (action === "promote") {
|
|
848
|
+
const sourceId = nonFlagArgs[0];
|
|
849
|
+
const targetId = nonFlagArgs[1];
|
|
850
|
+
if (!sourceId || !targetId) {
|
|
851
|
+
console.error("Usage: remnic space promote <source> <target>");
|
|
852
|
+
process.exit(1);
|
|
853
|
+
}
|
|
854
|
+
const result = promoteSpace(sourceId, targetId, {
|
|
855
|
+
force: rest.includes("--force"),
|
|
856
|
+
forceOverwrite: rest.includes("--force-overwrite")
|
|
857
|
+
});
|
|
858
|
+
if (json) {
|
|
859
|
+
console.log(JSON.stringify(result, null, 2));
|
|
860
|
+
} else {
|
|
861
|
+
console.log(`Promoted ${result.memoriesPromoted} memories`);
|
|
862
|
+
if (result.conflicts.length > 0) console.log(`Conflicts: ${result.conflicts.length}`);
|
|
863
|
+
console.log(`Duration: ${result.durationMs}ms`);
|
|
864
|
+
}
|
|
865
|
+
} else if (action === "audit") {
|
|
866
|
+
const entries = getAuditLog();
|
|
867
|
+
if (json) {
|
|
868
|
+
console.log(JSON.stringify(entries, null, 2));
|
|
869
|
+
} else {
|
|
870
|
+
if (entries.length === 0) {
|
|
871
|
+
console.log("No audit entries.");
|
|
872
|
+
} else {
|
|
873
|
+
for (const e of entries.slice(-50)) {
|
|
874
|
+
console.log(`[${e.timestamp}] ${e.action} ${e.details}`);
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
} else {
|
|
879
|
+
console.log("Usage: remnic space <list|switch|create|delete|push|pull|share|promote|audit>");
|
|
880
|
+
process.exit(1);
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
async function cmdBenchmark(action, rest, json) {
|
|
884
|
+
initLogger();
|
|
885
|
+
const configPath = resolveConfigPath();
|
|
886
|
+
const raw = fs2.existsSync(configPath) ? JSON.parse(fs2.readFileSync(configPath, "utf8")) : {};
|
|
887
|
+
const remnicCfg = raw.remnic ?? raw.engram ?? raw;
|
|
888
|
+
const config = parseConfig(remnicCfg);
|
|
889
|
+
const orchestrator = new Orchestrator(config);
|
|
890
|
+
const service = new EngramAccessService(orchestrator);
|
|
891
|
+
const benchConfig = {
|
|
892
|
+
queries: rest.filter((a) => !a.startsWith("--")).length > 0 ? rest.filter((a) => !a.startsWith("--")) : void 0,
|
|
893
|
+
explain: rest.includes("--explain"),
|
|
894
|
+
baselinePath: rest.find((a) => a.startsWith("--baseline="))?.slice("--baseline=".length),
|
|
895
|
+
reportPath: rest.find((a) => a.startsWith("--report="))?.slice("--report=".length)
|
|
896
|
+
};
|
|
897
|
+
if (action === "run") {
|
|
898
|
+
const suite = await runBenchSuite(service, benchConfig);
|
|
899
|
+
if (json) {
|
|
900
|
+
console.log(JSON.stringify(suite, null, 2));
|
|
901
|
+
} else {
|
|
902
|
+
console.log(`Benchmark suite completed in ${suite.totalDurationMs}ms`);
|
|
903
|
+
for (const r of suite.results) {
|
|
904
|
+
const tiers = r.tiersUsed.join(" \u2192 ");
|
|
905
|
+
console.log(` ${r.query}: ${r.latencyMs}ms (${r.resultsCount} results) [${tiers}]`);
|
|
906
|
+
}
|
|
907
|
+
if (suite.regressions.length > 0) {
|
|
908
|
+
console.log("\nRegressions:");
|
|
909
|
+
for (const reg of suite.regressions) {
|
|
910
|
+
const icon = reg.passed ? "\u2713" : "\u2717";
|
|
911
|
+
console.log(` ${icon} ${reg.metric}: ${reg.currentValue}ms (baseline: ${reg.baselineValue}ms, tolerance: ${reg.tolerance}%)`);
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
} else if (action === "check") {
|
|
916
|
+
const baselinePath = benchConfig.baselinePath;
|
|
917
|
+
const baseline = loadBaseline(baselinePath);
|
|
918
|
+
if (!baseline) {
|
|
919
|
+
console.log("No baseline found. Run `remnic benchmark run` first.");
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
const suite = await runBenchSuite(service, benchConfig);
|
|
923
|
+
const metrics = {};
|
|
924
|
+
for (const r of suite.results) {
|
|
925
|
+
metrics[r.query] = r.latencyMs;
|
|
926
|
+
}
|
|
927
|
+
const tolerance = benchConfig.regressionTolerance ?? 10;
|
|
928
|
+
const result = checkRegression(metrics, baseline, tolerance);
|
|
929
|
+
if (json) {
|
|
930
|
+
console.log(JSON.stringify(result, null, 2));
|
|
931
|
+
} else {
|
|
932
|
+
if (result.passed) {
|
|
933
|
+
console.log("No regressions detected.");
|
|
934
|
+
} else {
|
|
935
|
+
console.log("Regressions detected:");
|
|
936
|
+
for (const reg of result.regressions) {
|
|
937
|
+
if (!reg.passed) {
|
|
938
|
+
console.log(` \u2717 ${reg.metric}: ${reg.currentValue}ms vs ${reg.baselineValue}ms baseline (+${((reg.currentValue - reg.baselineValue) / reg.baselineValue * 100).toFixed(1)}%)`);
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
if (!result.passed) {
|
|
944
|
+
process.exit(1);
|
|
945
|
+
}
|
|
946
|
+
} else if (action === "report") {
|
|
947
|
+
const reportPath = benchConfig.reportPath;
|
|
948
|
+
const suite = await runBenchSuite(service, { ...benchConfig, reportPath });
|
|
949
|
+
console.log(`Report saved to ${reportPath ?? "benchmarks/report.json"}`);
|
|
950
|
+
if (json) {
|
|
951
|
+
console.log(JSON.stringify(suite.report, null, 2));
|
|
952
|
+
}
|
|
953
|
+
} else {
|
|
954
|
+
console.log("Usage: remnic benchmark <run|check|report> [queries...] [--explain] [--baseline=<path>] [--report=<path>]");
|
|
955
|
+
process.exit(1);
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
var LOGS_DIR = path2.join(PID_DIR, "logs");
|
|
959
|
+
var LAUNCHD_LABEL = "ai.remnic.daemon";
|
|
960
|
+
var LEGACY_LAUNCHD_LABEL = "ai.engram.daemon";
|
|
961
|
+
var LAUNCHD_PLIST_PATH = path2.join(
|
|
962
|
+
resolveHomeDir(),
|
|
963
|
+
"Library",
|
|
964
|
+
"LaunchAgents",
|
|
965
|
+
`${LAUNCHD_LABEL}.plist`
|
|
966
|
+
);
|
|
967
|
+
var LEGACY_LAUNCHD_PLIST_PATH = path2.join(
|
|
968
|
+
resolveHomeDir(),
|
|
969
|
+
"Library",
|
|
970
|
+
"LaunchAgents",
|
|
971
|
+
`${LEGACY_LAUNCHD_LABEL}.plist`
|
|
972
|
+
);
|
|
973
|
+
var SYSTEMD_SERVICE = "remnic.service";
|
|
974
|
+
var LEGACY_SYSTEMD_SERVICE = "engram.service";
|
|
975
|
+
var SYSTEMD_UNIT_PATH = path2.join(
|
|
976
|
+
resolveHomeDir(),
|
|
977
|
+
".config",
|
|
978
|
+
"systemd",
|
|
979
|
+
"user",
|
|
980
|
+
SYSTEMD_SERVICE
|
|
981
|
+
);
|
|
982
|
+
var LEGACY_SYSTEMD_UNIT_PATH = path2.join(
|
|
983
|
+
resolveHomeDir(),
|
|
984
|
+
".config",
|
|
985
|
+
"systemd",
|
|
986
|
+
"user",
|
|
987
|
+
LEGACY_SYSTEMD_SERVICE
|
|
988
|
+
);
|
|
989
|
+
function readPid() {
|
|
990
|
+
for (const file of [PID_FILE, LEGACY_PID_FILE]) {
|
|
991
|
+
try {
|
|
992
|
+
return parseInt(fs2.readFileSync(file, "utf8").trim(), 10);
|
|
993
|
+
} catch {
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
return void 0;
|
|
997
|
+
}
|
|
998
|
+
function inferPort() {
|
|
999
|
+
try {
|
|
1000
|
+
const configPath = resolveConfigPath();
|
|
1001
|
+
const raw = JSON.parse(fs2.readFileSync(configPath, "utf8"));
|
|
1002
|
+
return raw.server?.port ?? 4318;
|
|
1003
|
+
} catch {
|
|
1004
|
+
return 4318;
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
function resolveNodePath() {
|
|
1008
|
+
return process.execPath;
|
|
1009
|
+
}
|
|
1010
|
+
function resolveServerBin() {
|
|
1011
|
+
const distPath = path2.resolve(import.meta.dirname, "../../remnic-server/dist/index.js");
|
|
1012
|
+
if (fs2.existsSync(distPath)) return distPath;
|
|
1013
|
+
const srcPath = path2.resolve(import.meta.dirname, "../../remnic-server/src/index.ts");
|
|
1014
|
+
return srcPath;
|
|
1015
|
+
}
|
|
1016
|
+
function isMacOS() {
|
|
1017
|
+
return process.platform === "darwin";
|
|
1018
|
+
}
|
|
1019
|
+
function isLinux() {
|
|
1020
|
+
return process.platform === "linux";
|
|
1021
|
+
}
|
|
1022
|
+
function renderTemplate(templateContent, vars) {
|
|
1023
|
+
let result = templateContent;
|
|
1024
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
1025
|
+
result = result.replaceAll(`{{${key}}}`, value);
|
|
1026
|
+
}
|
|
1027
|
+
return result;
|
|
1028
|
+
}
|
|
1029
|
+
function daemonInstall() {
|
|
1030
|
+
const home = resolveHomeDir();
|
|
1031
|
+
const nodePath = resolveNodePath();
|
|
1032
|
+
const serverBin = resolveServerBin();
|
|
1033
|
+
if (serverBin.endsWith(".ts")) {
|
|
1034
|
+
console.error("Error: @remnic/server has not been built. Run 'pnpm run build --filter=@remnic/server' first.");
|
|
1035
|
+
console.error(` Expected: ${path2.resolve(import.meta.dirname, "../../remnic-server/dist/index.js")}`);
|
|
1036
|
+
console.error(` Found: ${serverBin} (TypeScript source \u2014 not loadable by node)`);
|
|
1037
|
+
process.exit(1);
|
|
1038
|
+
}
|
|
1039
|
+
const vars = { HOME: home, NODE_PATH: nodePath, REMNIC_SERVER_BIN: serverBin };
|
|
1040
|
+
fs2.mkdirSync(LOGS_DIR, { recursive: true });
|
|
1041
|
+
if (isMacOS()) {
|
|
1042
|
+
const templatePath = path2.resolve(import.meta.dirname, "../templates/launchd/ai.remnic.daemon.plist");
|
|
1043
|
+
const template = fs2.readFileSync(templatePath, "utf8");
|
|
1044
|
+
const plist = renderTemplate(template, vars);
|
|
1045
|
+
fs2.mkdirSync(path2.dirname(LAUNCHD_PLIST_PATH), { recursive: true });
|
|
1046
|
+
fs2.writeFileSync(LAUNCHD_PLIST_PATH, plist);
|
|
1047
|
+
try {
|
|
1048
|
+
childProcess.execSync(`launchctl load -w "${LAUNCHD_PLIST_PATH}"`, { stdio: "pipe" });
|
|
1049
|
+
} catch {
|
|
1050
|
+
}
|
|
1051
|
+
console.log(`Installed launchd service: ${LAUNCHD_PLIST_PATH}`);
|
|
1052
|
+
console.log(` Label: ${LAUNCHD_LABEL}`);
|
|
1053
|
+
console.log(` RunAtLoad: true, KeepAlive: true`);
|
|
1054
|
+
console.log(` Logs: ${LOGS_DIR}/daemon.log`);
|
|
1055
|
+
} else if (isLinux()) {
|
|
1056
|
+
const templatePath = path2.resolve(import.meta.dirname, "../templates/systemd/remnic.service");
|
|
1057
|
+
const template = fs2.readFileSync(templatePath, "utf8");
|
|
1058
|
+
const unit = renderTemplate(template, vars);
|
|
1059
|
+
fs2.mkdirSync(path2.dirname(SYSTEMD_UNIT_PATH), { recursive: true });
|
|
1060
|
+
fs2.writeFileSync(SYSTEMD_UNIT_PATH, unit);
|
|
1061
|
+
try {
|
|
1062
|
+
childProcess.execSync("systemctl --user daemon-reload", { stdio: "pipe" });
|
|
1063
|
+
childProcess.execSync(`systemctl --user enable ${SYSTEMD_SERVICE}`, { stdio: "pipe" });
|
|
1064
|
+
childProcess.execSync(`systemctl --user start ${SYSTEMD_SERVICE}`, { stdio: "pipe" });
|
|
1065
|
+
} catch {
|
|
1066
|
+
}
|
|
1067
|
+
console.log(`Installed systemd user service: ${SYSTEMD_UNIT_PATH}`);
|
|
1068
|
+
console.log(` Restart: on-failure, WantedBy: default.target`);
|
|
1069
|
+
console.log(` Logs: ${LOGS_DIR}/daemon.log`);
|
|
1070
|
+
} else {
|
|
1071
|
+
console.error(`Unsupported platform: ${process.platform}. Use 'remnic daemon start' for manual mode.`);
|
|
1072
|
+
process.exit(1);
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
function daemonUninstall() {
|
|
1076
|
+
if (isMacOS()) {
|
|
1077
|
+
let removed = false;
|
|
1078
|
+
for (const plistPath of [LAUNCHD_PLIST_PATH, LEGACY_LAUNCHD_PLIST_PATH]) {
|
|
1079
|
+
try {
|
|
1080
|
+
childProcess.execSync(`launchctl unload "${plistPath}"`, { stdio: "pipe" });
|
|
1081
|
+
} catch {
|
|
1082
|
+
}
|
|
1083
|
+
try {
|
|
1084
|
+
fs2.unlinkSync(plistPath);
|
|
1085
|
+
removed = true;
|
|
1086
|
+
console.log(`Removed launchd service: ${plistPath}`);
|
|
1087
|
+
} catch {
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
if (!removed) {
|
|
1091
|
+
console.log("Launchd plist not found \u2014 nothing to remove.");
|
|
1092
|
+
}
|
|
1093
|
+
} else if (isLinux()) {
|
|
1094
|
+
for (const serviceName of [SYSTEMD_SERVICE, LEGACY_SYSTEMD_SERVICE]) {
|
|
1095
|
+
try {
|
|
1096
|
+
childProcess.execSync(`systemctl --user stop ${serviceName}`, { stdio: "pipe" });
|
|
1097
|
+
childProcess.execSync(`systemctl --user disable ${serviceName}`, { stdio: "pipe" });
|
|
1098
|
+
} catch {
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
let removed = false;
|
|
1102
|
+
for (const unitPath of [SYSTEMD_UNIT_PATH, LEGACY_SYSTEMD_UNIT_PATH]) {
|
|
1103
|
+
try {
|
|
1104
|
+
fs2.unlinkSync(unitPath);
|
|
1105
|
+
removed = true;
|
|
1106
|
+
console.log(`Removed systemd service: ${unitPath}`);
|
|
1107
|
+
} catch {
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
if (removed) {
|
|
1111
|
+
try {
|
|
1112
|
+
childProcess.execSync("systemctl --user daemon-reload", { stdio: "pipe" });
|
|
1113
|
+
} catch {
|
|
1114
|
+
}
|
|
1115
|
+
} else {
|
|
1116
|
+
console.log("Systemd unit not found \u2014 nothing to remove.");
|
|
1117
|
+
}
|
|
1118
|
+
} else {
|
|
1119
|
+
console.error(`Unsupported platform: ${process.platform}.`);
|
|
1120
|
+
process.exit(1);
|
|
1121
|
+
}
|
|
1122
|
+
daemonStop();
|
|
1123
|
+
}
|
|
1124
|
+
function isServiceRunning() {
|
|
1125
|
+
const pidFromFile = readPid();
|
|
1126
|
+
if (pidFromFile) {
|
|
1127
|
+
try {
|
|
1128
|
+
process.kill(pidFromFile, 0);
|
|
1129
|
+
return { running: true, pid: pidFromFile };
|
|
1130
|
+
} catch {
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
if (isMacOS()) {
|
|
1134
|
+
const status = firstSuccessfulResult([LAUNCHD_LABEL, LEGACY_LAUNCHD_LABEL], (label) => {
|
|
1135
|
+
const out = childProcess.execSync(`launchctl list ${label} 2>/dev/null`, { encoding: "utf8" });
|
|
1136
|
+
const pidMatch = out.match(/"PID"\s*=\s*(\d+)/);
|
|
1137
|
+
if (pidMatch) return { running: true, pid: parseInt(pidMatch[1], 10) };
|
|
1138
|
+
return out.includes('"PID"') ? { running: true } : void 0;
|
|
1139
|
+
});
|
|
1140
|
+
if (status) return status;
|
|
1141
|
+
} else if (isLinux()) {
|
|
1142
|
+
const status = firstSuccessfulResult([SYSTEMD_SERVICE, LEGACY_SYSTEMD_SERVICE], (serviceName) => {
|
|
1143
|
+
const out = childProcess.execSync(`systemctl --user is-active ${serviceName} 2>/dev/null`, {
|
|
1144
|
+
encoding: "utf8"
|
|
1145
|
+
}).trim();
|
|
1146
|
+
if (out !== "active") return void 0;
|
|
1147
|
+
try {
|
|
1148
|
+
const pidOut = childProcess.execSync(
|
|
1149
|
+
`systemctl --user show ${serviceName} --property=MainPID --value`,
|
|
1150
|
+
{ encoding: "utf8" }
|
|
1151
|
+
).trim();
|
|
1152
|
+
const spid = parseInt(pidOut, 10);
|
|
1153
|
+
if (spid > 0) return { running: true, pid: spid };
|
|
1154
|
+
} catch {
|
|
1155
|
+
}
|
|
1156
|
+
return { running: true };
|
|
1157
|
+
});
|
|
1158
|
+
if (status) return status;
|
|
1159
|
+
}
|
|
1160
|
+
return { running: false };
|
|
1161
|
+
}
|
|
1162
|
+
function daemonStatus() {
|
|
1163
|
+
const { running, pid } = isServiceRunning();
|
|
1164
|
+
const port = inferPort();
|
|
1165
|
+
const serviceInstalled = isMacOS() ? fs2.existsSync(LAUNCHD_PLIST_PATH) || fs2.existsSync(LEGACY_LAUNCHD_PLIST_PATH) : isLinux() ? fs2.existsSync(SYSTEMD_UNIT_PATH) || fs2.existsSync(LEGACY_SYSTEMD_UNIT_PATH) : false;
|
|
1166
|
+
console.log(`Remnic daemon status:`);
|
|
1167
|
+
console.log(` Running: ${running ? `yes${pid ? ` (pid ${pid})` : ""}` : "no"}`);
|
|
1168
|
+
console.log(` Port: ${port}`);
|
|
1169
|
+
console.log(` Service: ${serviceInstalled ? "installed" : "not installed"}`);
|
|
1170
|
+
console.log(` Platform: ${process.platform}`);
|
|
1171
|
+
console.log(` PID file: ${fs2.existsSync(PID_FILE) ? PID_FILE : LEGACY_PID_FILE}`);
|
|
1172
|
+
console.log(` Log file: ${fs2.existsSync(LOG_FILE) ? LOG_FILE : LEGACY_LOG_FILE}`);
|
|
1173
|
+
}
|
|
1174
|
+
function daemonStart() {
|
|
1175
|
+
const svc = isServiceRunning();
|
|
1176
|
+
if (svc.running) {
|
|
1177
|
+
console.log(`Already running${svc.pid ? ` (pid ${svc.pid})` : " (via service manager)"}`);
|
|
1178
|
+
return;
|
|
1179
|
+
}
|
|
1180
|
+
if (isMacOS() && (fs2.existsSync(LAUNCHD_PLIST_PATH) || fs2.existsSync(LEGACY_LAUNCHD_PLIST_PATH))) {
|
|
1181
|
+
const label = firstSuccessfulCandidate([LAUNCHD_LABEL, LEGACY_LAUNCHD_LABEL], (candidate) => {
|
|
1182
|
+
childProcess.execSync(`launchctl start ${candidate} 2>/dev/null`, { stdio: "pipe" });
|
|
1183
|
+
});
|
|
1184
|
+
if (label) {
|
|
1185
|
+
console.log(`Started remnic daemon via launchd (${label})`);
|
|
1186
|
+
return;
|
|
1187
|
+
}
|
|
1188
|
+
} else if (isLinux() && (fs2.existsSync(SYSTEMD_UNIT_PATH) || fs2.existsSync(LEGACY_SYSTEMD_UNIT_PATH))) {
|
|
1189
|
+
const serviceName = firstSuccessfulCandidate([SYSTEMD_SERVICE, LEGACY_SYSTEMD_SERVICE], (candidate) => {
|
|
1190
|
+
childProcess.execSync(`systemctl --user start ${candidate}`, { stdio: "pipe" });
|
|
1191
|
+
});
|
|
1192
|
+
if (serviceName) {
|
|
1193
|
+
console.log(`Started remnic daemon via systemd (${serviceName})`);
|
|
1194
|
+
return;
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
fs2.mkdirSync(PID_DIR, { recursive: true });
|
|
1198
|
+
fs2.mkdirSync(LOGS_DIR, { recursive: true });
|
|
1199
|
+
const logStream = fs2.openSync(LOG_FILE, "a");
|
|
1200
|
+
const serverBin = resolveServerBin();
|
|
1201
|
+
const isSource = serverBin.endsWith(".ts");
|
|
1202
|
+
let cmd;
|
|
1203
|
+
let args;
|
|
1204
|
+
if (isSource) {
|
|
1205
|
+
cmd = "npx";
|
|
1206
|
+
args = ["tsx", serverBin];
|
|
1207
|
+
} else {
|
|
1208
|
+
cmd = process.execPath;
|
|
1209
|
+
args = [serverBin];
|
|
1210
|
+
}
|
|
1211
|
+
const child = childProcess.spawn(cmd, args, {
|
|
1212
|
+
detached: true,
|
|
1213
|
+
stdio: ["ignore", logStream, logStream],
|
|
1214
|
+
env: {
|
|
1215
|
+
...process.env,
|
|
1216
|
+
REMNIC_DAEMON: "1",
|
|
1217
|
+
ENGRAM_DAEMON: process.env.ENGRAM_DAEMON ?? "1"
|
|
1218
|
+
}
|
|
1219
|
+
});
|
|
1220
|
+
child.unref();
|
|
1221
|
+
fs2.writeFileSync(PID_FILE, String(child.pid));
|
|
1222
|
+
console.log(`Started remnic server (pid ${child.pid})`);
|
|
1223
|
+
console.log(` Log: ${LOG_FILE}`);
|
|
1224
|
+
}
|
|
1225
|
+
function daemonStop() {
|
|
1226
|
+
if (isMacOS() && (fs2.existsSync(LAUNCHD_PLIST_PATH) || fs2.existsSync(LEGACY_LAUNCHD_PLIST_PATH))) {
|
|
1227
|
+
const label = firstSuccessfulCandidate([LAUNCHD_LABEL, LEGACY_LAUNCHD_LABEL], (candidate) => {
|
|
1228
|
+
childProcess.execSync(`launchctl stop ${candidate} 2>/dev/null`, { stdio: "pipe" });
|
|
1229
|
+
});
|
|
1230
|
+
if (label) {
|
|
1231
|
+
console.log(`Stopped remnic daemon via launchd (${label})`);
|
|
1232
|
+
return;
|
|
1233
|
+
}
|
|
1234
|
+
} else if (isLinux() && (fs2.existsSync(SYSTEMD_UNIT_PATH) || fs2.existsSync(LEGACY_SYSTEMD_UNIT_PATH))) {
|
|
1235
|
+
const serviceName = firstSuccessfulCandidate([SYSTEMD_SERVICE, LEGACY_SYSTEMD_SERVICE], (candidate) => {
|
|
1236
|
+
childProcess.execSync(`systemctl --user stop ${candidate}`, { stdio: "pipe" });
|
|
1237
|
+
});
|
|
1238
|
+
if (serviceName) {
|
|
1239
|
+
console.log(`Stopped remnic daemon via systemd (${serviceName})`);
|
|
1240
|
+
return;
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
const pid = readPid();
|
|
1244
|
+
if (!pid) {
|
|
1245
|
+
console.log("Not running");
|
|
1246
|
+
return;
|
|
1247
|
+
}
|
|
1248
|
+
try {
|
|
1249
|
+
process.kill(pid, "SIGTERM");
|
|
1250
|
+
console.log(`Stopped remnic server (pid ${pid})`);
|
|
1251
|
+
} catch {
|
|
1252
|
+
console.log("Process not found (cleaning up PID file)");
|
|
1253
|
+
}
|
|
1254
|
+
try {
|
|
1255
|
+
fs2.unlinkSync(PID_FILE);
|
|
1256
|
+
} catch {
|
|
1257
|
+
}
|
|
1258
|
+
try {
|
|
1259
|
+
fs2.unlinkSync(LEGACY_PID_FILE);
|
|
1260
|
+
} catch {
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
function daemonRestart() {
|
|
1264
|
+
daemonStop();
|
|
1265
|
+
setTimeout(() => daemonStart(), 1e3);
|
|
1266
|
+
}
|
|
1267
|
+
function cmdTokenGenerate(connector) {
|
|
1268
|
+
if (!connector) {
|
|
1269
|
+
console.error("Usage: remnic token generate <connector-id>");
|
|
1270
|
+
console.error(" e.g.: remnic token generate claude-code");
|
|
1271
|
+
process.exit(1);
|
|
1272
|
+
}
|
|
1273
|
+
const entry = generateToken(connector);
|
|
1274
|
+
console.log(`Generated token for ${connector}:`);
|
|
1275
|
+
console.log(` Token: ${entry.token}`);
|
|
1276
|
+
console.log(` Created: ${entry.createdAt}`);
|
|
1277
|
+
console.log(`
|
|
1278
|
+
Use this token as the Bearer token when connecting from ${connector}.`);
|
|
1279
|
+
}
|
|
1280
|
+
function cmdTokenList(json) {
|
|
1281
|
+
const tokens = listTokens();
|
|
1282
|
+
if (json) {
|
|
1283
|
+
console.log(JSON.stringify(tokens, null, 2));
|
|
1284
|
+
return;
|
|
1285
|
+
}
|
|
1286
|
+
if (tokens.length === 0) {
|
|
1287
|
+
console.log("No tokens. Generate one with: remnic token generate <connector-id>");
|
|
1288
|
+
return;
|
|
1289
|
+
}
|
|
1290
|
+
console.log("Connector tokens:");
|
|
1291
|
+
for (const t of tokens) {
|
|
1292
|
+
const masked = t.token.slice(0, 20) + "\u2026";
|
|
1293
|
+
console.log(` ${t.connector.padEnd(16)} ${masked} (created ${t.createdAt})`);
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
function cmdTokenRevoke(connector) {
|
|
1297
|
+
if (!connector) {
|
|
1298
|
+
console.error("Usage: remnic token revoke <connector-id>");
|
|
1299
|
+
process.exit(1);
|
|
1300
|
+
}
|
|
1301
|
+
if (revokeToken(connector)) {
|
|
1302
|
+
console.log(`Revoked token for ${connector}`);
|
|
1303
|
+
} else {
|
|
1304
|
+
console.log(`No token found for ${connector}`);
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
async function main(argv = process.argv.slice(2)) {
|
|
1308
|
+
const [command, ...rest] = argv;
|
|
1309
|
+
if (command !== "migrate") {
|
|
1310
|
+
await migrateFromEngram();
|
|
1311
|
+
}
|
|
1312
|
+
switch (command) {
|
|
1313
|
+
case "init":
|
|
1314
|
+
cmdInit();
|
|
1315
|
+
break;
|
|
1316
|
+
case "migrate": {
|
|
1317
|
+
const json = rest.includes("--json");
|
|
1318
|
+
const rollback = rest.includes("--rollback");
|
|
1319
|
+
await cmdMigrate(json, rollback);
|
|
1320
|
+
break;
|
|
1321
|
+
}
|
|
1322
|
+
case "status": {
|
|
1323
|
+
const json = rest.includes("--json");
|
|
1324
|
+
await cmdStatus(json);
|
|
1325
|
+
break;
|
|
1326
|
+
}
|
|
1327
|
+
case "query": {
|
|
1328
|
+
const json = rest.includes("--json");
|
|
1329
|
+
const explain = rest.includes("--explain");
|
|
1330
|
+
const queryText = rest.filter((a) => !a.startsWith("--")).join(" ");
|
|
1331
|
+
await cmdQuery(queryText, json, explain);
|
|
1332
|
+
break;
|
|
1333
|
+
}
|
|
1334
|
+
case "doctor":
|
|
1335
|
+
cmdDoctor();
|
|
1336
|
+
break;
|
|
1337
|
+
case "config":
|
|
1338
|
+
cmdConfig();
|
|
1339
|
+
break;
|
|
1340
|
+
case "daemon": {
|
|
1341
|
+
const action = rest[0];
|
|
1342
|
+
switch (action) {
|
|
1343
|
+
case "start":
|
|
1344
|
+
daemonStart();
|
|
1345
|
+
break;
|
|
1346
|
+
case "stop":
|
|
1347
|
+
daemonStop();
|
|
1348
|
+
break;
|
|
1349
|
+
case "restart":
|
|
1350
|
+
daemonRestart();
|
|
1351
|
+
break;
|
|
1352
|
+
case "install":
|
|
1353
|
+
daemonInstall();
|
|
1354
|
+
break;
|
|
1355
|
+
case "uninstall":
|
|
1356
|
+
daemonUninstall();
|
|
1357
|
+
break;
|
|
1358
|
+
case "status":
|
|
1359
|
+
daemonStatus();
|
|
1360
|
+
break;
|
|
1361
|
+
default:
|
|
1362
|
+
console.log("Usage: remnic daemon <start|stop|restart|install|uninstall|status>");
|
|
1363
|
+
process.exit(1);
|
|
1364
|
+
}
|
|
1365
|
+
break;
|
|
1366
|
+
}
|
|
1367
|
+
case "token": {
|
|
1368
|
+
const action = rest[0];
|
|
1369
|
+
const json = rest.includes("--json");
|
|
1370
|
+
switch (action) {
|
|
1371
|
+
case "generate":
|
|
1372
|
+
cmdTokenGenerate(rest[1]);
|
|
1373
|
+
break;
|
|
1374
|
+
case "list":
|
|
1375
|
+
cmdTokenList(json);
|
|
1376
|
+
break;
|
|
1377
|
+
case "revoke":
|
|
1378
|
+
cmdTokenRevoke(rest[1]);
|
|
1379
|
+
break;
|
|
1380
|
+
default:
|
|
1381
|
+
console.log("Usage: remnic token <generate|list|revoke> [connector-id] [--json]");
|
|
1382
|
+
process.exit(1);
|
|
1383
|
+
}
|
|
1384
|
+
break;
|
|
1385
|
+
}
|
|
1386
|
+
case "tree": {
|
|
1387
|
+
const subAction = rest[0];
|
|
1388
|
+
const json = rest.includes("--json");
|
|
1389
|
+
const outputDir = resolveFlag(rest, "--output") ?? path2.join(process.cwd(), ".remnic", "context-tree");
|
|
1390
|
+
const categoriesFlag = resolveFlag(rest, "--categories");
|
|
1391
|
+
const categories = categoriesFlag ? categoriesFlag.split(",") : void 0;
|
|
1392
|
+
const maxPerCategoryRaw = resolveFlag(rest, "--max-per-category");
|
|
1393
|
+
let maxPerCategory;
|
|
1394
|
+
if (maxPerCategoryRaw !== void 0) {
|
|
1395
|
+
maxPerCategory = parseInt(maxPerCategoryRaw, 10);
|
|
1396
|
+
if (!Number.isFinite(maxPerCategory) || maxPerCategory < 1) {
|
|
1397
|
+
console.error(`Invalid --max-per-category: ${maxPerCategoryRaw}`);
|
|
1398
|
+
process.exit(1);
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
if (subAction === "generate") {
|
|
1402
|
+
const result = await generateContextTree({
|
|
1403
|
+
memoryDir: resolveMemoryDir(),
|
|
1404
|
+
outputDir,
|
|
1405
|
+
categories,
|
|
1406
|
+
maxPerCategory,
|
|
1407
|
+
includeEntities: !rest.includes("--no-entities"),
|
|
1408
|
+
includeQuestions: !rest.includes("--no-questions")
|
|
1409
|
+
});
|
|
1410
|
+
if (json) {
|
|
1411
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1412
|
+
} else {
|
|
1413
|
+
console.log(`Context tree generated at ${result.outputDir}`);
|
|
1414
|
+
console.log(` Nodes: ${result.nodesGenerated} generated, ${result.nodesSkipped} skipped`);
|
|
1415
|
+
for (const [cat, count] of Object.entries(result.categories)) {
|
|
1416
|
+
console.log(` ${cat}: ${count}`);
|
|
1417
|
+
}
|
|
1418
|
+
console.log(` Duration: ${result.durationMs}ms`);
|
|
1419
|
+
}
|
|
1420
|
+
} else if (subAction === "watch") {
|
|
1421
|
+
const memoryDir = resolveMemoryDir();
|
|
1422
|
+
console.log(`Watching ${memoryDir} for changes\u2026`);
|
|
1423
|
+
console.log(`Output: ${outputDir}`);
|
|
1424
|
+
console.log("Press Ctrl+C to stop.\n");
|
|
1425
|
+
const initial = await generateContextTree({
|
|
1426
|
+
memoryDir,
|
|
1427
|
+
outputDir,
|
|
1428
|
+
categories,
|
|
1429
|
+
maxPerCategory,
|
|
1430
|
+
includeEntities: !rest.includes("--no-entities"),
|
|
1431
|
+
includeQuestions: !rest.includes("--no-questions")
|
|
1432
|
+
});
|
|
1433
|
+
console.log(`Initial: ${initial.nodesGenerated} nodes (${initial.durationMs}ms)`);
|
|
1434
|
+
let debounceTimer = null;
|
|
1435
|
+
const rebuild = () => {
|
|
1436
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
1437
|
+
debounceTimer = setTimeout(async () => {
|
|
1438
|
+
const t0 = Date.now();
|
|
1439
|
+
try {
|
|
1440
|
+
const result = await generateContextTree({
|
|
1441
|
+
memoryDir,
|
|
1442
|
+
outputDir,
|
|
1443
|
+
categories,
|
|
1444
|
+
maxPerCategory,
|
|
1445
|
+
includeEntities: !rest.includes("--no-entities"),
|
|
1446
|
+
includeQuestions: !rest.includes("--no-questions")
|
|
1447
|
+
});
|
|
1448
|
+
console.log(`[${(/* @__PURE__ */ new Date()).toISOString()}] Rebuilt: ${result.nodesGenerated} nodes (${Date.now() - t0}ms)`);
|
|
1449
|
+
} catch (err) {
|
|
1450
|
+
console.error(`[${(/* @__PURE__ */ new Date()).toISOString()}] Rebuild failed:`, err instanceof Error ? err.message : err);
|
|
1451
|
+
}
|
|
1452
|
+
}, 500);
|
|
1453
|
+
};
|
|
1454
|
+
fs2.watch(memoryDir, { recursive: true }, (_event, filename) => {
|
|
1455
|
+
if (filename && filename.startsWith(".")) return;
|
|
1456
|
+
rebuild();
|
|
1457
|
+
});
|
|
1458
|
+
await new Promise(() => {
|
|
1459
|
+
});
|
|
1460
|
+
} else if (subAction === "validate") {
|
|
1461
|
+
const treeDir = outputDir;
|
|
1462
|
+
if (!fs2.existsSync(treeDir)) {
|
|
1463
|
+
console.error(`Context tree not found at ${treeDir}. Run 'remnic tree generate' first.`);
|
|
1464
|
+
process.exit(1);
|
|
1465
|
+
}
|
|
1466
|
+
const indexPath = path2.join(treeDir, "INDEX.md");
|
|
1467
|
+
if (!fs2.existsSync(indexPath)) {
|
|
1468
|
+
console.error(`INDEX.md missing in ${treeDir}. Tree may be corrupt \u2014 regenerate.`);
|
|
1469
|
+
process.exit(1);
|
|
1470
|
+
}
|
|
1471
|
+
console.log(`Context tree at ${treeDir} is valid.`);
|
|
1472
|
+
} else {
|
|
1473
|
+
console.log(`Usage: remnic tree <generate|watch|validate>
|
|
1474
|
+
generate Generate context tree from memory
|
|
1475
|
+
watch Watch memory dir and regenerate on changes
|
|
1476
|
+
validate Check that context tree exists and is valid
|
|
1477
|
+
|
|
1478
|
+
Options:
|
|
1479
|
+
--output <dir> Output directory (default: .remnic/context-tree)
|
|
1480
|
+
--categories <list> Comma-separated categories to include
|
|
1481
|
+
--max-per-category <n> Max nodes per category
|
|
1482
|
+
--no-entities Exclude entity nodes
|
|
1483
|
+
--no-questions Exclude question nodes
|
|
1484
|
+
--json JSON output (generate only)`);
|
|
1485
|
+
}
|
|
1486
|
+
break;
|
|
1487
|
+
}
|
|
1488
|
+
case "onboard": {
|
|
1489
|
+
const dir = rest[0] ?? ".";
|
|
1490
|
+
const json = rest.includes("--json");
|
|
1491
|
+
cmdOnboard(dir, json);
|
|
1492
|
+
break;
|
|
1493
|
+
}
|
|
1494
|
+
case "curate": {
|
|
1495
|
+
const targetPath = rest[0];
|
|
1496
|
+
const json = rest.includes("--json");
|
|
1497
|
+
if (!targetPath) {
|
|
1498
|
+
console.error("Usage: remnic curate <path>");
|
|
1499
|
+
process.exit(1);
|
|
1500
|
+
}
|
|
1501
|
+
await cmdCurate(targetPath, json);
|
|
1502
|
+
break;
|
|
1503
|
+
}
|
|
1504
|
+
case "review": {
|
|
1505
|
+
const action = rest[0] ?? "list";
|
|
1506
|
+
cmdReview(action, rest.slice(1));
|
|
1507
|
+
break;
|
|
1508
|
+
}
|
|
1509
|
+
case "sync": {
|
|
1510
|
+
const action = rest[0] ?? "run";
|
|
1511
|
+
const json = rest.includes("--json");
|
|
1512
|
+
cmdSync(action, rest.slice(1), json);
|
|
1513
|
+
break;
|
|
1514
|
+
}
|
|
1515
|
+
case "dedup": {
|
|
1516
|
+
const json = rest.includes("--json");
|
|
1517
|
+
cmdDedup(json);
|
|
1518
|
+
break;
|
|
1519
|
+
}
|
|
1520
|
+
case "connectors": {
|
|
1521
|
+
const action = rest[0] ?? "list";
|
|
1522
|
+
const json = rest.includes("--json");
|
|
1523
|
+
await cmdConnectors(action, rest.slice(1), json);
|
|
1524
|
+
break;
|
|
1525
|
+
}
|
|
1526
|
+
case "space": {
|
|
1527
|
+
const action = rest[0] ?? "list";
|
|
1528
|
+
const json = rest.includes("--json");
|
|
1529
|
+
await cmdSpace(action, rest.slice(1), json);
|
|
1530
|
+
break;
|
|
1531
|
+
}
|
|
1532
|
+
case "benchmark": {
|
|
1533
|
+
const action = rest[0] ?? "run";
|
|
1534
|
+
const json = rest.includes("--json");
|
|
1535
|
+
await cmdBenchmark(action, rest.slice(1), json);
|
|
1536
|
+
break;
|
|
1537
|
+
}
|
|
1538
|
+
default:
|
|
1539
|
+
console.log(`
|
|
1540
|
+
remnic \u2014 Remnic memory CLI
|
|
1541
|
+
|
|
1542
|
+
Usage:
|
|
1543
|
+
remnic init Create config file
|
|
1544
|
+
remnic migrate [--rollback] [--json] Run or undo first-run Engram migration
|
|
1545
|
+
remnic status [--json] Show server status
|
|
1546
|
+
remnic query <text> [--json] [--explain] Query memories (use --explain for tier breakdown)
|
|
1547
|
+
|
|
1548
|
+
remnic doctor Run diagnostics
|
|
1549
|
+
remnic config Show current config
|
|
1550
|
+
remnic daemon <start|stop|restart|install|uninstall|status> Manage background server
|
|
1551
|
+
remnic token <generate|list|revoke> [connector-id] Manage auth tokens
|
|
1552
|
+
remnic tree <generate|watch|validate> Generate context tree
|
|
1553
|
+
remnic onboard [dir] [--json] Onboard project directory
|
|
1554
|
+
remnic curate <path> [--json] Curate files into memory
|
|
1555
|
+
remnic review <list|approve|dismiss|flag> [id] Review inbox
|
|
1556
|
+
remnic sync <run|watch> [--source <dir>] Diff-aware sync
|
|
1557
|
+
remnic dedup [--json] Find duplicate memories
|
|
1558
|
+
remnic connectors <list|install|remove|doctor> [id] Manage connectors
|
|
1559
|
+
remnic space <list|switch|create|delete|push|pull|share|promote|audit> Manage spaces
|
|
1560
|
+
create accepts --parent <id> to set parent-child relationship
|
|
1561
|
+
remnic benchmark <run|check|report> [queries...] [--explain] [--baseline=<path>] [--report=<path>]
|
|
1562
|
+
|
|
1563
|
+
Options:
|
|
1564
|
+
--json Output in JSON format
|
|
1565
|
+
--help Show this help
|
|
1566
|
+
`);
|
|
1567
|
+
break;
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
var argv1 = process.argv[1] ?? "";
|
|
1571
|
+
var argv1Base = argv1.replace(/\\/g, "/");
|
|
1572
|
+
if (argv1Base.endsWith("remnic.ts") || argv1Base.endsWith("remnic.js") || argv1Base.endsWith("engram.ts") || argv1Base.endsWith("engram.js") || argv1Base.endsWith("/remnic") || argv1Base.endsWith("/engram") || argv1Base.includes("packages/remnic-cli/src/index.") || process.env.REMNIC_CLI_BIN === "1" || process.env.ENGRAM_CLI_BIN === "1") {
|
|
1573
|
+
main().catch((err) => {
|
|
1574
|
+
process.stderr.write(`Fatal: ${err instanceof Error ? err.message : String(err)}
|
|
1575
|
+
`);
|
|
1576
|
+
process.exit(1);
|
|
1577
|
+
});
|
|
1578
|
+
}
|
|
1579
|
+
export {
|
|
1580
|
+
main
|
|
1581
|
+
};
|