Skip to main content
Glama
unified-database.service.ts23.4 kB
import { Database } from 'sqlite3'; import * as path from 'path'; import * as fs from 'fs'; import OpenAI from 'openai'; export interface DecisionEntry { id?: number; summary: string; rationale?: string; implementation_details?: string; tags?: string[]; timestamp?: string; vector_embedding?: Buffer; embedding_hash?: string; } export interface CustomDataEntry { id?: number; category: string; key: string; value: any; tags?: string[]; timestamp?: string; metadata?: any; vector_embedding?: Buffer; embedding_hash?: string; } export interface SystemPatternEntry { id?: number; name: string; description?: string; tags?: string[]; timestamp?: string; vector_embedding?: Buffer; embedding_hash?: string; } export interface ProgressEntry { id?: number; description: string; status: string; parent_id?: number; timestamp?: string; tags?: string[]; vector_embedding?: Buffer; embedding_hash?: string; } export interface ContextEntry { id?: number; type: 'product_context' | 'active_context'; content: any; version: number; timestamp: string; } export interface SemanticSearchResult { item_id: number; item_type: string; content_text: string; similarity_score: number; metadata?: any; tags?: string[]; timestamp?: string; } export class UnifiedDatabaseService { private db: Database; private openai?: OpenAI; private dbPath: string; constructor(workspaceId: string) { // Use workspace as database location this.dbPath = path.join(workspaceId, 'sdof_knowledge.db'); // Ensure directory exists const dbDir = path.dirname(this.dbPath); if (!fs.existsSync(dbDir)) { fs.mkdirSync(dbDir, { recursive: true }); } this.db = new Database(this.dbPath); // Initialize OpenAI - REMOVE CLAUDE DEPENDENCY const apiKey = process.env.OPENAI_API_KEY; if (!apiKey) { console.warn('OPENAI_API_KEY not set - vector embeddings will be disabled'); } else { this.openai = new OpenAI({ apiKey }); } this.initializeTables(); } private initializeTables(): void { const tables = [ // Decisions table `CREATE TABLE IF NOT EXISTS decisions ( id INTEGER PRIMARY KEY AUTOINCREMENT, summary TEXT NOT NULL, rationale TEXT, implementation_details TEXT, tags TEXT, timestamp TEXT DEFAULT CURRENT_TIMESTAMP, vector_embedding BLOB, embedding_hash TEXT, embedding_model TEXT, embedding_created_at TEXT )`, // Custom data table `CREATE TABLE IF NOT EXISTS custom_data ( id INTEGER PRIMARY KEY AUTOINCREMENT, category TEXT NOT NULL, key TEXT NOT NULL, value TEXT NOT NULL, tags TEXT, timestamp TEXT DEFAULT CURRENT_TIMESTAMP, metadata TEXT, vector_embedding BLOB, embedding_hash TEXT, embedding_model TEXT, embedding_created_at TEXT, UNIQUE(category, key) )`, // System patterns table `CREATE TABLE IF NOT EXISTS system_patterns ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE, description TEXT, tags TEXT, timestamp TEXT DEFAULT CURRENT_TIMESTAMP, vector_embedding BLOB, embedding_hash TEXT, embedding_model TEXT, embedding_created_at TEXT )`, // Progress entries table `CREATE TABLE IF NOT EXISTS progress_entries ( id INTEGER PRIMARY KEY AUTOINCREMENT, description TEXT NOT NULL, status TEXT NOT NULL, parent_id INTEGER, timestamp TEXT DEFAULT CURRENT_TIMESTAMP, tags TEXT, vector_embedding BLOB, embedding_hash TEXT, embedding_model TEXT, embedding_created_at TEXT, FOREIGN KEY (parent_id) REFERENCES progress_entries (id) )`, // Context storage table `CREATE TABLE IF NOT EXISTS context_storage ( id INTEGER PRIMARY KEY AUTOINCREMENT, type TEXT NOT NULL, content TEXT NOT NULL, version INTEGER NOT NULL DEFAULT 1, timestamp TEXT DEFAULT CURRENT_TIMESTAMP )`, // Knowledge entries table (SDOF specific) `CREATE TABLE IF NOT EXISTS knowledge_entries ( id INTEGER PRIMARY KEY AUTOINCREMENT, plan_content TEXT NOT NULL, metadata TEXT, embedding BLOB, timestamp TEXT DEFAULT CURRENT_TIMESTAMP, title TEXT, tags TEXT )` ]; tables.forEach(table => { this.db.run(table, (err) => { if (err) { console.error('Error creating table:', err); } }); }); // Create indices for performance const indices = [ 'CREATE INDEX IF NOT EXISTS idx_decisions_tags ON decisions(tags)', 'CREATE INDEX IF NOT EXISTS idx_custom_data_category ON custom_data(category)', 'CREATE INDEX IF NOT EXISTS idx_custom_data_key ON custom_data(key)', 'CREATE INDEX IF NOT EXISTS idx_system_patterns_name ON system_patterns(name)', 'CREATE INDEX IF NOT EXISTS idx_progress_status ON progress_entries(status)', 'CREATE INDEX IF NOT EXISTS idx_context_type ON context_storage(type)' ]; indices.forEach(index => { this.db.run(index, (err) => { if (err && !err.message.includes('already exists')) { console.error('Error creating index:', err); } }); }); } // Generate embeddings using OpenAI (NO CLAUDE) private async generateEmbedding(text: string): Promise<number[] | null> { if (!this.openai) { return null; } try { const response = await this.openai.embeddings.create({ model: 'text-embedding-3-small', input: text, }); return response.data[0].embedding; } catch (error) { console.error('Error generating embedding:', error); return null; } } // Calculate cosine similarity private cosineSimilarity(a: number[], b: number[]): number { if (a.length !== b.length) return 0; let dotProduct = 0; let normA = 0; let normB = 0; for (let i = 0; i < a.length; i++) { dotProduct += a[i] * b[i]; normA += a[i] * a[i]; normB += b[i] * b[i]; } if (normA === 0 || normB === 0) return 0; return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB)); } // UNIFIED DECISION METHODS async logDecision(summary: string, rationale?: string, implementationDetails?: string, tags?: string[]): Promise<number> { return new Promise((resolve, reject) => { const tagsJson = tags ? JSON.stringify(tags) : null; const content = `${summary} ${rationale || ''} ${implementationDetails || ''}`.trim(); // Generate embedding this.generateEmbedding(content).then(embedding => { const embeddingBuffer = embedding ? Buffer.from(new Float32Array(embedding).buffer) : null; const embeddingHash = embedding ? require('crypto').createHash('md5').update(content).digest('hex') : null; this.db.run( `INSERT INTO decisions (summary, rationale, implementation_details, tags, vector_embedding, embedding_hash, embedding_model, embedding_created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [summary, rationale, implementationDetails, tagsJson, embeddingBuffer, embeddingHash, 'text-embedding-3-small', new Date().toISOString()], function(err) { if (err) reject(err); else resolve(this.lastID); } ); }).catch(reject); }); } async getDecisions(limit?: number, tagsIncludeAll?: string[], tagsIncludeAny?: string[]): Promise<DecisionEntry[]> { return new Promise((resolve, reject) => { let query = 'SELECT * FROM decisions'; const params: any[] = []; // Add tag filtering if needed if (tagsIncludeAll || tagsIncludeAny) { const conditions: string[] = []; if (tagsIncludeAll) { tagsIncludeAll.forEach(tag => { conditions.push('tags LIKE ?'); params.push(`%"${tag}"%`); }); } if (tagsIncludeAny) { const orConditions = tagsIncludeAny.map(tag => { params.push(`%"${tag}"%`); return 'tags LIKE ?'; }); conditions.push(`(${orConditions.join(' OR ')})`); } if (conditions.length > 0) { query += ' WHERE ' + conditions.join(' AND '); } } query += ' ORDER BY timestamp DESC'; if (limit) { query += ' LIMIT ?'; params.push(limit); } this.db.all(query, params, (err, rows: any[]) => { if (err) reject(err); else { const decisions = rows.map(row => ({ ...row, tags: row.tags ? JSON.parse(row.tags) : [] })); resolve(decisions); } }); }); } // UNIFIED CUSTOM DATA METHODS async logCustomData(category: string, key: string, value: any, metadata?: any): Promise<number> { return new Promise((resolve, reject) => { const valueStr = typeof value === 'string' ? value : JSON.stringify(value); const metadataStr = metadata ? JSON.stringify(metadata) : null; const content = `${category} ${key} ${valueStr}`; this.generateEmbedding(content).then(embedding => { const embeddingBuffer = embedding ? Buffer.from(new Float32Array(embedding).buffer) : null; const embeddingHash = embedding ? require('crypto').createHash('md5').update(content).digest('hex') : null; this.db.run( `INSERT OR REPLACE INTO custom_data (category, key, value, metadata, vector_embedding, embedding_hash, embedding_model, embedding_created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [category, key, valueStr, metadataStr, embeddingBuffer, embeddingHash, 'text-embedding-3-small', new Date().toISOString()], function(err) { if (err) reject(err); else resolve(this.lastID); } ); }).catch(reject); }); } async getCustomData(category?: string, key?: string): Promise<CustomDataEntry[]> { return new Promise((resolve, reject) => { let query = 'SELECT * FROM custom_data'; const params: any[] = []; if (category && key) { query += ' WHERE category = ? AND key = ?'; params.push(category, key); } else if (category) { query += ' WHERE category = ?'; params.push(category); } query += ' ORDER BY timestamp DESC'; this.db.all(query, params, (err, rows: any[]) => { if (err) reject(err); else { const entries = rows.map(row => ({ ...row, value: this.tryParseJSON(row.value), metadata: row.metadata ? this.tryParseJSON(row.metadata) : null })); resolve(entries); } }); }); } // UNIFIED SYSTEM PATTERN METHODS async logSystemPattern(name: string, description?: string, tags?: string[]): Promise<number> { return new Promise((resolve, reject) => { const tagsJson = tags ? JSON.stringify(tags) : null; const content = `${name} ${description || ''}`.trim(); this.generateEmbedding(content).then(embedding => { const embeddingBuffer = embedding ? Buffer.from(new Float32Array(embedding).buffer) : null; const embeddingHash = embedding ? require('crypto').createHash('md5').update(content).digest('hex') : null; this.db.run( `INSERT OR REPLACE INTO system_patterns (name, description, tags, vector_embedding, embedding_hash, embedding_model, embedding_created_at) VALUES (?, ?, ?, ?, ?, ?, ?)`, [name, description, tagsJson, embeddingBuffer, embeddingHash, 'text-embedding-3-small', new Date().toISOString()], function(err) { if (err) reject(err); else resolve(this.lastID); } ); }).catch(reject); }); } async getSystemPatterns(tagsIncludeAll?: string[], tagsIncludeAny?: string[]): Promise<SystemPatternEntry[]> { return new Promise((resolve, reject) => { let query = 'SELECT * FROM system_patterns'; const params: any[] = []; // Add tag filtering logic similar to decisions if (tagsIncludeAll || tagsIncludeAny) { const conditions: string[] = []; if (tagsIncludeAll) { tagsIncludeAll.forEach(tag => { conditions.push('tags LIKE ?'); params.push(`%"${tag}"%`); }); } if (tagsIncludeAny) { const orConditions = tagsIncludeAny.map(tag => { params.push(`%"${tag}"%`); return 'tags LIKE ?'; }); conditions.push(`(${orConditions.join(' OR ')})`); } if (conditions.length > 0) { query += ' WHERE ' + conditions.join(' AND '); } } query += ' ORDER BY timestamp DESC'; this.db.all(query, params, (err, rows: any[]) => { if (err) reject(err); else { const patterns = rows.map(row => ({ ...row, tags: row.tags ? JSON.parse(row.tags) : [] })); resolve(patterns); } }); }); } // UNIFIED PROGRESS METHODS async logProgress(description: string, status: string, parentId?: number): Promise<number> { return new Promise((resolve, reject) => { const content = `${description} ${status}`; this.generateEmbedding(content).then(embedding => { const embeddingBuffer = embedding ? Buffer.from(new Float32Array(embedding).buffer) : null; const embeddingHash = embedding ? require('crypto').createHash('md5').update(content).digest('hex') : null; this.db.run( `INSERT INTO progress_entries (description, status, parent_id, vector_embedding, embedding_hash, embedding_model, embedding_created_at) VALUES (?, ?, ?, ?, ?, ?, ?)`, [description, status, parentId, embeddingBuffer, embeddingHash, 'text-embedding-3-small', new Date().toISOString()], function(err) { if (err) reject(err); else resolve(this.lastID); } ); }).catch(reject); }); } async getProgress(statusFilter?: string, parentIdFilter?: number, limit?: number): Promise<ProgressEntry[]> { return new Promise((resolve, reject) => { let query = 'SELECT * FROM progress_entries'; const params: any[] = []; const conditions: string[] = []; if (statusFilter) { conditions.push('status = ?'); params.push(statusFilter); } if (parentIdFilter !== undefined) { conditions.push('parent_id = ?'); params.push(parentIdFilter); } if (conditions.length > 0) { query += ' WHERE ' + conditions.join(' AND '); } query += ' ORDER BY timestamp DESC'; if (limit) { query += ' LIMIT ?'; params.push(limit); } this.db.all(query, params, (err, rows: any[]) => { if (err) reject(err); else resolve(rows); }); }); } // UNIFIED CONTEXT METHODS async updateProductContext(content: any): Promise<void> { return this.updateContext('product_context', content); } async updateActiveContext(content: any): Promise<void> { return this.updateContext('active_context', content); } async getProductContext(): Promise<any> { return this.getContext('product_context'); } async getActiveContext(): Promise<any> { return this.getContext('active_context'); } private async updateContext(type: 'product_context' | 'active_context', content: any): Promise<void> { return new Promise((resolve, reject) => { const contentStr = JSON.stringify(content); // Get current version this.db.get( 'SELECT MAX(version) as max_version FROM context_storage WHERE type = ?', [type], (err, row: any) => { if (err) { reject(err); return; } const newVersion = (row?.max_version || 0) + 1; this.db.run( 'INSERT INTO context_storage (type, content, version) VALUES (?, ?, ?)', [type, contentStr, newVersion], (err) => { if (err) reject(err); else resolve(); } ); } ); }); } private async getContext(type: 'product_context' | 'active_context'): Promise<any> { return new Promise((resolve, reject) => { this.db.get( 'SELECT content FROM context_storage WHERE type = ? ORDER BY version DESC LIMIT 1', [type], (err, row: any) => { if (err) reject(err); else resolve(row ? this.tryParseJSON(row.content) : null); } ); }); } // UNIFIED SEMANTIC SEARCH async semanticSearch( queryText: string, limit: number = 10, itemTypes?: string[], tagsIncludeAny?: string[], tagsIncludeAll?: string[], customDataCategories?: string[] ): Promise<SemanticSearchResult[]> { const queryEmbedding = await this.generateEmbedding(queryText); if (!queryEmbedding) { // Fallback to text search if embeddings unavailable return this.textSearchFallback(queryText, limit, itemTypes); } const results: SemanticSearchResult[] = []; const searchTypes = itemTypes || ['decision', 'custom_data', 'system_pattern', 'progress_entry']; for (const itemType of searchTypes) { const tableName = this.getTableName(itemType); const rows = await this.getVectorRows(tableName); for (const row of rows) { if (!row.vector_embedding) continue; const embedding = this.bufferToArray(row.vector_embedding); const similarity = this.cosineSimilarity(queryEmbedding, embedding); if (similarity > 0.7) { // Threshold for relevance const contentText = this.extractContentText(row, itemType); results.push({ item_id: row.id, item_type: itemType, content_text: contentText, similarity_score: similarity, metadata: row.metadata ? this.tryParseJSON(row.metadata) : undefined, tags: row.tags ? JSON.parse(row.tags) : undefined, timestamp: row.timestamp }); } } } // Sort by similarity and apply filters results.sort((a, b) => b.similarity_score - a.similarity_score); return this.applyFilters(results, tagsIncludeAny, tagsIncludeAll, customDataCategories) .slice(0, limit); } // SDOF KNOWLEDGE ENTRY METHODS (existing functionality) async storePlan(planContent: string, metadata?: any): Promise<number> { return new Promise((resolve, reject) => { const embedding = this.generateEmbedding(planContent); const metadataStr = metadata ? JSON.stringify(metadata) : null; embedding.then(embVector => { const embeddingBuffer = embVector ? Buffer.from(new Float32Array(embVector).buffer) : null; this.db.run( 'INSERT INTO knowledge_entries (plan_content, metadata, embedding, title, tags) VALUES (?, ?, ?, ?, ?)', [planContent, metadataStr, embeddingBuffer, metadata?.planTitle || null, metadata?.tags ? JSON.stringify(metadata.tags) : null], function(err) { if (err) reject(err); else resolve(this.lastID); } ); }).catch(reject); }); } // Helper methods private getTableName(itemType: string): string { const tableMap: { [key: string]: string } = { 'decision': 'decisions', 'custom_data': 'custom_data', 'system_pattern': 'system_patterns', 'progress_entry': 'progress_entries' }; return tableMap[itemType] || itemType; } private async getVectorRows(tableName: string): Promise<any[]> { return new Promise((resolve, reject) => { this.db.all( `SELECT * FROM ${tableName} WHERE vector_embedding IS NOT NULL`, (err, rows: any[]) => { if (err) reject(err); else resolve(rows); } ); }); } private bufferToArray(buffer: Buffer): number[] { const float32Array = new Float32Array(buffer.buffer, buffer.byteOffset, buffer.byteLength / 4); return Array.from(float32Array); } private extractContentText(row: any, itemType: string): string { switch (itemType) { case 'decision': return `${row.summary} ${row.rationale || ''} ${row.implementation_details || ''}`.trim(); case 'custom_data': return `${row.category} ${row.key} ${row.value}`; case 'system_pattern': return `${row.name} ${row.description || ''}`.trim(); case 'progress_entry': return `${row.description} ${row.status}`; default: return row.content || row.description || row.summary || ''; } } private applyFilters( results: SemanticSearchResult[], tagsIncludeAny?: string[], tagsIncludeAll?: string[], customDataCategories?: string[] ): SemanticSearchResult[] { return results.filter(result => { // Apply tag filters if (tagsIncludeAll && result.tags) { if (!tagsIncludeAll.every(tag => result.tags!.includes(tag))) { return false; } } if (tagsIncludeAny && result.tags) { if (!tagsIncludeAny.some(tag => result.tags!.includes(tag))) { return false; } } // Apply custom data category filter if (customDataCategories && result.item_type === 'custom_data') { // Would need to query for category - simplified for now return true; } return true; }); } private async textSearchFallback(queryText: string, limit: number, itemTypes?: string[]): Promise<SemanticSearchResult[]> { // Simple text-based search fallback const results: SemanticSearchResult[] = []; const searchTypes = itemTypes || ['decision', 'custom_data', 'system_pattern', 'progress_entry']; for (const itemType of searchTypes) { const tableName = this.getTableName(itemType); const rows = await new Promise<any[]>((resolve, reject) => { this.db.all( `SELECT * FROM ${tableName} WHERE summary LIKE ? OR description LIKE ? OR name LIKE ? OR value LIKE ? LIMIT ?`, [`%${queryText}%`, `%${queryText}%`, `%${queryText}%`, `%${queryText}%`, limit], (err, rows: any[]) => { if (err) reject(err); else resolve(rows); } ); }); for (const row of rows) { const contentText = this.extractContentText(row, itemType); results.push({ item_id: row.id, item_type: itemType, content_text: contentText, similarity_score: 0.8, // Default score for text search metadata: row.metadata ? this.tryParseJSON(row.metadata) : undefined, tags: row.tags ? JSON.parse(row.tags) : undefined, timestamp: row.timestamp }); } } return results.slice(0, limit); } private tryParseJSON(str: string): any { try { return JSON.parse(str); } catch { return str; } } close(): void { this.db.close(); } }

Latest Blog Posts

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/tgf-between-your-legs/sdof-mcp'

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