UNPKG

@fastmcp-me/mcp-sqlew

Version:

MCP server for efficient context sharing between Claude Code sub-agents with 96% token reduction via action-based tools

340 lines 12.3 kB
/** * Database connection and initialization module * Handles SQLite database setup with configurable path */ import Database from 'better-sqlite3'; import { mkdirSync, existsSync } from 'fs'; import { dirname, resolve, isAbsolute } from 'path'; import { initializeSchema, isSchemaInitialized, verifySchemaIntegrity } from './schema.js'; import { DEFAULT_DB_PATH, DB_BUSY_TIMEOUT } from './constants.js'; import { performAutoCleanup } from './utils/cleanup.js'; import { needsMigration, runMigration, getMigrationInfo } from './migrations/add-table-prefixes.js'; let dbInstance = null; /** * Initialize database connection * Creates database file and folder if they don't exist * Initializes schema on first run * * @param dbPath - Optional database path (defaults to .sqlew/sqlew.db) * @returns SQLite database instance */ export function initializeDatabase(dbPath) { // If already initialized, return existing instance if (dbInstance) { return dbInstance; } try { // Use provided path or default const finalPath = dbPath || DEFAULT_DB_PATH; // Convert to absolute path if relative const absolutePath = isAbsolute(finalPath) ? finalPath : resolve(process.cwd(), finalPath); // Create directory if it doesn't exist const dbDir = dirname(absolutePath); if (!existsSync(dbDir)) { mkdirSync(dbDir, { recursive: true }); console.log(`✓ Created database directory: ${dbDir}`); } // Open database connection const db = new Database(absolutePath, { verbose: process.env.DEBUG_SQL ? console.log : undefined, }); // Configure database db.pragma('journal_mode = WAL'); // Write-Ahead Logging for better concurrency db.pragma('foreign_keys = ON'); // Enforce foreign key constraints db.pragma('synchronous = NORMAL'); // Balance between safety and performance db.pragma(`busy_timeout = ${DB_BUSY_TIMEOUT}`); // Set busy timeout console.log(`✓ Connected to database: ${absolutePath}`); // Check if migration is needed (v1.2.0 -> v1.3.0: table prefixes) if (needsMigration(db)) { console.log('→ Migration required: Adding table prefixes (v1.2.0 -> v1.3.0)'); console.log(getMigrationInfo()); const migrationResult = runMigration(db); if (!migrationResult.success) { console.error('\n❌ ERROR: Migration failed!'); console.error(migrationResult.message); db.close(); process.exit(1); } console.log('✓ Migration completed successfully'); if (migrationResult.details && migrationResult.details.length > 0) { migrationResult.details.forEach(detail => console.log(` - ${detail}`)); } // After migration, run schema initialization to create new views/triggers // (tables already exist, CREATE TABLE IF NOT EXISTS will skip them) console.log('→ Creating views and triggers for new schema...'); initializeSchema(db); } // Check if database has existing schema const schemaExists = isSchemaInitialized(db); if (schemaExists) { // Validate existing schema integrity console.log('→ Validating existing database schema...'); const validation = verifySchemaIntegrity(db); if (!validation.valid) { // Schema is invalid - display error and exit console.error('\n❌ ERROR: Database schema validation failed!'); console.error('\nThe existing database file has an incompatible schema.'); console.error(`Database location: ${absolutePath}`); if (validation.missing.length > 0) { console.error('\n📋 Missing components:'); validation.missing.forEach(item => console.error(` - ${item}`)); } if (validation.errors.length > 0) { console.error('\n⚠️ Validation errors:'); validation.errors.forEach(error => console.error(` - ${error}`)); } console.error('\n💡 Possible solutions:'); console.error(' 1. Backup and delete the existing database file to start fresh'); console.error(' 2. Use a different database path with --db-path option'); console.error(' 3. Restore from a backup if available\n'); // Close database and exit db.close(); process.exit(1); } console.log('✓ Database schema validation passed'); } else { // Initialize new schema console.log('→ Initializing database schema...'); initializeSchema(db); } // Store instance dbInstance = db; // Perform initial cleanup try { const cleanupResult = performAutoCleanup(db); if (cleanupResult.messagesDeleted > 0 || cleanupResult.fileChangesDeleted > 0) { console.log(`✓ Cleanup: ${cleanupResult.messagesDeleted} messages, ${cleanupResult.fileChangesDeleted} file changes deleted`); } } catch (error) { console.warn('⚠️ Initial cleanup failed:', error instanceof Error ? error.message : String(error)); } return db; } catch (error) { const message = error instanceof Error ? error.message : String(error); throw new Error(`Failed to initialize database: ${message}`); } } /** * Close database connection */ export function closeDatabase() { if (dbInstance) { dbInstance.close(); dbInstance = null; console.log('✓ Database connection closed'); } } /** * Get current database instance * Throws error if not initialized * * @returns Current database instance */ export function getDatabase() { if (!dbInstance) { throw new Error('Database not initialized. Call initializeDatabase() first.'); } return dbInstance; } // ============================================================================ // Helper Functions for Master Table Management // ============================================================================ /** * Get or create agent by name * Uses INSERT OR IGNORE for idempotent operation * * @param db - Database instance * @param name - Agent name * @returns Agent ID */ export function getOrCreateAgent(db, name) { // Try to insert db.prepare('INSERT OR IGNORE INTO m_agents (name) VALUES (?)').run(name); // Get the ID const result = db.prepare('SELECT id FROM m_agents WHERE name = ?').get(name); if (!result) { throw new Error(`Failed to get or create agent: ${name}`); } return result.id; } /** * Get or create context key by name * * @param db - Database instance * @param key - Context key name * @returns Context key ID */ export function getOrCreateContextKey(db, key) { db.prepare('INSERT OR IGNORE INTO m_context_keys (key) VALUES (?)').run(key); const result = db.prepare('SELECT id FROM m_context_keys WHERE key = ?').get(key); if (!result) { throw new Error(`Failed to get or create context key: ${key}`); } return result.id; } /** * Get or create file by path * * @param db - Database instance * @param path - File path * @returns File ID */ export function getOrCreateFile(db, path) { db.prepare('INSERT OR IGNORE INTO m_files (path) VALUES (?)').run(path); const result = db.prepare('SELECT id FROM m_files WHERE path = ?').get(path); if (!result) { throw new Error(`Failed to get or create file: ${path}`); } return result.id; } /** * Get or create tag by name * * @param db - Database instance * @param name - Tag name * @returns Tag ID */ export function getOrCreateTag(db, name) { db.prepare('INSERT OR IGNORE INTO m_tags (name) VALUES (?)').run(name); const result = db.prepare('SELECT id FROM m_tags WHERE name = ?').get(name); if (!result) { throw new Error(`Failed to get or create tag: ${name}`); } return result.id; } /** * Get or create scope by name * * @param db - Database instance * @param name - Scope name * @returns Scope ID */ export function getOrCreateScope(db, name) { db.prepare('INSERT OR IGNORE INTO m_scopes (name) VALUES (?)').run(name); const result = db.prepare('SELECT id FROM m_scopes WHERE name = ?').get(name); if (!result) { throw new Error(`Failed to get or create scope: ${name}`); } return result.id; } /** * Get layer ID by name * Does not auto-create (layers are predefined) * * @param db - Database instance * @param name - Layer name * @returns Layer ID or null if not found */ export function getLayerId(db, name) { const result = db.prepare('SELECT id FROM m_layers WHERE name = ?').get(name); return result ? result.id : null; } /** * Get constraint category ID by name * Does not auto-create (categories are predefined) * * @param db - Database instance * @param name - Category name * @returns Category ID or null if not found */ export function getCategoryId(db, name) { const result = db.prepare('SELECT id FROM m_constraint_categories WHERE name = ?').get(name); return result ? result.id : null; } // ============================================================================ // Configuration Management // ============================================================================ /** * Get configuration value from m_config table * * @param db - Database instance * @param key - Config key * @returns Config value as string or null if not found */ export function getConfigValue(db, key) { const result = db.prepare('SELECT value FROM m_config WHERE key = ?').get(key); return result ? result.value : null; } /** * Set configuration value in m_config table * * @param db - Database instance * @param key - Config key * @param value - Config value (will be converted to string) */ export function setConfigValue(db, key, value) { const stringValue = String(value); db.prepare('INSERT OR REPLACE INTO m_config (key, value) VALUES (?, ?)').run(key, stringValue); } /** * Get configuration value as boolean * * @param db - Database instance * @param key - Config key * @param defaultValue - Default value if key not found * @returns Boolean value */ export function getConfigBool(db, key, defaultValue = false) { const value = getConfigValue(db, key); if (value === null) return defaultValue; return value === '1' || value.toLowerCase() === 'true'; } /** * Get configuration value as integer * * @param db - Database instance * @param key - Config key * @param defaultValue - Default value if key not found * @returns Integer value */ export function getConfigInt(db, key, defaultValue = 0) { const value = getConfigValue(db, key); if (value === null) return defaultValue; const parsed = parseInt(value, 10); return isNaN(parsed) ? defaultValue : parsed; } /** * Get all configuration as an object * * @param db - Database instance * @returns Object with all m_config key-value pairs */ export function getAllConfig(db) { const rows = db.prepare('SELECT key, value FROM m_config').all(); const config = {}; for (const row of rows) { config[row.key] = row.value; } return config; } // ============================================================================ // Transaction Helpers // ============================================================================ /** * Execute a function within a transaction * Automatically handles commit/rollback * * @param db - Database instance * @param fn - Function to execute in transaction * @returns Result from function */ export function transaction(db, fn) { db.exec('BEGIN TRANSACTION'); try { const result = fn(); db.exec('COMMIT'); return result; } catch (error) { db.exec('ROLLBACK'); throw error; } } //# sourceMappingURL=database.js.map