engram-sdk 0.1.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.
Files changed (69) hide show
  1. package/CONTRIBUTING.md +65 -0
  2. package/Dockerfile +21 -0
  3. package/EVAL-FRAMEWORK.md +70 -0
  4. package/EVAL.md +127 -0
  5. package/LICENSE +17 -0
  6. package/README.md +309 -0
  7. package/ROADMAP.md +113 -0
  8. package/deploy/fly.toml +26 -0
  9. package/dist/auto-ingest.d.ts +3 -0
  10. package/dist/auto-ingest.d.ts.map +1 -0
  11. package/dist/auto-ingest.js +334 -0
  12. package/dist/auto-ingest.js.map +1 -0
  13. package/dist/brief.d.ts +45 -0
  14. package/dist/brief.d.ts.map +1 -0
  15. package/dist/brief.js +183 -0
  16. package/dist/brief.js.map +1 -0
  17. package/dist/claude-watcher.d.ts +3 -0
  18. package/dist/claude-watcher.d.ts.map +1 -0
  19. package/dist/claude-watcher.js +385 -0
  20. package/dist/claude-watcher.js.map +1 -0
  21. package/dist/cli.d.ts +3 -0
  22. package/dist/cli.d.ts.map +1 -0
  23. package/dist/cli.js +764 -0
  24. package/dist/cli.js.map +1 -0
  25. package/dist/embeddings.d.ts +42 -0
  26. package/dist/embeddings.d.ts.map +1 -0
  27. package/dist/embeddings.js +145 -0
  28. package/dist/embeddings.js.map +1 -0
  29. package/dist/eval.d.ts +2 -0
  30. package/dist/eval.d.ts.map +1 -0
  31. package/dist/eval.js +281 -0
  32. package/dist/eval.js.map +1 -0
  33. package/dist/extract.d.ts +11 -0
  34. package/dist/extract.d.ts.map +1 -0
  35. package/dist/extract.js +139 -0
  36. package/dist/extract.js.map +1 -0
  37. package/dist/hosted.d.ts +3 -0
  38. package/dist/hosted.d.ts.map +1 -0
  39. package/dist/hosted.js +144 -0
  40. package/dist/hosted.js.map +1 -0
  41. package/dist/index.d.ts +11 -0
  42. package/dist/index.d.ts.map +1 -0
  43. package/dist/index.js +7 -0
  44. package/dist/index.js.map +1 -0
  45. package/dist/ingest.d.ts +28 -0
  46. package/dist/ingest.d.ts.map +1 -0
  47. package/dist/ingest.js +192 -0
  48. package/dist/ingest.js.map +1 -0
  49. package/dist/mcp.d.ts +3 -0
  50. package/dist/mcp.d.ts.map +1 -0
  51. package/dist/mcp.js +349 -0
  52. package/dist/mcp.js.map +1 -0
  53. package/dist/server.d.ts +17 -0
  54. package/dist/server.d.ts.map +1 -0
  55. package/dist/server.js +515 -0
  56. package/dist/server.js.map +1 -0
  57. package/dist/store.d.ts +87 -0
  58. package/dist/store.d.ts.map +1 -0
  59. package/dist/store.js +548 -0
  60. package/dist/store.js.map +1 -0
  61. package/dist/types.d.ts +204 -0
  62. package/dist/types.d.ts.map +1 -0
  63. package/dist/types.js +77 -0
  64. package/dist/types.js.map +1 -0
  65. package/dist/vault.d.ts +116 -0
  66. package/dist/vault.d.ts.map +1 -0
  67. package/dist/vault.js +1234 -0
  68. package/dist/vault.js.map +1 -0
  69. package/package.json +61 -0
package/dist/cli.js ADDED
@@ -0,0 +1,764 @@
1
+ #!/usr/bin/env node
2
+ import { Vault } from './vault.js';
3
+ import { runEval } from './eval.js';
4
+ import path from 'path';
5
+ import { homedir } from 'os';
6
+ import { parseArgs } from 'util';
7
+ // ============================================================
8
+ // Engram CLI — Quick interface for testing & exploration
9
+ // ============================================================
10
+ const HELP = `
11
+ engram — Universal memory layer for AI agents
12
+
13
+ Usage:
14
+ engram init Set up Engram for Claude Code / Cursor / MCP clients
15
+ engram mcp Start the MCP server (stdio transport)
16
+ engram shadow start Start shadow mode (server + watcher, background)
17
+ engram shadow stop Stop shadow mode
18
+ engram shadow status Check shadow mode status and memory count
19
+ engram shadow results Compare Engram vs your CLAUDE.md
20
+ engram remember <text> Store a memory
21
+ engram recall <context> Retrieve relevant memories
22
+ engram stats Show vault statistics
23
+ engram entities List known entities
24
+ engram export Export entire vault as JSON
25
+ engram consolidate Run memory consolidation
26
+ engram forget <id> [--hard] Forget a memory (soft or hard delete)
27
+ engram search <query> Full-text search
28
+ engram eval Health report & value assessment
29
+ engram repl Interactive REPL mode
30
+
31
+ Options:
32
+ --db <path> Database file path (default: ~/.engram/default.db)
33
+ --owner <name> Owner identifier (default: "default")
34
+ --agent <id> Agent ID for source tracking
35
+ --json Output as JSON
36
+ --help Show this help
37
+ `;
38
+ function parseCliArgs() {
39
+ const { values, positionals } = parseArgs({
40
+ allowPositionals: true,
41
+ options: {
42
+ db: { type: 'string', default: '' },
43
+ owner: { type: 'string', default: 'default' },
44
+ agent: { type: 'string', default: '' },
45
+ json: { type: 'boolean', default: false },
46
+ hard: { type: 'boolean', default: false },
47
+ limit: { type: 'string', default: '20' },
48
+ help: { type: 'boolean', short: 'h', default: false },
49
+ entities: { type: 'string', default: '' },
50
+ topics: { type: 'string', default: '' },
51
+ type: { type: 'string', default: '' },
52
+ salience: { type: 'string', default: '' },
53
+ confidence: { type: 'string', default: '' },
54
+ },
55
+ });
56
+ return { values, positionals };
57
+ }
58
+ function createVault(values) {
59
+ const config = {
60
+ owner: values.owner || 'default',
61
+ dbPath: values.db || path.join(homedir(), '.engram', 'default.db'),
62
+ agentId: values.agent || undefined,
63
+ };
64
+ return new Vault(config);
65
+ }
66
+ function printMemory(mem, json) {
67
+ if (json) {
68
+ console.log(JSON.stringify(mem, null, 2));
69
+ return;
70
+ }
71
+ const m = mem;
72
+ const age = timeSince(m.createdAt);
73
+ const entityStr = m.entities?.length ? ` [${m.entities.join(', ')}]` : '';
74
+ const topicStr = m.topics?.length ? ` #${m.topics.join(' #')}` : '';
75
+ console.log(` ${dim(m.id.slice(0, 8))} ${m.type.padEnd(11)} ${bold(m.summary || m.content.slice(0, 80))}${entityStr}${topicStr}`);
76
+ console.log(` salience=${m.salience} confidence=${m.confidence} stability=${m.stability?.toFixed(3)} ${dim(age)}`);
77
+ }
78
+ function timeSince(iso) {
79
+ const ms = Date.now() - new Date(iso).getTime();
80
+ const mins = Math.floor(ms / 60000);
81
+ if (mins < 1)
82
+ return 'just now';
83
+ if (mins < 60)
84
+ return `${mins}m ago`;
85
+ const hours = Math.floor(mins / 60);
86
+ if (hours < 24)
87
+ return `${hours}h ago`;
88
+ const days = Math.floor(hours / 24);
89
+ return `${days}d ago`;
90
+ }
91
+ function bold(s) { return `\x1b[1m${s}\x1b[0m`; }
92
+ function dim(s) { return `\x1b[2m${s}\x1b[0m`; }
93
+ function green(s) { return `\x1b[32m${s}\x1b[0m`; }
94
+ function yellow(s) { return `\x1b[33m${s}\x1b[0m`; }
95
+ function cyan(s) { return `\x1b[36m${s}\x1b[0m`; }
96
+ // ============================================================
97
+ // Init — Zero-friction setup for Claude Code / Cursor / MCP
98
+ // ============================================================
99
+ async function runInit(values) {
100
+ const { existsSync, readFileSync, writeFileSync, mkdirSync } = await import('fs');
101
+ const { homedir } = await import('os');
102
+ const { join } = await import('path');
103
+ const { createInterface } = await import('readline');
104
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
105
+ const ask = (q) => new Promise(r => rl.question(q, r));
106
+ console.log(bold('\n🧠 Engram Setup\n'));
107
+ console.log('This will configure Engram as an MCP server for your AI coding agent.\n');
108
+ // 1. Detect which tools are installed
109
+ const home = homedir();
110
+ const claudeConfigDir = join(home, '.claude');
111
+ const claudeConfigPath = join(claudeConfigDir, 'claude_desktop_config.json');
112
+ const cursorConfigDir = join(home, '.cursor');
113
+ const cursorMcpPath = join(cursorConfigDir, 'mcp.json');
114
+ const hasClaudeDir = existsSync(claudeConfigDir);
115
+ const hasCursorDir = existsSync(cursorConfigDir);
116
+ // Also check for Claude Code's settings.json approach
117
+ const claudeCodeSettingsDir = join(home, '.claude');
118
+ const claudeCodeMcpPath = join(claudeCodeSettingsDir, 'claude_desktop_config.json');
119
+ // 2. Ask for owner name
120
+ const defaultOwner = values.owner || process.env.USER || 'my-agent';
121
+ const owner = (await ask(` Agent name [${cyan(defaultOwner)}]: `)).trim() || defaultOwner;
122
+ // 3. Ask for Gemini key (optional but recommended)
123
+ let geminiKey = process.env.GEMINI_API_KEY || '';
124
+ const geminiKeyPath = join(home, '.config', 'engram', 'gemini-key');
125
+ if (!geminiKey && existsSync(geminiKeyPath)) {
126
+ geminiKey = readFileSync(geminiKeyPath, 'utf-8').trim();
127
+ }
128
+ if (!geminiKey) {
129
+ console.log(dim('\n Gemini API key enables embeddings + consolidation (free tier available).'));
130
+ console.log(dim(' Get one at: https://aistudio.google.com/apikey\n'));
131
+ geminiKey = (await ask(' Gemini API key (optional, press Enter to skip): ')).trim();
132
+ }
133
+ else {
134
+ console.log(` ${green('āœ“')} Gemini API key found`);
135
+ }
136
+ // 4. Build the MCP server config block
137
+ const engramConfig = {
138
+ command: 'npx',
139
+ args: ['tsx', join(process.cwd(), 'src', 'mcp.ts')],
140
+ env: {
141
+ ENGRAM_OWNER: owner,
142
+ ...(geminiKey ? { GEMINI_API_KEY: geminiKey } : {}),
143
+ },
144
+ };
145
+ // For published package, use this instead:
146
+ const engramConfigPublished = {
147
+ command: 'npx',
148
+ args: ['engram', 'mcp'],
149
+ env: {
150
+ ENGRAM_OWNER: owner,
151
+ ...(geminiKey ? { GEMINI_API_KEY: geminiKey } : {}),
152
+ },
153
+ };
154
+ console.log('\n' + bold(' MCP Server Configuration:\n'));
155
+ console.log(dim(' ' + JSON.stringify({ engram: engramConfigPublished }, null, 2).split('\n').join('\n ')));
156
+ // 5. Write config to detected tools
157
+ const targets = [];
158
+ if (hasClaudeDir) {
159
+ const write = (await ask(`\n Write to Claude Code config? (${claudeConfigPath}) [Y/n]: `)).trim().toLowerCase();
160
+ if (write !== 'n') {
161
+ let config = {};
162
+ if (existsSync(claudeConfigPath)) {
163
+ try {
164
+ config = JSON.parse(readFileSync(claudeConfigPath, 'utf-8'));
165
+ }
166
+ catch { }
167
+ }
168
+ if (!config.mcpServers)
169
+ config.mcpServers = {};
170
+ config.mcpServers.engram = engramConfig;
171
+ mkdirSync(claudeConfigDir, { recursive: true });
172
+ writeFileSync(claudeConfigPath, JSON.stringify(config, null, 2));
173
+ targets.push('Claude Code');
174
+ console.log(` ${green('āœ“')} Written to ${claudeConfigPath}`);
175
+ }
176
+ }
177
+ if (hasCursorDir) {
178
+ const write = (await ask(`\n Write to Cursor MCP config? (${cursorMcpPath}) [Y/n]: `)).trim().toLowerCase();
179
+ if (write !== 'n') {
180
+ let config = {};
181
+ if (existsSync(cursorMcpPath)) {
182
+ try {
183
+ config = JSON.parse(readFileSync(cursorMcpPath, 'utf-8'));
184
+ }
185
+ catch { }
186
+ }
187
+ if (!config.mcpServers)
188
+ config.mcpServers = {};
189
+ config.mcpServers.engram = engramConfig;
190
+ mkdirSync(cursorConfigDir, { recursive: true });
191
+ writeFileSync(cursorMcpPath, JSON.stringify(config, null, 2));
192
+ targets.push('Cursor');
193
+ console.log(` ${green('āœ“')} Written to ${cursorMcpPath}`);
194
+ }
195
+ }
196
+ if (!hasClaudeDir && !hasCursorDir) {
197
+ console.log(yellow('\n No Claude Code or Cursor installation detected.'));
198
+ console.log(' Add this to your MCP client config manually:\n');
199
+ console.log(' ' + JSON.stringify({ mcpServers: { engram: engramConfigPublished } }, null, 2).split('\n').join('\n '));
200
+ }
201
+ // 5b. Add Engram instructions to CLAUDE.md (if Claude Code detected)
202
+ if (hasClaudeDir) {
203
+ const claudeMdPath = join(home, '.claude', 'CLAUDE.md');
204
+ const engramBlock = `
205
+ ## Engram
206
+ You have access to Engram memory tools via MCP. Use them:
207
+ - At session start: call \`engram_briefing\` to load relevant context
208
+ - When you learn something important: call \`engram_remember\`
209
+ - When you need context from past sessions: call \`engram_recall\`
210
+ - At the end of a work session: call \`engram_ingest\` with a summary of what was accomplished
211
+ `;
212
+ let claudeMd = '';
213
+ if (existsSync(claudeMdPath)) {
214
+ claudeMd = readFileSync(claudeMdPath, 'utf-8');
215
+ }
216
+ if (!claudeMd.includes('## Engram')) {
217
+ writeFileSync(claudeMdPath, claudeMd + '\n' + engramBlock.trim() + '\n');
218
+ console.log(` ${green('āœ“')} Added Engram instructions to ${claudeMdPath}`);
219
+ }
220
+ else {
221
+ console.log(dim(` ℹ CLAUDE.md already has Engram section, skipping`));
222
+ }
223
+ }
224
+ // 6. Save Gemini key if provided
225
+ if (geminiKey) {
226
+ const configDir = join(home, '.config', 'engram');
227
+ mkdirSync(configDir, { recursive: true });
228
+ writeFileSync(geminiKeyPath, geminiKey);
229
+ console.log(` ${green('āœ“')} Gemini key saved to ${geminiKeyPath}`);
230
+ }
231
+ // 7. Create initial vault to verify setup
232
+ const engramDir = join(home, '.engram');
233
+ mkdirSync(engramDir, { recursive: true });
234
+ const dbPath = join(engramDir, `${owner}.db`);
235
+ const testVault = new Vault({ owner, dbPath });
236
+ const stats = testVault.stats();
237
+ await testVault.close();
238
+ console.log(` ${green('āœ“')} Vault created at ${dbPath} (${stats.total} memories)`);
239
+ console.log(bold('\n šŸŽ‰ Setup complete!\n'));
240
+ if (targets.length > 0) {
241
+ console.log(` Restart ${targets.join(' and ')} to activate Engram.\n`);
242
+ console.log(' Your agent now has 10 memory tools:');
243
+ console.log(' engram_remember — Store a memory');
244
+ console.log(' engram_recall — Retrieve relevant memories');
245
+ console.log(' engram_surface — Proactive context surfacing');
246
+ console.log(' engram_briefing — Session start briefing');
247
+ console.log(' engram_consolidate — Sleep cycle consolidation');
248
+ console.log(' engram_connect — Link memories in the graph');
249
+ console.log(' engram_forget — Remove memories');
250
+ console.log(' engram_entities — List tracked entities');
251
+ console.log(' engram_stats — Vault statistics');
252
+ console.log(' engram_ingest — Auto-extract from text');
253
+ }
254
+ else {
255
+ console.log(' Add the config to your MCP client, then restart it.\n');
256
+ }
257
+ rl.close();
258
+ }
259
+ // ============================================================
260
+ // Commands
261
+ // ============================================================
262
+ // ============================================================
263
+ // Shadow Mode
264
+ // ============================================================
265
+ const SHADOW_PID_DIR = path.join(process.env.HOME ?? '', '.config', 'engram');
266
+ const SERVER_PID_FILE = path.join(SHADOW_PID_DIR, 'shadow-server.pid');
267
+ const WATCHER_PID_FILE = path.join(SHADOW_PID_DIR, 'shadow-watcher.pid');
268
+ async function runShadow(subcommand, values) {
269
+ const { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } = await import('fs');
270
+ const { execSync, spawn } = await import('child_process');
271
+ const { homedir } = await import('os');
272
+ mkdirSync(SHADOW_PID_DIR, { recursive: true });
273
+ const owner = values.owner || 'default';
274
+ const engramDir = path.join(homedir(), '.engram');
275
+ mkdirSync(engramDir, { recursive: true });
276
+ const dbPath = path.join(engramDir, `${owner}.db`);
277
+ const geminiKey = process.env.GEMINI_API_KEY ?? '';
278
+ function isRunning(pidFile) {
279
+ if (!existsSync(pidFile))
280
+ return false;
281
+ const pid = readFileSync(pidFile, 'utf-8').trim();
282
+ try {
283
+ process.kill(parseInt(pid), 0);
284
+ return true;
285
+ }
286
+ catch {
287
+ unlinkSync(pidFile);
288
+ return false;
289
+ }
290
+ }
291
+ switch (subcommand) {
292
+ case 'start': {
293
+ if (isRunning(SERVER_PID_FILE)) {
294
+ console.log('Shadow mode is already running. Use `engram shadow status` to check.');
295
+ return;
296
+ }
297
+ console.log('🧠 Starting Engram shadow mode...\n');
298
+ // Start server
299
+ const distDir = path.resolve(path.dirname(new URL(import.meta.url).pathname), '.');
300
+ const serverPath = path.join(distDir, 'server.js');
301
+ const watcherPath = path.join(distDir, 'claude-watcher.js');
302
+ const serverEnv = {
303
+ ...process.env,
304
+ ENGRAM_OWNER: owner,
305
+ ENGRAM_DB_PATH: dbPath,
306
+ GEMINI_API_KEY: geminiKey,
307
+ };
308
+ const server = spawn('node', [serverPath], {
309
+ env: serverEnv,
310
+ detached: true,
311
+ stdio: ['ignore', 'pipe', 'pipe'],
312
+ });
313
+ // Capture the port from stdout
314
+ let serverPort = '';
315
+ server.stdout?.on('data', (data) => {
316
+ const line = data.toString();
317
+ const match = line.match(/:(\d+)/);
318
+ if (match && !serverPort) {
319
+ serverPort = match[1];
320
+ }
321
+ });
322
+ server.unref();
323
+ writeFileSync(SERVER_PID_FILE, String(server.pid));
324
+ // Wait for server to start
325
+ await new Promise(r => setTimeout(r, 2000));
326
+ if (!serverPort)
327
+ serverPort = '3800'; // fallback
328
+ console.log(` āœ“ Server running on port ${serverPort} (PID ${server.pid})`);
329
+ console.log(` āœ“ Database: ${dbPath}`);
330
+ // Start Claude Code watcher
331
+ const watcherEnv = {
332
+ ...process.env,
333
+ ENGRAM_API: `http://127.0.0.1:${serverPort}/v1`,
334
+ GEMINI_API_KEY: geminiKey,
335
+ ENGRAM_INGEST_INTERVAL_MS: '300000',
336
+ };
337
+ const watcher = spawn('node', [watcherPath, '--watch'], {
338
+ env: watcherEnv,
339
+ detached: true,
340
+ stdio: 'ignore',
341
+ });
342
+ watcher.unref();
343
+ writeFileSync(WATCHER_PID_FILE, String(watcher.pid));
344
+ console.log(` āœ“ Claude Code watcher running (PID ${watcher.pid})`);
345
+ console.log(`\nāœ… Shadow mode active. Engram is silently learning from your sessions.`);
346
+ console.log(` Run \`engram shadow status\` to check progress.`);
347
+ console.log(` Run \`engram shadow results\` after a few days to see what Engram caught.`);
348
+ console.log(` Run \`engram shadow stop\` to stop.\n`);
349
+ break;
350
+ }
351
+ case 'stop': {
352
+ let stopped = 0;
353
+ for (const pidFile of [WATCHER_PID_FILE, SERVER_PID_FILE]) {
354
+ if (existsSync(pidFile)) {
355
+ const pid = parseInt(readFileSync(pidFile, 'utf-8').trim());
356
+ try {
357
+ process.kill(pid, 'SIGTERM');
358
+ stopped++;
359
+ console.log(`Stopped PID ${pid}`);
360
+ }
361
+ catch { /* already dead */ }
362
+ unlinkSync(pidFile);
363
+ }
364
+ }
365
+ if (stopped === 0) {
366
+ console.log('Shadow mode is not running.');
367
+ }
368
+ else {
369
+ console.log('Shadow mode stopped.');
370
+ }
371
+ break;
372
+ }
373
+ case 'status': {
374
+ const serverRunning = isRunning(SERVER_PID_FILE);
375
+ const watcherRunning = isRunning(WATCHER_PID_FILE);
376
+ console.log(`\n🧠 Engram Shadow Mode Status\n`);
377
+ console.log(` Server: ${serverRunning ? 'āœ“ running' : 'āœ— stopped'}`);
378
+ console.log(` Watcher: ${watcherRunning ? 'āœ“ running' : 'āœ— stopped'}`);
379
+ console.log(` Database: ${dbPath}`);
380
+ // Try to get stats from the server
381
+ if (serverRunning) {
382
+ try {
383
+ const serverPid = readFileSync(SERVER_PID_FILE, 'utf-8').trim();
384
+ // We don't know the port, so try common ones
385
+ for (const port of ['3800']) {
386
+ try {
387
+ const res = await fetch(`http://127.0.0.1:${port}/v1/stats`);
388
+ if (res.ok) {
389
+ const stats = await res.json();
390
+ console.log(`\n šŸ“Š Vault Stats:`);
391
+ console.log(` Total memories: ${stats.total}`);
392
+ console.log(` Semantic: ${stats.semantic} | Episodic: ${stats.episodic} | Procedural: ${stats.procedural}`);
393
+ console.log(` Entities: ${stats.entities}`);
394
+ break;
395
+ }
396
+ }
397
+ catch { /* try next port */ }
398
+ }
399
+ }
400
+ catch { /* can't reach server */ }
401
+ }
402
+ // Show vault stats directly from file
403
+ if (existsSync(dbPath)) {
404
+ const vault = new Vault({ owner, dbPath });
405
+ const stats = vault.stats();
406
+ console.log(`\n šŸ“Š Vault Stats:`);
407
+ console.log(` Total memories: ${stats.total}`);
408
+ console.log(` Entities: ${stats.entities}`);
409
+ await vault.close();
410
+ }
411
+ else {
412
+ console.log(`\n No vault yet — memories will appear after your first Claude Code session.`);
413
+ }
414
+ console.log('');
415
+ break;
416
+ }
417
+ case 'results': {
418
+ // Find the user's CLAUDE.md
419
+ const claudeMdPaths = [
420
+ path.join(homedir(), '.claude', 'CLAUDE.md'),
421
+ path.join(process.cwd(), 'CLAUDE.md'),
422
+ path.join(process.cwd(), '.claude', 'CLAUDE.md'),
423
+ ];
424
+ let claudeMdContent = '';
425
+ let claudeMdPath = '';
426
+ for (const p of claudeMdPaths) {
427
+ if (existsSync(p)) {
428
+ claudeMdContent = readFileSync(p, 'utf-8');
429
+ claudeMdPath = p;
430
+ break;
431
+ }
432
+ }
433
+ if (!existsSync(dbPath)) {
434
+ console.log('\nāŒ No Engram vault found. Start shadow mode first: `engram shadow start`\n');
435
+ return;
436
+ }
437
+ const vault = new Vault({ owner, dbPath });
438
+ const stats = vault.stats();
439
+ console.log(`\n🧠 Engram Shadow Mode Results\n`);
440
+ console.log(` Vault: ${stats.total} memories, ${stats.entities} entities\n`);
441
+ if (stats.total < 10) {
442
+ console.log(` āš ļø Not enough memories yet. Keep using Claude Code for a few more sessions.`);
443
+ console.log(` Engram needs at least 10-20 sessions to show meaningful results.\n`);
444
+ await vault.close();
445
+ return;
446
+ }
447
+ // Get Engram's briefing
448
+ const briefing = await vault.briefing('', 20);
449
+ console.log(` šŸ“‹ What Engram Knows (top items):`);
450
+ for (const fact of briefing.keyFacts.slice(0, 8)) {
451
+ console.log(` • ${fact.content.slice(0, 100)}`);
452
+ }
453
+ if (claudeMdContent) {
454
+ console.log(`\n šŸ“„ Your CLAUDE.md: ${claudeMdPath}`);
455
+ const fileLines = claudeMdContent.split('\n')
456
+ .map(l => l.replace(/^[\s\-*#>]+/, '').trim())
457
+ .filter(l => l.length > 20);
458
+ console.log(` ${fileLines.length} meaningful lines\n`);
459
+ // Simple overlap analysis
460
+ const briefingText = briefing.keyFacts.map(f => f.content.toLowerCase()).join(' ');
461
+ const engramOnly = [];
462
+ for (const fact of briefing.keyFacts) {
463
+ const keywords = fact.content.toLowerCase().split(/\s+/).filter(w => w.length > 4).slice(0, 5);
464
+ const matchCount = keywords.filter(kw => claudeMdContent.toLowerCase().includes(kw)).length;
465
+ if (keywords.length > 0 && matchCount / keywords.length < 0.4) {
466
+ engramOnly.push(fact.content.slice(0, 120));
467
+ }
468
+ }
469
+ if (engramOnly.length > 0) {
470
+ console.log(` šŸ†• Things Engram caught that your CLAUDE.md missed:`);
471
+ for (const item of engramOnly.slice(0, 10)) {
472
+ console.log(` • ${item}`);
473
+ }
474
+ }
475
+ else {
476
+ console.log(` Your CLAUDE.md and Engram are well-aligned.`);
477
+ }
478
+ }
479
+ else {
480
+ console.log(` No CLAUDE.md found to compare against.`);
481
+ }
482
+ console.log('');
483
+ await vault.close();
484
+ break;
485
+ }
486
+ default:
487
+ console.log(`
488
+ engram shadow — Test Engram alongside your existing memory
489
+
490
+ Commands:
491
+ engram shadow start Start shadow mode (server + watcher, runs in background)
492
+ engram shadow stop Stop shadow mode
493
+ engram shadow status Check how many memories Engram has collected
494
+ engram shadow results Compare what Engram knows vs your CLAUDE.md
495
+ `);
496
+ }
497
+ }
498
+ async function main() {
499
+ const { values, positionals } = parseCliArgs();
500
+ if (values.help || positionals.length === 0) {
501
+ console.log(HELP);
502
+ process.exit(0);
503
+ }
504
+ const command = positionals[0];
505
+ // ── Commands that don't need a vault ──
506
+ if (command === 'init') {
507
+ await runInit(values);
508
+ process.exit(0);
509
+ }
510
+ if (command === 'mcp') {
511
+ // Delegate to the MCP server entry point
512
+ await import('./mcp.js');
513
+ return; // MCP server runs until killed
514
+ }
515
+ if (command === 'shadow') {
516
+ const subcommand = positionals[1] ?? 'help';
517
+ await runShadow(subcommand, values);
518
+ return;
519
+ }
520
+ if (command === 'eval') {
521
+ await runEval(values);
522
+ return;
523
+ }
524
+ const vault = createVault(values);
525
+ try {
526
+ switch (command) {
527
+ case 'remember': {
528
+ const text = positionals.slice(1).join(' ');
529
+ if (!text) {
530
+ console.error('Error: provide text to remember');
531
+ process.exit(1);
532
+ }
533
+ const input = { content: text };
534
+ if (values.entities)
535
+ input.entities = values.entities.split(',');
536
+ if (values.topics)
537
+ input.topics = values.topics.split(',');
538
+ if (values.type)
539
+ input.type = values.type;
540
+ if (values.salience)
541
+ input.salience = parseFloat(values.salience);
542
+ if (values.confidence)
543
+ input.confidence = parseFloat(values.confidence);
544
+ const mem = vault.remember(input);
545
+ if (values.json) {
546
+ console.log(JSON.stringify(mem, null, 2));
547
+ }
548
+ else {
549
+ console.log(green('āœ“ Remembered:'));
550
+ printMemory(mem, false);
551
+ }
552
+ break;
553
+ }
554
+ case 'recall': {
555
+ const context = positionals.slice(1).join(' ');
556
+ if (!context) {
557
+ console.error('Error: provide context for recall');
558
+ process.exit(1);
559
+ }
560
+ const input = { context, limit: parseInt(values.limit) };
561
+ if (values.entities)
562
+ input.entities = values.entities.split(',');
563
+ if (values.topics)
564
+ input.topics = values.topics.split(',');
565
+ if (values.type)
566
+ input.types = [values.type];
567
+ const memories = await vault.recall(input);
568
+ if (values.json) {
569
+ console.log(JSON.stringify(memories, null, 2));
570
+ }
571
+ else {
572
+ console.log(cyan(`Found ${memories.length} relevant memories:\n`));
573
+ for (const mem of memories) {
574
+ printMemory(mem, false);
575
+ console.log();
576
+ }
577
+ }
578
+ break;
579
+ }
580
+ case 'search': {
581
+ const query = positionals.slice(1).join(' ');
582
+ if (!query) {
583
+ console.error('Error: provide search query');
584
+ process.exit(1);
585
+ }
586
+ // Access store directly isn't possible from Vault, so use recall with keywords
587
+ const memories = await vault.recall({ context: query, limit: parseInt(values.limit) });
588
+ if (values.json) {
589
+ console.log(JSON.stringify(memories, null, 2));
590
+ }
591
+ else {
592
+ console.log(cyan(`Search results for "${query}":\n`));
593
+ for (const mem of memories) {
594
+ printMemory(mem, false);
595
+ console.log();
596
+ }
597
+ }
598
+ break;
599
+ }
600
+ case 'stats': {
601
+ const stats = vault.stats();
602
+ if (values.json) {
603
+ console.log(JSON.stringify(stats, null, 2));
604
+ }
605
+ else {
606
+ console.log(bold('\nšŸ“Š Vault Statistics\n'));
607
+ console.log(` Total memories: ${bold(String(stats.total))}`);
608
+ console.log(` Episodic: ${stats.episodic}`);
609
+ console.log(` Semantic: ${stats.semantic}`);
610
+ console.log(` Procedural: ${stats.procedural}`);
611
+ console.log(` Entities: ${stats.entities}`);
612
+ console.log();
613
+ }
614
+ break;
615
+ }
616
+ case 'entities': {
617
+ const entities = vault.entities();
618
+ if (values.json) {
619
+ console.log(JSON.stringify(entities, null, 2));
620
+ }
621
+ else {
622
+ console.log(bold(`\n🧠 Known Entities (${entities.length})\n`));
623
+ for (const e of entities) {
624
+ console.log(` ${bold(e.name)} ${dim(e.type)} mentions=${e.memoryCount} importance=${e.importance}`);
625
+ }
626
+ console.log();
627
+ }
628
+ break;
629
+ }
630
+ case 'consolidate': {
631
+ console.log(yellow('ā³ Running consolidation...'));
632
+ const report = await vault.consolidate();
633
+ if (values.json) {
634
+ console.log(JSON.stringify(report, null, 2));
635
+ }
636
+ else {
637
+ console.log(green('\nāœ“ Consolidation Complete\n'));
638
+ console.log(` Episodes processed: ${report.episodesProcessed}`);
639
+ console.log(` Semantic memories created: ${report.semanticMemoriesCreated}`);
640
+ console.log(` Semantic memories updated: ${report.semanticMemoriesUpdated}`);
641
+ console.log(` Entities discovered: ${report.entitiesDiscovered}`);
642
+ console.log(` Connections formed: ${report.connectionsFormed}`);
643
+ console.log(` Contradictions found: ${report.contradictionsFound}`);
644
+ console.log(` Memories decayed: ${report.memoriesDecayed}`);
645
+ console.log(` Memories archived: ${report.memoriesArchived}`);
646
+ console.log();
647
+ }
648
+ break;
649
+ }
650
+ case 'forget': {
651
+ const id = positionals[1];
652
+ if (!id) {
653
+ console.error('Error: provide memory ID to forget');
654
+ process.exit(1);
655
+ }
656
+ vault.forget(id, values.hard);
657
+ console.log(values.hard ? green('āœ“ Hard deleted') : yellow('āœ“ Soft forgotten (salience → 0)'));
658
+ break;
659
+ }
660
+ case 'export': {
661
+ const data = vault.export();
662
+ console.log(JSON.stringify(data, null, 2));
663
+ break;
664
+ }
665
+ case 'repl': {
666
+ await repl(vault, values.json);
667
+ break;
668
+ }
669
+ default:
670
+ console.error(`Unknown command: ${command}`);
671
+ console.log(HELP);
672
+ process.exit(1);
673
+ }
674
+ }
675
+ finally {
676
+ await vault.close();
677
+ }
678
+ }
679
+ // ============================================================
680
+ // Interactive REPL
681
+ // ============================================================
682
+ async function repl(vault, jsonMode) {
683
+ const { createInterface } = await import('readline');
684
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
685
+ console.log(bold('\n🧠 Engram REPL'));
686
+ console.log(dim('Commands: remember <text> | recall <context> | stats | entities | consolidate | quit\n'));
687
+ const prompt = () => {
688
+ rl.question(cyan('engram> '), async (line) => {
689
+ const trimmed = line.trim();
690
+ if (!trimmed) {
691
+ prompt();
692
+ return;
693
+ }
694
+ if (trimmed === 'quit' || trimmed === 'exit') {
695
+ rl.close();
696
+ await vault.close();
697
+ return;
698
+ }
699
+ const [cmd, ...rest] = trimmed.split(/\s+/);
700
+ const text = rest.join(' ');
701
+ try {
702
+ switch (cmd) {
703
+ case 'remember':
704
+ case 'r':
705
+ if (!text) {
706
+ console.log('Usage: remember <text>');
707
+ break;
708
+ }
709
+ const mem = vault.remember(text);
710
+ console.log(green('āœ“ Remembered'));
711
+ printMemory(mem, jsonMode);
712
+ break;
713
+ case 'recall':
714
+ case 'q':
715
+ if (!text) {
716
+ console.log('Usage: recall <context>');
717
+ break;
718
+ }
719
+ const results = await vault.recall(text);
720
+ console.log(cyan(`\n${results.length} memories:\n`));
721
+ for (const r of results) {
722
+ printMemory(r, jsonMode);
723
+ console.log();
724
+ }
725
+ break;
726
+ case 'stats':
727
+ case 's':
728
+ const stats = vault.stats();
729
+ console.log(`Total: ${stats.total} | Episodic: ${stats.episodic} | Semantic: ${stats.semantic} | Procedural: ${stats.procedural} | Entities: ${stats.entities}`);
730
+ break;
731
+ case 'entities':
732
+ case 'e':
733
+ const entities = vault.entities();
734
+ for (const e of entities) {
735
+ console.log(` ${bold(e.name)} (${e.type}) — ${e.memoryCount} mentions`);
736
+ }
737
+ break;
738
+ case 'consolidate':
739
+ case 'c':
740
+ console.log(yellow('Consolidating...'));
741
+ const report = await vault.consolidate();
742
+ console.log(green(`āœ“ ${report.episodesProcessed} episodes → ${report.semanticMemoriesCreated} semantic, ${report.connectionsFormed} connections`));
743
+ break;
744
+ default:
745
+ // Treat unknown input as a remember shortcut
746
+ const m = vault.remember(trimmed);
747
+ console.log(green('āœ“ Remembered'));
748
+ printMemory(m, jsonMode);
749
+ }
750
+ }
751
+ catch (err) {
752
+ console.error('Error:', err.message);
753
+ }
754
+ console.log();
755
+ prompt();
756
+ });
757
+ };
758
+ prompt();
759
+ }
760
+ main().catch(err => {
761
+ console.error('Fatal:', err);
762
+ process.exit(1);
763
+ });
764
+ //# sourceMappingURL=cli.js.map