mnueron 0.3.0 → 0.4.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/README.md +123 -1
- package/dashboard/index.html +38 -0
- package/dist/cli.js +1187 -1
- package/dist/cli.js.map +1 -1
- package/dist/dashboard/server.js +95 -0
- package/dist/dashboard/server.js.map +1 -1
- package/dist/detectors/claude_desktop.js +79 -22
- package/dist/detectors/claude_desktop.js.map +1 -1
- package/dist/import/claude_cowork.js +359 -0
- package/dist/import/claude_cowork.js.map +1 -0
- package/dist/import/claude_desktop.js +196 -0
- package/dist/import/claude_desktop.js.map +1 -0
- package/dist/store/consolidator.js +168 -0
- package/dist/store/consolidator.js.map +1 -0
- package/dist/store/entity-extractor.js +283 -0
- package/dist/store/entity-extractor.js.map +1 -0
- package/dist/store/entity-resolver.js +378 -0
- package/dist/store/entity-resolver.js.map +1 -0
- package/dist/store/local.js +522 -14
- package/dist/store/local.js.map +1 -1
- package/dist/store/procedural.js +328 -0
- package/dist/store/procedural.js.map +1 -0
- package/dist/store/relation-extractor.js +292 -0
- package/dist/store/relation-extractor.js.map +1 -0
- package/dist/store/remote.js +182 -20
- package/dist/store/remote.js.map +1 -1
- package/dist/tools.js +84 -0
- package/dist/tools.js.map +1 -1
- package/dist/watch/cowork.js +137 -0
- package/dist/watch/cowork.js.map +1 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -15,6 +15,7 @@ import { loadConfig, makeProvider } from './config.js';
|
|
|
15
15
|
import { importClaudeExport } from './import/claude.js';
|
|
16
16
|
import { importOpenAIExport } from './import/openai.js';
|
|
17
17
|
import { runSetup, formatReport } from './setup.js';
|
|
18
|
+
import { extractEntities } from './store/entity-extractor.js';
|
|
18
19
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
19
20
|
async function main() {
|
|
20
21
|
const [cmd, ...rest] = process.argv.slice(2);
|
|
@@ -31,6 +32,12 @@ async function main() {
|
|
|
31
32
|
case 'migrate-to-hosted': return cmdMigrateToHosted(rest);
|
|
32
33
|
case 'plugin': return cmdPlugin(rest);
|
|
33
34
|
case 'primer': return cmdPrimer(rest);
|
|
35
|
+
case 'extract-entities': return cmdExtractEntities(rest);
|
|
36
|
+
case 'entities': return cmdEntities(rest); // P2.3 — list/show/merge
|
|
37
|
+
case 'graph': return cmdGraph(rest); // P3 + P4 — knowledge graph
|
|
38
|
+
case 'consolidate': return cmdConsolidate(rest); // P5 — detection-only review queue
|
|
39
|
+
case 'procedural': return cmdProcedural(rest); // Procedural memory (Mem0 leapfrog)
|
|
40
|
+
case 'watch': return cmdWatch(rest);
|
|
34
41
|
case 'help':
|
|
35
42
|
case '--help':
|
|
36
43
|
case '-h':
|
|
@@ -53,6 +60,17 @@ Commands:
|
|
|
53
60
|
mnueron import <file> Bulk-import a Claude or OpenAI export
|
|
54
61
|
[--ns <name>] Target namespace (default: "default")
|
|
55
62
|
[--format claude|openai] Skip format auto-detection
|
|
63
|
+
mnueron import --claude-desktop Probe + auto-import the local Claude Desktop app
|
|
64
|
+
[--probe] Show what's found without importing
|
|
65
|
+
[--ns <name>] Target namespace (default: "claude-desktop")
|
|
66
|
+
mnueron import --claude-cowork Auto-import every Cowork (desktop "local agent")
|
|
67
|
+
[--probe] chat. Walks platform-specific roots
|
|
68
|
+
[--ns <name>] (incl. the Microsoft Store sandboxed path);
|
|
69
|
+
[--limit <n>] each session becomes a chunked memory;
|
|
70
|
+
[--dry-run] idempotent via source_ref dedup.
|
|
71
|
+
Example: mnueron import --claude-cowork --probe
|
|
72
|
+
mnueron import --claude-cowork --ns elevizio --limit 10
|
|
73
|
+
mnueron import --claude-cowork --dry-run
|
|
56
74
|
mnueron search <query> Search memories from the terminal
|
|
57
75
|
[--ns <name>] [--k <n>]
|
|
58
76
|
mnueron stats Show counts by namespace
|
|
@@ -85,6 +103,42 @@ Commands:
|
|
|
85
103
|
[--recent <n>] Sample n most-recent memory titles (default: 12).
|
|
86
104
|
[--out <file>] Write to file instead of stdout.
|
|
87
105
|
Example: mnueron primer > CLAUDE.md
|
|
106
|
+
mnueron extract-entities P2 — retroactively extract entities from existing memories.
|
|
107
|
+
[--ns <name>] Restrict to one namespace.
|
|
108
|
+
[--since <epoch_ms>] Only memories created on/after this time.
|
|
109
|
+
[--limit <n>] Cap how many to process (default 100, max 1000).
|
|
110
|
+
[--force] Re-extract memories that already have entities.
|
|
111
|
+
[--dry-run] Preview without making LLM calls.
|
|
112
|
+
Requires ANTHROPIC_API_KEY or OPENAI_API_KEY in the environment.
|
|
113
|
+
|
|
114
|
+
mnueron entities <sub> P2.3 — Canonical entity browser (resolved across memories).
|
|
115
|
+
list [--type <t>] [--q <substr>] [--sort recent|mentions|alpha] [--limit <n>]
|
|
116
|
+
show <entity-id> [--memories <n>]
|
|
117
|
+
merge --winner <id> --loser <id>
|
|
118
|
+
Example: mnueron entities list --type person --sort mentions
|
|
119
|
+
|
|
120
|
+
mnueron graph <sub> P3 + P4 — Knowledge graph (relationships + bi-temporal).
|
|
121
|
+
show <entity-id> [--as-of <ISO-date>]
|
|
122
|
+
traverse <entity-id> [--depth <n>] [--as-of <ISO-date>]
|
|
123
|
+
relations [--from <id>] [--to <id>] [--predicate <p>] [--as-of <ISO-date>]
|
|
124
|
+
The --as-of flag enables "what was true at that point in time" recall.
|
|
125
|
+
|
|
126
|
+
mnueron consolidate <sub> P5 — Self-revising memory (phase 5a: detection only).
|
|
127
|
+
detect [--limit <n>] [--threshold <0..1>] [--ns <name>]
|
|
128
|
+
list [--status pending|approved|rejected|all]
|
|
129
|
+
approve <proposal-id>
|
|
130
|
+
reject <proposal-id>
|
|
131
|
+
Detection scans for likely-duplicate memories via embedding similarity
|
|
132
|
+
and surfaces them as proposals. Phase 5a does NOT auto-merge.
|
|
133
|
+
|
|
134
|
+
mnueron watch Background sync — keep memory in step with new chats.
|
|
135
|
+
--claude-cowork Watch the on-disk Cowork transcripts and
|
|
136
|
+
[--interval <minutes>] incrementally import new/changed sessions.
|
|
137
|
+
[--ns <name>] Default interval 5m. State at
|
|
138
|
+
[--once] ~/.mnueron/cowork-sync.json. Ctrl+C stops it.
|
|
139
|
+
Example: mnueron watch --claude-cowork
|
|
140
|
+
mnueron watch --claude-cowork --interval 2 --ns elevizio
|
|
141
|
+
mnueron watch --claude-cowork --once
|
|
88
142
|
|
|
89
143
|
Environment:
|
|
90
144
|
MNUERON_DB_PATH Local SQLite location (default: ~/.mnueron/memories.db)
|
|
@@ -151,8 +205,138 @@ async function cmdSetup(args) {
|
|
|
151
205
|
}
|
|
152
206
|
}
|
|
153
207
|
async function cmdImport(args) {
|
|
208
|
+
// v0.2.6 — `--claude-cowork` mode auto-imports every Cowork chat
|
|
209
|
+
// transcript from ~/.claude/projects/. No positional <file>.
|
|
210
|
+
if (args.includes('--claude-cowork')) {
|
|
211
|
+
const { probeClaudeCowork, autoImport } = await import('./import/claude_cowork.js');
|
|
212
|
+
const probeOnly = args.includes('--probe') || args.includes('--probe-only');
|
|
213
|
+
const dryRun = args.includes('--dry-run');
|
|
214
|
+
let ns = 'claude-cowork';
|
|
215
|
+
let limit;
|
|
216
|
+
for (let i = 0; i < args.length; i++) {
|
|
217
|
+
if (args[i] === '--ns' && args[i + 1])
|
|
218
|
+
ns = args[++i];
|
|
219
|
+
else if (args[i] === '--limit' && args[i + 1]) {
|
|
220
|
+
const n = Number(args[++i]);
|
|
221
|
+
if (Number.isFinite(n) && n > 0)
|
|
222
|
+
limit = Math.floor(n);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
const probe = probeClaudeCowork();
|
|
226
|
+
console.log('🧠 Claude Cowork probe');
|
|
227
|
+
if (probe.scannedRoots.length > 0) {
|
|
228
|
+
console.log(` Scanned ${probe.scannedRoots.length} root(s):`);
|
|
229
|
+
for (const r of probe.scannedRoots)
|
|
230
|
+
console.log(` - ${r}`);
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
console.log(` Scanned 0 root(s) — none of the candidate paths exist.`);
|
|
234
|
+
console.log(` Paths attempted (${probe.pathsAttempted.length}):`);
|
|
235
|
+
for (const p of probe.pathsAttempted)
|
|
236
|
+
console.log(` - ${p}`);
|
|
237
|
+
}
|
|
238
|
+
console.log(` Cowork sessions: ${probe.sessions.length}`);
|
|
239
|
+
if (probe.sessions.length > 0) {
|
|
240
|
+
const preview = probe.sessions.slice(0, 5);
|
|
241
|
+
console.log(' Latest sessions:');
|
|
242
|
+
for (const s of preview) {
|
|
243
|
+
const title = s.title ?? '(untitled)';
|
|
244
|
+
const kb = (s.sizeBytes / 1024).toFixed(1);
|
|
245
|
+
console.log(` • ${title} — ${s.messageCount} msgs, ${kb} KB [${s.sessionId.slice(0, 8)}…]`);
|
|
246
|
+
}
|
|
247
|
+
if (probe.sessions.length > preview.length) {
|
|
248
|
+
console.log(` … and ${probe.sessions.length - preview.length} more`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
for (const h of probe.hints)
|
|
252
|
+
console.log(` ${h}`);
|
|
253
|
+
if (probeOnly) {
|
|
254
|
+
console.log(' (--probe mode — not importing anything)');
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
if (!probe.found || probe.sessions.length === 0) {
|
|
258
|
+
console.error('✗ Nothing to import.');
|
|
259
|
+
process.exit(1);
|
|
260
|
+
}
|
|
261
|
+
try {
|
|
262
|
+
const provider = makeProvider(loadConfig());
|
|
263
|
+
const result = await autoImport(provider, ns, { dryRun, limit });
|
|
264
|
+
await provider.close();
|
|
265
|
+
if (dryRun) {
|
|
266
|
+
console.log(` (--dry-run) Would import ${result.parsed} session(s), ` +
|
|
267
|
+
`skip ${result.empty} empty, into namespace "${ns}".`);
|
|
268
|
+
}
|
|
269
|
+
else {
|
|
270
|
+
console.log(`✓ Imported ${result.parsed} / ${result.totalSessions} session(s)`);
|
|
271
|
+
console.log(` Saved ${result.saved} memory item(s), empty ${result.empty}, errors ${result.errors}`);
|
|
272
|
+
console.log(` Namespace: "${ns}". Re-runs are idempotent (source_ref dedup).`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
catch (e) {
|
|
276
|
+
console.error(`✗ ${e.message}`);
|
|
277
|
+
process.exit(1);
|
|
278
|
+
}
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
// v0.2.5 — `--claude-desktop` mode probes for and auto-imports the
|
|
282
|
+
// user's locally-installed Claude Desktop export. No positional <file>.
|
|
283
|
+
if (args.includes('--claude-desktop')) {
|
|
284
|
+
const { probeClaudeDesktop, autoImport } = await import('./import/claude_desktop.js');
|
|
285
|
+
const probeOnly = args.includes('--probe') || args.includes('--probe-only');
|
|
286
|
+
let ns = 'claude-desktop';
|
|
287
|
+
let dirOverride = null;
|
|
288
|
+
for (let i = 0; i < args.length; i++) {
|
|
289
|
+
if (args[i] === '--ns' && args[i + 1])
|
|
290
|
+
ns = args[++i];
|
|
291
|
+
else if (args[i] === '--dir' && args[i + 1])
|
|
292
|
+
dirOverride = args[++i];
|
|
293
|
+
}
|
|
294
|
+
const probe = probeClaudeDesktop();
|
|
295
|
+
console.log('🧠 Claude Desktop probe');
|
|
296
|
+
console.log(` Location: ${probe.path ?? 'NOT FOUND'}`);
|
|
297
|
+
if (!probe.path) {
|
|
298
|
+
console.log(' Paths tried:');
|
|
299
|
+
for (const p of probe.pathsAttempted)
|
|
300
|
+
console.log(` - ${p}`);
|
|
301
|
+
}
|
|
302
|
+
else {
|
|
303
|
+
console.log(` Contents: ${probe.contents.length} entries`);
|
|
304
|
+
}
|
|
305
|
+
if (probe.exportCandidates.length > 0) {
|
|
306
|
+
console.log(' Export candidates:');
|
|
307
|
+
for (const e of probe.exportCandidates)
|
|
308
|
+
console.log(` • ${e}`);
|
|
309
|
+
}
|
|
310
|
+
if (probe.hints.length > 0) {
|
|
311
|
+
console.log(' Notes:');
|
|
312
|
+
for (const h of probe.hints)
|
|
313
|
+
console.log(` ${h}`);
|
|
314
|
+
}
|
|
315
|
+
if (probeOnly) {
|
|
316
|
+
console.log(' (--probe mode — not importing anything)');
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
if (dirOverride) {
|
|
320
|
+
console.log(` --dir ${dirOverride}: custom-path import not yet implemented; use Settings → Privacy → Export data.`);
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
try {
|
|
324
|
+
const provider = makeProvider(loadConfig());
|
|
325
|
+
const result = await autoImport(provider, ns);
|
|
326
|
+
await provider.close();
|
|
327
|
+
console.log(`✓ Imported from ${result.path}`);
|
|
328
|
+
console.log(` Saved ${result.saved}, errors ${result.errors}, namespace="${ns}"`);
|
|
329
|
+
}
|
|
330
|
+
catch (e) {
|
|
331
|
+
console.error(`✗ ${e.message}`);
|
|
332
|
+
process.exit(1);
|
|
333
|
+
}
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
154
336
|
if (args.length === 0) {
|
|
155
337
|
console.error('Usage: mnueron import <file> [--ns <namespace>] [--format claude|openai]');
|
|
338
|
+
console.error(' mnueron import --claude-desktop [--probe] [--ns <namespace>]');
|
|
339
|
+
console.error(' mnueron import --claude-cowork [--probe] [--ns <namespace>] [--limit N] [--dry-run]');
|
|
156
340
|
process.exit(1);
|
|
157
341
|
}
|
|
158
342
|
const file = args[0];
|
|
@@ -807,7 +991,1009 @@ function renderPrimer(input) {
|
|
|
807
991
|
lines.push('');
|
|
808
992
|
return lines.join('\n');
|
|
809
993
|
}
|
|
810
|
-
|
|
994
|
+
/**
|
|
995
|
+
* P2.4 — retroactive entity extraction for the local SQLite store.
|
|
996
|
+
*
|
|
997
|
+
* For each existing memory:
|
|
998
|
+
* 1. Skip if metadata.entities already exists (unless --force).
|
|
999
|
+
* 2. Extract entities via Haiku/OpenAI (whichever env key is set).
|
|
1000
|
+
* 3. Stamp the extracted list into metadata.entities via provider.update().
|
|
1001
|
+
*
|
|
1002
|
+
* Note: this only does P1 (extraction) for local. Cross-session entity
|
|
1003
|
+
* resolution (P2) on local SQLite is deferred — entities here are saved
|
|
1004
|
+
* as standalone lists per-memory without canonical_id linking yet.
|
|
1005
|
+
* Hosted-mode users get the full P1+P2 pipeline via /api/entities/backfill.
|
|
1006
|
+
*/
|
|
1007
|
+
async function cmdExtractEntities(args) {
|
|
1008
|
+
let ns;
|
|
1009
|
+
let since;
|
|
1010
|
+
let limit = 100;
|
|
1011
|
+
let force = false;
|
|
1012
|
+
let dryRun = false;
|
|
1013
|
+
for (let i = 0; i < args.length; i++) {
|
|
1014
|
+
const a = args[i];
|
|
1015
|
+
if (a === '--ns' && args[i + 1])
|
|
1016
|
+
ns = args[++i];
|
|
1017
|
+
else if (a === '--since' && args[i + 1])
|
|
1018
|
+
since = parseInt(args[++i], 10);
|
|
1019
|
+
else if (a === '--limit' && args[i + 1]) {
|
|
1020
|
+
const v = parseInt(args[++i], 10);
|
|
1021
|
+
if (Number.isFinite(v))
|
|
1022
|
+
limit = Math.max(1, Math.min(1000, v));
|
|
1023
|
+
}
|
|
1024
|
+
else if (a === '--force')
|
|
1025
|
+
force = true;
|
|
1026
|
+
else if (a === '--dry-run')
|
|
1027
|
+
dryRun = true;
|
|
1028
|
+
else if (a === '--help' || a === '-h') {
|
|
1029
|
+
console.log('Usage: mnueron extract-entities [--ns <name>] [--since <epoch_ms>]\n' +
|
|
1030
|
+
' [--limit <n>] [--force] [--dry-run]');
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
if (!process.env.ANTHROPIC_API_KEY && !process.env.OPENAI_API_KEY) {
|
|
1035
|
+
console.error('mnueron extract-entities requires ANTHROPIC_API_KEY or OPENAI_API_KEY in the environment.');
|
|
1036
|
+
process.exit(1);
|
|
1037
|
+
}
|
|
1038
|
+
const provider = makeProvider(loadConfig());
|
|
1039
|
+
// update() is optional on the Provider interface — bail early with a
|
|
1040
|
+
// clear error so TypeScript narrows it for the rest of this function.
|
|
1041
|
+
if (typeof provider.update !== 'function') {
|
|
1042
|
+
console.error('This provider does not support memory.update — cannot persist extracted entities.');
|
|
1043
|
+
process.exit(1);
|
|
1044
|
+
}
|
|
1045
|
+
const updateFn = provider.update.bind(provider);
|
|
1046
|
+
try {
|
|
1047
|
+
// Pull candidates newest-first. The provider's list() honors namespace
|
|
1048
|
+
// and date filters and returns memories with their metadata.
|
|
1049
|
+
const memories = await provider.list({
|
|
1050
|
+
namespace: ns,
|
|
1051
|
+
created_after: since,
|
|
1052
|
+
limit,
|
|
1053
|
+
offset: 0,
|
|
1054
|
+
});
|
|
1055
|
+
if (memories.length === 0) {
|
|
1056
|
+
console.log('No memories matched. Nothing to do.');
|
|
1057
|
+
return;
|
|
1058
|
+
}
|
|
1059
|
+
console.log(`Found ${memories.length} candidate memor${memories.length === 1 ? 'y' : 'ies'}${ns ? ` in namespace "${ns}"` : ''}.`);
|
|
1060
|
+
if (dryRun) {
|
|
1061
|
+
let withEntities = 0;
|
|
1062
|
+
for (const m of memories) {
|
|
1063
|
+
const meta = (m.metadata ?? {});
|
|
1064
|
+
if (Array.isArray(meta.entities) && meta.entities.length > 0) {
|
|
1065
|
+
withEntities += 1;
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
console.log(`[dry-run] Would extract for ${memories.length - withEntities} memories ` +
|
|
1069
|
+
`(skipping ${withEntities} that already have entities; pass --force to override).`);
|
|
1070
|
+
return;
|
|
1071
|
+
}
|
|
1072
|
+
let processed = 0;
|
|
1073
|
+
let extracted = 0;
|
|
1074
|
+
let skipped = 0;
|
|
1075
|
+
let errors = 0;
|
|
1076
|
+
for (const m of memories) {
|
|
1077
|
+
const meta = (m.metadata ?? {});
|
|
1078
|
+
const hasExisting = Array.isArray(meta.entities) && meta.entities.length > 0;
|
|
1079
|
+
if (hasExisting && !force) {
|
|
1080
|
+
skipped += 1;
|
|
1081
|
+
process.stdout.write('.');
|
|
1082
|
+
continue;
|
|
1083
|
+
}
|
|
1084
|
+
try {
|
|
1085
|
+
const ents = await extractEntities(m.content, {});
|
|
1086
|
+
processed += 1;
|
|
1087
|
+
if (ents.length === 0) {
|
|
1088
|
+
process.stdout.write('-');
|
|
1089
|
+
continue;
|
|
1090
|
+
}
|
|
1091
|
+
extracted += ents.length;
|
|
1092
|
+
// Merge into existing metadata; overwrite the entities key.
|
|
1093
|
+
await updateFn(m.id, {
|
|
1094
|
+
metadata: { ...meta, entities: ents },
|
|
1095
|
+
});
|
|
1096
|
+
process.stdout.write('+');
|
|
1097
|
+
}
|
|
1098
|
+
catch (e) {
|
|
1099
|
+
errors += 1;
|
|
1100
|
+
process.stdout.write('x');
|
|
1101
|
+
console.warn('\n[extract-entities] memory', m.id, 'failed:', e instanceof Error ? e.message : e);
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
process.stdout.write('\n');
|
|
1105
|
+
console.log(`extract-entities done — extracted=${extracted} processed=${processed} skipped=${skipped} errors=${errors}`);
|
|
1106
|
+
}
|
|
1107
|
+
catch (e) {
|
|
1108
|
+
console.error('extract-entities failed:', e);
|
|
1109
|
+
process.exit(1);
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
/**
|
|
1113
|
+
* P2.3 — `mnueron entities <list|show|merge>`
|
|
1114
|
+
*
|
|
1115
|
+
* Surfaces canonical entities the resolver has built up. Three subcommands:
|
|
1116
|
+
*
|
|
1117
|
+
* list — table view of entities, filtered by type / sorted by recent
|
|
1118
|
+
* / most-mentioned / alphabetical.
|
|
1119
|
+
* show — full detail for one entity, including the linked memories.
|
|
1120
|
+
* merge — collapse two duplicate canonicals into one (winner keeps id;
|
|
1121
|
+
* loser's memories + aliases absorbed into winner).
|
|
1122
|
+
*
|
|
1123
|
+
* All three only work with the local SQLite provider — hosted entity
|
|
1124
|
+
* resolution lives on the mnueron.com backend and the SDK's `entities`
|
|
1125
|
+
* namespace is the way to talk to it.
|
|
1126
|
+
*/
|
|
1127
|
+
async function cmdEntities(args) {
|
|
1128
|
+
const [sub, ...rest] = args;
|
|
1129
|
+
switch (sub) {
|
|
1130
|
+
case 'list': return cmdEntitiesList(rest);
|
|
1131
|
+
case 'show': return cmdEntitiesShow(rest);
|
|
1132
|
+
case 'merge': return cmdEntitiesMerge(rest);
|
|
1133
|
+
case 'backfill': return cmdEntitiesBackfill(rest);
|
|
1134
|
+
case undefined:
|
|
1135
|
+
case '--help':
|
|
1136
|
+
case '-h':
|
|
1137
|
+
console.log('Usage:\n' +
|
|
1138
|
+
' mnueron entities list [--type <t>] [--q <substr>] [--sort recent|mentions|alpha] [--limit <n>]\n' +
|
|
1139
|
+
' mnueron entities show <entity-id> [--memories <n>]\n' +
|
|
1140
|
+
' mnueron entities merge --winner <id> --loser <id>\n\n' +
|
|
1141
|
+
'Examples:\n' +
|
|
1142
|
+
' mnueron entities list --type person --sort mentions\n' +
|
|
1143
|
+
' mnueron entities show ent-abc123 --memories 50\n' +
|
|
1144
|
+
' mnueron entities merge --winner ent-abc --loser ent-def');
|
|
1145
|
+
return;
|
|
1146
|
+
default:
|
|
1147
|
+
console.error(`Unknown entities subcommand: ${sub}`);
|
|
1148
|
+
process.exit(1);
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
async function cmdEntitiesList(args) {
|
|
1152
|
+
let type;
|
|
1153
|
+
let q;
|
|
1154
|
+
let sort = 'recent';
|
|
1155
|
+
let limit = 50;
|
|
1156
|
+
for (let i = 0; i < args.length; i++) {
|
|
1157
|
+
const a = args[i];
|
|
1158
|
+
if (a === '--type' && args[i + 1])
|
|
1159
|
+
type = args[++i];
|
|
1160
|
+
else if (a === '--q' && args[i + 1])
|
|
1161
|
+
q = args[++i];
|
|
1162
|
+
else if (a === '--sort' && args[i + 1]) {
|
|
1163
|
+
const s = args[++i];
|
|
1164
|
+
if (s === 'recent' || s === 'mentions' || s === 'alpha')
|
|
1165
|
+
sort = s;
|
|
1166
|
+
}
|
|
1167
|
+
else if (a === '--limit' && args[i + 1]) {
|
|
1168
|
+
const v = parseInt(args[++i], 10);
|
|
1169
|
+
if (Number.isFinite(v))
|
|
1170
|
+
limit = Math.max(1, Math.min(500, v));
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
const provider = makeProvider(loadConfig());
|
|
1174
|
+
if (typeof provider.listEntities !== 'function') {
|
|
1175
|
+
console.error('This provider does not support entities (hosted-only — use the SDK / dashboard).');
|
|
1176
|
+
process.exit(1);
|
|
1177
|
+
}
|
|
1178
|
+
const entities = await provider.listEntities({ type, q, sort, limit });
|
|
1179
|
+
if (entities.length === 0) {
|
|
1180
|
+
console.log('No entities yet. Save some memories with entity extraction enabled and they\'ll appear here.');
|
|
1181
|
+
return;
|
|
1182
|
+
}
|
|
1183
|
+
// Pretty table. Truncate display_name + alias preview so 80-col terminals stay clean.
|
|
1184
|
+
const trunc = (s, n) => (s.length > n ? s.slice(0, n - 1) + '…' : s);
|
|
1185
|
+
const fmt = (d) => new Date(d).toISOString().slice(0, 10);
|
|
1186
|
+
console.log('id'.padEnd(38) +
|
|
1187
|
+
'type'.padEnd(14) +
|
|
1188
|
+
'name'.padEnd(30) +
|
|
1189
|
+
'mentions'.padStart(10) +
|
|
1190
|
+
' last seen');
|
|
1191
|
+
console.log('-'.repeat(110));
|
|
1192
|
+
for (const e of entities) {
|
|
1193
|
+
console.log(e.id.padEnd(38) +
|
|
1194
|
+
trunc(e.entity_type, 12).padEnd(14) +
|
|
1195
|
+
trunc(e.display_name, 28).padEnd(30) +
|
|
1196
|
+
String(e.mention_count).padStart(10) +
|
|
1197
|
+
' ' +
|
|
1198
|
+
fmt(e.last_seen_at));
|
|
1199
|
+
}
|
|
1200
|
+
console.log(`\n${entities.length} entit${entities.length === 1 ? 'y' : 'ies'} shown.`);
|
|
1201
|
+
}
|
|
1202
|
+
async function cmdEntitiesShow(args) {
|
|
1203
|
+
const id = args.find((a) => !a.startsWith('--'));
|
|
1204
|
+
if (!id) {
|
|
1205
|
+
console.error('Usage: mnueron entities show <entity-id> [--memories <n>]');
|
|
1206
|
+
process.exit(1);
|
|
1207
|
+
}
|
|
1208
|
+
let memLimit = 20;
|
|
1209
|
+
for (let i = 0; i < args.length; i++) {
|
|
1210
|
+
if (args[i] === '--memories' && args[i + 1]) {
|
|
1211
|
+
const v = parseInt(args[++i], 10);
|
|
1212
|
+
if (Number.isFinite(v))
|
|
1213
|
+
memLimit = Math.max(1, Math.min(200, v));
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
const provider = makeProvider(loadConfig());
|
|
1217
|
+
if (typeof provider.getEntity !== 'function' || typeof provider.getEntityMemories !== 'function') {
|
|
1218
|
+
console.error('This provider does not support entities.');
|
|
1219
|
+
process.exit(1);
|
|
1220
|
+
}
|
|
1221
|
+
const entity = await provider.getEntity(id);
|
|
1222
|
+
if (!entity) {
|
|
1223
|
+
console.error(`Entity not found: ${id}`);
|
|
1224
|
+
process.exit(1);
|
|
1225
|
+
}
|
|
1226
|
+
console.log(`# ${entity.display_name}`);
|
|
1227
|
+
console.log(` id: ${entity.id}`);
|
|
1228
|
+
console.log(` type: ${entity.entity_type}`);
|
|
1229
|
+
console.log(` mentions: ${entity.mention_count}`);
|
|
1230
|
+
console.log(` first seen: ${new Date(entity.first_seen_at).toISOString()}`);
|
|
1231
|
+
console.log(` last seen: ${new Date(entity.last_seen_at).toISOString()}`);
|
|
1232
|
+
if (entity.aliases.length > 0) {
|
|
1233
|
+
console.log(` aliases: ${entity.aliases.join(', ')}`);
|
|
1234
|
+
}
|
|
1235
|
+
const memories = await provider.getEntityMemories(entity.id, memLimit);
|
|
1236
|
+
console.log(`\n## Linked memories (${memories.length}${memories.length >= memLimit ? '+' : ''})`);
|
|
1237
|
+
for (const m of memories) {
|
|
1238
|
+
const date = new Date(m.created_at).toISOString().slice(0, 10);
|
|
1239
|
+
const preview = m.content.replace(/\s+/g, ' ').slice(0, 100);
|
|
1240
|
+
const conf = m.confidence === 1 ? 'exact' : m.confidence.toFixed(2);
|
|
1241
|
+
console.log(` - [${date}] (${conf}) "${m.surface_form}" — ${preview}${m.content.length > 100 ? '…' : ''}`);
|
|
1242
|
+
console.log(` memory: ${m.id} ns: ${m.namespace}`);
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
async function cmdEntitiesMerge(args) {
|
|
1246
|
+
let winner;
|
|
1247
|
+
let loser;
|
|
1248
|
+
for (let i = 0; i < args.length; i++) {
|
|
1249
|
+
if (args[i] === '--winner' && args[i + 1])
|
|
1250
|
+
winner = args[++i];
|
|
1251
|
+
else if (args[i] === '--loser' && args[i + 1])
|
|
1252
|
+
loser = args[++i];
|
|
1253
|
+
}
|
|
1254
|
+
if (!winner || !loser) {
|
|
1255
|
+
console.error('Usage: mnueron entities merge --winner <id> --loser <id>');
|
|
1256
|
+
process.exit(1);
|
|
1257
|
+
}
|
|
1258
|
+
if (winner === loser) {
|
|
1259
|
+
console.error('Winner and loser must be different entities.');
|
|
1260
|
+
process.exit(1);
|
|
1261
|
+
}
|
|
1262
|
+
const provider = makeProvider(loadConfig());
|
|
1263
|
+
if (typeof provider.mergeEntities !== 'function') {
|
|
1264
|
+
console.error('This provider does not support entities.');
|
|
1265
|
+
process.exit(1);
|
|
1266
|
+
}
|
|
1267
|
+
const merged = await provider.mergeEntities(winner, loser);
|
|
1268
|
+
if (!merged) {
|
|
1269
|
+
console.error('Merge failed — one or both entities not found.');
|
|
1270
|
+
process.exit(1);
|
|
1271
|
+
}
|
|
1272
|
+
console.log(`✅ Merged. Winner: ${merged.display_name} (${merged.id})`);
|
|
1273
|
+
console.log(` aliases now: ${merged.aliases.join(', ')}`);
|
|
1274
|
+
console.log(` mention count: ${merged.mention_count}`);
|
|
1275
|
+
}
|
|
1276
|
+
/**
|
|
1277
|
+
* P2.3 backfill — retro-fit canonical entity IDs onto memories that were
|
|
1278
|
+
* saved before the resolver shipped.
|
|
1279
|
+
*
|
|
1280
|
+
* Two-stage walk per memory:
|
|
1281
|
+
* 1. If `metadata.entities` is missing AND --extract was passed, run the
|
|
1282
|
+
* extractor (uses ANTHROPIC_API_KEY or OPENAI_API_KEY from env).
|
|
1283
|
+
* 2. If `metadata.entities` is now populated but lacks canonical_id values,
|
|
1284
|
+
* run the resolver. Updates the entities/memory_entities tables AND
|
|
1285
|
+
* stamps canonical_id back onto the stored metadata.
|
|
1286
|
+
*
|
|
1287
|
+
* Idempotent — re-running skips memories that are already resolved.
|
|
1288
|
+
*
|
|
1289
|
+
* Flags:
|
|
1290
|
+
* --ns <name> restrict to one namespace
|
|
1291
|
+
* --limit <n> cap how many memories to process (default 100, max 1000)
|
|
1292
|
+
* --since <epoch_ms> only memories created on/after this time
|
|
1293
|
+
* --extract also call the extractor for memories with no entities
|
|
1294
|
+
* --dry-run preview counts without writing
|
|
1295
|
+
* --force re-resolve memories that already have canonical_ids
|
|
1296
|
+
*/
|
|
1297
|
+
async function cmdEntitiesBackfill(args) {
|
|
1298
|
+
let ns;
|
|
1299
|
+
let since;
|
|
1300
|
+
let limit = 100;
|
|
1301
|
+
let alsoExtract = false;
|
|
1302
|
+
let dryRun = false;
|
|
1303
|
+
let force = false;
|
|
1304
|
+
for (let i = 0; i < args.length; i++) {
|
|
1305
|
+
const a = args[i];
|
|
1306
|
+
if (a === '--ns' && args[i + 1])
|
|
1307
|
+
ns = args[++i];
|
|
1308
|
+
else if (a === '--since' && args[i + 1])
|
|
1309
|
+
since = parseInt(args[++i], 10);
|
|
1310
|
+
else if (a === '--limit' && args[i + 1]) {
|
|
1311
|
+
const v = parseInt(args[++i], 10);
|
|
1312
|
+
if (Number.isFinite(v))
|
|
1313
|
+
limit = Math.max(1, Math.min(10000, v));
|
|
1314
|
+
}
|
|
1315
|
+
else if (a === '--extract')
|
|
1316
|
+
alsoExtract = true;
|
|
1317
|
+
else if (a === '--dry-run')
|
|
1318
|
+
dryRun = true;
|
|
1319
|
+
else if (a === '--force')
|
|
1320
|
+
force = true;
|
|
1321
|
+
else if (a === '--help' || a === '-h') {
|
|
1322
|
+
console.log('Usage: mnueron entities backfill [--ns <name>] [--limit <n>]\n' +
|
|
1323
|
+
' [--since <epoch_ms>] [--extract]\n' +
|
|
1324
|
+
' [--dry-run] [--force]\n\n' +
|
|
1325
|
+
' --extract also runs the extractor for memories with no entities\n' +
|
|
1326
|
+
' (requires ANTHROPIC_API_KEY or OPENAI_API_KEY env var)\n' +
|
|
1327
|
+
' --force re-resolve memories that already have canonical_ids');
|
|
1328
|
+
return;
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
const provider = makeProvider(loadConfig());
|
|
1332
|
+
if (typeof provider.backfillResolveMemory !== 'function' ||
|
|
1333
|
+
typeof provider.update !== 'function') {
|
|
1334
|
+
console.error('This provider does not support entity backfill.');
|
|
1335
|
+
process.exit(1);
|
|
1336
|
+
}
|
|
1337
|
+
const resolveFn = provider.backfillResolveMemory.bind(provider);
|
|
1338
|
+
const updateFn = provider.update.bind(provider);
|
|
1339
|
+
if (alsoExtract && !process.env.ANTHROPIC_API_KEY && !process.env.OPENAI_API_KEY) {
|
|
1340
|
+
console.error('--extract requires ANTHROPIC_API_KEY or OPENAI_API_KEY in the environment.');
|
|
1341
|
+
process.exit(1);
|
|
1342
|
+
}
|
|
1343
|
+
const memories = await provider.list({
|
|
1344
|
+
namespace: ns,
|
|
1345
|
+
created_after: since,
|
|
1346
|
+
limit,
|
|
1347
|
+
offset: 0,
|
|
1348
|
+
});
|
|
1349
|
+
if (memories.length === 0) {
|
|
1350
|
+
console.log('No memories matched. Nothing to do.');
|
|
1351
|
+
return;
|
|
1352
|
+
}
|
|
1353
|
+
console.log(`Found ${memories.length} memor${memories.length === 1 ? 'y' : 'ies'}${ns ? ` in "${ns}"` : ''}.`);
|
|
1354
|
+
let extracted = 0;
|
|
1355
|
+
let resolved = 0;
|
|
1356
|
+
let alreadyResolved = 0;
|
|
1357
|
+
let noEntities = 0;
|
|
1358
|
+
let errors = 0;
|
|
1359
|
+
for (const m of memories) {
|
|
1360
|
+
const meta = (m.metadata ?? {});
|
|
1361
|
+
let entities = Array.isArray(meta.entities)
|
|
1362
|
+
? meta.entities
|
|
1363
|
+
: [];
|
|
1364
|
+
// Stage 1 — optional extraction
|
|
1365
|
+
if (entities.length === 0) {
|
|
1366
|
+
if (!alsoExtract) {
|
|
1367
|
+
noEntities += 1;
|
|
1368
|
+
process.stdout.write('.');
|
|
1369
|
+
continue;
|
|
1370
|
+
}
|
|
1371
|
+
if (dryRun) {
|
|
1372
|
+
process.stdout.write('e');
|
|
1373
|
+
extracted += 1;
|
|
1374
|
+
continue;
|
|
1375
|
+
}
|
|
1376
|
+
try {
|
|
1377
|
+
const ents = await extractEntities(m.content, {});
|
|
1378
|
+
if (ents.length === 0) {
|
|
1379
|
+
process.stdout.write('-');
|
|
1380
|
+
continue;
|
|
1381
|
+
}
|
|
1382
|
+
entities = ents;
|
|
1383
|
+
extracted += ents.length;
|
|
1384
|
+
await updateFn(m.id, { metadata: { ...meta, entities } });
|
|
1385
|
+
process.stdout.write('e');
|
|
1386
|
+
}
|
|
1387
|
+
catch (e) {
|
|
1388
|
+
errors += 1;
|
|
1389
|
+
process.stdout.write('x');
|
|
1390
|
+
// Surface the error so we can diagnose — silent failures hid a real
|
|
1391
|
+
// bug in update() previously. Verbose by design: low-volume use case.
|
|
1392
|
+
console.error(`\n [extract/update failed] memory ${m.id}:`, e instanceof Error ? e.message : e);
|
|
1393
|
+
continue;
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
// Stage 2 — resolve (skip if already resolved unless --force)
|
|
1397
|
+
const allResolved = entities.every((e) => typeof e.canonical_id === 'string' && e.canonical_id);
|
|
1398
|
+
if (allResolved && !force) {
|
|
1399
|
+
alreadyResolved += 1;
|
|
1400
|
+
process.stdout.write('=');
|
|
1401
|
+
continue;
|
|
1402
|
+
}
|
|
1403
|
+
if (dryRun) {
|
|
1404
|
+
resolved += entities.length;
|
|
1405
|
+
process.stdout.write('r');
|
|
1406
|
+
continue;
|
|
1407
|
+
}
|
|
1408
|
+
try {
|
|
1409
|
+
const res = await resolveFn(m.id, entities.map((e) => ({ name: e.name, type: e.type, context: e.context })));
|
|
1410
|
+
const stamped = entities.map((e, i) => ({
|
|
1411
|
+
...e,
|
|
1412
|
+
canonical_id: res[i]?.canonical_id ?? null,
|
|
1413
|
+
}));
|
|
1414
|
+
await updateFn(m.id, { metadata: { ...meta, entities: stamped } });
|
|
1415
|
+
resolved += entities.length;
|
|
1416
|
+
process.stdout.write('+');
|
|
1417
|
+
}
|
|
1418
|
+
catch (e) {
|
|
1419
|
+
errors += 1;
|
|
1420
|
+
process.stdout.write('x');
|
|
1421
|
+
console.error(`\n [resolve failed] memory ${m.id}:`, e instanceof Error ? e.message : e);
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
process.stdout.write('\n');
|
|
1425
|
+
console.log(`backfill done — resolved=${resolved} extracted=${extracted} already_resolved=${alreadyResolved} no_entities=${noEntities} errors=${errors}` +
|
|
1426
|
+
(dryRun ? ' (dry-run — no writes)' : ''));
|
|
1427
|
+
if (!dryRun && resolved > 0) {
|
|
1428
|
+
console.log('\nRun "mnueron entities list --sort mentions" to see them.');
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
/**
|
|
1432
|
+
* P3 + P4 — `mnueron graph <show|traverse|relations>`
|
|
1433
|
+
*
|
|
1434
|
+
* show <entity-id> — overview: entity + its direct relations.
|
|
1435
|
+
* traverse <entity-id> — BFS out to --depth hops (default 2).
|
|
1436
|
+
* relations [--from <id>] [--to <id>] [--predicate <p>] [--as-of <iso>]
|
|
1437
|
+
* — raw edge query. Useful for scripting.
|
|
1438
|
+
*
|
|
1439
|
+
* All three accept `--as-of <ISO-date>` for bi-temporal queries: "what
|
|
1440
|
+
* relationships were valid at that point in time?" Falls through to "all
|
|
1441
|
+
* relations" when --as-of isn't passed.
|
|
1442
|
+
*/
|
|
1443
|
+
async function cmdGraph(args) {
|
|
1444
|
+
const [sub, ...rest] = args;
|
|
1445
|
+
switch (sub) {
|
|
1446
|
+
case 'show': return cmdGraphShow(rest);
|
|
1447
|
+
case 'traverse': return cmdGraphTraverse(rest);
|
|
1448
|
+
case 'relations': return cmdGraphRelations(rest);
|
|
1449
|
+
case undefined:
|
|
1450
|
+
case '--help':
|
|
1451
|
+
case '-h':
|
|
1452
|
+
console.log('Usage:\n' +
|
|
1453
|
+
' mnueron graph show <entity-id> [--as-of <ISO-date>]\n' +
|
|
1454
|
+
' mnueron graph traverse <entity-id> [--depth <n>] [--as-of <ISO-date>]\n' +
|
|
1455
|
+
' mnueron graph relations [--from <id>] [--to <id>] [--predicate <p>] [--as-of <ISO-date>] [--limit <n>]\n\n' +
|
|
1456
|
+
'The --as-of flag enables bi-temporal recall ("what was true at that date").\n' +
|
|
1457
|
+
'Example: mnueron graph traverse ent-john --depth 3 --as-of 2025-04-01');
|
|
1458
|
+
return;
|
|
1459
|
+
default:
|
|
1460
|
+
console.error(`Unknown graph subcommand: ${sub}`);
|
|
1461
|
+
process.exit(1);
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
/** Parse --as-of <ISO> into epoch ms, or undefined when absent. */
|
|
1465
|
+
function parseAsOf(args) {
|
|
1466
|
+
for (let i = 0; i < args.length; i++) {
|
|
1467
|
+
if (args[i] === '--as-of' && args[i + 1]) {
|
|
1468
|
+
const t = Date.parse(args[i + 1]);
|
|
1469
|
+
if (!Number.isFinite(t)) {
|
|
1470
|
+
console.error(`Invalid --as-of date: ${args[i + 1]}`);
|
|
1471
|
+
process.exit(1);
|
|
1472
|
+
}
|
|
1473
|
+
return t;
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
return undefined;
|
|
1477
|
+
}
|
|
1478
|
+
async function cmdGraphShow(args) {
|
|
1479
|
+
const id = args.find((a) => !a.startsWith('--'));
|
|
1480
|
+
if (!id) {
|
|
1481
|
+
console.error('Usage: mnueron graph show <entity-id> [--as-of <ISO-date>]');
|
|
1482
|
+
process.exit(1);
|
|
1483
|
+
}
|
|
1484
|
+
const asOf = parseAsOf(args);
|
|
1485
|
+
const provider = makeProvider(loadConfig());
|
|
1486
|
+
if (typeof provider.getEntity !== 'function' ||
|
|
1487
|
+
typeof provider.getRelations !== 'function') {
|
|
1488
|
+
console.error('This provider does not support the knowledge graph.');
|
|
1489
|
+
process.exit(1);
|
|
1490
|
+
}
|
|
1491
|
+
const entity = await provider.getEntity(id);
|
|
1492
|
+
if (!entity) {
|
|
1493
|
+
console.error(`Entity not found: ${id}`);
|
|
1494
|
+
process.exit(1);
|
|
1495
|
+
}
|
|
1496
|
+
console.log(`# ${entity.display_name} (${entity.entity_type})`);
|
|
1497
|
+
console.log(` id: ${entity.id}`);
|
|
1498
|
+
if (asOf)
|
|
1499
|
+
console.log(` as of: ${new Date(asOf).toISOString()}`);
|
|
1500
|
+
const outgoing = await provider.getRelations({ fromEntityId: id, asOf, limit: 200 });
|
|
1501
|
+
const incoming = await provider.getRelations({ toEntityId: id, asOf, limit: 200 });
|
|
1502
|
+
console.log(`\n## Outgoing (${outgoing.length})`);
|
|
1503
|
+
for (const rel of outgoing) {
|
|
1504
|
+
const target = await provider.getEntity(rel.to_entity_id);
|
|
1505
|
+
const targetName = target?.display_name ?? rel.to_entity_id;
|
|
1506
|
+
console.log(` -[${rel.predicate}]-> ${targetName} (conf ${rel.confidence.toFixed(2)}${formatWindow(rel)})`);
|
|
1507
|
+
}
|
|
1508
|
+
console.log(`\n## Incoming (${incoming.length})`);
|
|
1509
|
+
for (const rel of incoming) {
|
|
1510
|
+
const source = await provider.getEntity(rel.from_entity_id);
|
|
1511
|
+
const sourceName = source?.display_name ?? rel.from_entity_id;
|
|
1512
|
+
console.log(` ${sourceName} -[${rel.predicate}]-> ${entity.display_name} (conf ${rel.confidence.toFixed(2)}${formatWindow(rel)})`);
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
async function cmdGraphTraverse(args) {
|
|
1516
|
+
const id = args.find((a) => !a.startsWith('--'));
|
|
1517
|
+
if (!id) {
|
|
1518
|
+
console.error('Usage: mnueron graph traverse <entity-id> [--depth <n>] [--as-of <ISO-date>]');
|
|
1519
|
+
process.exit(1);
|
|
1520
|
+
}
|
|
1521
|
+
let depth = 2;
|
|
1522
|
+
for (let i = 0; i < args.length; i++) {
|
|
1523
|
+
if (args[i] === '--depth' && args[i + 1]) {
|
|
1524
|
+
const v = parseInt(args[++i], 10);
|
|
1525
|
+
if (Number.isFinite(v))
|
|
1526
|
+
depth = Math.max(0, Math.min(5, v));
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
const asOf = parseAsOf(args);
|
|
1530
|
+
const provider = makeProvider(loadConfig());
|
|
1531
|
+
if (typeof provider.traverseGraph !== 'function') {
|
|
1532
|
+
console.error('This provider does not support the knowledge graph.');
|
|
1533
|
+
process.exit(1);
|
|
1534
|
+
}
|
|
1535
|
+
const hops = await provider.traverseGraph(id, { depth, asOf });
|
|
1536
|
+
if (hops.length === 0) {
|
|
1537
|
+
console.error(`Entity not found: ${id}`);
|
|
1538
|
+
process.exit(1);
|
|
1539
|
+
}
|
|
1540
|
+
console.log(`# Traversal from ${hops[0].entity.display_name} depth=${depth}${asOf ? ` as-of=${new Date(asOf).toISOString()}` : ''}`);
|
|
1541
|
+
for (const hop of hops) {
|
|
1542
|
+
const indent = ' '.repeat(hop.depth);
|
|
1543
|
+
if (hop.depth === 0) {
|
|
1544
|
+
console.log(`${indent}● ${hop.entity.display_name} (${hop.entity.entity_type})`);
|
|
1545
|
+
}
|
|
1546
|
+
else {
|
|
1547
|
+
const arrow = hop.direction === 'out' ? '→' : '←';
|
|
1548
|
+
const pred = hop.via?.predicate ?? '?';
|
|
1549
|
+
console.log(`${indent}${arrow} [${pred}] ${hop.entity.display_name} (${hop.entity.entity_type})${formatWindow(hop.via)}`);
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
console.log(`\n${hops.length} node${hops.length === 1 ? '' : 's'} visited.`);
|
|
1553
|
+
}
|
|
1554
|
+
async function cmdGraphRelations(args) {
|
|
1555
|
+
let from;
|
|
1556
|
+
let to;
|
|
1557
|
+
let predicate;
|
|
1558
|
+
let limit = 100;
|
|
1559
|
+
for (let i = 0; i < args.length; i++) {
|
|
1560
|
+
if (args[i] === '--from' && args[i + 1])
|
|
1561
|
+
from = args[++i];
|
|
1562
|
+
else if (args[i] === '--to' && args[i + 1])
|
|
1563
|
+
to = args[++i];
|
|
1564
|
+
else if (args[i] === '--predicate' && args[i + 1])
|
|
1565
|
+
predicate = args[++i];
|
|
1566
|
+
else if (args[i] === '--limit' && args[i + 1]) {
|
|
1567
|
+
const v = parseInt(args[++i], 10);
|
|
1568
|
+
if (Number.isFinite(v))
|
|
1569
|
+
limit = Math.max(1, Math.min(1000, v));
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
const asOf = parseAsOf(args);
|
|
1573
|
+
const provider = makeProvider(loadConfig());
|
|
1574
|
+
if (typeof provider.getRelations !== 'function') {
|
|
1575
|
+
console.error('This provider does not support the knowledge graph.');
|
|
1576
|
+
process.exit(1);
|
|
1577
|
+
}
|
|
1578
|
+
const rels = await provider.getRelations({ fromEntityId: from, toEntityId: to, predicate, asOf, limit });
|
|
1579
|
+
console.log(JSON.stringify(rels, null, 2));
|
|
1580
|
+
}
|
|
1581
|
+
/** Human-readable validity window: " (valid 2022→2025)" or "" if none. */
|
|
1582
|
+
function formatWindow(rel) {
|
|
1583
|
+
if (!rel)
|
|
1584
|
+
return '';
|
|
1585
|
+
if (rel.valid_from == null && rel.valid_to == null)
|
|
1586
|
+
return '';
|
|
1587
|
+
const f = rel.valid_from ? new Date(rel.valid_from).toISOString().slice(0, 10) : '?';
|
|
1588
|
+
const t = rel.valid_to ? new Date(rel.valid_to).toISOString().slice(0, 10) : 'now';
|
|
1589
|
+
return ` (${f}→${t})`;
|
|
1590
|
+
}
|
|
1591
|
+
/**
|
|
1592
|
+
* P5 — `mnueron consolidate <detect|review|list|approve|reject>`
|
|
1593
|
+
*
|
|
1594
|
+
* Phase 5a is detection-only — no automatic mutation of memories. The
|
|
1595
|
+
* detector finds likely-duplicate pairs via embedding similarity and
|
|
1596
|
+
* enqueues them as proposals. The user reviews from the dashboard or
|
|
1597
|
+
* via these CLI commands.
|
|
1598
|
+
*
|
|
1599
|
+
* detect run the duplicate scan
|
|
1600
|
+
* list show pending proposals
|
|
1601
|
+
* approve <id> mark approved (5b will act on this; 5a just records the decision)
|
|
1602
|
+
* reject <id> mark rejected
|
|
1603
|
+
*/
|
|
1604
|
+
async function cmdConsolidate(args) {
|
|
1605
|
+
const [sub, ...rest] = args;
|
|
1606
|
+
switch (sub) {
|
|
1607
|
+
case 'detect': return cmdConsolidateDetect(rest);
|
|
1608
|
+
case 'list': return cmdConsolidateList(rest);
|
|
1609
|
+
case 'approve': return cmdConsolidateReview(rest, 'approved');
|
|
1610
|
+
case 'reject': return cmdConsolidateReview(rest, 'rejected');
|
|
1611
|
+
case undefined:
|
|
1612
|
+
case '--help':
|
|
1613
|
+
case '-h':
|
|
1614
|
+
console.log('Usage:\n' +
|
|
1615
|
+
' mnueron consolidate detect [--limit <n>] [--threshold <0..1>] [--ns <name>]\n' +
|
|
1616
|
+
' mnueron consolidate list [--status pending|approved|rejected] [--limit <n>]\n' +
|
|
1617
|
+
' mnueron consolidate approve <proposal-id>\n' +
|
|
1618
|
+
' mnueron consolidate reject <proposal-id>\n\n' +
|
|
1619
|
+
'Phase 5a — DETECTION ONLY. Approving a proposal records the decision\n' +
|
|
1620
|
+
'but does not yet merge memories. Phase 5b will action approved merges.');
|
|
1621
|
+
return;
|
|
1622
|
+
default:
|
|
1623
|
+
console.error(`Unknown consolidate subcommand: ${sub}`);
|
|
1624
|
+
process.exit(1);
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
async function cmdConsolidateDetect(args) {
|
|
1628
|
+
let limit = 200;
|
|
1629
|
+
let threshold;
|
|
1630
|
+
let namespace;
|
|
1631
|
+
for (let i = 0; i < args.length; i++) {
|
|
1632
|
+
if (args[i] === '--limit' && args[i + 1]) {
|
|
1633
|
+
const v = parseInt(args[++i], 10);
|
|
1634
|
+
if (Number.isFinite(v))
|
|
1635
|
+
limit = Math.max(1, Math.min(5000, v));
|
|
1636
|
+
}
|
|
1637
|
+
else if (args[i] === '--threshold' && args[i + 1]) {
|
|
1638
|
+
const v = parseFloat(args[++i]);
|
|
1639
|
+
if (Number.isFinite(v) && v >= 0 && v <= 1)
|
|
1640
|
+
threshold = v;
|
|
1641
|
+
}
|
|
1642
|
+
else if (args[i] === '--ns' && args[i + 1]) {
|
|
1643
|
+
namespace = args[++i];
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
const provider = makeProvider(loadConfig());
|
|
1647
|
+
if (typeof provider.detectConsolidation !== 'function') {
|
|
1648
|
+
console.error('This provider does not support consolidation (hosted-only — use the SDK).');
|
|
1649
|
+
process.exit(1);
|
|
1650
|
+
}
|
|
1651
|
+
console.log(`Scanning up to ${limit} memories${namespace ? ` in namespace "${namespace}"` : ''}...`);
|
|
1652
|
+
const result = await provider.detectConsolidation({ limit, threshold, namespace });
|
|
1653
|
+
console.log(`consolidate detect done — scanned=${result.scanned} created=${result.proposalsCreated} already_known=${result.proposalsAlreadyKnown}`);
|
|
1654
|
+
if (result.proposalsCreated > 0) {
|
|
1655
|
+
console.log(`\nRun "mnueron consolidate list" to review pending proposals.`);
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
async function cmdConsolidateList(args) {
|
|
1659
|
+
let status = 'pending';
|
|
1660
|
+
let limit = 50;
|
|
1661
|
+
for (let i = 0; i < args.length; i++) {
|
|
1662
|
+
if (args[i] === '--status' && args[i + 1]) {
|
|
1663
|
+
const s = args[++i];
|
|
1664
|
+
if (s === 'pending' || s === 'approved' || s === 'rejected')
|
|
1665
|
+
status = s;
|
|
1666
|
+
else if (s === 'all')
|
|
1667
|
+
status = undefined;
|
|
1668
|
+
}
|
|
1669
|
+
else if (args[i] === '--limit' && args[i + 1]) {
|
|
1670
|
+
const v = parseInt(args[++i], 10);
|
|
1671
|
+
if (Number.isFinite(v))
|
|
1672
|
+
limit = Math.max(1, Math.min(500, v));
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
const provider = makeProvider(loadConfig());
|
|
1676
|
+
if (typeof provider.proposalsList !== 'function') {
|
|
1677
|
+
console.error('This provider does not support consolidation.');
|
|
1678
|
+
process.exit(1);
|
|
1679
|
+
}
|
|
1680
|
+
const props = await provider.proposalsList({ status, limit });
|
|
1681
|
+
if (props.length === 0) {
|
|
1682
|
+
console.log(`No ${status ?? ''} proposals.`);
|
|
1683
|
+
return;
|
|
1684
|
+
}
|
|
1685
|
+
const trunc = (s, n) => (s.length > n ? s.slice(0, n - 1) + '…' : s);
|
|
1686
|
+
console.log('id'.padEnd(38) +
|
|
1687
|
+
'kind'.padEnd(14) +
|
|
1688
|
+
'score'.padStart(6) +
|
|
1689
|
+
' ' +
|
|
1690
|
+
'memory_a → memory_b');
|
|
1691
|
+
console.log('-'.repeat(110));
|
|
1692
|
+
for (const p of props) {
|
|
1693
|
+
console.log(p.id.padEnd(38) +
|
|
1694
|
+
trunc(p.kind, 12).padEnd(14) +
|
|
1695
|
+
p.score.toFixed(2).padStart(6) +
|
|
1696
|
+
' ' +
|
|
1697
|
+
`${p.memory_a_id.slice(0, 8)} → ${p.memory_b_id.slice(0, 8)}` +
|
|
1698
|
+
(p.status !== 'pending' ? ` [${p.status}]` : ''));
|
|
1699
|
+
}
|
|
1700
|
+
console.log(`\n${props.length} proposal${props.length === 1 ? '' : 's'}.`);
|
|
1701
|
+
}
|
|
1702
|
+
async function cmdConsolidateReview(args, decision) {
|
|
1703
|
+
const id = args.find((a) => !a.startsWith('--'));
|
|
1704
|
+
if (!id) {
|
|
1705
|
+
console.error(`Usage: mnueron consolidate ${decision === 'approved' ? 'approve' : 'reject'} <proposal-id>`);
|
|
1706
|
+
process.exit(1);
|
|
1707
|
+
}
|
|
1708
|
+
const provider = makeProvider(loadConfig());
|
|
1709
|
+
if (typeof provider.proposalReview !== 'function') {
|
|
1710
|
+
console.error('This provider does not support consolidation.');
|
|
1711
|
+
process.exit(1);
|
|
1712
|
+
}
|
|
1713
|
+
const updated = await provider.proposalReview(id, decision);
|
|
1714
|
+
if (!updated) {
|
|
1715
|
+
console.error(`Proposal not found: ${id}`);
|
|
1716
|
+
process.exit(1);
|
|
1717
|
+
}
|
|
1718
|
+
console.log(`✅ Proposal ${id} marked ${decision}.`);
|
|
1719
|
+
}
|
|
1720
|
+
/**
|
|
1721
|
+
* Procedural memory CLI — `mnueron procedural <sub>`
|
|
1722
|
+
*
|
|
1723
|
+
* The "remembered workflows" feature. Mem0 doesn't ship this — semantic +
|
|
1724
|
+
* episodic only. Procedural memory captures step-by-step runbooks so an
|
|
1725
|
+
* agent (or you) can recall "how do I deploy the API" and get the same
|
|
1726
|
+
* sequence of steps that worked last time.
|
|
1727
|
+
*
|
|
1728
|
+
* save <name> --steps "..." Save explicit steps (JSON file or stdin)
|
|
1729
|
+
* save <name> --from-memory <id> LLM-extract steps from an existing memory
|
|
1730
|
+
* list Show all procedural memories
|
|
1731
|
+
* show <name> Print the full runbook
|
|
1732
|
+
* recall <name> Same as show; bumps last_used_at
|
|
1733
|
+
* delete <id> Hard-delete a procedural memory
|
|
1734
|
+
*/
|
|
1735
|
+
async function cmdProcedural(args) {
|
|
1736
|
+
const [sub, ...rest] = args;
|
|
1737
|
+
switch (sub) {
|
|
1738
|
+
case 'save': return cmdProceduralSave(rest);
|
|
1739
|
+
case 'list': return cmdProceduralList(rest);
|
|
1740
|
+
case 'show':
|
|
1741
|
+
case 'recall': return cmdProceduralShow(rest, sub === 'recall');
|
|
1742
|
+
case 'delete': return cmdProceduralDelete(rest);
|
|
1743
|
+
case undefined:
|
|
1744
|
+
case '--help':
|
|
1745
|
+
case '-h':
|
|
1746
|
+
console.log('Usage:\n' +
|
|
1747
|
+
' mnueron procedural save <name> --from-memory <memory-id> [--ns <name>]\n' +
|
|
1748
|
+
' mnueron procedural save <name> --steps-file <path.json> [--ns <name>] [--summary "..."]\n' +
|
|
1749
|
+
' mnueron procedural list [--ns <name>] [--limit <n>]\n' +
|
|
1750
|
+
' mnueron procedural show <name> [--ns <name>]\n' +
|
|
1751
|
+
' mnueron procedural recall <name> [--ns <name>] (same as show, bumps last_used_at)\n' +
|
|
1752
|
+
' mnueron procedural delete <id>\n\n' +
|
|
1753
|
+
'Examples:\n' +
|
|
1754
|
+
' mnueron procedural save deploy-api --from-memory abc-123\n' +
|
|
1755
|
+
' mnueron procedural recall deploy-api');
|
|
1756
|
+
return;
|
|
1757
|
+
default:
|
|
1758
|
+
console.error(`Unknown procedural subcommand: ${sub}`);
|
|
1759
|
+
process.exit(1);
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
async function cmdProceduralSave(args) {
|
|
1763
|
+
const name = args.find((a) => !a.startsWith('--'));
|
|
1764
|
+
if (!name) {
|
|
1765
|
+
console.error('Usage: mnueron procedural save <name> [--from-memory <id> | --steps-file <path>] [--ns <name>] [--summary "..."]');
|
|
1766
|
+
process.exit(1);
|
|
1767
|
+
}
|
|
1768
|
+
let fromMemory;
|
|
1769
|
+
let stepsFile;
|
|
1770
|
+
let ns;
|
|
1771
|
+
let summary;
|
|
1772
|
+
for (let i = 0; i < args.length; i++) {
|
|
1773
|
+
if (args[i] === '--from-memory' && args[i + 1])
|
|
1774
|
+
fromMemory = args[++i];
|
|
1775
|
+
else if (args[i] === '--steps-file' && args[i + 1])
|
|
1776
|
+
stepsFile = args[++i];
|
|
1777
|
+
else if (args[i] === '--ns' && args[i + 1])
|
|
1778
|
+
ns = args[++i];
|
|
1779
|
+
else if (args[i] === '--summary' && args[i + 1])
|
|
1780
|
+
summary = args[++i];
|
|
1781
|
+
}
|
|
1782
|
+
const provider = makeProvider(loadConfig());
|
|
1783
|
+
if (typeof provider.saveProcedural !== 'function') {
|
|
1784
|
+
console.error('This provider does not support procedural memory.');
|
|
1785
|
+
process.exit(1);
|
|
1786
|
+
}
|
|
1787
|
+
if (fromMemory) {
|
|
1788
|
+
// LLM-extract path: pull the memory's content and ask Haiku/OpenAI
|
|
1789
|
+
// to turn it into a runbook, then save the result.
|
|
1790
|
+
if (!process.env.ANTHROPIC_API_KEY && !process.env.OPENAI_API_KEY) {
|
|
1791
|
+
console.error('--from-memory requires ANTHROPIC_API_KEY or OPENAI_API_KEY in the environment.');
|
|
1792
|
+
process.exit(1);
|
|
1793
|
+
}
|
|
1794
|
+
if (typeof provider.get !== 'function') {
|
|
1795
|
+
console.error('Provider does not support memory.get.');
|
|
1796
|
+
process.exit(1);
|
|
1797
|
+
}
|
|
1798
|
+
const memory = await provider.get(fromMemory);
|
|
1799
|
+
if (!memory) {
|
|
1800
|
+
console.error(`Memory not found: ${fromMemory}`);
|
|
1801
|
+
process.exit(1);
|
|
1802
|
+
}
|
|
1803
|
+
const { extractProcedural } = await import('./store/procedural.js');
|
|
1804
|
+
console.log('Extracting procedural memory via LLM…');
|
|
1805
|
+
const extracted = await extractProcedural(memory.content, {});
|
|
1806
|
+
if (!extracted) {
|
|
1807
|
+
console.error('Could not extract a procedural memory from that content. Use --steps-file for explicit input.');
|
|
1808
|
+
process.exit(1);
|
|
1809
|
+
}
|
|
1810
|
+
const saved = await provider.saveProcedural({
|
|
1811
|
+
name: name,
|
|
1812
|
+
namespace: ns,
|
|
1813
|
+
summary: summary ?? extracted.summary,
|
|
1814
|
+
steps: extracted.steps,
|
|
1815
|
+
tools: extracted.tools,
|
|
1816
|
+
});
|
|
1817
|
+
console.log(`✅ Saved procedural memory "${saved.name}" (${saved.steps.length} steps)`);
|
|
1818
|
+
return;
|
|
1819
|
+
}
|
|
1820
|
+
if (stepsFile) {
|
|
1821
|
+
const fs = await import('node:fs/promises');
|
|
1822
|
+
let raw;
|
|
1823
|
+
try {
|
|
1824
|
+
raw = await fs.readFile(stepsFile, 'utf8');
|
|
1825
|
+
}
|
|
1826
|
+
catch (e) {
|
|
1827
|
+
console.error(`Could not read ${stepsFile}: ${e instanceof Error ? e.message : e}`);
|
|
1828
|
+
process.exit(1);
|
|
1829
|
+
}
|
|
1830
|
+
let parsed;
|
|
1831
|
+
try {
|
|
1832
|
+
parsed = JSON.parse(raw);
|
|
1833
|
+
}
|
|
1834
|
+
catch (e) {
|
|
1835
|
+
console.error('Steps file must be valid JSON.');
|
|
1836
|
+
process.exit(1);
|
|
1837
|
+
}
|
|
1838
|
+
if (!Array.isArray(parsed)) {
|
|
1839
|
+
console.error('Steps file must be a JSON array of { step, code?, why? } objects.');
|
|
1840
|
+
process.exit(1);
|
|
1841
|
+
}
|
|
1842
|
+
const steps = parsed
|
|
1843
|
+
.filter((s) => s && typeof s.step === 'string')
|
|
1844
|
+
.map((s) => ({
|
|
1845
|
+
step: String(s.step).trim(),
|
|
1846
|
+
code: typeof s.code === 'string' ? s.code : undefined,
|
|
1847
|
+
why: typeof s.why === 'string' ? s.why : undefined,
|
|
1848
|
+
}));
|
|
1849
|
+
if (steps.length === 0) {
|
|
1850
|
+
console.error('No valid steps found in file.');
|
|
1851
|
+
process.exit(1);
|
|
1852
|
+
}
|
|
1853
|
+
const saved = await provider.saveProcedural({
|
|
1854
|
+
name,
|
|
1855
|
+
namespace: ns,
|
|
1856
|
+
summary: summary ?? '',
|
|
1857
|
+
steps,
|
|
1858
|
+
});
|
|
1859
|
+
console.log(`✅ Saved procedural memory "${saved.name}" (${saved.steps.length} steps)`);
|
|
1860
|
+
return;
|
|
1861
|
+
}
|
|
1862
|
+
console.error('Either --from-memory <id> or --steps-file <path.json> is required.');
|
|
1863
|
+
process.exit(1);
|
|
1864
|
+
}
|
|
1865
|
+
async function cmdProceduralList(args) {
|
|
1866
|
+
let ns;
|
|
1867
|
+
let limit = 50;
|
|
1868
|
+
for (let i = 0; i < args.length; i++) {
|
|
1869
|
+
if (args[i] === '--ns' && args[i + 1])
|
|
1870
|
+
ns = args[++i];
|
|
1871
|
+
else if (args[i] === '--limit' && args[i + 1]) {
|
|
1872
|
+
const v = parseInt(args[++i], 10);
|
|
1873
|
+
if (Number.isFinite(v))
|
|
1874
|
+
limit = Math.max(1, Math.min(500, v));
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
const provider = makeProvider(loadConfig());
|
|
1878
|
+
if (typeof provider.listProcedural !== 'function') {
|
|
1879
|
+
console.error('This provider does not support procedural memory.');
|
|
1880
|
+
process.exit(1);
|
|
1881
|
+
}
|
|
1882
|
+
const list = await provider.listProcedural({ namespace: ns, limit });
|
|
1883
|
+
if (list.length === 0) {
|
|
1884
|
+
console.log('No procedural memories yet. Use `mnueron procedural save` to create one.');
|
|
1885
|
+
return;
|
|
1886
|
+
}
|
|
1887
|
+
const trunc = (s, n) => (s.length > n ? s.slice(0, n - 1) + '…' : s);
|
|
1888
|
+
const fmt = (d) => new Date(d).toISOString().slice(0, 10);
|
|
1889
|
+
console.log('name'.padEnd(28) +
|
|
1890
|
+
'steps'.padStart(6) +
|
|
1891
|
+
'used'.padStart(6) +
|
|
1892
|
+
' ' +
|
|
1893
|
+
'last used'.padEnd(12) +
|
|
1894
|
+
'summary');
|
|
1895
|
+
console.log('-'.repeat(110));
|
|
1896
|
+
for (const p of list) {
|
|
1897
|
+
console.log(trunc(p.name, 26).padEnd(28) +
|
|
1898
|
+
String(p.steps.length).padStart(6) +
|
|
1899
|
+
String(p.use_count).padStart(6) +
|
|
1900
|
+
' ' +
|
|
1901
|
+
fmt(p.last_used_at).padEnd(12) +
|
|
1902
|
+
trunc(p.summary, 60));
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
async function cmdProceduralShow(args, bump) {
|
|
1906
|
+
const name = args.find((a) => !a.startsWith('--'));
|
|
1907
|
+
if (!name) {
|
|
1908
|
+
console.error('Usage: mnueron procedural show <name> [--ns <name>]');
|
|
1909
|
+
process.exit(1);
|
|
1910
|
+
}
|
|
1911
|
+
let ns;
|
|
1912
|
+
for (let i = 0; i < args.length; i++) {
|
|
1913
|
+
if (args[i] === '--ns' && args[i + 1])
|
|
1914
|
+
ns = args[++i];
|
|
1915
|
+
}
|
|
1916
|
+
const provider = makeProvider(loadConfig());
|
|
1917
|
+
const fn = bump ? provider.recallProcedural : provider.getProcedural;
|
|
1918
|
+
if (typeof fn !== 'function') {
|
|
1919
|
+
console.error('This provider does not support procedural memory.');
|
|
1920
|
+
process.exit(1);
|
|
1921
|
+
}
|
|
1922
|
+
const p = await fn.call(provider, name, ns);
|
|
1923
|
+
if (!p) {
|
|
1924
|
+
console.error(`Procedural memory not found: "${name}"${ns ? ` in namespace "${ns}"` : ''}`);
|
|
1925
|
+
process.exit(1);
|
|
1926
|
+
}
|
|
1927
|
+
console.log(`# ${p.name}\n`);
|
|
1928
|
+
if (p.summary)
|
|
1929
|
+
console.log(` ${p.summary}\n`);
|
|
1930
|
+
console.log(` id: ${p.id}`);
|
|
1931
|
+
console.log(` ns: ${p.namespace}`);
|
|
1932
|
+
console.log(` used: ${p.use_count} time${p.use_count === 1 ? '' : 's'}`);
|
|
1933
|
+
console.log(` last used: ${new Date(p.last_used_at).toISOString()}\n`);
|
|
1934
|
+
console.log('## Steps');
|
|
1935
|
+
p.steps.forEach((s, i) => {
|
|
1936
|
+
console.log(` ${i + 1}. ${s.step}`);
|
|
1937
|
+
if (s.code)
|
|
1938
|
+
console.log(` $ ${s.code}`);
|
|
1939
|
+
if (s.why)
|
|
1940
|
+
console.log(` — ${s.why}`);
|
|
1941
|
+
});
|
|
1942
|
+
if (p.tools.length > 0) {
|
|
1943
|
+
console.log(`\n## Tools used\n ${p.tools.join(', ')}`);
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
async function cmdProceduralDelete(args) {
|
|
1947
|
+
const id = args.find((a) => !a.startsWith('--'));
|
|
1948
|
+
if (!id) {
|
|
1949
|
+
console.error('Usage: mnueron procedural delete <id>');
|
|
1950
|
+
process.exit(1);
|
|
1951
|
+
}
|
|
1952
|
+
const provider = makeProvider(loadConfig());
|
|
1953
|
+
if (typeof provider.deleteProcedural !== 'function') {
|
|
1954
|
+
console.error('This provider does not support procedural memory.');
|
|
1955
|
+
process.exit(1);
|
|
1956
|
+
}
|
|
1957
|
+
const ok = await provider.deleteProcedural(id);
|
|
1958
|
+
if (!ok) {
|
|
1959
|
+
console.error('Procedural memory not found.');
|
|
1960
|
+
process.exit(1);
|
|
1961
|
+
}
|
|
1962
|
+
console.log('✅ Deleted.');
|
|
1963
|
+
}
|
|
1964
|
+
async function cmdWatch(args) {
|
|
1965
|
+
// For now the only mode is `--claude-cowork`. Future modes can dispatch here.
|
|
1966
|
+
if (!args.includes('--claude-cowork')) {
|
|
1967
|
+
console.error('Usage: mnueron watch --claude-cowork [--interval <minutes>] [--ns <name>] [--once]');
|
|
1968
|
+
process.exit(1);
|
|
1969
|
+
}
|
|
1970
|
+
let intervalMs;
|
|
1971
|
+
let ns = 'claude-cowork';
|
|
1972
|
+
let once = false;
|
|
1973
|
+
for (let i = 0; i < args.length; i++) {
|
|
1974
|
+
const a = args[i];
|
|
1975
|
+
if (a === '--interval' && args[i + 1]) {
|
|
1976
|
+
const n = Number(args[++i]);
|
|
1977
|
+
if (Number.isFinite(n) && n > 0)
|
|
1978
|
+
intervalMs = Math.floor(n * 60 * 1000);
|
|
1979
|
+
}
|
|
1980
|
+
else if (a === '--ns' && args[i + 1]) {
|
|
1981
|
+
ns = args[++i];
|
|
1982
|
+
}
|
|
1983
|
+
else if (a === '--once') {
|
|
1984
|
+
once = true;
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
const { runCoworkWatch } = await import('./watch/cowork.js');
|
|
1988
|
+
const provider = makeProvider(loadConfig());
|
|
1989
|
+
try {
|
|
1990
|
+
await runCoworkWatch(provider, { intervalMs, namespace: ns, once });
|
|
1991
|
+
}
|
|
1992
|
+
finally {
|
|
1993
|
+
await provider.close();
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
main().catch((e) => {
|
|
811
1997
|
console.error(e?.stack ?? e);
|
|
812
1998
|
process.exit(1);
|
|
813
1999
|
});
|