UNPKG

agentdb

Version:

AgentDB - Frontier Memory Features with MCP Integration and Direct Vector Search: Causal reasoning, reflexion memory, skill library, automated learning, and raw vector similarity queries. 150x faster vector search. Full Claude Desktop support via Model Co

1,495 lines (1,277 loc) 82.9 kB
#!/usr/bin/env node /** * AgentDB CLI - Command-line interface for frontier memory features * * Provides commands for: * - Causal memory graph operations * - Explainable recall with certificates * - Nightly learner automation * - Database management */ import { createDatabase } from '../db-fallback.js'; import { CausalMemoryGraph } from '../controllers/CausalMemoryGraph.js'; import { CausalRecall } from '../controllers/CausalRecall.js'; import { ExplainableRecall } from '../controllers/ExplainableRecall.js'; import { NightlyLearner } from '../controllers/NightlyLearner.js'; import { ReflexionMemory, Episode, ReflexionQuery } from '../controllers/ReflexionMemory.js'; import { SkillLibrary, Skill, SkillQuery } from '../controllers/SkillLibrary.js'; import { EmbeddingService } from '../controllers/EmbeddingService.js'; import { MMRDiversityRanker } from '../controllers/MMRDiversityRanker.js'; import { ContextSynthesizer } from '../controllers/ContextSynthesizer.js'; import { MetadataFilter } from '../controllers/MetadataFilter.js'; import * as fs from 'fs'; import * as path from 'path'; import * as zlib from 'zlib'; import { fileURLToPath } from 'url'; import { dirname } from 'path'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); // Color codes for terminal output const colors = { reset: '\x1b[0m', bright: '\x1b[1m', green: '\x1b[32m', yellow: '\x1b[33m', blue: '\x1b[34m', red: '\x1b[31m', cyan: '\x1b[36m' }; const log = { success: (msg: string) => console.log(`${colors.green}✅ ${msg}${colors.reset}`), error: (msg: string) => console.error(`${colors.red}❌ ${msg}${colors.reset}`), info: (msg: string) => console.log(`${colors.blue}ℹ ${msg}${colors.reset}`), warning: (msg: string) => console.log(`${colors.yellow}⚠ ${msg}${colors.reset}`), header: (msg: string) => console.log(`${colors.bright}${colors.cyan}${msg}${colors.reset}`) }; class AgentDBCLI { public db?: any; // Database instance from createDatabase (public for init command) private causalGraph?: CausalMemoryGraph; private causalRecall?: CausalRecall; private explainableRecall?: ExplainableRecall; private nightlyLearner?: NightlyLearner; private reflexion?: ReflexionMemory; private skills?: SkillLibrary; private embedder?: EmbeddingService; async initialize(dbPath: string = './agentdb.db'): Promise<void> { // Initialize database this.db = await createDatabase(dbPath); // Configure for performance this.db.pragma('journal_mode = WAL'); this.db.pragma('synchronous = NORMAL'); this.db.pragma('cache_size = -64000'); // Load both schemas: main schema (episodes, skills) + frontier schema (causal) const schemaFiles = ['schema.sql', 'frontier-schema.sql']; const basePaths = [ path.join(__dirname, '../schemas'), // dist/cli/../schemas path.join(__dirname, '../../src/schemas'), // dist/cli/../../src/schemas path.join(process.cwd(), 'dist/schemas'), // current/dist/schemas path.join(process.cwd(), 'src/schemas'), // current/src/schemas path.join(process.cwd(), 'node_modules/agentdb/dist/schemas') // installed package ]; let schemasLoaded = 0; for (const basePath of basePaths) { if (fs.existsSync(basePath)) { for (const schemaFile of schemaFiles) { const schemaPath = path.join(basePath, schemaFile); if (fs.existsSync(schemaPath)) { try { const schema = fs.readFileSync(schemaPath, 'utf-8'); this.db.exec(schema); schemasLoaded++; } catch (error) { log.error(`Failed to load schema from ${schemaPath}: ${(error as Error).message}`); } } } // If we found at least one schema in this path, we're done if (schemasLoaded > 0) break; } } if (schemasLoaded === 0) { log.warning('Schema files not found, database may not be initialized properly'); log.info('__dirname: ' + __dirname); log.info('process.cwd(): ' + process.cwd()); log.info('Tried base paths:'); basePaths.forEach(p => { log.info(` - ${p} (exists: ${fs.existsSync(p)})`); }); } // Initialize embedding service this.embedder = new EmbeddingService({ model: 'Xenova/all-MiniLM-L6-v2', dimension: 384, provider: 'transformers' }); await this.embedder.initialize(); // Initialize controllers this.causalGraph = new CausalMemoryGraph(this.db); this.explainableRecall = new ExplainableRecall(this.db); this.causalRecall = new CausalRecall(this.db, this.embedder, { alpha: 0.7, beta: 0.2, gamma: 0.1, minConfidence: 0.6 }); this.nightlyLearner = new NightlyLearner(this.db, this.embedder); this.reflexion = new ReflexionMemory(this.db, this.embedder); this.skills = new SkillLibrary(this.db, this.embedder); } // ============================================================================ // Causal Commands // ============================================================================ async causalAddEdge(params: { cause: string; effect: string; uplift: number; confidence?: number; sampleSize?: number; }): Promise<void> { if (!this.causalGraph) throw new Error('Not initialized'); log.header('\n📊 Adding Causal Edge'); log.info(`Cause: ${params.cause}`); log.info(`Effect: ${params.effect}`); log.info(`Uplift: ${params.uplift}`); const edgeId = this.causalGraph.addCausalEdge({ fromMemoryId: 1, fromMemoryType: 'episode', toMemoryId: 2, toMemoryType: 'episode', similarity: 0.9, uplift: params.uplift, confidence: params.confidence || 0.95, sampleSize: params.sampleSize || 0, mechanism: `${params.cause} → ${params.effect}`, evidenceIds: [] }); log.success(`Added causal edge #${edgeId}`); } async causalExperimentCreate(params: { name: string; cause: string; effect: string; }): Promise<void> { if (!this.causalGraph) throw new Error('Not initialized'); log.header('\n🧪 Creating A/B Experiment'); log.info(`Name: ${params.name}`); log.info(`Cause: ${params.cause}`); log.info(`Effect: ${params.effect}`); const expId = this.causalGraph.createExperiment({ name: params.name, hypothesis: `Does ${params.cause} causally affect ${params.effect}?`, treatmentId: 0, treatmentType: params.cause, controlId: undefined, startTime: Math.floor(Date.now() / 1000), sampleSize: 0, status: 'running', metadata: { effect: params.effect } }); log.success(`Created experiment #${expId}`); log.info('Use `agentdb causal experiment add-observation` to record data'); } async causalExperimentAddObservation(params: { experimentId: number; isTreatment: boolean; outcome: number; context?: string; }): Promise<void> { if (!this.causalGraph) throw new Error('Not initialized'); // Create a dummy episode to get an episode ID const insertResult = this.db!.prepare('INSERT INTO episodes (session_id, task, reward, success, created_at) VALUES (?, ?, ?, ?, ?)').run('cli-session', 'experiment', params.outcome, 1, Math.floor(Date.now() / 1000)); if (!insertResult || !insertResult.lastInsertRowid) { throw new Error('Failed to create episode'); } const episodeId = Number(insertResult.lastInsertRowid); this.causalGraph.recordObservation({ experimentId: params.experimentId, episodeId: episodeId, isTreatment: params.isTreatment, outcomeValue: params.outcome, outcomeType: 'reward', context: params.context ? JSON.parse(params.context) : undefined }); // Save database to persist changes this.db.save(); log.success(`Recorded ${params.isTreatment ? 'treatment' : 'control'} observation: ${params.outcome}`); } async causalExperimentCalculate(experimentId: number): Promise<void> { if (!this.causalGraph) throw new Error('Not initialized'); log.header('\n📈 Calculating Uplift'); const result = this.causalGraph.calculateUplift(experimentId); // Fetch experiment details (now includes calculated means) const experiment = this.db!.prepare('SELECT * FROM causal_experiments WHERE id = ?').get(experimentId) as any; if (!experiment) { throw new Error(`Experiment ${experimentId} not found`); } log.info(`Experiment: ${experiment.hypothesis || 'Unknown'}`); log.info(`Treatment Mean: ${experiment.treatment_mean?.toFixed(3) || 'N/A'}`); log.info(`Control Mean: ${experiment.control_mean?.toFixed(3) || 'N/A'}`); log.success(`Uplift: ${result?.uplift?.toFixed(3) || 'N/A'}`); if (result?.confidenceInterval && result.confidenceInterval.length === 2) { log.info(`95% CI: [${result.confidenceInterval[0]?.toFixed(3) || 'N/A'}, ${result.confidenceInterval[1]?.toFixed(3) || 'N/A'}]`); } if (result?.pValue !== undefined) { log.info(`p-value: ${result.pValue.toFixed(4)}`); } // Get sample sizes from observations const counts = this.db!.prepare('SELECT COUNT(*) as total, SUM(is_treatment) as treatment FROM causal_observations WHERE experiment_id = ?').get(experimentId) as any; if (!counts) { throw new Error(`Failed to get observation counts for experiment ${experimentId}`); } log.info(`Sample Sizes: ${counts.treatment || 0} treatment, ${(counts.total || 0) - (counts.treatment || 0)} control`); if (result && result.pValue !== undefined && result.pValue < 0.05) { log.success('Result is statistically significant (p < 0.05)'); } else { log.warning('Result is not statistically significant'); } } async causalQuery(params: { cause?: string; effect?: string; minConfidence?: number; minUplift?: number; limit?: number; }): Promise<void> { if (!this.causalGraph) throw new Error('Not initialized'); log.header('\n🔍 Querying Causal Edges'); const edges = this.causalGraph.queryCausalEffects({ interventionMemoryId: 0, interventionMemoryType: params.cause || '', outcomeMemoryId: params.effect ? 0 : undefined, minConfidence: params.minConfidence || 0.7, minUplift: params.minUplift || 0.1 }); if (edges.length === 0) { log.warning('No causal edges found'); return; } console.log('\n' + '═'.repeat(80)); edges.slice(0, params.limit || 10).forEach((edge, i) => { console.log(`${colors.bright}#${i + 1}: ${edge.fromMemoryType} → ${edge.toMemoryType}${colors.reset}`); console.log(` Uplift: ${colors.green}${(edge.uplift || 0).toFixed(3)}${colors.reset}`); console.log(` Confidence: ${edge.confidence.toFixed(2)} (n=${edge.sampleSize})`); console.log('─'.repeat(80)); }); log.success(`Found ${edges.length} causal edges`); } // ============================================================================ // Recall Commands // ============================================================================ async recallWithCertificate(params: { query: string; k?: number; alpha?: number; beta?: number; gamma?: number; }): Promise<void> { if (!this.causalRecall) throw new Error('Not initialized'); log.header('\n🔍 Causal Recall with Certificate'); log.info(`Query: "${params.query}"`); log.info(`k: ${params.k || 12}`); const startTime = Date.now(); const result = await this.causalRecall.recall( 'cli-' + Date.now(), params.query, params.k || 12, undefined, 'internal' ); const duration = Date.now() - startTime; console.log('\n' + '═'.repeat(80)); console.log(`${colors.bright}Results (${result.candidates.length})${colors.reset}`); console.log('═'.repeat(80)); result.candidates.slice(0, 5).forEach((r, i) => { console.log(`\n${colors.bright}#${i + 1}: ${r.type} ${r.id}${colors.reset}`); console.log(` Content: ${r.content.substring(0, 50)}...`); console.log(` Similarity: ${colors.cyan}${r.similarity.toFixed(3)}${colors.reset}`); console.log(` Uplift: ${colors.green}${r.uplift?.toFixed(3) || 'N/A'}${colors.reset}`); console.log(` Utility: ${colors.yellow}${r.utilityScore.toFixed(3)}${colors.reset}`); }); console.log('\n' + '═'.repeat(80)); log.info(`Certificate ID: ${result.certificate.id}`); log.info(`Query: ${result.certificate.queryText}`); log.info(`Completeness: ${result.certificate.completenessScore.toFixed(2)}`); log.success(`Completed in ${duration}ms`); } // ============================================================================ // Learner Commands // ============================================================================ async learnerRun(params: { minAttempts?: number; minSuccessRate?: number; minConfidence?: number; dryRun?: boolean; }): Promise<void> { if (!this.nightlyLearner) throw new Error('Not initialized'); log.header('\n🌙 Running Nightly Learner'); log.info(`Min Attempts: ${params.minAttempts || 3}`); log.info(`Min Success Rate: ${params.minSuccessRate || 0.6}`); log.info(`Min Confidence: ${params.minConfidence || 0.7}`); const startTime = Date.now(); const discovered = await this.nightlyLearner.discover({ minAttempts: params.minAttempts || 3, minSuccessRate: params.minSuccessRate || 0.6, minConfidence: params.minConfidence || 0.7, dryRun: params.dryRun || false }); const duration = Date.now() - startTime; log.success(`Discovered ${discovered.length} causal edges in ${(duration / 1000).toFixed(1)}s`); if (discovered.length > 0) { console.log('\n' + '═'.repeat(80)); discovered.slice(0, 10).forEach((edge: any, i: number) => { console.log(`${colors.bright}#${i + 1}: ${edge.cause} → ${edge.effect}${colors.reset}`); console.log(` Uplift: ${colors.green}${edge.uplift.toFixed(3)}${colors.reset} (CI: ${edge.confidence.toFixed(2)})`); console.log(` Sample size: ${edge.sampleSize}`); console.log('─'.repeat(80)); }); } } async learnerPrune(params: { minConfidence?: number; minUplift?: number; maxAgeDays?: number; }): Promise<void> { if (!this.nightlyLearner) throw new Error('Not initialized'); log.header('\n🧹 Pruning Low-Quality Edges'); // Update config and run pruning this.nightlyLearner.updateConfig({ confidenceThreshold: params.minConfidence || 0.6, upliftThreshold: params.minUplift || 0.05, edgeMaxAgeDays: params.maxAgeDays || 90 }); const report = await this.nightlyLearner.run(); log.success(`Pruned ${report.edgesPruned} edges`); } // ============================================================================ // Reflexion Commands // ============================================================================ async reflexionStoreEpisode(params: { sessionId: string; task: string; input?: string; output?: string; critique?: string; reward: number; success: boolean; latencyMs?: number; tokensUsed?: number; }): Promise<void> { if (!this.reflexion) throw new Error('Not initialized'); log.header('\n💭 Storing Episode'); log.info(`Task: ${params.task}`); log.info(`Success: ${params.success ? 'Yes' : 'No'}`); log.info(`Reward: ${params.reward.toFixed(2)}`); const episodeId = await this.reflexion.storeEpisode(params as Episode); // Save database to persist changes this.db.save(); log.success(`Stored episode #${episodeId}`); if (params.critique) { log.info(`Critique: "${params.critique}"`); } } async reflexionRetrieve(params: { task: string; k?: number; onlyFailures?: boolean; onlySuccesses?: boolean; minReward?: number; synthesizeContext?: boolean; filters?: any; }): Promise<void> { if (!this.reflexion) throw new Error('Not initialized'); log.header('\n🔍 Retrieving Past Episodes'); log.info(`Task: "${params.task}"`); log.info(`k: ${params.k || 5}`); if (params.onlyFailures) log.info('Filter: Failures only'); if (params.onlySuccesses) log.info('Filter: Successes only'); if (params.synthesizeContext) log.info('Context synthesis: enabled'); let episodes = await this.reflexion.retrieveRelevant({ task: params.task, k: params.k || 5, onlyFailures: params.onlyFailures, onlySuccesses: params.onlySuccesses, minReward: params.minReward }); // Apply metadata filters if provided if (params.filters && Object.keys(params.filters).length > 0) { episodes = MetadataFilter.apply(episodes, params.filters); log.info(`Filtered to ${episodes.length} results matching metadata criteria`); } if (episodes.length === 0) { log.warning('No episodes found'); return; } console.log('\n' + '═'.repeat(80)); episodes.forEach((ep, i) => { console.log(`${colors.bright}#${i + 1}: Episode ${ep.id}${colors.reset}`); console.log(` Task: ${ep.task}`); console.log(` Reward: ${colors.green}${ep.reward.toFixed(2)}${colors.reset}`); console.log(` Success: ${ep.success ? colors.green + 'Yes' : colors.red + 'No'}${colors.reset}`); console.log(` Similarity: ${colors.cyan}${ep.similarity?.toFixed(3) || 'N/A'}${colors.reset}`); if (ep.critique) { console.log(` Critique: "${ep.critique}"`); } console.log('─'.repeat(80)); }); log.success(`Retrieved ${episodes.length} relevant episodes`); // Synthesize context if requested if (params.synthesizeContext && episodes.length > 0) { const context = ContextSynthesizer.synthesize(episodes.map(ep => ({ task: ep.task, reward: ep.reward, success: ep.success, critique: ep.critique, input: ep.input, output: ep.output, similarity: ep.similarity }))); console.log('\n' + '═'.repeat(80)); console.log(`${colors.bright}${colors.cyan}SYNTHESIZED CONTEXT${colors.reset}`); console.log('═'.repeat(80)); console.log(`\n${context.summary}\n`); if (context.patterns.length > 0) { console.log(`${colors.yellow}Common Patterns:${colors.reset}`); context.patterns.forEach(p => console.log(` • ${p}`)); console.log(''); } if (context.keyInsights.length > 0) { console.log(`${colors.cyan}Key Insights:${colors.reset}`); context.keyInsights.forEach(i => console.log(` • ${i}`)); console.log(''); } if (context.recommendations.length > 0) { console.log(`${colors.green}Recommendations:${colors.reset}`); context.recommendations.forEach((r, i) => console.log(` ${i + 1}. ${r}`)); console.log(''); } console.log('═'.repeat(80)); } } async reflexionRetrieveJson(params: { task: string; k?: number; onlyFailures?: boolean; onlySuccesses?: boolean; minReward?: number; }): Promise<any[]> { if (!this.reflexion) throw new Error('Not initialized'); const episodes = await this.reflexion.retrieveRelevant({ task: params.task, k: params.k || 5, onlyFailures: params.onlyFailures, onlySuccesses: params.onlySuccesses, minReward: params.minReward }); return episodes; } async reflexionGetCritiqueSummary(params: { task: string; k?: number; }): Promise<void> { if (!this.reflexion) throw new Error('Not initialized'); log.header('\n📋 Critique Summary'); log.info(`Task: "${params.task}"`); const summary = await this.reflexion.getCritiqueSummary({ task: params.task, k: params.k }); console.log('\n' + '═'.repeat(80)); console.log(colors.bright + 'Past Lessons:' + colors.reset); console.log(summary); console.log('═'.repeat(80)); } async reflexionPrune(params: { minReward?: number; maxAgeDays?: number; keepMinPerTask?: number; }): Promise<void> { if (!this.reflexion) throw new Error('Not initialized'); log.header('\n🧹 Pruning Episodes'); const pruned = this.reflexion.pruneEpisodes({ minReward: params.minReward || 0.3, maxAgeDays: params.maxAgeDays || 30, keepMinPerTask: params.keepMinPerTask || 5 }); log.success(`Pruned ${pruned} low-quality episodes`); } // ============================================================================ // Skill Library Commands // ============================================================================ async skillCreate(params: { name: string; description: string; code?: string; successRate?: number; episodeId?: number; }): Promise<void> { if (!this.skills) throw new Error('Not initialized'); log.header('\n🎯 Creating Skill'); log.info(`Name: ${params.name}`); log.info(`Description: ${params.description}`); const skillId = await this.skills.createSkill({ name: params.name, description: params.description, signature: { inputs: {}, outputs: {} }, code: params.code, successRate: params.successRate || 0.0, uses: 0, avgReward: 0.0, avgLatencyMs: 0.0, createdFromEpisode: params.episodeId }); // Save database to persist changes this.db.save(); log.success(`Created skill #${skillId}`); } async skillSearch(params: { task: string; k?: number; minSuccessRate?: number; }): Promise<void> { if (!this.skills) throw new Error('Not initialized'); log.header('\n🔍 Searching Skills'); log.info(`Task: "${params.task}"`); log.info(`Min Success Rate: ${params.minSuccessRate || 0.0}`); const skills = await this.skills.searchSkills({ task: params.task, k: params.k || 10, minSuccessRate: params.minSuccessRate || 0.0 }); if (skills.length === 0) { log.warning('No skills found'); return; } console.log('\n' + '═'.repeat(80)); skills.forEach((skill: any, i: number) => { console.log(`${colors.bright}#${i + 1}: ${skill.name}${colors.reset}`); console.log(` Description: ${skill.description}`); console.log(` Success Rate: ${colors.green}${(skill.successRate * 100).toFixed(1)}%${colors.reset}`); console.log(` Uses: ${skill.uses}`); console.log(` Avg Reward: ${skill.avgReward.toFixed(2)}`); console.log(` Avg Latency: ${skill.avgLatencyMs.toFixed(0)}ms`); console.log('─'.repeat(80)); }); log.success(`Found ${skills.length} matching skills`); } async skillConsolidate(params: { minAttempts?: number; minReward?: number; timeWindowDays?: number; extractPatterns?: boolean; }): Promise<void> { if (!this.skills) throw new Error('Not initialized'); log.header('\n🔄 Consolidating Episodes into Skills with Pattern Extraction'); log.info(`Min Attempts: ${params.minAttempts || 3}`); log.info(`Min Reward: ${params.minReward || 0.7}`); log.info(`Time Window: ${params.timeWindowDays || 7} days`); log.info(`Pattern Extraction: ${params.extractPatterns !== false ? 'Enabled' : 'Disabled'}`); const startTime = Date.now(); const result = await this.skills.consolidateEpisodesIntoSkills({ minAttempts: params.minAttempts || 3, minReward: params.minReward || 0.7, timeWindowDays: params.timeWindowDays || 7, extractPatterns: params.extractPatterns !== false }); // Save database to persist changes this.db.save(); const duration = Date.now() - startTime; log.success(`Created ${result.created} new skills, updated ${result.updated} existing skills in ${duration}ms`); // Display extracted patterns if available if (result.patterns.length > 0) { console.log('\n' + '═'.repeat(80)); console.log(`${colors.bright}${colors.cyan}Extracted Patterns:${colors.reset}`); console.log('═'.repeat(80)); result.patterns.forEach((pattern, i) => { console.log(`\n${colors.bright}#${i + 1}: ${pattern.task}${colors.reset}`); console.log(` Avg Reward: ${colors.green}${pattern.avgReward.toFixed(2)}${colors.reset}`); if (pattern.commonPatterns.length > 0) { console.log(` ${colors.cyan}Common Patterns:${colors.reset}`); pattern.commonPatterns.forEach(p => console.log(` • ${p}`)); } if (pattern.successIndicators.length > 0) { console.log(` ${colors.yellow}Success Indicators:${colors.reset}`); pattern.successIndicators.forEach(s => console.log(` • ${s}`)); } console.log('─'.repeat(80)); }); } if (result.created === 0 && result.updated === 0) { log.warning('No episodes met the criteria for skill consolidation'); log.info('Try lowering minReward or increasing timeWindowDays'); } } async skillPrune(params: { minUses?: number; minSuccessRate?: number; maxAgeDays?: number; }): Promise<void> { if (!this.skills) throw new Error('Not initialized'); log.header('\n🧹 Pruning Skills'); const pruned = this.skills.pruneSkills({ minUses: params.minUses || 3, minSuccessRate: params.minSuccessRate || 0.4, maxAgeDays: params.maxAgeDays || 60 }); log.success(`Pruned ${pruned} underperforming skills`); } // ============================================================================ // QUIC Synchronization Commands // ============================================================================ async quicStartServer(params: { port?: number; cert?: string; key?: string; authToken?: string; }): Promise<void> { log.header('\n🚀 Starting QUIC Sync Server'); const port = params.port || 4433; const authToken = params.authToken || this.generateAuthToken(); log.info(`Port: ${port}`); log.info(`Auth Token: ${authToken}`); if (params.cert && params.key) { log.info(`TLS Certificate: ${params.cert}`); log.info(`TLS Key: ${params.key}`); } else { log.warning('No TLS certificate provided - using self-signed certificate'); } // TODO: Implement QUIC server using existing QUICSync controller log.info('\nServer started. Waiting for connections...'); log.info('Press Ctrl+C to stop'); // Keep process alive await new Promise(() => {}); } async quicConnect(params: { host: string; port: number; authToken?: string; cert?: string; }): Promise<void> { log.header('\n🔌 Connecting to QUIC Sync Server'); log.info(`Host: ${params.host}`); log.info(`Port: ${params.port}`); if (params.authToken) { log.info('Authentication: Enabled'); } if (params.cert) { log.info(`TLS Certificate: ${params.cert}`); } else { log.warning('No TLS certificate provided - using insecure connection'); } // TODO: Implement QUIC client connection log.success('Connected to remote server'); log.info('Server info: AgentDB QUIC Sync v1.0'); } async quicPush(params: { server: string; incremental?: boolean; filter?: string; }): Promise<void> { if (!this.db) throw new Error('Not initialized'); log.header('\n⬆️ Pushing Changes to Remote'); log.info(`Server: ${params.server}`); log.info(`Mode: ${params.incremental ? 'Incremental' : 'Full'}`); if (params.filter) { log.info(`Filter: ${params.filter}`); } // Get pending changes const changes = this.getPendingChanges(params.incremental, params.filter); console.log('\n' + '═'.repeat(80)); console.log(`${colors.bright}Pending Changes${colors.reset}`); console.log('═'.repeat(80)); console.log(` Episodes: ${changes.episodes}`); console.log(` Skills: ${changes.skills}`); console.log(` Causal Edges: ${changes.causalEdges}`); console.log(` Total Size: ${(changes.totalSize / 1024).toFixed(2)} KB`); console.log('═'.repeat(80)); // TODO: Implement QUIC push log.success('Changes pushed successfully'); log.info(`Transferred: ${(changes.totalSize / 1024).toFixed(2)} KB`); } async quicPull(params: { server: string; incremental?: boolean; filter?: string; }): Promise<void> { if (!this.db) throw new Error('Not initialized'); log.header('\n⬇️ Pulling Changes from Remote'); log.info(`Server: ${params.server}`); log.info(`Mode: ${params.incremental ? 'Incremental' : 'Full'}`); if (params.filter) { log.info(`Filter: ${params.filter}`); } // TODO: Implement QUIC pull const changes = { episodes: 5, skills: 2, causalEdges: 3, totalSize: 12800 }; console.log('\n' + '═'.repeat(80)); console.log(`${colors.bright}Received Changes${colors.reset}`); console.log('═'.repeat(80)); console.log(` Episodes: ${changes.episodes}`); console.log(` Skills: ${changes.skills}`); console.log(` Causal Edges: ${changes.causalEdges}`); console.log(` Total Size: ${(changes.totalSize / 1024).toFixed(2)} KB`); console.log('═'.repeat(80)); log.success('Changes pulled and merged successfully'); log.info(`Downloaded: ${(changes.totalSize / 1024).toFixed(2)} KB`); } async quicStatus(): Promise<void> { if (!this.db) throw new Error('Not initialized'); log.header('\n📊 QUIC Sync Status'); // Get sync metadata const syncMeta = this.getSyncMetadata(); console.log('\n' + '═'.repeat(80)); console.log(`${colors.bright}Sync Status${colors.reset}`); console.log('═'.repeat(80)); if (syncMeta.lastSyncTime) { const lastSync = new Date(syncMeta.lastSyncTime * 1000); console.log(` Last Sync: ${colors.cyan}${lastSync.toLocaleString()}${colors.reset}`); console.log(` Time Ago: ${this.timeAgo(syncMeta.lastSyncTime)}`); } else { console.log(` Last Sync: ${colors.yellow}Never${colors.reset}`); } console.log('\n' + '─'.repeat(80)); console.log(`${colors.bright}Pending Changes${colors.reset}`); console.log('─'.repeat(80)); console.log(` Episodes: ${colors.green}${syncMeta.pendingEpisodes}${colors.reset}`); console.log(` Skills: ${colors.green}${syncMeta.pendingSkills}${colors.reset}`); console.log(` Causal Edges: ${colors.green}${syncMeta.pendingCausalEdges}${colors.reset}`); console.log('\n' + '─'.repeat(80)); console.log(`${colors.bright}Connected Servers${colors.reset}`); console.log('─'.repeat(80)); if (syncMeta.servers.length === 0) { console.log(` ${colors.yellow}No servers connected${colors.reset}`); } else { syncMeta.servers.forEach((server: any) => { console.log(` ${colors.cyan}${server.host}:${server.port}${colors.reset}`); console.log(` Status: ${server.connected ? colors.green + 'Connected' : colors.red + 'Disconnected'}${colors.reset}`); console.log(` Last Seen: ${new Date(server.lastSeen * 1000).toLocaleString()}`); }); } console.log('═'.repeat(80)); } private generateAuthToken(): string { // Generate a random 32-character token return Array.from({ length: 32 }, () => Math.floor(Math.random() * 16).toString(16) ).join(''); } private getPendingChanges(incremental?: boolean, filter?: string): { episodes: number; skills: number; causalEdges: number; totalSize: number; } { // Mock implementation - would query sync metadata table return { episodes: 10, skills: 3, causalEdges: 5, totalSize: 25600 }; } private getSyncMetadata(): { lastSyncTime: number | null; pendingEpisodes: number; pendingSkills: number; pendingCausalEdges: number; servers: Array<{ host: string; port: number; connected: boolean; lastSeen: number; }>; } { // Mock implementation - would query sync metadata table return { lastSyncTime: null, pendingEpisodes: 10, pendingSkills: 3, pendingCausalEdges: 5, servers: [] }; } private timeAgo(timestamp: number): string { const now = Math.floor(Date.now() / 1000); const diff = now - timestamp; if (diff < 60) return `${diff} seconds ago`; if (diff < 3600) return `${Math.floor(diff / 60)} minutes ago`; if (diff < 86400) return `${Math.floor(diff / 3600)} hours ago`; return `${Math.floor(diff / 86400)} days ago`; } // ============================================================================ // Database Commands // ============================================================================ async dbStats(): Promise<void> { if (!this.db) throw new Error('Not initialized'); log.header('\n📊 Database Statistics'); const tables = ['causal_edges', 'causal_experiments', 'causal_observations', 'certificates', 'provenance_lineage', 'episodes']; console.log('\n' + '═'.repeat(80)); tables.forEach(table => { try { const count = this.db!.prepare(`SELECT COUNT(*) as count FROM ${table}`).get() as { count: number }; console.log(`${colors.bright}${table}:${colors.reset} ${colors.cyan}${count.count}${colors.reset} records`); } catch (e) { console.log(`${colors.bright}${table}:${colors.reset} ${colors.yellow}N/A${colors.reset}`); } }); console.log('═'.repeat(80)); } } // ============================================================================ // CLI Entry Point // ============================================================================ async function main() { const args = process.argv.slice(2); // Handle version flag if (args[0] === '--version' || args[0] === '-v' || args[0] === 'version') { const packageJsonPath = path.join(__dirname, '../../package.json'); try { const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')); console.log(`agentdb v${packageJson.version}`); } catch { console.log('agentdb v1.4.5'); } process.exit(0); } // Handle help - show help if no args or help flag if (args.length === 0 || args[0] === 'help' || args[0] === '--help' || args[0] === '-h') { printHelp(); process.exit(0); } const command = args[0]; // Handle MCP server command separately (doesn't need CLI initialization) if (command === 'mcp') { await handleMcpCommand(args.slice(1)); return; } // Handle init command if (command === 'init') { await handleInitCommand(args.slice(1)); return; } // Handle vector search commands (no CLI initialization needed) if (command === 'vector-search') { await handleVectorSearchCommand(args.slice(1)); return; } if (command === 'export') { await handleExportCommand(args.slice(1)); return; } if (command === 'import') { await handleImportCommand(args.slice(1)); return; } if (command === 'stats') { await handleStatsCommand(args.slice(1)); return; } const cli = new AgentDBCLI(); const dbPath = process.env.AGENTDB_PATH || './agentdb.db'; try { await cli.initialize(dbPath); const subcommand = args[1]; if (command === 'causal') { await handleCausalCommands(cli, subcommand, args.slice(2)); } else if (command === 'recall') { await handleRecallCommands(cli, subcommand, args.slice(2)); } else if (command === 'learner') { await handleLearnerCommands(cli, subcommand, args.slice(2)); } else if (command === 'reflexion') { await handleReflexionCommands(cli, subcommand, args.slice(2)); } else if (command === 'skill') { await handleSkillCommands(cli, subcommand, args.slice(2)); } else if (command === 'db') { await handleDbCommands(cli, subcommand, args.slice(2)); } else if (command === 'sync') { await handleSyncCommands(cli, subcommand, args.slice(2)); } else if (command === 'query') { await handleQueryCommand(cli, args.slice(1)); } else if (command === 'store-pattern') { await handleStorePatternCommand(cli, args.slice(1)); } else if (command === 'train') { await handleTrainCommand(cli, args.slice(1)); } else if (command === 'optimize-memory') { await handleOptimizeMemoryCommand(cli, args.slice(1)); } else { log.error(`Unknown command: ${command}`); printHelp(); process.exit(1); } } catch (error) { log.error((error as Error).message); process.exit(1); } } // Command handlers // Init command handler async function handleInitCommand(args: string[]) { // Parse arguments let dbPath = './agentdb.db'; let dimension = 1536; // Default OpenAI ada-002 let preset: 'small' | 'medium' | 'large' | null = null; let inMemory = false; for (let i = 0; i < args.length; i++) { const arg = args[i]; if (arg === '--dimension' && i + 1 < args.length) { dimension = parseInt(args[++i]); } else if (arg === '--preset' && i + 1 < args.length) { preset = args[++i] as 'small' | 'medium' | 'large'; } else if (arg === '--in-memory') { inMemory = true; dbPath = ':memory:'; } else if (!arg.startsWith('--')) { dbPath = arg; } } // Apply preset configurations if (preset) { if (preset === 'small') { log.info('Using SMALL preset (<10K vectors)'); } else if (preset === 'medium') { log.info('Using MEDIUM preset (10K-100K vectors)'); } else if (preset === 'large') { log.info('Using LARGE preset (>100K vectors)'); } } log.info(`Initializing AgentDB at: ${dbPath}`); log.info(`Embedding dimension: ${dimension}`); if (inMemory) { log.info('Using in-memory database (data will not persist)'); } // Check if database already exists if (!inMemory && fs.existsSync(dbPath)) { log.warning(`Database already exists at ${dbPath}`); log.info('Use a different path or remove the existing file to reinitialize'); return; } // Create parent directories if needed if (!inMemory) { const parentDir = path.dirname(dbPath); if (parentDir !== '.' && !fs.existsSync(parentDir)) { log.info(`Creating directory: ${parentDir}`); fs.mkdirSync(parentDir, { recursive: true }); } } // Create new database with schemas const cli = new AgentDBCLI(); await cli.initialize(dbPath); // CRITICAL: Save the database to disk (unless in-memory) // sql.js keeps everything in memory until explicitly saved if (!inMemory) { if (cli.db && typeof cli.db.save === 'function') { cli.db.save(); } else if (cli.db && typeof cli.db.close === 'function') { // close() calls save() internally cli.db.close(); } // Verify database file was created if (!fs.existsSync(dbPath)) { log.error(`Failed to create database file at ${dbPath}`); log.error('The database may be in memory only'); process.exit(1); } } // Verify database has tables try { const db = await createDatabase(dbPath); const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all(); db.close(); if (tables.length === 0) { log.warning('Database file created but no tables found'); log.warning('Schemas may not have been loaded correctly'); } else { log.success(`Database created with ${tables.length} tables`); } } catch (error) { log.warning(`Could not verify database tables: ${(error as Error).message}`); } log.success(`✅ AgentDB initialized successfully at ${dbPath}`); log.info('Database includes:'); log.info(' - Core vector tables (episodes, embeddings)'); log.info(' - Causal memory graph'); log.info(' - Reflexion memory'); log.info(' - Skill library'); log.info(' - Learning system'); log.info(''); log.info('Next steps:'); log.info(' - Use "agentdb mcp start" to start MCP server'); log.info(' - Use "agentdb causal add" to add causal edges'); log.info(' - Use "agentdb reflexion add" to store episodes'); log.info(' - See "agentdb help" for all commands'); } async function handleMcpCommand(args: string[]) { const subcommand = args[0]; if (subcommand === 'start' || !subcommand) { log.info('Starting AgentDB MCP Server...'); // Spawn the MCP server as a child process (like agentic-flow does) // This ensures the server stays running with proper stdio inheritance const mcpServerPath = path.join(__dirname, '../mcp/agentdb-mcp-server.js'); if (!fs.existsSync(mcpServerPath)) { log.error('MCP server not found. Please rebuild the package: npm run build'); process.exit(1); } // Spawn server process with stdio inheritance const { spawn } = await import('child_process'); const serverProcess = spawn('node', [mcpServerPath], { stdio: 'inherit', env: process.env }); // Handle server exit serverProcess.on('exit', (code) => { process.exit(code || 0); }); // Forward termination signals process.on('SIGINT', () => { serverProcess.kill('SIGINT'); }); process.on('SIGTERM', () => { serverProcess.kill('SIGTERM'); }); // Keep this process alive until server exits return new Promise(() => { // Never resolve - wait for server process }); } else { log.error(`Unknown mcp subcommand: ${subcommand}`); log.info('Usage: agentdb mcp start'); process.exit(1); } } async function handleCausalCommands(cli: AgentDBCLI, subcommand: string, args: string[]) { if (subcommand === 'add-edge') { await cli.causalAddEdge({ cause: args[0], effect: args[1], uplift: parseFloat(args[2]), confidence: args[3] ? parseFloat(args[3]) : undefined, sampleSize: args[4] ? parseInt(args[4]) : undefined }); } else if (subcommand === 'experiment' && args[0] === 'create') { await cli.causalExperimentCreate({ name: args[1], cause: args[2], effect: args[3] }); } else if (subcommand === 'experiment' && args[0] === 'add-observation') { await cli.causalExperimentAddObservation({ experimentId: parseInt(args[1]), isTreatment: args[2] === 'true', outcome: parseFloat(args[3]), context: args[4] }); } else if (subcommand === 'experiment' && args[0] === 'calculate') { await cli.causalExperimentCalculate(parseInt(args[1])); } else if (subcommand === 'query') { await cli.causalQuery({ cause: args[0], effect: args[1], minConfidence: args[2] ? parseFloat(args[2]) : undefined, minUplift: args[3] ? parseFloat(args[3]) : undefined, limit: args[4] ? parseInt(args[4]) : undefined }); } else { log.error(`Unknown causal subcommand: ${subcommand}`); printHelp(); } } async function handleRecallCommands(cli: AgentDBCLI, subcommand: string, args: string[]) { if (subcommand === 'with-certificate') { await cli.recallWithCertificate({ query: args[0], k: args[1] ? parseInt(args[1]) : undefined, alpha: args[2] ? parseFloat(args[2]) : undefined, beta: args[3] ? parseFloat(args[3]) : undefined, gamma: args[4] ? parseFloat(args[4]) : undefined }); } else { log.error(`Unknown recall subcommand: ${subcommand}`); printHelp(); } } async function handleLearnerCommands(cli: AgentDBCLI, subcommand: string, args: string[]) { if (subcommand === 'run') { await cli.learnerRun({ minAttempts: args[0] ? parseInt(args[0]) : undefined, minSuccessRate: args[1] ? parseFloat(args[1]) : undefined, minConfidence: args[2] ? parseFloat(args[2]) : undefined, dryRun: args[3] === 'true' }); } else if (subcommand === 'prune') { await cli.learnerPrune({ minConfidence: args[0] ? parseFloat(args[0]) : undefined, minUplift: args[1] ? parseFloat(args[1]) : undefined, maxAgeDays: args[2] ? parseInt(args[2]) : undefined }); } else { log.error(`Unknown learner subcommand: ${subcommand}`); printHelp(); } } async function handleReflexionCommands(cli: AgentDBCLI, subcommand: string, args: string[]) { if (subcommand === 'store') { await cli.reflexionStoreEpisode({ sessionId: args[0], task: args[1], reward: parseFloat(args[2]), success: args[3] === 'true', critique: args[4], input: args[5], output: args[6], latencyMs: args[7] ? parseInt(args[7]) : undefined, tokensUsed: args[8] ? parseInt(args[8]) : undefined }); } else if (subcommand === 'retrieve') { // Parse retrieve command with new flags let task = args[0]; let k: number | undefined = undefined; let minReward: number | undefined = undefined; let onlyFailures: boolean | undefined = undefined; let onlySuccesses: boolean | undefined = undefined; let synthesizeContext = false; let filters: any = {}; for (let i = 1; i < args.length; i++) { const arg = args[i]; if (arg === '--k' && i + 1 < args.length) { k = parseInt(args[++i]); } else if (arg === '--min-reward' && i + 1 < args.length) { minReward = parseFloat(args[++i]); } else if (arg === '--only-failures') { onlyFailures = true; } else if (arg === '--only-successes') { onlySuccesses = true; } else if (arg === '--synthesize-context') { synthesizeContext = true; } else if (arg === '--filters' && i + 1 < args.length) { try { filters = JSON.parse(args[++i]); } catch (error) { log.error(`Invalid JSON in --filters parameter: ${(error as Error).message}`); process.exit(1); } } else if (!arg.startsWith('--') && k === undefined) { // Legacy positional argument parsing k = parseInt(arg); if (i + 1 < args.length && !args[i + 1].startsWith('--')) { minReward = parseFloat(args[++i]); } if (i + 1 < args.length && args[i + 1] === 'true') { onlyFailures = true; i++; } if (i + 1 < args.length && args[i + 1] === 'true') { onlySuccesses = true; i++; } } } await cli.reflexionRetrieve({ task, k, minReward, onlyFailures, onlySuccesses, synthesizeContext, filters: Object.keys(filters).length > 0 ? filters : undefined }); } else if (subcommand === 'critique-summary') { await cli.reflexionGetCritiqueSummary({ task: args[0], k: args[1] ? parseInt(args[1]) : undefined }); } else if (subcommand === 'prune') { await cli.reflexionPrune({ maxAgeDays: args[0] ? parseInt(args[0]) : undefined, minReward: args[1] ? parseFloat(args[1]) : undefined }); } else { log.error(`Unknown reflexion subcommand: ${subcommand}`); printHelp(); } } async function handleSkillCommands(cli: AgentDBCLI, subcommand: string, args: string[]) { if (subcommand === 'create') { await cli.skillCreate({ name: args[0], description: args[1], code: args[2] }); } else if (subcommand === 'search') { await cli.skillSearch({ task: args[0], k: args[1] ? parseInt(args[1]) : undefined }); } else if (subcommand === 'consolidate') { await cli.skillConsolidate({ minAttempts: args[0] ? parseInt(args[0]) : undefined, minReward: args[1] ? parseFloat(args[1]) : undefined, timeWindowDays: args[2] ? parseInt(args[2]) : undefined, extractPatterns: args[3] !== 'false' // Default true, set to false if explicitly passed }); } else if (subcommand === 'prune') { await cli.skillPrune({ minUses: args[0] ? parseInt(args[0]) : undefined, minSuccessRate: args[1] ? parseFloat(args[1]) : undefined, maxAgeDays: args[2] ? parseInt(args[2]) : undefined }); } else { log.error(`Unknown skill subcommand: ${subcommand}`); printHelp(); } } async function handleDbCommands(cli: AgentDBCLI, subcommand: string, args: string[]) { if (subcommand === 'stats') { await cli.dbStats(); } else { log.error(`Unknown db subcommand: ${subcommand}`); printHelp(); } } async function handleSyncCommands(cli: AgentDBCLI, subcommand: string, args: string[]) { if (subcommand === 'start-server') { // Parse options let port: number | undefined; let cert: string | undefined; let key: string | undefined; let authToken: string | undefined; for (let i = 0; i < args.length; i++) { if (args[i] === '--port' && i + 1 < args.length) { port = parseInt(args[++i]); } else if (args[i] === '--cert' && i + 1 < args.length) { cert = args[++i]; } else if (args[i] === '--key' && i + 1 < args.length) { key = args[++i]; } else if (args[i] === '--auth-token' && i + 1 < args.length) { authToken = args[++i]; } } await cli.quicStartServer({ port, cert, key, authToken }); } else if (subcommand === 'connect') { // Parse host and port const host = args[0]; const port = parseInt(args[1]); if (!host || !port) { log.error('Missing required arguments: host and port'); log.info('Usage: agentdb sync connect <host> <port> [--auth-token <token>] [--cert <path>]'); process.exit(1); } let authToken: string | undefined; let cert: string | undefined; for (let i = 2; i < args.length; i++) { if (args[i] === '--auth-token' && i + 1 < args.length) { authToken = args[++i]; } else if (args[i] === '--cert' && i + 1 < args.length) { cert = args[++i]; } } await cli.quicConnect({ host, port, authToken, cert }); } else if (subcommand === 'push') { // Parse options let server: string | undefined; let incremental = false; let filter: string | undefined; for (let i = 0; i <