UNPKG

devcontext

Version:

DevContext is a cutting-edge Model Context Protocol (MCP) server designed to provide developers with continuous, project-centric context awareness.

691 lines (614 loc) 26.2 kB
/** * Database client module for TursoDB connections * Manages the connection to the TursoDB database and provides query utilities */ import { createClient } from "@libsql/client"; import { TURSO_DATABASE_URL, TURSO_AUTH_TOKEN } from "./config.js"; import { logMessage } from "./utils/logger.js"; // Module-scoped singleton instance let dbClient = null; /** * Initialize and return a TursoDB client instance (singleton) * @returns {Object} TursoDB client instance * @throws {Error} If database URL or auth token is missing */ export const getDbClient = () => { // Check if we're in MCP mode - never log in MCP mode const inMcpMode = process.env.MCP_MODE === "true"; if (dbClient) { return dbClient; } if (!TURSO_DATABASE_URL) { const errorMsg = "TURSO_DATABASE_URL is not defined in environment variables"; if (inMcpMode) { // In MCP mode, don't throw with a message that could be logged throw new Error(); } else { throw new Error(errorMsg); } } if (!TURSO_AUTH_TOKEN) { const errorMsg = "TURSO_AUTH_TOKEN is not defined in environment variables"; if (inMcpMode) { // In MCP mode, don't throw with a message that could be logged throw new Error(); } else { throw new Error(errorMsg); } } dbClient = createClient({ url: TURSO_DATABASE_URL, authToken: TURSO_AUTH_TOKEN, }); // Only log in non-MCP mode if (!inMcpMode) { logMessage("info", "Database client created", { url: TURSO_DATABASE_URL.substring(0, 20) + "...", // Don't log full URL for security }); } return dbClient; }; /** * Test the database connection by executing a simple query * @param {Object} client - TursoDB client instance from getDbClient() * @returns {Promise<boolean>} True if connection is successful * @throws {Error} If connection fails */ export const testDbConnection = async (client = null) => { // Check if we're in MCP mode - never log in MCP mode const inMcpMode = process.env.MCP_MODE === "true"; try { // Maximum number of retry attempts const maxRetries = 3; let retryCount = 0; while (retryCount < maxRetries) { try { const dbClient = client || getDbClient(); await dbClient.execute("SELECT 1"); return true; } catch (connError) { retryCount++; // If we've reached max retries, handle the error if (retryCount >= maxRetries) { // In MCP mode, just return false instead of throwing if (inMcpMode) { return false; } throw new Error( `Database connection test failed: ${connError.message}` ); } // Wait before retrying (exponential backoff) const waitTime = Math.pow(2, retryCount) * 100; await new Promise((resolve) => setTimeout(resolve, waitTime)); // Reset client for retry if we're using the singleton if (!client) { dbClient = null; } } } return true; // Should never reach here but just in case } catch (error) { // In MCP mode, just return false instead of throwing if (inMcpMode) { return false; } throw new Error(`Database connection test failed: ${error.message}`); } }; /** * Execute a SQL query with optional parameters * @param {string} sqlQuery - SQL query to execute * @param {Array} [args=[]] - Optional array of query parameters for parameterized queries * @returns {Promise<Object>} Query result * @throws {Error} If query execution fails */ export const executeQuery = async (sqlQuery, args = []) => { // Strict MCP mode check to prevent any possible logging const inMcpMode = process.env.MCP_MODE === "true"; try { // Only log if not in MCP mode - double check to be absolutely sure if (!inMcpMode) { // Log the query for debugging logMessage("debug", "DB - EXECUTING QUERY", { sql: sqlQuery.substring(0, 150) + (sqlQuery.length > 150 ? "..." : ""), args: args.length > 0 ? JSON.stringify(args.slice(0, 3)) + (args.length > 3 ? "..." : "") : "[]", }); } // Maximum number of retry attempts const maxRetries = 3; let retryCount = 0; let result; while (retryCount < maxRetries) { try { const client = getDbClient(); result = await client.execute({ sql: sqlQuery, args: args, }); break; // If query succeeds, exit the retry loop } catch (queryError) { retryCount++; // Check if this is a connection error that can be retried const isConnectionError = queryError.message?.includes("connection") || queryError.message?.includes("timeout") || queryError.message?.includes("closed"); // If we've reached max retries or it's not a connection error, rethrow if (retryCount >= maxRetries || !isConnectionError) { throw queryError; } // Wait before retrying (exponential backoff) const waitTime = Math.pow(2, retryCount) * 100; await new Promise((resolve) => setTimeout(resolve, waitTime)); // Reset client for retry dbClient = null; } } // Only log if not in MCP mode - double check to be absolutely sure if (!inMcpMode) { // Log the result for debugging logMessage("debug", "DB - QUERY RESULT", { rowCount: result.rows?.length || 0, rowsPreview: result.rows?.length > 0 ? JSON.stringify(result.rows[0]).substring(0, 100) + "..." : "No rows", affectedRows: result.rowsAffected || 0, }); } return result; } catch (error) { // Only log if not in MCP mode - double check to be absolutely sure if (!inMcpMode) { logMessage("error", "DB - QUERY ERROR", { message: error.message, query: sqlQuery.substring(0, 150), args: args.length > 0 ? JSON.stringify(args.slice(0, 3)) : "[]", }); } // In MCP mode, throw a plain error without any message to prevent logging if (inMcpMode) { throw new Error(); } else { throw new Error( `Query execution failed: ${error.message}\nQuery: ${sqlQuery}` ); } } }; /** * Migrate the project_patterns table to add the language column if it doesn't exist * @returns {Promise<void>} */ async function migrateProjectPatternsTable() { // Silent operation in MCP mode - triple check to be safe const inMcpMode = process.env.MCP_MODE === "true"; try { // First check if the table exists - use a more robust approach with error handling let tableExists = false; try { const tableExistsQuery = await executeQuery(` SELECT name FROM sqlite_master WHERE type='table' AND name='project_patterns' `); tableExists = tableExistsQuery && tableExistsQuery.rows && tableExistsQuery.rows.length > 0; } catch (tableCheckError) { // If error checking table existence, assume table doesn't exist if (!inMcpMode) { logMessage( "warn", `Error checking table existence: ${tableCheckError.message}` ); } return; // Exit early, nothing to migrate } // If table doesn't exist, nothing to migrate if (!tableExists) { if (!inMcpMode) { logMessage( "info", "project_patterns table doesn't exist yet, skipping migration" ); } return; } // Check if the language column exists directly with a query // This avoids relying on the columnExists function that might get minified let hasLanguageColumn = false; try { const columnCheckQuery = await executeQuery(` PRAGMA table_info(project_patterns) `); if (columnCheckQuery && columnCheckQuery.rows) { // Check each row for the language column for (const row of columnCheckQuery.rows) { if (row && row.name === "language") { hasLanguageColumn = true; break; } } } } catch (columnCheckError) { // If error checking column existence, be safe and skip migration if (!inMcpMode) { logMessage( "warn", `Error checking column existence: ${columnCheckError.message}` ); } return; // Exit early, skip migration on error } // If language column already exists, nothing to do if (hasLanguageColumn) { if (!inMcpMode) { logMessage( "debug", "Language column already exists in project_patterns table" ); } return; } // Only reach here if table exists and column doesn't exist if (!inMcpMode) { logMessage("info", "Adding language column to project_patterns table"); } try { // Add the language column to the table - wrap in try/catch await executeQuery( "ALTER TABLE project_patterns ADD COLUMN language TEXT" ); if (!inMcpMode) { logMessage( "info", "Successfully added language column to project_patterns table" ); } } catch (alterError) { // Handle column already exists error - SQLite specific if ( alterError.message && alterError.message.includes("duplicate column") ) { if (!inMcpMode) { logMessage("info", "Language column already exists, skipping"); } } else { // For other errors in non-MCP mode, log warning but don't fail if (!inMcpMode) { logMessage( "warn", `Error adding language column: ${alterError.message}` ); } return; // Skip index creation on error } } // Create index for the language column if needed - with separate error handling try { await executeQuery( "CREATE INDEX IF NOT EXISTS idx_project_patterns_language ON project_patterns(language)" ); if (!inMcpMode) { logMessage("info", "Created index for language column"); } } catch (indexError) { // Log index creation error but don't fail - not critical if (!inMcpMode) { logMessage("warn", `Error creating index: ${indexError.message}`); } } } catch (error) { // In MCP mode, don't throw or log errors - just silently fail if (!inMcpMode) { // Log error but don't throw - make migration non-fatal logMessage("warn", `Migration warning: ${error.message}`); } } } /** * Initialize the database schema by creating all required tables and indexes * This function executes all CREATE TABLE, CREATE INDEX and CREATE TRIGGER statements * defined in the project blueprint * @returns {Promise<boolean>} True if schema initialization was successful */ export const initializeDatabaseSchema = async () => { // Check if we're in MCP mode - triple check to prevent any logging const inMcpMode = process.env.MCP_MODE === "true"; try { const client = getDbClient(); let success = true; // First, check if we need to migrate the project_patterns table by adding the language column // Wrap in try/catch and continue regardless of migration outcome try { await migrateProjectPatternsTable(); } catch (migrationError) { // Completely ignore migration errors in MCP mode if (!inMcpMode) { logMessage("warn", `Migration warning: ${migrationError.message}`); } // Continue with schema initialization, migration error is not fatal } // Array of SQL statements to execute sequentially const schemaStatements = [ // ========= CODE ENTITIES ========= `CREATE TABLE IF NOT EXISTS code_entities ( entity_id TEXT PRIMARY KEY, -- UUID file_path TEXT, -- Full path for file entities, or path to file containing the entity entity_type TEXT NOT NULL, -- e.g., 'file', 'function', 'class', 'method', 'variable', 'interface', 'comment_block' name TEXT, -- Name of the function, class, variable etc. start_line INTEGER, end_line INTEGER, content_hash TEXT, -- Hash of the raw content to detect changes raw_content TEXT, summary TEXT, -- AI or rule-based summary language TEXT, -- Programming language parent_entity_id TEXT, -- For hierarchical structure (e.g., function inside a class, class inside a file) last_modified_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, last_accessed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, -- For recency importance_score REAL DEFAULT 1.0, -- For prioritization, can decay custom_metadata TEXT, -- JSON blob for other properties FOREIGN KEY (parent_entity_id) REFERENCES code_entities(entity_id) ON DELETE CASCADE )`, // Indexes for code_entities `CREATE INDEX IF NOT EXISTS idx_code_entities_file_path ON code_entities(file_path)`, `CREATE INDEX IF NOT EXISTS idx_code_entities_type ON code_entities(entity_type)`, `CREATE INDEX IF NOT EXISTS idx_code_entities_name ON code_entities(name)`, `CREATE INDEX IF NOT EXISTS idx_code_entities_last_accessed ON code_entities(last_accessed_at DESC)`, `CREATE INDEX IF NOT EXISTS idx_code_entities_importance ON code_entities(importance_score DESC)`, // ========= ENTITY KEYWORDS ========= `CREATE TABLE IF NOT EXISTS entity_keywords ( keyword_id INTEGER PRIMARY KEY AUTOINCREMENT, entity_id TEXT NOT NULL, keyword TEXT NOT NULL, term_frequency REAL, weight REAL DEFAULT 1.0, keyword_type TEXT, -- e.g., 'identifier', 'comment', 'string_literal', 'n_gram_2', 'n_gram_3' FOREIGN KEY (entity_id) REFERENCES code_entities(entity_id) ON DELETE CASCADE )`, // Indexes for entity_keywords `CREATE INDEX IF NOT EXISTS idx_entity_keywords_keyword ON entity_keywords(keyword)`, `CREATE INDEX IF NOT EXISTS idx_entity_keywords_entity ON entity_keywords(entity_id)`, `CREATE UNIQUE INDEX IF NOT EXISTS idx_entity_keywords_entity_keyword_type ON entity_keywords(entity_id, keyword, keyword_type)`, // ========= FULL-TEXT SEARCH ========= `CREATE VIRTUAL TABLE IF NOT EXISTS code_entities_fts USING fts5( entity_id UNINDEXED, name, searchable_content )`, // Triggers to keep FTS table in sync with code_entities `CREATE TRIGGER IF NOT EXISTS code_entities_ai AFTER INSERT ON code_entities BEGIN INSERT INTO code_entities_fts (entity_id, name, searchable_content) VALUES (new.entity_id, new.name, new.raw_content || ' ' || COALESCE(new.summary, '')); END`, `CREATE TRIGGER IF NOT EXISTS code_entities_ad AFTER DELETE ON code_entities BEGIN DELETE FROM code_entities_fts WHERE entity_id = old.entity_id; END`, `CREATE TRIGGER IF NOT EXISTS code_entities_au AFTER UPDATE ON code_entities BEGIN UPDATE code_entities_fts SET name = new.name, searchable_content = new.raw_content || ' ' || COALESCE(new.summary, '') WHERE entity_id = old.entity_id; END`, // ========= CODE RELATIONSHIPS ========= `CREATE TABLE IF NOT EXISTS code_relationships ( relationship_id TEXT PRIMARY KEY, -- UUID source_entity_id TEXT NOT NULL, target_entity_id TEXT NOT NULL, relationship_type TEXT NOT NULL, weight REAL DEFAULT 1.0, metadata TEXT, -- JSON blob created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (source_entity_id) REFERENCES code_entities(entity_id) ON DELETE CASCADE, FOREIGN KEY (target_entity_id) REFERENCES code_entities(entity_id) ON DELETE CASCADE )`, // Indexes for code_relationships `CREATE INDEX IF NOT EXISTS idx_code_relationships_source ON code_relationships(source_entity_id, relationship_type)`, `CREATE INDEX IF NOT EXISTS idx_code_relationships_target ON code_relationships(target_entity_id, relationship_type)`, `CREATE UNIQUE INDEX IF NOT EXISTS idx_code_relationships_unique ON code_relationships(source_entity_id, target_entity_id, relationship_type)`, // ========= CONVERSATION HISTORY ========= `CREATE TABLE IF NOT EXISTS conversation_history ( message_id TEXT PRIMARY KEY, -- UUID conversation_id TEXT NOT NULL, role TEXT NOT NULL, -- 'user', 'assistant', 'system' content TEXT NOT NULL, timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP, related_context_entity_ids TEXT, -- JSON array of entity_ids summary TEXT, user_intent TEXT, topic_segment_id TEXT, -- Reference to topic segment semantic_markers TEXT, -- JSON array of semantic markers found in message sentiment_indicators TEXT -- JSON structure for sentiment analysis )`, // Indexes for conversation_history `CREATE INDEX IF NOT EXISTS idx_conversation_history_conversation_ts ON conversation_history(conversation_id, timestamp DESC)`, `CREATE INDEX IF NOT EXISTS idx_conversation_history_topic ON conversation_history(topic_segment_id)`, // ========= CONVERSATION TOPICS ========= `CREATE TABLE IF NOT EXISTS conversation_topics ( topic_id TEXT PRIMARY KEY, -- UUID conversation_id TEXT NOT NULL, topic_name TEXT NOT NULL, description TEXT, start_message_id TEXT NOT NULL, end_message_id TEXT, -- NULL if ongoing start_timestamp TIMESTAMP NOT NULL, end_timestamp TIMESTAMP, -- NULL if ongoing primary_entities TEXT, -- JSON array of entity_ids keywords TEXT, -- JSON array of keywords summary TEXT, parent_topic_id TEXT, -- For hierarchical topic structure FOREIGN KEY (start_message_id) REFERENCES conversation_history(message_id) ON DELETE CASCADE, FOREIGN KEY (parent_topic_id) REFERENCES conversation_topics(topic_id) ON DELETE SET NULL )`, // Indexes for conversation_topics `CREATE INDEX IF NOT EXISTS idx_conversation_topics_conversation ON conversation_topics(conversation_id)`, `CREATE INDEX IF NOT EXISTS idx_conversation_topics_timestamps ON conversation_topics(start_timestamp, end_timestamp)`, // ========= CONVERSATION PURPOSES ========= `CREATE TABLE IF NOT EXISTS conversation_purposes ( purpose_id TEXT PRIMARY KEY, -- UUID conversation_id TEXT NOT NULL, purpose_type TEXT NOT NULL, -- 'debugging', 'feature_planning', 'code_review', etc. confidence REAL DEFAULT 0.0, -- Confidence score (0.0 to 1.0) start_timestamp TEXT NOT NULL, -- ISO timestamp end_timestamp TEXT, -- ISO timestamp, NULL if still active metadata TEXT -- Additional metadata in JSON format )`, // Indexes for conversation_purposes `CREATE INDEX IF NOT EXISTS idx_conversation_purposes_conversation ON conversation_purposes(conversation_id)`, `CREATE INDEX IF NOT EXISTS idx_conversation_purposes_type ON conversation_purposes(purpose_type, confidence DESC)`, // ========= TIMELINE EVENTS ========= `CREATE TABLE IF NOT EXISTS timeline_events ( event_id TEXT PRIMARY KEY, -- UUID event_type TEXT NOT NULL, timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP, data TEXT, -- JSON blob associated_entity_ids TEXT, -- JSON array of code_entity_ids conversation_id TEXT -- If related to a specific conversation )`, // Indexes for timeline_events `CREATE INDEX IF NOT EXISTS idx_timeline_events_ts ON timeline_events(timestamp DESC)`, `CREATE INDEX IF NOT EXISTS idx_timeline_events_type ON timeline_events(event_type)`, // ========= CONTEXT SNAPSHOTS ========= `CREATE TABLE IF NOT EXISTS context_snapshots ( snapshot_id TEXT PRIMARY KEY, -- UUID name TEXT, description TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, timeline_event_id TEXT, snapshot_data TEXT NOT NULL, -- JSON blob FOREIGN KEY (timeline_event_id) REFERENCES timeline_events(event_id) ON DELETE SET NULL )`, // Indexes for context_snapshots `CREATE INDEX IF NOT EXISTS idx_context_snapshots_name ON context_snapshots(name)`, // ========= FOCUS AREAS ========= `CREATE TABLE IF NOT EXISTS focus_areas ( focus_id TEXT PRIMARY KEY, -- UUID focus_type TEXT NOT NULL, identifier TEXT, description TEXT, related_entity_ids TEXT, -- JSON array of code_entity_ids keywords TEXT, -- JSON array of defining keywords last_activated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, is_active BOOLEAN DEFAULT FALSE )`, // Indexes for focus_areas `CREATE INDEX IF NOT EXISTS idx_focus_areas_active ON focus_areas(is_active, last_activated_at DESC)`, // ========= PROJECT PATTERNS ========= `CREATE TABLE IF NOT EXISTS project_patterns ( pattern_id TEXT PRIMARY KEY, -- UUID pattern_type TEXT NOT NULL, name TEXT, description TEXT, representation TEXT NOT NULL, -- JSON or textual detection_rules TEXT, frequency INTEGER DEFAULT 0, last_detected_at TIMESTAMP, utility_score REAL DEFAULT 0.0, confidence_score REAL DEFAULT 0.5, -- confidence in pattern validity reinforcement_count INTEGER DEFAULT 1, -- times pattern was reinforced is_global BOOLEAN DEFAULT FALSE, -- indicates if promoted to global status session_origin_id TEXT, -- originating session if any language TEXT, -- NEW COLUMN: programming language the pattern applies to created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP )`, // Indexes for project_patterns `CREATE INDEX IF NOT EXISTS idx_project_patterns_type ON project_patterns(pattern_type)`, `CREATE INDEX IF NOT EXISTS idx_project_patterns_global ON project_patterns(is_global, confidence_score DESC)`, `CREATE INDEX IF NOT EXISTS idx_project_patterns_utility ON project_patterns(utility_score DESC)`, // ========= PATTERN OBSERVATIONS ========= `CREATE TABLE IF NOT EXISTS pattern_observations ( observation_id TEXT PRIMARY KEY, -- UUID pattern_id TEXT NOT NULL, conversation_id TEXT, context_entities TEXT, -- JSON array of entity_ids observation_type TEXT NOT NULL, -- 'usage', 'confirmation', 'rejection' observation_data TEXT, -- JSON with details timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (pattern_id) REFERENCES project_patterns(pattern_id) ON DELETE CASCADE )`, // Indexes for pattern_observations `CREATE INDEX IF NOT EXISTS idx_pattern_observations_pattern ON pattern_observations(pattern_id)`, `CREATE INDEX IF NOT EXISTS idx_pattern_observations_type_ts ON pattern_observations(observation_type, timestamp DESC)`, // ========= SYSTEM LOGS ========= `CREATE TABLE IF NOT EXISTS system_logs ( log_id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP, level TEXT NOT NULL, -- 'INFO', 'WARN', 'ERROR', 'DEBUG' message TEXT NOT NULL, data TEXT -- Optional JSON blob )`, // Indexes for system_logs `CREATE INDEX IF NOT EXISTS idx_system_logs_timestamp_level ON system_logs(timestamp DESC, level)`, // Make sure knowledge_items table exists `CREATE TABLE IF NOT EXISTS knowledge_items ( item_id TEXT PRIMARY KEY, -- UUID item_type TEXT NOT NULL, -- Type of knowledge item (concept, code_pattern, domain_term, etc.) name TEXT NOT NULL, -- Display name of the knowledge item content TEXT, -- JSON or raw content depending on type metadata TEXT, -- Additional metadata as JSON confidence_score REAL DEFAULT 0.5, -- Confidence score created_at TEXT NOT NULL, -- Creation timestamp updated_at TEXT NOT NULL, -- Last update timestamp conversation_id TEXT -- Source conversation ID )`, // Indexes for knowledge_items `CREATE INDEX IF NOT EXISTS idx_knowledge_items_type ON knowledge_items(item_type)`, `CREATE INDEX IF NOT EXISTS idx_knowledge_items_conversation ON knowledge_items(conversation_id)`, `CREATE INDEX IF NOT EXISTS idx_knowledge_items_confidence ON knowledge_items(confidence_score DESC)`, // Make sure context_states table exists `CREATE TABLE IF NOT EXISTS context_states ( state_id TEXT PRIMARY KEY, -- UUID milestone_id TEXT, conversation_id TEXT, topic_id TEXT, state_type TEXT NOT NULL, -- Type of context state state_data TEXT, -- JSON data representing the state created_at TEXT NOT NULL, -- Creation timestamp metadata TEXT -- Additional metadata as JSON )`, // Indexes for context_states `CREATE INDEX IF NOT EXISTS idx_context_states_milestone ON context_states(milestone_id)`, `CREATE INDEX IF NOT EXISTS idx_context_states_conversation ON context_states(conversation_id)`, ]; // Execute each statement in sequence for (const statement of schemaStatements) { try { await client.execute(statement); } catch (error) { // Only log error in non-MCP mode if (!inMcpMode) { logMessage( "error", `Error executing schema statement: ${error.message}` ); logMessage( "error", `Failed statement: ${statement.substring(0, 100)}...` ); } success = false; } } return success; } catch (error) { if (inMcpMode) { // In MCP mode, return false silently instead of throwing return false; } throw new Error(`Database schema initialization failed: ${error.message}`); } }; export default { getDbClient, testDbConnection, executeQuery, initializeDatabaseSchema, };