UNPKG

claude-collab

Version:

Claude Collab - The AI collaboration framework that prevents echo chambers

345 lines 11.2 kB
"use strict"; /** * Memory Manager for Claude-Collab * Handles persistent key-value storage using SQLite */ const Database = require('better-sqlite3'); const path = require('path'); const fs = require('fs'); class MemoryManager { constructor() { // Ensure .claude-collab directory exists const dataDir = path.join(process.cwd(), '.claude-collab'); fs.mkdirSync(dataDir, { recursive: true }); // Initialize database this.dbPath = path.join(dataDir, 'memory.db'); this.db = new Database(this.dbPath); // Create table if not exists this.initDatabase(); } /** * Initialize database schema */ initDatabase() { const createTable = ` CREATE TABLE IF NOT EXISTS memory ( key TEXT PRIMARY KEY, value TEXT NOT NULL, type TEXT DEFAULT 'string', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, accessed_at DATETIME DEFAULT CURRENT_TIMESTAMP, access_count INTEGER DEFAULT 0, tags TEXT, ttl INTEGER, expires_at DATETIME ) `; this.db.exec(createTable); // Create indexes this.db.exec('CREATE INDEX IF NOT EXISTS idx_memory_created_at ON memory(created_at)'); this.db.exec('CREATE INDEX IF NOT EXISTS idx_memory_expires_at ON memory(expires_at)'); this.db.exec('CREATE INDEX IF NOT EXISTS idx_memory_tags ON memory(tags)'); // Clean up expired entries this.cleanupExpired(); } /** * Store a value in memory */ store(key, value, options = {}) { try { const type = typeof value; const serializedValue = type === 'object' ? JSON.stringify(value) : String(value); const tags = options.tags ? JSON.stringify(options.tags) : null; const ttl = options.ttl || null; const expiresAt = ttl ? new Date(Date.now() + ttl * 1000).toISOString() : null; const stmt = this.db.prepare(` INSERT OR REPLACE INTO memory (key, value, type, tags, ttl, expires_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) `); stmt.run(key, serializedValue, type, tags, ttl, expiresAt); return { success: true, key: key, type: type, size: serializedValue.length }; } catch (error) { return { success: false, error: error.message }; } } /** * Retrieve a value from memory */ get(key) { try { // Clean expired entries first this.cleanupExpired(); const stmt = this.db.prepare(` UPDATE memory SET accessed_at = CURRENT_TIMESTAMP, access_count = access_count + 1 WHERE key = ? AND (expires_at IS NULL OR expires_at > CURRENT_TIMESTAMP) RETURNING value, type `); const row = stmt.get(key); if (!row) { return { success: false, error: 'Key not found' }; } let value = row.value; if (row.type === 'object') { try { value = JSON.parse(row.value); } catch { // Return as string if parse fails } } else if (row.type === 'number') { value = Number(row.value); } else if (row.type === 'boolean') { value = row.value === 'true'; } return { success: true, value: value, type: row.type }; } catch (error) { return { success: false, error: error.message }; } } /** * List all keys with optional filtering */ list(options = {}) { try { let query = 'SELECT key, type, created_at, updated_at, access_count, tags FROM memory WHERE 1=1'; const params = []; // Filter by pattern if (options.pattern) { query += ' AND key LIKE ?'; params.push(options.pattern.replace('*', '%')); } // Filter by tags if (options.tags && options.tags.length > 0) { const tagConditions = options.tags.map(() => 'tags LIKE ?').join(' OR '); query += ` AND (${tagConditions})`; options.tags.forEach(tag => params.push(`%"${tag}"%`)); } // Filter out expired query += ' AND (expires_at IS NULL OR expires_at > CURRENT_TIMESTAMP)'; // Sort const sortBy = options.sortBy || 'created_at'; const sortOrder = options.sortOrder || 'DESC'; query += ` ORDER BY ${sortBy} ${sortOrder}`; // Limit if (options.limit) { query += ' LIMIT ?'; params.push(options.limit); } const stmt = this.db.prepare(query); const rows = stmt.all(...params); return { success: true, count: rows.length, keys: rows.map(row => ({ key: row.key, type: row.type, created: row.created_at, updated: row.updated_at, accessed: row.access_count, tags: row.tags ? JSON.parse(row.tags) : [] })) }; } catch (error) { return { success: false, error: error.message }; } } /** * Delete a key from memory */ delete(key) { try { const stmt = this.db.prepare('DELETE FROM memory WHERE key = ?'); const result = stmt.run(key); return { success: true, deleted: result.changes > 0 }; } catch (error) { return { success: false, error: error.message }; } } /** * Clear all memory */ clear() { try { this.db.exec('DELETE FROM memory'); return { success: true }; } catch (error) { return { success: false, error: error.message }; } } /** * Get memory statistics */ stats() { try { const stats = this.db.prepare(` SELECT COUNT(*) as total_keys, SUM(LENGTH(value)) as total_size, AVG(access_count) as avg_access_count, MAX(access_count) as max_access_count, COUNT(CASE WHEN expires_at IS NOT NULL THEN 1 END) as keys_with_ttl FROM memory WHERE expires_at IS NULL OR expires_at > CURRENT_TIMESTAMP `).get(); const typeStats = this.db.prepare(` SELECT type, COUNT(*) as count FROM memory WHERE expires_at IS NULL OR expires_at > CURRENT_TIMESTAMP GROUP BY type `).all(); return { success: true, stats: { totalKeys: stats.total_keys || 0, totalSize: stats.total_size || 0, avgAccessCount: Math.round(stats.avg_access_count || 0), maxAccessCount: stats.max_access_count || 0, keysWithTTL: stats.keys_with_ttl || 0, types: typeStats.reduce((acc, row) => { acc[row.type] = row.count; return acc; }, {}) } }; } catch (error) { return { success: false, error: error.message }; } } /** * Export memory to JSON */ export(filepath) { try { const rows = this.db.prepare(` SELECT * FROM memory WHERE expires_at IS NULL OR expires_at > CURRENT_TIMESTAMP `).all(); const data = rows.map(row => ({ key: row.key, value: row.type === 'object' ? JSON.parse(row.value) : row.value, type: row.type, tags: row.tags ? JSON.parse(row.tags) : [], created: row.created_at, updated: row.updated_at, accessed: row.access_count })); fs.writeFileSync(filepath, JSON.stringify(data, null, 2)); return { success: true, count: data.length, filepath: filepath }; } catch (error) { return { success: false, error: error.message }; } } /** * Import memory from JSON */ import(filepath) { try { const data = JSON.parse(fs.readFileSync(filepath, 'utf-8')); if (!Array.isArray(data)) { throw new Error('Invalid import format: expected array'); } let imported = 0; const stmt = this.db.prepare(` INSERT OR REPLACE INTO memory (key, value, type, tags, created_at, access_count) VALUES (?, ?, ?, ?, ?, ?) `); const transaction = this.db.transaction(() => { for (const item of data) { if (!item.key) continue; const value = typeof item.value === 'object' ? JSON.stringify(item.value) : String(item.value); const type = item.type || typeof item.value; const tags = item.tags ? JSON.stringify(item.tags) : null; stmt.run(item.key, value, type, tags, item.created || new Date().toISOString(), item.accessed || 0); imported++; } }); transaction(); return { success: true, imported: imported }; } catch (error) { return { success: false, error: error.message }; } } /** * Clean up expired entries */ cleanupExpired() { try { const stmt = this.db.prepare('DELETE FROM memory WHERE expires_at IS NOT NULL AND expires_at <= CURRENT_TIMESTAMP'); const result = stmt.run(); return result.changes; } catch (error) { console.error('Error cleaning up expired entries:', error); return 0; } } /** * Close database connection */ close() { if (this.db) { this.db.close(); } } } module.exports = { MemoryManager }; //# sourceMappingURL=memory-manager.js.map