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.
- package/CONTRIBUTING.md +65 -0
- package/Dockerfile +21 -0
- package/EVAL-FRAMEWORK.md +70 -0
- package/EVAL.md +127 -0
- package/LICENSE +17 -0
- package/README.md +309 -0
- package/ROADMAP.md +113 -0
- package/deploy/fly.toml +26 -0
- package/dist/auto-ingest.d.ts +3 -0
- package/dist/auto-ingest.d.ts.map +1 -0
- package/dist/auto-ingest.js +334 -0
- package/dist/auto-ingest.js.map +1 -0
- package/dist/brief.d.ts +45 -0
- package/dist/brief.d.ts.map +1 -0
- package/dist/brief.js +183 -0
- package/dist/brief.js.map +1 -0
- package/dist/claude-watcher.d.ts +3 -0
- package/dist/claude-watcher.d.ts.map +1 -0
- package/dist/claude-watcher.js +385 -0
- package/dist/claude-watcher.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +764 -0
- package/dist/cli.js.map +1 -0
- package/dist/embeddings.d.ts +42 -0
- package/dist/embeddings.d.ts.map +1 -0
- package/dist/embeddings.js +145 -0
- package/dist/embeddings.js.map +1 -0
- package/dist/eval.d.ts +2 -0
- package/dist/eval.d.ts.map +1 -0
- package/dist/eval.js +281 -0
- package/dist/eval.js.map +1 -0
- package/dist/extract.d.ts +11 -0
- package/dist/extract.d.ts.map +1 -0
- package/dist/extract.js +139 -0
- package/dist/extract.js.map +1 -0
- package/dist/hosted.d.ts +3 -0
- package/dist/hosted.d.ts.map +1 -0
- package/dist/hosted.js +144 -0
- package/dist/hosted.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/ingest.d.ts +28 -0
- package/dist/ingest.d.ts.map +1 -0
- package/dist/ingest.js +192 -0
- package/dist/ingest.js.map +1 -0
- package/dist/mcp.d.ts +3 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +349 -0
- package/dist/mcp.js.map +1 -0
- package/dist/server.d.ts +17 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +515 -0
- package/dist/server.js.map +1 -0
- package/dist/store.d.ts +87 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +548 -0
- package/dist/store.js.map +1 -0
- package/dist/types.d.ts +204 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +77 -0
- package/dist/types.js.map +1 -0
- package/dist/vault.d.ts +116 -0
- package/dist/vault.d.ts.map +1 -0
- package/dist/vault.js +1234 -0
- package/dist/vault.js.map +1 -0
- 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
|