Skip to main content
Glama
database-utils.js•43.3 kB
import sqlite3 from 'sqlite3'; import path from 'path'; import { mkdirSync, existsSync, readFileSync } from 'fs'; // NEW: Connection pool and timeout improvements let connectionPool = []; const MAX_POOL_SIZE = 5; const WARMUP_TIMEOUT = 30000; // 30 seconds const RETRY_ATTEMPTS = 3; const RETRY_DELAY_BASE = 1000; // 1 second base delay // Global warmer instance - shared across all database instances let globalWarmer = null; let globalDB = null; let globalHeartbeat = null; // Track active heartbeat let lastHeartbeatTime = null; // Track when last heartbeat occurred const HEARTBEAT_TIMEOUT = 60000; // 60 seconds - heartbeat must be within this time // NEW: Connection pool management class ConnectionPool { constructor(dbPath, maxSize = MAX_POOL_SIZE) { this.dbPath = dbPath; this.maxSize = maxSize; this.connections = []; this.waitingQueue = []; } async getConnection() { return new Promise((resolve, reject) => { // Try to get existing connection if (this.connections.length > 0) { const connection = this.connections.pop(); resolve(connection); return; } // Create new connection if under limit if (this.connections.length < this.maxSize) { try { const db = new sqlite3.Database(this.dbPath); resolve(db); } catch (error) { reject(error); } return; } // Add to waiting queue this.waitingQueue.push({ resolve, reject }); }); } releaseConnection(db) { // Check if someone is waiting if (this.waitingQueue.length > 0) { const { resolve } = this.waitingQueue.shift(); resolve(db); } else { this.connections.push(db); } } closeAll() { [...this.connections, ...this.waitingQueue.map(w => w.connection)].forEach(db => { if (db && typeof db.close === 'function') { db.close(); } }); this.connections = []; this.waitingQueue = []; } } // NEW: Retry utility with exponential backoff async function retryOperation(operation, maxAttempts = RETRY_ATTEMPTS, baseDelay = RETRY_DELAY_BASE) { let lastError; for (let attempt = 1; attempt <= maxAttempts; attempt++) { try { return await operation(); } catch (error) { lastError = error; console.warn(`āš ļø Attempt ${attempt}/${maxAttempts} failed: ${error.message}`); if (attempt < maxAttempts) { const delay = baseDelay * Math.pow(2, attempt - 1); // Exponential backoff console.log(`šŸ”„ Retrying in ${delay}ms...`); await new Promise(resolve => setTimeout(resolve, delay)); } } } throw lastError; } // Embedded database warmer functionality - IMPROVED class EmbeddedDatabaseWarmer { constructor() { this.warmupQueries = [ 'faith', 'love', 'prayer', 'hope', 'charity' ]; this.isWarm = false; this.lastWarmup = null; this.warmupInterval = 5 * 60 * 1000; // 5 minutes this.warmupPromise = null; } async initialize(db) { console.log('šŸ”„ Initializing embedded database warmer...'); // Prevent multiple concurrent warmups if (this.warmupPromise) { return this.warmupPromise; } this.warmupPromise = this.performWarmupWithTimeout(db); try { await this.warmupPromise; // Set up periodic warmup to keep database hot setInterval(() => { this.maintenanceWarmup(db); }, this.warmupInterval); console.log('āœ… Embedded database warmer initialized and ready'); } catch (error) { this.warmupPromise = null; // Reset on failure throw error; } return this.warmupPromise; } async performWarmupWithTimeout(db) { return new Promise(async (resolve, reject) => { const timeoutId = setTimeout(() => { reject(new Error(`Database warmup timeout after ${WARMUP_TIMEOUT}ms`)); }, WARMUP_TIMEOUT); try { const result = await this.warmupDatabase(db); clearTimeout(timeoutId); resolve(result); } catch (error) { clearTimeout(timeoutId); reject(error); } }); } async warmupDatabase(db) { console.log('šŸ”„ Warming up database (cold start)...'); const startTime = Date.now(); try { // Warmup with common search queries for (const query of this.warmupQueries) { await this.performWarmupQuery(db, query); } // Additional warmup: Get stats to load book data await this.getStats(db); const warmupTime = Date.now() - startTime; console.log(`āœ… Database warmup completed in ${warmupTime}ms`); this.isWarm = true; this.lastWarmup = new Date(); return warmupTime; } catch (error) { console.error('āŒ Database warmup failed:', error.message); throw error; } } async performWarmupQuery(db, query) { return new Promise((resolve, reject) => { // Try FTS first, fallback to LIKE if FTS table doesn't exist const ftsSearchStmt = db.prepare(` SELECT fts.para_id, fts.book_id, p.content_plain as content, p.element_type, b.code as pub_code, b.title as pub_name, b.author as book_author, b.pub_year FROM paragraphs_fts fts JOIN paragraphs p ON fts.para_id = p.para_id JOIN books b ON fts.book_id = b.book_id WHERE paragraphs_fts MATCH ? AND (b.author = 'Ellen Gould White' OR b.author = 'Ellen G. White') AND p.element_type = 'p' ORDER BY rank LIMIT 5 `); ftsSearchStmt.all([query], (err, results) => { ftsSearchStmt.finalize(); if (err) { // FTS table doesn't exist, try simple LIKE query instead const likeStmt = db.prepare(` SELECT p.para_id, p.book_id, p.content_plain as content, p.element_type, b.code as pub_code, b.title as pub_name, b.author as book_author, b.pub_year FROM paragraphs p JOIN books b ON p.book_id = b.book_id WHERE p.content_plain LIKE ? AND (b.author = 'Ellen Gould White' OR b.author = 'Ellen G. White') AND p.element_type = 'p' ORDER BY RANDOM() LIMIT 5 `); likeStmt.all([`%${query}%`], (likeErr, likeResults) => { likeStmt.finalize(); // Don't fail warmup for individual queries resolve(); }); return; } resolve(); }); }); } async getStats(db) { return new Promise((resolve, reject) => { db.get('SELECT COUNT(*) as count FROM books', [], (err, row) => { if (err) reject(err); else resolve(row?.count || 0); }); }); } async maintenanceWarmup(db) { if (!this.isWarm) { console.log('šŸ”„ Performing maintenance warmup...'); return await this.warmupDatabase(db); } // Light maintenance - just one quick query try { const startTime = Date.now(); await this.performWarmupQuery(db, 'faith'); const duration = Date.now() - startTime; console.log(`šŸ”„ Maintenance warmup: ${duration}ms`); this.lastWarmup = new Date(); } catch (error) { console.error('āŒ Maintenance warmup failed:', error.message); this.isWarm = false; } } getStatus() { return { isWarm: this.isWarm, lastWarmup: this.lastWarmup, warmupInterval: this.warmupInterval }; } } // NEW: Persistent heartbeat storage class PersistentHeartbeat { constructor() { this.heartbeatFile = path.join(process.cwd(), 'apps', 'local-server', 'data', 'heartbeat.json'); } async saveHeartbeat(data) { try { const heartbeatData = { ...data, timestamp: new Date().toISOString(), pid: process.pid }; const fs = await import('fs'); await fs.promises.writeFile(this.heartbeatFile, JSON.stringify(heartbeatData, null, 2)); return true; } catch (error) { console.error('āŒ Failed to save heartbeat:', error.message); return false; } } async loadHeartbeat() { try { const fs = await import('fs'); const data = await fs.promises.readFile(this.heartbeatFile, 'utf8'); const heartbeat = JSON.parse(data); // Check if heartbeat is still valid (within timeout) const lastTime = new Date(heartbeat.timestamp); const now = new Date(); const age = now - lastTime; if (age < HEARTBEAT_TIMEOUT) { return { ...heartbeat, isValid: true, age: age }; } return { isValid: false }; } catch (error) { return { isValid: false }; } } async clearHeartbeat() { try { const fs = await import('fs'); await fs.promises.unlink(this.heartbeatFile); return true; } catch (error) { // File might not exist, that's OK return true; } } } // Global instances let persistentHeartbeat = null; let connectionPoolInstance = null; export class SimpleEGWDatabase { constructor(dbPath = './data/egw-writings.db', requireWarming = true) { console.error('šŸ“Š Initializing database connection...'); // Initialize persistent heartbeat if (!persistentHeartbeat) { persistentHeartbeat = new PersistentHeartbeat(); } // Initialize connection pool if (!connectionPoolInstance && dbPath) { connectionPoolInstance = new ConnectionPool(dbPath); } // IMPROVED: Initialize warmer with timeout and retry if (requireWarming && !globalWarmer) { console.error('šŸ”„ Creating new database connection for warming...'); // Ensure database directory exists const dbDir = path.dirname(dbPath); if (!existsSync(dbDir)) { mkdirSync(dbDir, { recursive: true }); } // Create fresh connection for warming const freshDB = new sqlite3.Database(dbPath); // Initialize warmer with timeout and retry globalWarmer = new EmbeddedDatabaseWarmer(); retryOperation(() => globalWarmer.initialize(freshDB)) .then(() => { globalDB = freshDB; console.error('āœ… Database warmed and ready for shared use'); }) .catch(error => { console.error('āŒ Failed to warm database after retries:', error.message); // Don't throw error, allow system to work with cold database globalDB = freshDB; }); // Store warm connection for this instance this.db = freshDB; } else if (requireWarming && globalWarmer) { // Use existing warm connection this.db = globalDB; console.error('āœ… Using existing warm database connection'); } else { // Create fresh connection without warming (for testing only) const dbDir = path.dirname(dbPath); if (!existsSync(dbDir)) { mkdirSync(dbDir, { recursive: true }); } this.db = new sqlite3.Database(dbPath); console.error('āœ… Database connection ready (no warming)'); } } // IMPROVED: Heartbeat validation with persistent storage and retry async validateHeartbeat(toolName = 'unknown-tool') { try { // First check persistent heartbeat const heartbeatData = await persistentHeartbeat.loadHeartbeat(); if (heartbeatData.isValid) { console.log(`šŸ’“ Persistent heartbeat valid for "${toolName}" (${Math.round(heartbeatData.age / 1000)}s old)`); lastHeartbeatTime = new Date(heartbeatData.timestamp); return true; } // No valid persistent heartbeat, try auto-start console.log(`šŸš‘ Tool "${toolName}" needs heartbeat - attempting auto-start...`); return await retryOperation(async () => { const { AutoHeartbeatStarter } = await import('./auto-heartbeat-starter.cjs'); const autoStartResult = await AutoHeartbeatStarter.validateWithAutoStart(toolName); if (autoStartResult.success) { // Save persistent heartbeat await persistentHeartbeat.saveHeartbeat({ toolName, autoStarted: autoStartResult.autoStarted, pid: autoStartResult.pid }); lastHeartbeatTime = new Date(); return true; } else { throw new Error(`Auto-start failed: ${autoStartResult.message}`); } }); } catch (error) { console.error('āŒ Heartbeat validation failed:', error.message); // Fallback: Save temporary heartbeat to allow tools to work await persistentHeartbeat.saveHeartbeat({ toolName, fallback: true, error: error.message }); lastHeartbeatTime = new Date(); return true; // Allow tools to work even with heartbeat issues } } // HEARTBEAT REGISTRATION: Register active heartbeat static registerHeartbeat(heartbeatInstance) { globalHeartbeat = heartbeatInstance; lastHeartbeatTime = new Date(); // Save to persistent storage if (persistentHeartbeat) { persistentHeartbeat.saveHeartbeat({ registered: true, pid: process.pid }); } console.log('šŸ’“ Heartbeat registered - database tools now active'); } // NEW: Update heartbeat time (for external heartbeat updates) static async updateHeartbeatTime() { lastHeartbeatTime = new Date(); if (persistentHeartbeat) { await persistentHeartbeat.saveHeartbeat({ updated: true, pid: process.pid }); } } // NEW: Health check for system monitoring static async performHealthCheck() { const health = { timestamp: new Date().toISOString(), database: { isWarm: globalWarmer ? globalWarmer.isWarm : false, lastWarmup: globalWarmer ? globalWarmer.lastWarmup : null }, heartbeat: { isActive: !!globalHeartbeat, lastHeartbeat: lastHeartbeatTime, persistent: persistentHeartbeat ? await persistentHeartbeat.loadHeartbeat() : null }, connectionPool: { size: connectionPoolInstance ? connectionPoolInstance.connections.length : 0, waiting: connectionPoolInstance ? connectionPoolInstance.waitingQueue.length : 0 } }; console.error('šŸ„ System health check:', JSON.stringify(health, null, 2)); return health; } // Search database with retry async search(query, limit = 20, offset = 0, toolName = 'search-function') { await this.validateHeartbeat(toolName); return retryOperation(async () => { return new Promise((resolve, reject) => { const stmt = this.db.prepare(` SELECT p.para_id, p.book_id, b.code as pub_code, b.title as pub_name, b.author, p.refcode_long, p.refcode_short, b.pub_year, p.content_plain as snippet, b.folder_color_group as "group" FROM paragraphs p JOIN books b ON p.book_id = b.book_id WHERE p.content_plain LIKE ? ORDER BY p.puborder LIMIT ? OFFSET ? `); const searchTerm = `%${query}%`; stmt.all([searchTerm, limit, offset], (err, rows) => { if (err) { console.error('āŒ Search error:', err.message); reject(err); return; } const results = rows.map((row, index) => ({ index: offset + index, para_id: row.para_id, book_id: row.book_id, pub_code: row.pub_code, pub_name: row.pub_name, author: row.author, refcode_long: row.refcode_long, refcode_short: row.refcode_short, pub_year: row.pub_year, snippet: row.snippet, weight: 1, group: row.group })); console.error(`šŸ” Found ${results.length} results for query: "${query}"`); resolve(results); }); stmt.finalize(); }); }); } // Get book information with retry async getBook(bookId, toolName = 'getBook-function') { await this.validateHeartbeat(toolName); return retryOperation(async () => { return new Promise((resolve, reject) => { this.db.get('SELECT * FROM books WHERE book_id = ?', [bookId], (err, row) => { if (err) { console.error('āŒ Get book error:', err.message); reject(err); return; } console.error(`šŸ“š Retrieved book: ${row?.title || 'Unknown'}`); resolve(row); }); }); }); } // Get paragraphs from a book with retry async getParagraphs(bookId, limit = 3, offset = 0, toolName = 'getParagraphs-function') { await this.validateHeartbeat(toolName); return retryOperation(async () => { return new Promise((resolve, reject) => { let query = 'SELECT * FROM paragraphs WHERE book_id = ? ORDER BY puborder'; const params = [bookId]; if (limit) { query += ' LIMIT ?'; params.push(limit); if (offset) { query += ' OFFSET ?'; params.push(offset); } } this.db.all(query, params, (err, rows) => { if (err) { console.error('āŒ Get paragraphs error:', err.message); reject(err); return; } console.error(`šŸ“– Retrieved ${rows.length} paragraphs from book ${bookId}`); resolve(rows); }); }); }); } // List books with retry async getBooks(limit = 10, toolName = 'getBooks-function') { await this.validateHeartbeat(toolName); return retryOperation(async () => { return new Promise((resolve, reject) => { let query = 'SELECT * FROM books ORDER BY title'; const params = []; if (limit) { query += ' LIMIT ?'; params.push(limit); } this.db.all(query, params, (err, rows) => { if (err) { console.error('āŒ Get books error:', err.message); reject(err); return; } console.error(`šŸ“š Retrieved ${rows.length} books`); resolve(rows); }); }); }); } // Get database statistics with retry async getStats(toolName = 'getStats-function') { await this.validateHeartbeat(toolName); // First try to get enhanced stats from parsing report const reportStats = await this.getEnhancedStatsFromReport(); if (reportStats) { return reportStats; } // Fallback to database-only stats return retryOperation(async () => { return new Promise((resolve, reject) => { Promise.all([ // Get total books count new Promise((resolveBooks, rejectBooks) => { this.db.get('SELECT COUNT(*) as count FROM books', [], (err, row) => { if (err) rejectBooks(err); else resolveBooks(row.count); }); }), // Get total paragraphs count new Promise((resolveParas, rejectParas) => { this.db.get('SELECT COUNT(*) as count FROM paragraphs', [], (err, row) => { if (err) rejectParas(err); else resolveParas(row.count); }); }), // Get languages count new Promise((resolveLangs, rejectLangs) => { this.db.get('SELECT COUNT(*) as count FROM languages', [], (err, row) => { if (err) rejectLangs(err); else resolveLangs(row.count); }); }), // Get completed parsed books (with content) new Promise((resolveDownloaded, rejectDownloaded) => { this.db.get('SELECT COUNT(*) as count FROM books WHERE downloaded_at IS NOT NULL', [], (err, row) => { if (err) rejectDownloaded(err); else resolveDownloaded(row.count); }); }), // Get detailed completion stats by book new Promise((resolveDetails, rejectDetails) => { this.db.all(` SELECT b.book_id, b.code, b.title, b.author, b.downloaded_at, COUNT(p.para_id) as paragraph_count FROM books b LEFT JOIN paragraphs p ON b.book_id = p.book_id GROUP BY b.book_id, b.code, b.title, b.author, b.downloaded_at ORDER BY b.downloaded_at DESC, paragraph_count DESC `, [], (err, rows) => { if (err) rejectDetails(err); else resolveDetails(rows); }); }), // Get completion percentage by category new Promise((resolveCategories, rejectCategories) => { this.db.all(` SELECT category, subcategory, COUNT(*) as total_books, SUM(CASE WHEN downloaded_at IS NOT NULL THEN 1 ELSE 0 END) as completed_books, ROUND( (SUM(CASE WHEN downloaded_at IS NOT NULL THEN 1 ELSE 0 END) * 100.0 / COUNT(*)), 2 ) as completion_percentage FROM books GROUP BY category, subcategory ORDER BY completion_percentage DESC, category, subcategory `, [], (err, rows) => { if (err) rejectCategories(err); else resolveCategories(rows); }); }) ]).then(([books, paragraphs, languages, downloadedBooks, bookDetails, categoryStats]) => { const stats = { books, paragraphs, languages, downloadedBooks, completion_percentage: ((downloadedBooks / books) * 100).toFixed(1), bookDetails, categoryStats }; console.error('šŸ“Š Enhanced Database stats:', stats); resolve(stats); }).catch(reject); }); }); } // Get enhanced statistics from parsing report file async getEnhancedStatsFromReport() { const reportPath = path.join(process.cwd(), 'apps', 'local-server', 'data', 'parse-report.txt'); try { if (!existsSync(reportPath)) { return null; } const reportContent = readFileSync(reportPath, 'utf8'); const lines = reportContent.split('\n'); const stats = { fromReport: true, reportGenerated: lines.find(line => line.includes('Generated:'))?.split(': ')[1] || 'Unknown', totalBooks: 0, completedParsedBooks: 0, completionPercentage: '0.0', totalParagraphs: 0, languagesSupported: 0, databaseSize: 'Unknown', sessionStarted: 'Unknown', sessionElapsed: 'Unknown', processingRate: 'Unknown', etaForCompletion: 'Unknown', recentlyCompleted: [], failedBooks: [] }; // Enhanced parsing with better labels for (const line of lines) { if (line.includes('Total Books in Database:')) { stats.totalBooks = parseInt(line.split(': ')[1]) || 0; } else if (line.includes('Completed Parsed Books:')) { stats.completedParsedBooks = parseInt(line.split(': ')[1]) || 0; } else if (line.includes('Completion Percentage:')) { const match = line.match(/(\d+\.\d+)%/); stats.completionPercentage = match ? match[1] : '0.0'; } else if (line.includes('Total Paragraphs:')) { stats.totalParagraphs = parseInt(line.split(': ')[1]) || 0; } else if (line.includes('Languages Supported:')) { stats.languagesSupported = parseInt(line.split(': ')[1]) || 0; } else if (line.includes('Database Size:')) { stats.databaseSize = line.split(': ')[1] || 'Unknown'; } else if (line.includes('Session Started:')) { stats.sessionStarted = line.split(': ')[1] || 'Unknown'; } else if (line.includes('Elapsed Time:')) { stats.sessionElapsed = line.split(': ')[1] || 'Unknown'; } else if (line.includes('Processing Rate:')) { stats.processingRate = line.split(': ')[1] || 'Unknown'; } else if (line.includes('ETA for completion:')) { stats.etaForCompletion = line.split(': ')[1] || 'Unknown'; } } // Add clear parsed books summary for logging console.error(`šŸ“š PARSED BOOKS STATUS: ${stats.completedParsedBooks}/${stats.totalBooks} books parsed (${stats.completionPercentage}%)`); console.error(`šŸ“Š DATABASE STATUS: ${stats.totalParagraphs} paragraphs across ${stats.languagesSupported} languages`); console.error(`šŸ’¾ DATABASE SIZE: ${stats.databaseSize}`); console.error('šŸ“Š Using enhanced stats from parsing report:', stats); return stats; } catch (error) { console.warn('āš ļø Could not read parsing report, falling back to database stats:', error.message); return null; } } // Find EGW quotes containing a search term - OPTIMIZED using FTS async findEGWQuotes(query, numQuotes = 3, toolName = 'findEGWQuotes-function') { await this.validateHeartbeat(toolName); return retryOperation(async () => { const startTime = Date.now(); return new Promise((resolve, reject) => { console.error(`šŸ” Searching for quotes containing *${query}*...`); // OPTIMIZED: Use FTS for fast search, then join with books table // FIXED: Select actual paragraph content, not FTS content (which may contain book titles) // FILTER: Only return paragraphs (element_type = 'p'), not headings (h4) const ftsSearchStmt = this.db.prepare(` SELECT fts.para_id, fts.book_id, p.content_plain as content, p.content as content_html, p.element_type, p.refcode_short, p.refcode_long, b.code as pub_code, b.title as pub_name, b.author as book_author, b.pub_year, b.folder_color_group as "group" FROM paragraphs_fts fts JOIN paragraphs p ON fts.para_id = p.para_id JOIN books b ON fts.book_id = b.book_id WHERE paragraphs_fts MATCH ? AND (b.author = 'Ellen Gould White' OR b.author = 'Ellen G. White') AND p.element_type = 'p' ORDER BY rank LIMIT ? `); // Use FTS MATCH for optimized search const searchTerm = query; // OPTIMIZED: Increase LIMIT to get more variety, but still reasonable ftsSearchStmt.all([searchTerm, Math.min(numQuotes * 20, 80)], (err, searchResults) => { if (err) { console.error(`āŒ FTS search error for "*${query}*":`, err.message); // Fallback to slower LIKE search if FTS fails console.error(`šŸ”„ Falling back to LIKE search...`); this.fallbackSearch(query, numQuotes, startTime, resolve, reject); return; } console.error(`āœ… Found ${searchResults.length} FTS results for "*${query}*"`); if (searchResults.length === 0) { ftsSearchStmt.finalize(); const duration = Date.now() - startTime; resolve({ success: false, message: `No EGW content found containing "*${query}*"` , query_time_ms: duration }); return; } // OPTIMIZED: Filter out non-EGW content based on HTML markers (if any) // Use faster string methods and early returns to improve performance const validResults = []; const nonEGWCount = { count: 0 }; for (let i = 0; i < searchResults.length; i++) { const result = searchResults[i]; // Quick check for non-EGW content using optimized string search const htmlContent = result.content_html || ''; if (htmlContent.length > 20 && htmlContent.includes('class="non-egw-')) { nonEGWCount.count++; continue; } validResults.push(result); } if (nonEGWCount.count > 0) { console.error(`🚫 Filtered out ${nonEGWCount.count} non-EGW entries (performance optimized)`); } console.error(`āœ… Using ${validResults.length} results after filtering non-EGW content`); // SIMPLIFIED: Take larger pool for variety but still process efficiently const expandedResults = validResults.slice(0, Math.min(validResults.length, numQuotes * 8)); // OPTIMIZED: Fast deduplication with lower similarity threshold const deduplicatedResults = this.fastDeduplicateQuotes(expandedResults); // SIMPLIFIED: Random selection with minimal variety processing to avoid repetition const finalQuotes = this.getRandomSample(deduplicatedResults, numQuotes); // Format quotes for display const formattedQuotes = finalQuotes.map(quote => { const cleanContent = (quote.content || '').replace(/\s+/g, ' ').trim(); const author = quote.book_author && quote.book_author.includes('Ellen') ? 'Ellen G. White' : quote.book_author; const refcodeShort = quote.refcode_short || quote.pub_code || 'EGW'; // Use full book title from pub_name instead of truncated refcode_long const bookTitle = quote.pub_name || quote.refcode_long || quote.refcode_short || ''; const refcodeParts = (quote.refcode_long || quote.refcode_short || '').split(','); let page_and_paragraph = ''; if (refcodeParts[1]) { const pageMatch = refcodeParts[1].match(/p\.\s*(\d+\.\d+)/); page_and_paragraph = pageMatch ? pageMatch[1] : ''; } const uniqueRefcodeShort = refcodeShort.includes(page_and_paragraph) ? refcodeShort.replace(page_and_paragraph, '') : refcodeShort; const yearString = bookTitle.includes(quote.pub_year) ? '' : `(${quote.pub_year})`; const trimmedShort = uniqueRefcodeShort.trim(); const trimmedParagraph = page_and_paragraph.trim(); const refBlock = (trimmedShort && trimmedParagraph ? `${trimmedShort} ${trimmedParagraph}` : trimmedShort || trimmedParagraph).replace(/\s+/g, ' '); return `"${cleanContent}" -- ${author}, ${bookTitle}${yearString}. - {${refBlock}}`; }); let resultText = `Found ${finalQuotes.length} EGW quote(s) containing "*${query}*":\n\n`; resultText += `Source Books: Multiple EGW writings\n`; resultText += `Author: Ellen G. White\n\n`; resultText += '='.repeat(80) + '\n\n'; formattedQuotes.forEach((quote, idx) => { resultText += `QUOTE ${idx + 1}\n\n`; resultText += `${quote}\n\n`; resultText += '-'.repeat(80) + '\n\n'; }); const duration = Date.now() - startTime; console.error(`āœ… Successfully found ${finalQuotes.length} EGW quotes for "*${query}*" in ${duration}ms (FTS optimized)`); ftsSearchStmt.finalize(); resolve({ success: true, query: query, quotes: finalQuotes.map(quote => ({ content: (quote.content || '').replace(/\s+/g, ' ').trim(), reference: quote.refcode_long || quote.refcode_short, book_title: quote.pub_name, author: quote.book_author, pub_year: quote.pub_year, para_id: quote.para_id })), total_found: finalQuotes.length, source_book: 'Multiple EGW writings', author: 'Ellen G. White', pub_year: 'Various', query_time_ms: duration, formatted_output: resultText }); }); }); }); } // OPTIMIZED: Fast deduplication using string hashing instead of Levenshtein fastDeduplicateQuotes(quotes) { const deduplicated = []; const seenHashes = new Set(); for (const quote of quotes) { const cleanContent = (quote.content || '').replace(/\s+/g, ' ').trim().toLowerCase(); // Skip very short content (likely not meaningful quotes) if (cleanContent.length < 20) { continue; } // Create simple hash for exact duplicates only const contentHash = cleanContent.split('').sort().join(''); // Check for exact duplicates only if (seenHashes.has(contentHash)) { continue; } deduplicated.push(quote); seenHashes.add(contentHash); // Stop early if we have enough quotes if (deduplicated.length >= 25) { break; } } console.error(`šŸ”„ Fast deduplicated ${quotes.length} results to ${deduplicated.length} unique quotes`); return deduplicated; } // Fallback search method using LIKE (slower) fallbackSearch(query, numQuotes, startTime, resolve, reject) { const fallbackStmt = this.db.prepare(` SELECT p.para_id, p.book_id, p.content_plain as content, p.content as content_html, p.element_type, p.refcode_short, p.refcode_long, b.code as pub_code, b.title as pub_name, b.author, b.pub_year, b.folder_color_group as "group" FROM paragraphs p JOIN books b ON p.book_id = b.book_id WHERE p.content_plain LIKE ? COLLATE NOCASE AND (b.author = 'Ellen Gould White' OR b.author = 'Ellen G. White') AND p.element_type = 'p' ORDER BY RANDOM() LIMIT 80 `); const searchTerm = `%${query}%`; fallbackStmt.all([searchTerm], (err, searchResults) => { if (err) { console.error(`āŒ Fallback search error for "*${query}*":`, err.message); fallbackStmt.finalize(); resolve({ success: false, message: `Search error: ${err.message}`, query_time_ms: Date.now() - startTime }); return; } console.error(`āš ļø Found ${searchResults.length} fallback results for "*${query}*"`); // OPTIMIZED: Use faster filtering for fallback search too const validResults = []; const nonEGWCount = { count: 0 }; for (let i = 0; i < searchResults.length; i++) { const result = searchResults[i]; // Quick check for non-EGW content using optimized string search const htmlContent = result.content_html || ''; if (htmlContent.length > 20 && htmlContent.includes('class="non-egw-')) { nonEGWCount.count++; continue; } validResults.push(result); } if (nonEGWCount.count > 0) { console.error(`🚫 Filtered out ${nonEGWCount.count} non-EGW entries (performance optimized)`); } // OPTIMIZED: Use fast deduplication for fallback too const deduplicatedResults = this.fastDeduplicateQuotes(validResults); // SIMPLIFIED: Random selection with minimal variety processing to avoid repetition const finalQuotes = this.getRandomSample(deduplicatedResults, numQuotes); const duration = Date.now() - startTime; console.error(`āš ļø Used fallback search for "*${query}*" in ${duration}ms`); fallbackStmt.finalize(); resolve({ success: finalQuotes.length > 0, query: query, quotes: finalQuotes.slice(0, numQuotes).map(quote => ({ content: (quote.content || '').replace(/\s+/g, ' ').trim(), reference: quote.refcode_long || quote.refcode_short, book_title: quote.pub_name, author: quote.author, pub_year: quote.pub_year, para_id: quote.para_id })), total_found: finalQuotes.length, query_time_ms: duration, message: finalQuotes.length > 0 ? `Found ${finalQuotes.length} quotes using fallback search` : `No results found using fallback search` }); }); } // Helper method to get random sample getRandomSample(array, size) { if (array.length <= size) return array; const shuffled = [...array]; for (let i = shuffled.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]; } return shuffled.slice(0, size); } // NEW: Get warmer status static getWarmerStatus() { return globalWarmer ? globalWarmer.getStatus() : { isWarm: false }; } // NEW: Wait for database to be warmed with timeout static async waitForWarmup() { if (!globalWarmer) { throw new Error('āŒ No warmer initialized. Create a database with requireWarming=true first.'); } // Wait for warming to complete with timeout const startTime = Date.now(); while (!globalWarmer.isWarm && (Date.now() - startTime < WARMUP_TIMEOUT)) { await new Promise(resolve => setTimeout(resolve, 100)); } if (!globalWarmer.isWarm) { throw new Error(`āŒ Database warmup timeout after ${WARMUP_TIMEOUT}ms`); } return globalDB; } // NEW: Get heartbeat status static getHeartbeatStatus() { return { isActive: !!globalHeartbeat, lastHeartbeat: lastHeartbeatTime, timeout: HEARTBEAT_TIMEOUT }; } close() { if (this.db && this.db !== globalDB) { this.db.close(); console.error('šŸ”’ Database connection closed'); } // Close connection pool if (connectionPoolInstance) { connectionPoolInstance.closeAll(); } } // NEW: Launch local EGW setup tool - IMPROVED with coldstart/heartbeat handling async launchLocalSetup(toolName = 'launchLocalSetup') { console.log('šŸš€ Launching EGW Writings MCP Server Setup...'); try { const { spawn } = await import('child_process'); console.log('\nšŸ“‹ Starting MCP Server with optimized startup...'); // IMPROVED: Start server with environment variables to handle coldstart/heartbeat const serverProcess = spawn('node', ['apps/local-server/src/server-universal.js'], { stdio: 'inherit', shell: true, env: { ...process.env, // PREVENT COLDSTART: Use existing warm connection if available EGW_SKIP_WARMING: 'true', // HANDLE HEARTBEAT: Use auto-heartbeat starter EGW_AUTO_HEARTBEAT: 'true', // OPTIMIZE: Use shared database connection EGW_SHARED_DB: 'true' } }); serverProcess.on('error', (error) => { console.error('āŒ Failed to start MCP server:', error.message); }); serverProcess.on('close', (code) => { if (code !== 0) { console.error(`āŒ MCP server exited with code ${code}`); } }); // Wait longer for server to initialize properly (warming and heartbeat) console.log('ā³ Waiting for server initialization (warming + heartbeat setup)...'); await new Promise(resolve => setTimeout(resolve, 8000)); // Increased wait time console.log('\nšŸ’¬ Starting EGW Chat CLI...'); const chatProcess = spawn('node', ['egw-chat-cli/egw-chat-cli-http.js'], { stdio: 'inherit', shell: true }); chatProcess.on('error', (error) => { console.error('āŒ Failed to start chat CLI:', error.message); }); chatProcess.on('close', (code) => { if (code !== 0) { console.error(`āŒ Chat CLI exited with code ${code}`); } }); console.log('\nāœ… EGW Writings MCP Server and Chat CLI started successfully!'); console.log('šŸ”— MCP Server: http://localhost:3000/mcp'); console.log('šŸ’¬ Chat CLI: Interactive terminal ready'); console.log('\nšŸ“š Usage Tips:'); console.log(' • Ask questions about Ellen G. White writings'); console.log(' • Try: "give me a quote on love"'); console.log(' • Try: "what did EGW say about faith?"'); console.log(' • Try: "list all books"'); console.log(' • Type "exit" to quit the chat CLI'); console.log('\nšŸ”„ Optimization: Server started with shared warm connection and auto-heartbeat'); console.log('šŸ” Security: Your DeepSeek API key will be prompted for each session'); console.log('\nšŸ’” If server takes time to warm up, please wait a moment before using tools'); console.log('šŸ„ Health checks enabled - system will monitor and auto-recover from failures'); return { success: true, server_process: serverProcess.pid, chat_process: chatProcess.pid, optimization_enabled: true, message: 'Setup launched with coldstart and heartbeat optimization' }; } catch (error) { console.error('āŒ Failed to launch local setup:', error.message); throw error; } } // NEW: PyForge IDE integration for AI agent automation async pyforgeExecute(code, action = 'execute', packageName = null, toolName = 'pyforgeExecute-function') { await this.validateHeartbeat(toolName); return retryOperation(async () => { try { console.error(`šŸ PyForge IDE executing action: ${action}`); // Import PyForge integration const { PyForgeIntegration } = await import('./pyforge-integration.js'); const pyforge = new PyForgeIntegration(); // Check if PyForge IDE is available if (!pyforge.isAvailable()) { throw new Error('āŒ PyForge IDE not found. Please ensure PyForge IDE is properly installed in packages/shared/src/database-utils'); } switch (action) { case 'execute': return await pyforge.executePythonCode(code); case 'install_package': if (!packageName) { throw new Error('āŒ Package name required for install_package action'); } return await pyforge.installPackage(packageName); case 'list_files': return await pyforge.listFiles(); default: throw new Error(`āŒ Unknown action: ${action}`); } } catch (error) { console.error('āŒ PyForge IDE execution error:', error.message); throw error; } }); } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/pythondev-pro/egw_writings_mcp_server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server