#!/usr/bin/env node /** * Engram CLI - Management commands for the Memory Palace * * Usage: * engram stats - Show knowledge base statistics and health * engram health - Detailed health check * engram review - Start a review session * engram people - List people in your network * engram backup - Create a backup * engram backups - List available backups * engram restore - Restore from a backup * engram optimize - Optimize the database * engram decay - Apply memory decay * engram eat - Ingest documentation/content (Man Page Absorber) */ import { EngramDatabase, EngramDatabaseError } from './core/database.js'; import { captureContext, formatContextForInjection, startContextWatcher, readSavedContext, } from './core/context-watcher.js'; import { runREMCycle, previewREMCycle } from './core/rem-cycle.js'; import { ShadowSelf, runShadowCycle } from './core/shadow-self.js'; import { validatePath, validateUrl, sanitizeContent, logSecurityEvent, MAX_CONTENT_LENGTH, } from './core/security.js'; import { runConsolidation } from './core/consolidation.js'; import { createEmbeddingService, OllamaEmbeddingService } from './core/embeddings.js'; import { getConfig, resetConfig, loadConfig } from './core/config.js'; import { createVectorStore, ChromaVectorStore } from './core/vector-store.js'; import fs from 'fs'; import path from 'path'; import os from 'os'; import { marked } from 'marked'; // ============================================================================ // MAN PAGE ABSORBER - Feed your brain // ============================================================================ interface ContentChunk { title: string; content: string; section: string; index: number; } /** * Fetch content from URL (with SSRF protection) */ async function fetchUrl(url: string): Promise { // Validate URL to prevent SSRF attacks const validation = validateUrl(url); if (!validation.valid) { logSecurityEvent({ type: 'ssrf_attempt', details: { error: validation.error || 'URL validation failed', url: url.slice(0, 100) }, severity: 'high', blocked: true, }); throw new Error(`Security: ${validation.error}`); } const safeUrl = validation.sanitizedUrl!; const response = await fetch(safeUrl, { // Add timeout to prevent hanging on slow responses signal: AbortSignal.timeout(30000), // 30 second timeout }); if (!response.ok) { throw new Error(`Failed to fetch ${safeUrl}: ${response.status} ${response.statusText}`); } // Check content length to prevent DoS const contentLength = response.headers.get('content-length'); if (contentLength && parseInt(contentLength, 10) > MAX_CONTENT_LENGTH) { throw new Error(`Content too large: ${contentLength} bytes exceeds ${MAX_CONTENT_LENGTH} byte limit`); } const contentType = response.headers.get('content-type') || ''; let content: string; if (contentType.includes('text/html')) { // Strip HTML to get text content const html = await response.text(); content = stripHtml(html); } else { content = await response.text(); } // Sanitize the content return sanitizeContent(content); } /** * Simple HTML stripper - extracts text content */ function stripHtml(html: string): string { // Remove script and style tags and their content let text = html.replace(/]*>[\s\S]*?<\/script>/gi, ''); text = text.replace(/]*>[\s\S]*?<\/style>/gi, ''); // Remove HTML tags text = text.replace(/<[^>]+>/g, ' '); // Decode HTML entities text = text.replace(/ /g, ' '); text = text.replace(/&/g, '&'); text = text.replace(/</g, '<'); text = text.replace(/>/g, '>'); text = text.replace(/"/g, '"'); text = text.replace(/'/g, "'"); // Clean up whitespace text = text.replace(/\s+/g, ' ').trim(); return text; } /** * Read content from file (with path traversal protection) */ async function readFile(filePath: string): Promise { // Validate path to prevent path traversal attacks const validation = validatePath(filePath); if (!validation.valid) { logSecurityEvent({ type: 'path_traversal', details: { error: validation.error || 'Path validation failed', path: filePath.slice(0, 100) }, severity: 'high', blocked: true, }); throw new Error(`Security: ${validation.error}`); } const safePath = validation.sanitizedPath!; if (!fs.existsSync(safePath)) { throw new Error(`File not found: ${safePath}`); } // Check file size before reading const stats = fs.statSync(safePath); if (stats.size > MAX_CONTENT_LENGTH) { throw new Error(`File too large: ${stats.size} bytes exceeds ${MAX_CONTENT_LENGTH} byte limit`); } const content = fs.readFileSync(safePath, 'utf-8'); // Sanitize the content return sanitizeContent(content); } /** * Chunk content intelligently * - Respects markdown headers as section boundaries * - Creates overlapping chunks for context preservation * - Targets ~500-1000 tokens per chunk */ function chunkContent(content: string, source: string): ContentChunk[] { const chunks: ContentChunk[] = []; // Try to detect if it's markdown const isMarkdown = content.includes('# ') || content.includes('## ') || content.includes('```'); if (isMarkdown) { // Split by headers const sections = content.split(/^(#{1,3} .+)$/m); let currentSection = 'Introduction'; let currentContent = ''; let chunkIndex = 0; for (let i = 0; i < sections.length; i++) { const sectionRaw = sections[i]; if (!sectionRaw) continue; const section = sectionRaw.trim(); if (!section) continue; // Check if this is a header if (section.match(/^#{1,3} /)) { // Save previous section if it has content if (currentContent.trim()) { chunks.push(...splitLargeSection(currentContent, currentSection, chunkIndex, source)); chunkIndex = chunks.length; } currentSection = section.replace(/^#{1,3} /, '').trim(); currentContent = ''; } else { currentContent += section + '\n\n'; } } // Don't forget the last section if (currentContent.trim()) { chunks.push(...splitLargeSection(currentContent, currentSection, chunkIndex, source)); } } else { // Plain text - split by paragraphs const paragraphs = content.split(/\n\n+/); let currentChunk = ''; let chunkIndex = 0; for (const para of paragraphs) { const trimmed = para.trim(); if (!trimmed) continue; if ((currentChunk + trimmed).length > 2000) { if (currentChunk) { chunks.push({ title: `Section ${chunkIndex + 1}`, content: currentChunk.trim(), section: source, index: chunkIndex, }); chunkIndex++; } currentChunk = trimmed + '\n\n'; } else { currentChunk += trimmed + '\n\n'; } } if (currentChunk.trim()) { chunks.push({ title: `Section ${chunkIndex + 1}`, content: currentChunk.trim(), section: source, index: chunkIndex, }); } } return chunks; } /** * Split large sections into smaller chunks */ function splitLargeSection(content: string, section: string, startIndex: number, source: string): ContentChunk[] { const MAX_CHUNK_SIZE = 2000; // ~500 tokens const chunks: ContentChunk[] = []; if (content.length <= MAX_CHUNK_SIZE) { chunks.push({ title: section, content: content.trim(), section: source, index: startIndex, }); return chunks; } // Split by paragraphs const paragraphs = content.split(/\n\n+/); let currentChunk = ''; let partNumber = 1; for (const para of paragraphs) { const trimmed = para.trim(); if (!trimmed) continue; if ((currentChunk + trimmed).length > MAX_CHUNK_SIZE) { if (currentChunk) { chunks.push({ title: `${section} (Part ${partNumber})`, content: currentChunk.trim(), section: source, index: startIndex + chunks.length, }); partNumber++; } currentChunk = trimmed + '\n\n'; } else { currentChunk += trimmed + '\n\n'; } } if (currentChunk.trim()) { chunks.push({ title: partNumber > 1 ? `${section} (Part ${partNumber})` : section, content: currentChunk.trim(), section: source, index: startIndex + chunks.length, }); } return chunks; } /** * Ingest content from URL or file path */ async function eatContent(source: string, db: EngramDatabase): Promise { console.log(`\n Fetching content from: ${source}`); // Determine if URL or file const isUrl = source.startsWith('http://') || source.startsWith('https://'); let content: string; let sourceType: 'webpage' | 'article' = 'webpage'; let sourceName: string; if (isUrl) { content = await fetchUrl(source); sourceName = new URL(source).hostname + new URL(source).pathname; } else { content = await readFile(source); sourceName = path.basename(source); sourceType = 'article'; // Local files are treated as articles } console.log(` Content length: ${content.length} characters`); // Chunk the content const chunks = chunkContent(content, sourceName); console.log(` Created ${chunks.length} knowledge chunks`); if (chunks.length === 0) { console.log(' No content to ingest.\n'); return; } // Ingest each chunk console.log('\n Ingesting chunks:'); const nodeIds: string[] = []; for (const chunk of chunks) { const node = db.insertNode({ content: chunk.content, summary: chunk.title, sourceType: sourceType, sourcePlatform: isUrl ? 'browser' : 'manual', sourceUrl: isUrl ? source : undefined, createdAt: new Date(), updatedAt: new Date(), lastAccessedAt: new Date(), accessCount: 0, retentionStrength: 1.0, stabilityFactor: 1.0, reviewCount: 0, confidence: 0.9, // Ingested docs are high confidence isContradicted: false, contradictionIds: [], people: [], concepts: [], events: [], tags: ['ingested', chunk.section.toLowerCase().replace(/[^a-z0-9]+/g, '-')], sourceChain: [source], }); nodeIds.push(node.id); console.log(` [${node.id.slice(0, 8)}] ${chunk.title.slice(0, 50)}${chunk.title.length > 50 ? '...' : ''}`); } // Create edges between sequential chunks (they're related!) console.log('\n Creating knowledge connections...'); let edgesCreated = 0; for (let i = 0; i < nodeIds.length - 1; i++) { const fromId = nodeIds[i]; const toId = nodeIds[i + 1]; if (!fromId || !toId) continue; try { db.insertEdge({ fromId, toId, edgeType: 'follows', weight: 0.8, metadata: { source: 'ingestion', order: i }, createdAt: new Date(), }); edgesCreated++; } catch { // Edge might already exist } } console.log(` Created ${edgesCreated} sequential connections`); console.log(`\n Successfully ingested ${chunks.length} chunks from ${sourceName}`); console.log(` Use 'engram recall' or ask Claude to find this knowledge.\n`); } const command = process.argv[2]; const args = process.argv.slice(3); async function main() { const db = new EngramDatabase(); try { switch (command) { case 'stats': { const detailed = args[0] === 'detailed'; const stats = db.getStats(); const health = db.checkHealth(); const size = db.getDatabaseSize(); console.log('\n Memory Statistics'); console.log(' -----------------'); console.log(` Status: ${getStatusEmoji(health.status)} ${health.status.toUpperCase()}`); console.log(` Total nodes: ${stats.totalNodes}`); console.log(` Total people: ${stats.totalPeople}`); console.log(` Total connections: ${stats.totalEdges}`); console.log(` Database Size: ${size.formatted}`); console.log(` Last Backup: ${health.lastBackup || 'Never'}`); if (health.warnings.length > 0) { console.log('\n Warnings:'); for (const warning of health.warnings) { console.log(` - ${warning}`); } } const decaying = db.getDecayingNodes(0.5, { limit: 100 }); console.log(`\n Knowledge needing review: ${decaying.total} nodes`); if (detailed) { // Retention strength distribution console.log('\n Retention Strength Distribution'); console.log(' --------------------------------'); const allNodes = db.getRecentNodes({ limit: 10000 }); const distribution = { strong: 0, // 0.8-1.0 good: 0, // 0.6-0.8 moderate: 0, // 0.4-0.6 weak: 0, // 0.2-0.4 fading: 0, // 0.0-0.2 }; for (const node of allNodes.items) { const strength = node.retentionStrength; if (strength >= 0.8) distribution.strong++; else if (strength >= 0.6) distribution.good++; else if (strength >= 0.4) distribution.moderate++; else if (strength >= 0.2) distribution.weak++; else distribution.fading++; } const total = allNodes.items.length || 1; console.log(` Strong (80-100%): ${distribution.strong} (${((distribution.strong / total) * 100).toFixed(1)}%)`); console.log(` Good (60-80%): ${distribution.good} (${((distribution.good / total) * 100).toFixed(1)}%)`); console.log(` Moderate (40-60%): ${distribution.moderate} (${((distribution.moderate / total) * 100).toFixed(1)}%)`); console.log(` Weak (20-40%): ${distribution.weak} (${((distribution.weak / total) * 100).toFixed(1)}%)`); console.log(` Fading (0-20%): ${distribution.fading} (${((distribution.fading / total) * 100).toFixed(1)}%)`); // Source type breakdown console.log('\n Source Type Breakdown'); console.log(' ---------------------'); const sourceTypes: Record = {}; for (const node of allNodes.items) { const type = node.sourceType || 'unknown'; sourceTypes[type] = (sourceTypes[type] || 0) + 1; } const sortedTypes = Object.entries(sourceTypes).sort((a, b) => b[1] - a[1]); for (const [type, count] of sortedTypes.slice(0, 10)) { console.log(` ${type.padEnd(15)} ${count}`); } // Services status console.log('\n Services Status'); console.log(' ---------------'); try { const embService = new OllamaEmbeddingService(); const embAvailable = await embService.isAvailable(); console.log(` Embeddings: ${embAvailable ? 'Available (Ollama)' : 'Fallback mode'}`); } catch { console.log(' Embeddings: Check failed'); } try { const chromaStore = new ChromaVectorStore(); const vecAvailable = await chromaStore.isAvailable(); if (vecAvailable) { const vecStats = await chromaStore.getStats(); console.log(` Vector Store: ChromaDB (${vecStats.embeddingCount} embeddings)`); } else { console.log(' Vector Store: SQLite fallback'); } await chromaStore.close(); } catch { console.log(' Vector Store: Check failed'); } // FSRS config const config = getConfig(); console.log(` FSRS Retention: ${(config.fsrs.desiredRetention * 100).toFixed(0)}%`); } console.log(); break; } case 'health': { const health = db.checkHealth(); const size = db.getDatabaseSize(); console.log('\n Engram Health Check\n'); console.log(` Status: ${getStatusEmoji(health.status)} ${health.status.toUpperCase()}`); console.log(` Database Path: ${health.dbPath}`); console.log(` Database Size: ${size.formatted}`); console.log(` WAL Mode: ${health.walMode ? 'Enabled' : 'Disabled'}`); console.log(` Integrity Check: ${health.integrityCheck ? 'Passed' : 'FAILED'}`); console.log(` Node Count: ${health.nodeCount}`); console.log(` People Count: ${health.peopleCount}`); console.log(` Edge Count: ${health.edgeCount}`); console.log(` Last Backup: ${health.lastBackup || 'Never'}`); if (health.warnings.length > 0) { console.log('\n Warnings:'); for (const warning of health.warnings) { console.log(` - ${warning}`); } } else { console.log('\n No warnings - everything looks good!'); } console.log(); break; } case 'review': { const decaying = db.getDecayingNodes(0.5, { limit: 10 }); if (decaying.items.length === 0) { console.log('\n No knowledge needs review right now!\n'); break; } console.log('\n Knowledge Due for Review\n'); console.log(` Showing ${decaying.items.length} of ${decaying.total} items\n`); for (const node of decaying.items) { console.log(` [${node.id.slice(0, 8)}] ${node.content.slice(0, 80)}...`); console.log(` Retention: ${(node.retentionStrength * 100).toFixed(1)}%`); const daysSince = Math.floor((Date.now() - node.lastAccessedAt.getTime()) / (1000 * 60 * 60 * 24)); console.log(` Last accessed: ${daysSince} days ago`); console.log(); } if (decaying.hasMore) { console.log(` ... and ${decaying.total - decaying.items.length} more items need review\n`); } break; } case 'people': { const result = db.getAllPeople({ limit: 50 }); if (result.items.length === 0) { console.log('\n No people in your network yet.\n'); break; } console.log('\n Your Network\n'); console.log(` Showing ${result.items.length} of ${result.total} people\n`); for (const person of result.items) { const daysSince = person.lastContactAt ? Math.floor((Date.now() - person.lastContactAt.getTime()) / (1000 * 60 * 60 * 24)) : null; console.log(` ${person.name}`); if (person.organization) console.log(` Organization: ${person.organization}`); if (person.relationshipType) console.log(` Relationship: ${person.relationshipType}`); if (daysSince !== null) console.log(` Last contact: ${daysSince} days ago`); if (person.sharedTopics.length > 0) console.log(` Topics: ${person.sharedTopics.join(', ')}`); console.log(); } break; } case 'backup': { console.log('\n Creating backup...'); const backupPath = db.backup(); console.log(` Backup created: ${backupPath}`); const backups = db.listBackups(); console.log(`\n Total backups: ${backups.length}`); console.log(' Recent backups:'); for (const backup of backups.slice(0, 3)) { console.log(` - ${backup.path}`); console.log(` Size: ${(backup.size / 1024 / 1024).toFixed(2)}MB`); console.log(` Date: ${backup.date.toISOString()}`); } console.log(); break; } case 'backups': { const backups = db.listBackups(); if (backups.length === 0) { console.log('\n No backups found. Create one with: engram backup\n'); break; } console.log('\n Available Backups\n'); for (const backup of backups) { console.log(` ${backup.path}`); console.log(` Size: ${(backup.size / 1024 / 1024).toFixed(2)}MB`); console.log(` Date: ${backup.date.toISOString()}`); console.log(); } break; } case 'restore': { const backupPath = args[0]; if (!backupPath) { console.log('\n Usage: engram restore '); console.log(' Use "engram backups" to see available backups.\n'); break; } // Validate path to prevent path traversal attacks const pathValidation = validatePath(backupPath); if (!pathValidation.valid) { logSecurityEvent({ type: 'path_traversal', details: { error: pathValidation.error || 'Path validation failed', path: backupPath.slice(0, 100) }, severity: 'high', blocked: true, }); console.error(`\n Security Error: ${pathValidation.error}\n`); break; } const safePath = pathValidation.sanitizedPath!; console.log(`\n Restoring from: ${safePath}`); console.log(' WARNING: This will replace your current database!\n'); // In a real CLI, you'd prompt for confirmation here // For now, we just do it try { db.restore(safePath); console.log(' Restore completed successfully!\n'); } catch (error) { if (error instanceof EngramDatabaseError) { console.error(` Error: ${error.message} (${error.code})\n`); } else { console.error(` Error: ${error instanceof Error ? error.message : 'Unknown error'}\n`); } } break; } case 'optimize': { console.log('\n Optimizing database...'); const sizeBefore = db.getDatabaseSize(); db.optimize(); const sizeAfter = db.getDatabaseSize(); console.log(` Size before: ${sizeBefore.formatted}`); console.log(` Size after: ${sizeAfter.formatted}`); console.log(` Space saved: ${(sizeBefore.mb - sizeAfter.mb).toFixed(2)}MB`); console.log(); break; } case 'decay': { console.log('\n Applying memory decay...'); const updated = db.applyDecay(); console.log(` Updated ${updated} knowledge nodes\n`); break; } case 'consolidate': case 'sleep': { console.log('\n Running sleep consolidation cycle...\n'); const consResult = await runConsolidation(db); console.log(` Short-term processed: ${consResult.shortTermProcessed}`); console.log(` Promoted to long-term: ${consResult.promotedToLongTerm}`); console.log(` Connections discovered: ${consResult.connectionsDiscovered}`); console.log(` Edges pruned: ${consResult.edgesPruned}`); console.log(` Decay applied: ${consResult.decayApplied}`); console.log(`\n Duration: ${consResult.duration}ms\n`); break; } case 'embeddings': { const embCmd = args[0]; switch (embCmd) { case 'status': { console.log('\n Embedding Service Status\n'); const embService = new OllamaEmbeddingService(); const available = await embService.isAvailable(); console.log(` Service: ${available ? 'Available' : 'Not available'}`); if (available) { console.log(` Provider: Ollama`); console.log(` Model: ${embService.getModel()}`); console.log(` Host: ${process.env['OLLAMA_HOST'] || 'http://localhost:11434'}`); } else { console.log('\n To enable embeddings:'); console.log(' 1. Install Ollama: https://ollama.ai'); console.log(' 2. Run: ollama pull nomic-embed-text'); console.log(' 3. Start Ollama service'); } console.log(); break; } case 'generate': { const nodeId = args[1]; console.log('\n Generating Embeddings\n'); try { const embService = await createEmbeddingService(); if (nodeId) { // Generate for specific node const node = db.getNode(nodeId); if (!node) { console.log(` Error: Node not found: ${nodeId}\n`); break; } console.log(` Generating embedding for node: ${nodeId.slice(0, 8)}...`); const embedding = await embService.generateEmbedding(node.content); console.log(` Embedding generated: ${embedding.length} dimensions`); console.log(` First 5 values: [${embedding.slice(0, 5).map(v => v.toFixed(4)).join(', ')}...]`); } else { // Generate for all nodes without embeddings console.log(' Generating embeddings for all nodes...'); const allNodes = db.getRecentNodes({ limit: 1000 }); let generated = 0; let failed = 0; for (const node of allNodes.items) { try { await embService.generateEmbedding(node.content); generated++; if (generated % 10 === 0) { process.stdout.write(`\r Progress: ${generated}/${allNodes.items.length}`); } } catch { failed++; } } console.log(`\n Generated: ${generated}, Failed: ${failed}`); } } catch (error) { console.log(` Error: ${error instanceof Error ? error.message : 'Unknown error'}`); } console.log(); break; } case 'search': { const query = args.slice(1).join(' '); if (!query) { console.log('\n Usage: engram embeddings search ""\n'); break; } console.log(`\n Semantic Search: "${query}"\n`); try { const embService = await createEmbeddingService(); const queryEmbedding = await embService.generateEmbedding(query); // Get all nodes and compute similarity const allNodes = db.getRecentNodes({ limit: 500 }); const results: Array<{ node: typeof allNodes.items[0]; similarity: number }> = []; for (const node of allNodes.items) { try { const nodeEmbedding = await embService.generateEmbedding(node.content); const similarity = embService.getSimilarity(queryEmbedding, nodeEmbedding); results.push({ node, similarity }); } catch { // Skip nodes that fail to embed } } // Sort by similarity and show top 10 results.sort((a, b) => b.similarity - a.similarity); const topResults = results.slice(0, 10); if (topResults.length === 0) { console.log(' No results found.\n'); break; } console.log(' Top Results:'); for (const { node, similarity } of topResults) { const preview = node.content.slice(0, 60).replace(/\n/g, ' '); console.log(` [${(similarity * 100).toFixed(1)}%] ${preview}...`); } } catch (error) { console.log(` Error: ${error instanceof Error ? error.message : 'Unknown error'}`); } console.log(); break; } default: console.log(` Engram Embeddings - Semantic Understanding Usage: engram embeddings Commands: status Check embedding service availability generate [nodeId] Generate embeddings (all nodes or specific) search "" Semantic similarity search Examples: engram embeddings status engram embeddings generate engram embeddings generate abc12345 engram embeddings search "authentication flow" `); } break; } case 'config': { const configCmd = args[0]; const configPath = path.join(os.homedir(), '.engram', 'config.json'); switch (configCmd) { case 'show': { console.log('\n Engram Configuration\n'); const config = getConfig(); console.log(JSON.stringify(config, null, 2)); console.log(`\n Config file: ${configPath}\n`); break; } case 'set': { const key = args[1]; const value = args.slice(2).join(' '); if (!key || !value) { console.log('\n Usage: engram config set '); console.log('\n Examples:'); console.log(' engram config set logging.level debug'); console.log(' engram config set fsrs.desiredRetention 0.85'); console.log(' engram config set rem.enabled false\n'); break; } console.log(`\n Setting ${key} = ${value}\n`); // Load existing config or create empty let fileConfig: Record = {}; if (fs.existsSync(configPath)) { try { fileConfig = JSON.parse(fs.readFileSync(configPath, 'utf-8')); } catch { console.log(' Warning: Could not parse existing config, starting fresh'); } } // Parse the key path (e.g., "logging.level") const keyParts = key.split('.'); let current: Record = fileConfig; for (let i = 0; i < keyParts.length - 1; i++) { const part = keyParts[i]!; if (!(part in current) || typeof current[part] !== 'object') { current[part] = {}; } current = current[part] as Record; } // Parse value (try as JSON, fall back to string) let parsedValue: unknown = value; try { parsedValue = JSON.parse(value); } catch { // Keep as string } current[keyParts[keyParts.length - 1]!] = parsedValue; // Ensure directory exists const configDir = path.dirname(configPath); if (!fs.existsSync(configDir)) { fs.mkdirSync(configDir, { recursive: true }); } // Write config fs.writeFileSync(configPath, JSON.stringify(fileConfig, null, 2)); // Reset singleton to reload resetConfig(); console.log(` Configuration updated: ${key} = ${JSON.stringify(parsedValue)}`); console.log(` Saved to: ${configPath}\n`); break; } case 'reset': { console.log('\n Resetting configuration to defaults...\n'); if (fs.existsSync(configPath)) { // Create backup before deleting const backupPath = `${configPath}.backup.${Date.now()}`; fs.copyFileSync(configPath, backupPath); console.log(` Backup created: ${backupPath}`); fs.unlinkSync(configPath); console.log(` Removed: ${configPath}`); } resetConfig(); console.log(' Configuration reset to defaults.\n'); break; } default: console.log(` Engram Configuration Management Usage: engram config Commands: show Display current configuration set Update a configuration value reset Reset to default configuration Examples: engram config show engram config set logging.level debug engram config set fsrs.desiredRetention 0.85 engram config reset Configuration Sections: database - Database paths and settings fsrs - Spaced repetition algorithm memory - Dual-strength memory model rem - REM cycle settings consolidation - Sleep consolidation embeddings - Embedding service vectorStore - Vector database logging - Log levels limits - Size limits `); } break; } case 'test': { console.log('\n Engram Self-Test Suite\n'); console.log(' Running diagnostic tests...\n'); let allPassed = true; // Test 1: Database try { const stats = db.getStats(); console.log(` [PASS] Database: ${stats.totalNodes} nodes, ${stats.totalPeople} people, ${stats.totalEdges} edges`); } catch (error) { console.log(` [FAIL] Database: ${error instanceof Error ? error.message : 'Unknown error'}`); allPassed = false; } // Test 2: Embeddings try { const embService = new OllamaEmbeddingService(); const embAvailable = await embService.isAvailable(); if (embAvailable) { console.log(` [PASS] Embeddings: Ollama available (${embService.getModel()})`); } else { console.log(' [WARN] Embeddings: Ollama not available (fallback will be used)'); } } catch (error) { console.log(` [WARN] Embeddings: ${error instanceof Error ? error.message : 'Check failed'}`); } // Test 3: Vector Store try { const chromaStore = new ChromaVectorStore(); const vecAvailable = await chromaStore.isAvailable(); if (vecAvailable) { const vecStats = await chromaStore.getStats(); console.log(` [PASS] Vector Store: ChromaDB available (${vecStats.embeddingCount} embeddings)`); } else { console.log(' [WARN] Vector Store: ChromaDB not available (SQLite fallback will be used)'); } await chromaStore.close(); } catch (error) { console.log(` [WARN] Vector Store: ${error instanceof Error ? error.message : 'Check failed'}`); } // Test 4: Configuration try { const config = getConfig(); console.log(` [PASS] Configuration: Loaded (FSRS retention: ${config.fsrs.desiredRetention})`); } catch (error) { console.log(` [FAIL] Configuration: ${error instanceof Error ? error.message : 'Load failed'}`); allPassed = false; } // Test 5: Database health try { const health = db.checkHealth(); if (health.status === 'healthy') { console.log(` [PASS] Health Check: ${health.status}`); } else if (health.status === 'warning') { console.log(` [WARN] Health Check: ${health.warnings.length} warning(s)`); } else { console.log(` [FAIL] Health Check: ${health.status}`); allPassed = false; } } catch (error) { console.log(` [FAIL] Health Check: ${error instanceof Error ? error.message : 'Check failed'}`); allPassed = false; } console.log(); if (allPassed) { console.log(' All core tests passed!\n'); } else { console.log(' Some tests failed. Review the output above.\n'); } break; } case 'ingest': { const content = args.join(' '); if (!content) { console.log('\n Usage: engram ingest ""'); console.log('\n Store knowledge directly into Engram.'); console.log('\n Examples:'); console.log(' engram ingest "API rate limit is 100 req/min"'); console.log(' engram ingest "Meeting with John: discussed Q4 roadmap"\n'); break; } console.log('\n Ingesting knowledge...\n'); try { const node = db.insertNode({ content: content, summary: content.slice(0, 100), sourceType: 'note', sourcePlatform: 'manual', createdAt: new Date(), updatedAt: new Date(), lastAccessedAt: new Date(), accessCount: 0, retentionStrength: 1.0, stabilityFactor: 1.0, reviewCount: 0, confidence: 0.9, isContradicted: false, contradictionIds: [], people: [], concepts: [], events: [], tags: ['cli-ingested'], sourceChain: ['cli'], }); console.log(` Stored as node: ${node.id}`); console.log(` Content: "${content.slice(0, 60)}${content.length > 60 ? '...' : ''}"`); console.log('\n Knowledge successfully ingested!\n'); } catch (error) { console.error(` Error: ${error instanceof Error ? error.message : 'Unknown error'}\n`); } break; } case 'recall': { const query = args.join(' '); if (!query) { console.log('\n Usage: engram recall ""'); console.log('\n Search your memories.'); console.log('\n Examples:'); console.log(' engram recall "rate limit"'); console.log(' engram recall "meeting John"\n'); break; } console.log(`\n Searching memories for: "${query}"\n`); try { const result = db.searchNodes(query, { limit: 10 }); if (result.items.length === 0) { console.log(' No memories found matching your query.\n'); break; } console.log(` Found ${result.total} memories (showing ${result.items.length}):\n`); for (const node of result.items) { const preview = node.content.slice(0, 80).replace(/\n/g, ' '); const daysSince = Math.floor((Date.now() - node.lastAccessedAt.getTime()) / (1000 * 60 * 60 * 24)); console.log(` [${node.id.slice(0, 8)}] ${preview}${node.content.length > 80 ? '...' : ''}`); console.log(` Retention: ${(node.retentionStrength * 100).toFixed(1)}% | Last accessed: ${daysSince}d ago`); console.log(); } } catch (error) { console.error(` Error: ${error instanceof Error ? error.message : 'Unknown error'}\n`); } break; } case 'eat': { const source = args[0]; if (!source) { console.log('\n Usage: engram eat '); console.log('\n Examples:'); console.log(' engram eat https://docs.rs/tauri/latest/'); console.log(' engram eat ./README.md'); console.log(' engram eat ~/Documents/notes.txt'); console.log('\n The Man Page Absorber chunks content intelligently and'); console.log(' creates interconnected knowledge nodes for retrieval.\n'); break; } try { await eatContent(source, db); } catch (error) { console.error(`\n Error ingesting content: ${error instanceof Error ? error.message : 'Unknown error'}\n`); } break; } case 'context': { console.log('\n Ghost in the Shell - Current Context\n'); const context = captureContext(); if (context.activeWindow) { console.log(` Active App: ${context.activeWindow.app}`); console.log(` Window Title: ${context.activeWindow.title}`); } else { console.log(' Active Window: (unable to detect)'); } console.log(` Working Dir: ${context.workingDirectory}`); if (context.gitBranch) { console.log(` Git Branch: ${context.gitBranch}`); } if (context.recentFiles.length > 0) { console.log('\n Recent Files (last hour):'); for (const file of context.recentFiles.slice(0, 5)) { console.log(` - ${file}`); } } if (context.clipboard) { console.log('\n Clipboard:'); const preview = context.clipboard.slice(0, 200); console.log(` "${preview}${context.clipboard.length > 200 ? '...' : ''}"`); } console.log('\n Injection String:'); console.log(` ${formatContextForInjection(context)}`); console.log(); break; } case 'watch': { console.log('\n Starting Ghost in the Shell context watcher...'); console.log(' Press Ctrl+C to stop.\n'); startContextWatcher(5000); // Keep running until interrupted process.on('SIGINT', () => { console.log('\n Stopping context watcher...'); process.exit(0); }); // Keep the process alive await new Promise(() => {}); // Never resolves break; } case 'dream': { console.log('\n REM Cycle - Discovering Hidden Connections\n'); // Preview first console.log(' Analyzing knowledge graph...'); const preview = await previewREMCycle(db); if (preview.connectionsDiscovered === 0) { console.log(' No new connections discovered.'); console.log(' Your knowledge graph is well-connected or needs more nodes.\n'); break; } console.log(` Found ${preview.connectionsDiscovered} potential connections!\n`); // Show previews console.log(' Discoveries:'); for (const d of preview.discoveries.slice(0, 10)) { console.log(` "${d.nodeA}..."`); console.log(` <-> "${d.nodeB}..."`); console.log(` Reason: ${d.reason}\n`); } if (preview.discoveries.length > 10) { console.log(` ... and ${preview.discoveries.length - 10} more\n`); } // Actually create the connections if (args[0] !== '--dry-run') { console.log(' Creating connections...'); const result = await runREMCycle(db); console.log(` Created ${result.connectionsCreated} new edges in ${result.duration}ms\n`); } else { console.log(' (Dry run - no connections created. Remove --dry-run to create them)\n'); } break; } case 'rem': { // Alias for dream console.log('\n Starting REM Cycle (alias for "dream")...\n'); const result = await runREMCycle(db); console.log(` Analyzed: ${result.nodesAnalyzed} nodes`); console.log(` Discovered: ${result.connectionsDiscovered} connections`); console.log(` Created: ${result.connectionsCreated} edges`); console.log(` Duration: ${result.duration}ms\n`); if (result.discoveries.length > 0) { console.log(' New connections:'); for (const d of result.discoveries.slice(0, 5)) { console.log(` - ${d.reason}`); } if (result.discoveries.length > 5) { console.log(` ... and ${result.discoveries.length - 5} more`); } } console.log(); break; } // ==================================================================== // SHADOW SELF - Unsolved Problems Queue // ==================================================================== case 'problem': { const description = args.join(' '); if (!description) { console.log('\n Usage: engram problem '); console.log('\n Log an unsolved problem for your Shadow to work on.\n'); console.log(' Examples:'); console.log(' engram problem "How to implement efficient graph traversal"'); console.log(' engram problem "Why is the memory leak happening in the worker"'); console.log('\n The Shadow Self will periodically revisit these problems'); console.log(' when new knowledge might provide insights.\n'); break; } const shadow = new ShadowSelf(); try { const problem = shadow.logProblem(description, { context: formatContextForInjection(captureContext()), priority: 3, }); console.log('\n Problem logged to the Shadow Self\n'); console.log(` ID: ${problem.id}`); console.log(` Description: ${problem.description.slice(0, 60)}${problem.description.length > 60 ? '...' : ''}`); console.log(` Priority: ${problem.priority}`); console.log(` Status: ${problem.status}`); console.log('\n Your Shadow will work on this while you rest.\n'); } finally { shadow.close(); } break; } case 'problems': { const shadow = new ShadowSelf(); try { const problems = shadow.getOpenProblems(); const stats = shadow.getStats(); console.log('\n Shadow Self - Unsolved Problems Queue\n'); console.log(` Total: ${stats.total} | Open: ${stats.open} | Investigating: ${stats.investigating} | Solved: ${stats.solved}\n`); if (problems.length === 0) { console.log(' No open problems. Your mind is at peace.\n'); console.log(' Log a problem with: engram problem ""\n'); break; } for (const p of problems) { const priority = '!'.repeat(p.priority); const daysSince = Math.floor((Date.now() - p.createdAt.getTime()) / (1000 * 60 * 60 * 24)); console.log(` [${p.id.slice(0, 8)}] ${priority.padEnd(5)} ${p.description.slice(0, 50)}${p.description.length > 50 ? '...' : ''}`); console.log(` Status: ${p.status} | Attempts: ${p.attempts} | Age: ${daysSince}d`); // Show any insights const insights = shadow.getInsights(p.id); if (insights.length > 0) { console.log(` Latest insight: "${insights[0]?.insight.slice(0, 40)}..."`); } console.log(); } } finally { shadow.close(); } break; } case 'solve': { const problemId = args[0]; const solution = args.slice(1).join(' '); if (!problemId) { console.log('\n Usage: engram solve '); console.log('\n Mark a problem as solved with the solution.\n'); console.log(' Example:'); console.log(' engram solve abc123 "Used memoization to optimize the traversal"'); console.log('\n Use "engram problems" to see problem IDs.\n'); break; } const shadow = new ShadowSelf(); try { // Find the problem (match on prefix) const problems = shadow.getOpenProblems(); const match = problems.find(p => p.id.startsWith(problemId)); if (!match) { console.log(`\n Problem not found: ${problemId}`); console.log(' Use "engram problems" to see open problems.\n'); break; } shadow.markSolved(match.id, solution || 'Solved (no details provided)'); console.log('\n Problem marked as SOLVED\n'); console.log(` Problem: ${match.description.slice(0, 50)}...`); console.log(` Solution: ${solution || '(no details)'}`); console.log(` Attempts: ${match.attempts}`); console.log('\n The Shadow rejoices.\n'); } finally { shadow.close(); } break; } case 'shadow': { console.log('\n Shadow Self - Running Background Analysis\n'); const shadow = new ShadowSelf(); try { const stats = shadow.getStats(); console.log(` Problems: ${stats.open} open, ${stats.investigating} investigating, ${stats.solved} solved`); console.log(` Total insights generated: ${stats.totalInsights}\n`); if (stats.open === 0 && stats.investigating === 0) { console.log(' No problems to work on. The Shadow rests.\n'); break; } console.log(' Running shadow cycle...'); const result = runShadowCycle(shadow, db); console.log(` Analyzed: ${result.problemsAnalyzed} problems`); console.log(` New insights: ${result.insightsGenerated}\n`); if (result.insights.length > 0) { console.log(' Discoveries:'); for (const i of result.insights) { console.log(` Problem: "${i.problem}..."`); console.log(` Insight: ${i.insight}\n`); } } else { console.log(' No new insights yet. The Shadow continues to watch.\n'); } } finally { shadow.close(); } break; } case 'help': default: console.log(` Engram CLI - Git Blame for AI Thoughts Usage: engram [options] Core Commands: ingest Store knowledge directly recall Search memories review Review memories due for reinforcement stats [detailed] Show memory statistics Memory Processing: dream Run REM cycle (connection discovery) consolidate Run sleep consolidation (alias: sleep) decay Apply memory decay Embeddings: embeddings status Check embedding service availability embeddings generate Generate embeddings for all nodes embeddings search Semantic similarity search Configuration: config show Display current configuration config set Update a configuration value config reset Reset to default configuration test Run self-tests Ghost in the Shell: context Show current system context watch Start context watcher daemon eat Ingest docs/content Shadow Self (Unsolved Problems): problem Log a new unsolved problem problems List all open problems solve Mark a problem as solved shadow Run shadow cycle for insights Maintenance: backup Create database backup backups List available backups restore Restore from backup optimize Optimize database health Detailed health check people List people in your network Examples: engram ingest "API rate limit is 100 req/min" engram recall "rate limit" engram stats detailed engram embeddings search "authentication" engram config set logging.level debug engram eat https://docs.example.com/api The Engram MCP server runs automatically when connected to Claude. Your brain gets smarter while you sleep. `); } } finally { db.close(); } } function getStatusEmoji(status: string): string { switch (status) { case 'healthy': return '(healthy)'; case 'warning': return '(warning)'; case 'critical': return '(CRITICAL)'; default: return ''; } } main().catch((error) => { console.error('Error:', error instanceof Error ? error.message : error); process.exit(1); });