Skip to main content
Glama
universal-engine.ts63.5 kB
import { HistorySearchEngine } from './search.js'; import { SearchResult, FileContext, ErrorSolution, CompactMessage, PlanResult } from './types.js'; import { detectClaudeDesktop, getClaudeDesktopStoragePath, getClaudeDesktopIndexedDBPath, } from './utils.js'; import { readdir, readFile, mkdtemp, copyFile, rm, chmod } from 'fs/promises'; import { readFileSync, readdirSync } from 'fs'; import { tmpdir } from 'os'; import { join } from 'path'; export interface UniversalSearchResult { source: 'claude-code' | 'claude-desktop'; results: SearchResult; enhanced: boolean; } export class UniversalHistorySearchEngine { private claudeCodeEngine: HistorySearchEngine; private claudeDesktopAvailable: boolean | null = null; private desktopStoragePath: string | null = null; private desktopIndexedDBPath: string | null = null; private levelDB: any = null; private sqlite3: any = null; private enhancedMode: boolean = false; constructor() { this.claudeCodeEngine = new HistorySearchEngine(); this.detectLevelDB(); } private async detectLevelDB(): Promise<void> { try { const { Level } = await import('level'); this.levelDB = Level; this.enhancedMode = true; console.log('✅ Level package detected - Enhanced Desktop mode available'); } catch (e) { // Try SQLite instead try { const sqlite3Module = await import('better-sqlite3'); this.sqlite3 = sqlite3Module.default; this.enhancedMode = true; console.log('✅ SQLite package detected - Enhanced Desktop mode available'); } catch (sqliteError) { console.log('📁 No database packages available - Claude Code only mode'); } } } async initialize(): Promise<void> { await this.detectLevelDB(); this.claudeDesktopAvailable = await detectClaudeDesktop(); if (this.claudeDesktopAvailable) { this.desktopStoragePath = await getClaudeDesktopStoragePath(); this.desktopIndexedDBPath = await getClaudeDesktopIndexedDBPath(); } } async searchConversations( query: string, project?: string, timeframe?: string, limit?: number ): Promise<UniversalSearchResult> { await this.initialize(); const claudeCodeResults = await this.claudeCodeEngine.searchConversations( query, project, timeframe, limit ); if (!this.claudeDesktopAvailable) { return { source: 'claude-code', results: claudeCodeResults, enhanced: false, }; } const desktopMessages = await this.searchClaudeDesktopConversations(query, timeframe, limit); const combinedResults = this.combineSearchResults(claudeCodeResults, desktopMessages); // Only mark as enhanced if we actually found Desktop data const hasDesktopData = desktopMessages.length > 0; return { source: hasDesktopData ? 'claude-desktop' : 'claude-code', results: combinedResults, enhanced: hasDesktopData, }; } private async searchClaudeDesktopConversations( query: string, timeframe?: string, limit?: number ): Promise<CompactMessage[]> { // Smart query heuristics - only search Desktop for relevant queries if (!this.shouldSearchDesktop(query)) { return []; } if (!this.desktopIndexedDBPath) { return []; } const results: CompactMessage[] = []; try { // Try Local Storage data first (where actual conversation text is found) const localStorageResults = await this.searchLocalStorageData(query, timeframe, limit); results.push(...localStorageResults); // Try SQLite WebStorage for additional metadata if (this.sqlite3) { const sqliteResults = await this.searchSQLiteWebStorage(query, timeframe, limit); results.push(...sqliteResults); } // Try both IndexedDB and Local Storage LevelDB locations const indexedDBResults = await this.searchIndexedDBWithMicroCopy(query, timeframe, limit); results.push(...indexedDBResults); const levelDBResults = await this.searchLocalStorageWithMicroCopy(query, timeframe, limit); results.push(...levelDBResults); } catch (error) { // Silent timeout protection - don't log errors for performance return []; } return results.slice(0, limit || 10); } private shouldSearchDesktop(query: string): boolean { // Search Desktop for all queries - let the fast timeout and smart fallback handle performance return true; } private async searchLocalStorageData( query: string, timeframe?: string, limit?: number ): Promise<CompactMessage[]> { const results: CompactMessage[] = []; const queryLower = query.toLowerCase(); try { // Use the initialized storage path instead of hardcoded path const localStoragePath = this.desktopStoragePath ? join(this.desktopStoragePath, 'leveldb') : null; if (!localStoragePath) { return []; } const files = readdirSync(localStoragePath); for (const file of files) { if (file.endsWith('.ldb') || file.endsWith('.log')) { const filePath = join(localStoragePath, file); const content = readFileSync(filePath); const textContent = content.toString('utf8').replace(/\x00/g, ''); // Search for conversation content that matches our query if (textContent.toLowerCase().includes(queryLower)) { // Look for text around the query match const queryIndex = textContent.toLowerCase().indexOf(queryLower); const start = Math.max(0, queryIndex - 200); const end = Math.min(textContent.length, queryIndex + 300); const snippet = textContent.substring(start, end); // Enhanced Desktop content extraction const cleanSnippet = this.extractCleanDesktopContent(snippet, query); if (cleanSnippet && cleanSnippet.length > 30) { const message: CompactMessage = { uuid: `desktop-local-${Date.now()}-${Math.random()}`, timestamp: new Date().toISOString(), type: 'assistant', // Desktop conversations are typically assistant responses content: cleanSnippet, sessionId: 'claude-desktop', projectPath: 'Claude Desktop', relevanceScore: this.calculateDesktopRelevanceScore(cleanSnippet, query), context: { filesReferenced: this.extractFileReferences({ content: cleanSnippet }), toolsUsed: this.extractToolUsages({ content: cleanSnippet }), errorPatterns: this.extractErrorPatterns({ content: cleanSnippet }), claudeInsights: this.extractClaudeInsights({ content: cleanSnippet }), codeSnippets: this.extractCodeSnippets({ content: cleanSnippet }), actionItems: this.extractActionItems({ content: cleanSnippet }), }, }; results.push(message); } } // Also extract LSS (Local Storage Store) entries for structured data const lssMatches = textContent.match(/LSS-[^:]+:[^}]+/g) || []; for (const lssEntry of lssMatches) { try { // Parse conversation data from LSS entries if (lssEntry.includes('textInput')) { const jsonMatch = lssEntry.match(/\{[^}]*\}/); if (jsonMatch) { const parsed = JSON.parse(jsonMatch[0]); if (parsed.content && Array.isArray(parsed.content)) { for (const item of parsed.content) { if (item.content && Array.isArray(item.content)) { for (const textItem of item.content) { if (textItem.text && textItem.text.toLowerCase().includes(queryLower)) { const message: CompactMessage = { uuid: `desktop-lss-${Date.now()}-${Math.random()}`, timestamp: new Date().toISOString(), type: 'user', content: textItem.text, sessionId: 'claude-desktop-lss', projectPath: 'claude-desktop-local-storage', relevanceScore: this.calculateRelevanceScore(textItem.text, query), context: { filesReferenced: this.extractFileReferences({ content: textItem.text, }), toolsUsed: this.extractToolUsages({ content: textItem.text }), errorPatterns: this.extractErrorPatterns({ content: textItem.text, }), claudeInsights: this.extractClaudeInsights({ content: textItem.text, }), codeSnippets: this.extractCodeSnippets({ content: textItem.text }), actionItems: this.extractActionItems({ content: textItem.text }), }, }; results.push(message); } } } } } } } } catch (parseError) { // Skip malformed entries continue; } } } } } catch (error) { // Silent failure return []; } return results.slice(0, limit || 10); } private getClaudeDesktopLocalStoragePath(): string | null { try { const path = require('path'); const os = require('os'); switch (process.platform) { case 'darwin': return path.join( os.homedir(), 'Library/Application Support/Claude/Local Storage/leveldb' ); case 'win32': return path.join(process.env.APPDATA || '', 'Claude/Local Storage/leveldb'); case 'linux': return path.join( process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config'), 'Claude/Local Storage/leveldb' ); default: return null; } } catch { return null; } } private async searchSQLiteWebStorage( query: string, timeframe?: string, limit?: number ): Promise<CompactMessage[]> { if (!this.sqlite3) { return []; } const results: CompactMessage[] = []; const queryLower = query.toLowerCase(); try { // Get the WebStorage path where SQLite databases are stored const webStoragePath = this.getClaudeDesktopWebStoragePath(); if (!webStoragePath) { return []; } // Look for SQLite databases in WebStorage/QuotaManager const quotaManagerPath = join(webStoragePath, 'QuotaManager'); // Copy the database to a temporary location to avoid lock issues let tempDir: string | null = null; let db: any = null; try { tempDir = await mkdtemp(join(require('os').tmpdir(), 'claude-historian-sqlite-')); await chmod(tempDir, 0o700); const sourceDbPath = quotaManagerPath; const tempDbPath = join(tempDir, 'temp-quota.db'); // Check if source database exists try { await import('fs').then((fs) => fs.promises.access(sourceDbPath, fs.constants.F_OK)); } catch { return []; // Database doesn't exist } // Copy database to temporary location await copyFile(sourceDbPath, tempDbPath); // Try to copy journal file too if it exists try { await copyFile(sourceDbPath + '-journal', tempDbPath + '-journal'); } catch { // Journal file might not exist, that's okay } db = new this.sqlite3(tempDbPath, { readonly: true, timeout: 1000, }); // Query the database for conversation data // Claude Desktop typically stores data in 'messages' or 'conversations' tables const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all(); for (const table of tables) { try { // Look for text content in each table const columns = db.prepare(`PRAGMA table_info(${table.name})`).all(); const textColumns = columns.filter( (col: any) => col.type.toLowerCase().includes('text') || col.type.toLowerCase().includes('varchar') || col.name.toLowerCase().includes('content') || col.name.toLowerCase().includes('message') || col.name.toLowerCase().includes('data') ); if (textColumns.length > 0) { // Search for query in text columns for (const col of textColumns) { try { const searchQuery = `SELECT * FROM ${table.name} WHERE ${col.name} LIKE ? COLLATE NOCASE LIMIT ?`; const rows = db.prepare(searchQuery).all(`%${query}%`, limit || 10); for (const row of rows) { const content = row[col.name]; if ( content && typeof content === 'string' && content.toLowerCase().includes(queryLower) ) { const message: CompactMessage = { uuid: `desktop-sqlite-${Date.now()}-${Math.random()}`, timestamp: row.timestamp || row.created_at || new Date().toISOString(), type: 'assistant', content: this.extractRelevantSnippet(content, query), sessionId: 'claude-desktop-sqlite', projectPath: 'claude-desktop-webstorage', relevanceScore: this.calculateRelevanceScore(content, query), context: { filesReferenced: this.extractFileReferences({ content }), toolsUsed: this.extractToolUsages({ content }), errorPatterns: this.extractErrorPatterns({ content }), claudeInsights: this.extractClaudeInsights({ content }), codeSnippets: this.extractCodeSnippets({ content }), actionItems: this.extractActionItems({ content }), }, }; results.push(message); if (results.length >= (limit || 10)) { break; } } } } catch (queryError) { // Skip columns that can't be queried continue; } } } } catch (tableError) { // Skip tables that can't be accessed continue; } } db.close(); } catch (copyError) { // If copy fails, try direct read-only access as fallback try { db = new this.sqlite3(quotaManagerPath, { readonly: true, timeout: 100, // Very short timeout for fallback }); const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all(); // ... same search logic here but simplified for fallback db.close(); } catch (directError) { // Both copy and direct access failed return []; } } finally { // Clean up temporary directory if (tempDir) { try { await rm(tempDir, { recursive: true, force: true }); } catch { // Silent cleanup failure } } } } catch (error) { // Silent failure for any other issues return []; } return results; } private getClaudeDesktopWebStoragePath(): string | null { try { const { join } = require('path'); const { homedir } = require('os'); switch (process.platform) { case 'darwin': return join(homedir(), 'Library/Application Support/Claude/WebStorage'); case 'win32': return join(process.env.APPDATA || '', 'Claude/WebStorage'); case 'linux': return join( process.env.XDG_CONFIG_HOME || join(homedir(), '.config'), 'Claude/WebStorage' ); default: return null; } } catch { return null; } } private async searchIndexedDBWithMicroCopy( query: string, timeframe?: string, limit?: number ): Promise<CompactMessage[]> { if (!this.desktopIndexedDBPath) { return []; } let tempDir: string | null = null; try { // Create secure temp directory tempDir = await mkdtemp(join(tmpdir(), 'claude-historian-')); await chmod(tempDir, 0o700); // Secure permissions - owner only const sourceDbPath = join(this.desktopIndexedDBPath, 'https_claude.ai_0.indexeddb.leveldb'); const tempDbPath = join(tempDir, 'temp.leveldb'); // Micro-copy: only copy .log files (active data, ~2KB vs 48KB total) const sourceFiles = await readdir(sourceDbPath); const logFiles = sourceFiles.filter((file) => file.endsWith('.log')); if (logFiles.length === 0) { return []; } // Silent timeout protection - max 100ms for copy operation const copyPromise = this.copyLogFiles(sourceDbPath, tempDbPath, logFiles); const timeoutPromise = new Promise<void>((_, reject) => setTimeout(() => reject(new Error('Timeout')), 100) ); await Promise.race([copyPromise, timeoutPromise]); // Fast text search in copied log files (no LevelDB parsing needed) const results = await this.searchLogFiles(tempDbPath, query, timeframe, limit); return results; } catch (error) { // Silent failure for performance return []; } finally { // Immediate cleanup if (tempDir) { try { await rm(tempDir, { recursive: true, force: true }); } catch { // Silent cleanup failure } } } } private async copyLogFiles( sourcePath: string, destPath: string, logFiles: string[] ): Promise<void> { // Copy all available files for better search coverage const allFiles = await readdir(sourcePath); const filesToCopy = allFiles .filter((file) => file.endsWith('.log') || file.endsWith('.ldb') || file === 'CURRENT') .slice(0, 5); // Limit to 5 most relevant files for (const file of filesToCopy) { const sourceFile = join(sourcePath, file); const destFile = join(destPath, file); await copyFile(sourceFile, destFile); } } private async searchLogFiles( dbPath: string, query: string, timeframe?: string, limit?: number ): Promise<CompactMessage[]> { const results: CompactMessage[] = []; const queryLower = query.toLowerCase(); try { const files = await readdir(dbPath); for (const file of files) { if (file.endsWith('.log') || file.endsWith('.ldb')) { // Read as binary first to handle LevelDB format const buffer = await readFile(join(dbPath, file)); const content = buffer.toString('utf8', 0, Math.min(buffer.length, 50000)); // Limit to prevent massive content // Search for text content in the binary data if (content.toLowerCase().includes(queryLower)) { const message: CompactMessage = { uuid: `desktop-${Date.now()}-${Math.random()}`, timestamp: new Date().toISOString(), type: 'assistant', content: this.extractRelevantSnippet(content, query), sessionId: 'claude-desktop-session', projectPath: 'claude-desktop', relevanceScore: this.calculateRelevanceScore(content, query), context: { filesReferenced: [], toolsUsed: [], errorPatterns: [], claudeInsights: [], codeSnippets: [], actionItems: [], }, }; results.push(message); if (results.length >= (limit || 10)) { break; } } } } } catch { // Silent failure } return results; } private extractRelevantSnippet(content: string, query: string): string { // Extract relevant snippet around query match const queryIndex = content.toLowerCase().indexOf(query.toLowerCase()); if (queryIndex === -1) return content.slice(0, 200); const start = Math.max(0, queryIndex - 100); const end = Math.min(content.length, queryIndex + 100); return content.slice(start, end); } private async searchLocalStorageWithMicroCopy( query: string, timeframe?: string, limit?: number ): Promise<CompactMessage[]> { if (!this.desktopStoragePath) { return []; } let tempDir: string | null = null; try { // Create secure temp directory tempDir = await mkdtemp(join(tmpdir(), 'claude-historian-local-')); await chmod(tempDir, 0o700); const sourceDbPath = join(this.desktopStoragePath, 'leveldb'); const tempDbPath = join(tempDir, 'temp-local.leveldb'); // Copy Local Storage LevelDB files const sourceFiles = await readdir(sourceDbPath); const filesToCopy = sourceFiles .filter((file) => file.endsWith('.log') || file.endsWith('.ldb') || file === 'CURRENT') .slice(0, 5); if (filesToCopy.length === 0) { return []; } // Silent timeout protection - max 100ms for copy operation const copyPromise = this.copyLocalStorageFiles(sourceDbPath, tempDbPath, filesToCopy); const timeoutPromise = new Promise<void>((_, reject) => setTimeout(() => reject(new Error('Timeout')), 100) ); await Promise.race([copyPromise, timeoutPromise]); // Search in Local Storage files const results = await this.searchLocalStorageFiles(tempDbPath, query, timeframe, limit); return results; } catch (error) { // Silent failure return []; } finally { // Immediate cleanup if (tempDir) { try { await rm(tempDir, { recursive: true, force: true }); } catch { // Silent cleanup failure } } } } private async copyLocalStorageFiles( sourcePath: string, destPath: string, files: string[] ): Promise<void> { for (const file of files) { const sourceFile = join(sourcePath, file); const destFile = join(destPath, file); await copyFile(sourceFile, destFile); } } private async searchLocalStorageFiles( dbPath: string, query: string, timeframe?: string, limit?: number ): Promise<CompactMessage[]> { const results: CompactMessage[] = []; const queryLower = query.toLowerCase(); try { const files = await readdir(dbPath); for (const file of files) { if (file.endsWith('.log') || file.endsWith('.ldb')) { const buffer = await readFile(join(dbPath, file)); const content = buffer.toString('utf8', 0, Math.min(buffer.length, 50000)); // Look for conversation content in the Local Storage format if (content.toLowerCase().includes(queryLower)) { const message: CompactMessage = { uuid: `desktop-local-${Date.now()}-${Math.random()}`, timestamp: new Date().toISOString(), type: 'assistant', content: this.extractRelevantSnippet(content, query), sessionId: 'claude-desktop-local-session', projectPath: 'claude-desktop-local', relevanceScore: this.calculateRelevanceScore(content, query), context: { filesReferenced: [], toolsUsed: [], errorPatterns: [], claudeInsights: [], codeSnippets: [], actionItems: [], }, }; results.push(message); if (results.length >= (limit || 10)) { break; } } } } } catch { // Silent failure } return results; } private async searchLocalStorage( query: string, timeframe?: string, limit?: number ): Promise<any[]> { if (!this.desktopStoragePath) return []; try { const entries = await readdir(this.desktopStoragePath); const results: any[] = []; for (const entry of entries) { if (entry.startsWith('leveldb_')) { const entryPath = join(this.desktopStoragePath, entry); const conversations = await this.extractConversationsFromFile(entryPath); for (const conversation of conversations) { if ( this.matchesQuery(conversation, query) && this.matchesTimeframe(conversation, timeframe) ) { results.push({ ...conversation, source: 'claude-desktop-local-storage', timestamp: conversation.timestamp || new Date().toISOString(), }); } } } } return results.slice(0, limit || 10); } catch (error) { console.error('Error searching Local Storage:', error); return []; } } private async searchIndexedDB(query: string, timeframe?: string, limit?: number): Promise<any[]> { if (!this.desktopIndexedDBPath) return []; try { const entries = await readdir(this.desktopIndexedDBPath); const results: any[] = []; for (const entry of entries) { if (entry.includes('claude')) { const entryPath = join(this.desktopIndexedDBPath, entry); const conversations = await this.extractConversationsFromFile(entryPath); for (const conversation of conversations) { if ( this.matchesQuery(conversation, query) && this.matchesTimeframe(conversation, timeframe) ) { results.push({ ...conversation, source: 'claude-desktop-indexed-db', timestamp: conversation.timestamp || new Date().toISOString(), }); } } } } return results.slice(0, limit || 10); } catch (error) { console.error('Error searching IndexedDB:', error); return []; } } private async extractConversationsFromFile(filePath: string): Promise<any[]> { try { const content = await readFile(filePath, 'utf8'); const conversations: any[] = []; const lines = content.split('\n'); for (const line of lines) { if (line.trim()) { try { const data = JSON.parse(line); if (data.type === 'conversation' || data.messages) { conversations.push(data); } } catch { if (line.includes('assistant') || line.includes('user')) { conversations.push({ content: line, type: 'raw', timestamp: new Date().toISOString(), uuid: `desktop-${Date.now()}-${Math.random()}`, sessionId: 'desktop-session', projectPath: 'claude-desktop', }); } } } } return conversations; } catch (error) { console.error(`Error extracting from file ${filePath}:`, error); return []; } } private async searchIndexedDBWithLevel( query: string, timeframe?: string, limit?: number ): Promise<CompactMessage[]> { if (!this.desktopIndexedDBPath || !this.levelDB) { return []; } try { const dbPath = join(this.desktopIndexedDBPath, 'https_claude.ai_0.indexeddb.leveldb'); const db = new this.levelDB(dbPath, { readOnly: true }); const conversations: CompactMessage[] = []; // Read entries from the LevelDB database const entries = await db.iterator({ limit: 100 }).all(); for (const [key, value] of entries) { try { const keyStr = key.toString(); const valueStr = value.toString(); // Parse conversation data from LevelDB entries if (this.isConversationEntry(keyStr, valueStr)) { const message = await this.parseConversationEntry(keyStr, valueStr, query, timeframe); if (message) { conversations.push(message); } } } catch (parseError) { // Skip invalid entries continue; } } await db.close(); return conversations.slice(0, limit || 10); } catch (error: unknown) { if (error && typeof error === 'object' && 'code' in error && error.code === 'LEVEL_LOCKED') { console.log('Claude Desktop database is locked (application is running)'); return []; } if ( error && typeof error === 'object' && 'message' in error && typeof error.message === 'string' && error.message.includes('LOCK') ) { console.log('Claude Desktop database is locked (application is running)'); return []; } console.error('Error reading IndexedDB with Level:', error); return []; } } private async searchLocalStorageWithLevel( query: string, timeframe?: string, limit?: number ): Promise<CompactMessage[]> { if (!this.desktopStoragePath || !this.levelDB) { return []; } try { const dbPath = join(this.desktopStoragePath, 'leveldb'); const db = this.levelDB(dbPath, { readOnly: true }); const conversations: CompactMessage[] = []; // Read all entries from the Local Storage LevelDB const iterator = db.iterator(); for await (const [key, value] of iterator) { try { const keyStr = key.toString(); const valueStr = value.toString(); // Parse local storage data for conversation references if (this.isLocalStorageConversationEntry(keyStr, valueStr)) { const message = await this.parseLocalStorageEntry(keyStr, valueStr, query, timeframe); if (message) { conversations.push(message); } } } catch (parseError) { // Skip invalid entries continue; } } await iterator.close(); await db.close(); return conversations.slice(0, limit || 10); } catch (error) { console.error('Error reading Local Storage with Level:', error); return []; } } private isConversationEntry(key: string, value: string): boolean { // Check if this LevelDB entry contains conversation data return ( value.includes('conversation') || value.includes('message') || value.includes('assistant') || value.includes('user') || value.includes('sketchybar') || value.includes('analog clock') ); } private isLocalStorageConversationEntry(key: string, value: string): boolean { // Check if this Local Storage entry contains conversation references return ( key.includes('conversation') || key.includes('chat') || value.includes('message') || value.includes('assistant') ); } private async parseConversationEntry( key: string, value: string, query: string, timeframe?: string ): Promise<CompactMessage | null> { try { // Try to parse as JSON first let data; try { data = JSON.parse(value); } catch { // If not JSON, treat as plain text data = { content: value, type: 'raw' }; } // Check if this entry matches our query if (!this.matchesQuery(data, query)) { return null; } // Check timeframe if specified if (timeframe && !this.matchesTimeframe(data, timeframe)) { return null; } // Convert to CompactMessage format return { uuid: `desktop-${Date.now()}-${Math.random()}`, timestamp: data.timestamp || new Date().toISOString(), type: this.determineMessageType(data), content: this.extractMessageContent(data), sessionId: data.sessionId || 'claude-desktop-session', projectPath: 'claude-desktop', relevanceScore: this.calculateRelevanceScore(data, query), context: { filesReferenced: this.extractFileReferences(data), toolsUsed: this.extractToolUsages(data), errorPatterns: this.extractErrorPatterns(data), claudeInsights: this.extractClaudeInsights(data), codeSnippets: this.extractCodeSnippets(data), actionItems: this.extractActionItems(data), }, }; } catch (error) { console.error('Error parsing conversation entry:', error); return null; } } private async parseLocalStorageEntry( key: string, value: string, query: string, timeframe?: string ): Promise<CompactMessage | null> { try { // Similar parsing logic for Local Storage entries let data; try { data = JSON.parse(value); } catch { data = { content: value, type: 'raw' }; } if ( !this.matchesQuery(data, query) || (timeframe && !this.matchesTimeframe(data, timeframe)) ) { return null; } return { uuid: `desktop-local-${Date.now()}-${Math.random()}`, timestamp: data.timestamp || new Date().toISOString(), type: this.determineMessageType(data), content: this.extractMessageContent(data), sessionId: data.sessionId || 'claude-desktop-local-session', projectPath: 'claude-desktop-local', relevanceScore: this.calculateRelevanceScore(data, query), context: { filesReferenced: this.extractFileReferences(data), toolsUsed: this.extractToolUsages(data), errorPatterns: this.extractErrorPatterns(data), claudeInsights: this.extractClaudeInsights(data), codeSnippets: this.extractCodeSnippets(data), actionItems: this.extractActionItems(data), }, }; } catch (error) { console.error('Error parsing local storage entry:', error); return null; } } private matchesQuery(conversation: any, query: string): boolean { if (!query) return true; const content = JSON.stringify(conversation).toLowerCase(); const queryLower = query.toLowerCase(); return ( content.includes(queryLower) || queryLower.split(' ').some((word) => content.includes(word)) ); } private matchesTimeframe(conversation: any, timeframe?: string): boolean { if (!timeframe || !conversation.timestamp) return true; const messageDate = new Date(conversation.timestamp); const now = new Date(); switch (timeframe.toLowerCase()) { case 'today': return messageDate.toDateString() === now.toDateString(); case 'week': return now.getTime() - messageDate.getTime() < 7 * 24 * 60 * 60 * 1000; case 'month': return now.getTime() - messageDate.getTime() < 30 * 24 * 60 * 60 * 1000; default: return true; } } private combineSearchResults( claudeCodeResults: SearchResult, desktopMessages: CompactMessage[] ): SearchResult { const combinedMessages = [...claudeCodeResults.messages, ...desktopMessages]; combinedMessages.sort((a, b) => { const aScore = a.relevanceScore || 0; const bScore = b.relevanceScore || 0; if (aScore !== bScore) return bScore - aScore; const aTime = new Date(a.timestamp || 0).getTime(); const bTime = new Date(b.timestamp || 0).getTime(); return bTime - aTime; }); return { messages: combinedMessages, totalResults: claudeCodeResults.totalResults + desktopMessages.length, searchQuery: claudeCodeResults.searchQuery, executionTime: claudeCodeResults.executionTime, }; } async findFileContext( filepath: string, limit?: number ): Promise<{ source: string; results: FileContext[]; enhanced: boolean }> { await this.initialize(); const claudeCodeResults = await this.claudeCodeEngine.findFileContext(filepath, limit); if (!this.claudeDesktopAvailable) { return { source: 'claude-code', results: claudeCodeResults, enhanced: false, }; } const desktopMessages = await this.searchClaudeDesktopConversations(filepath, undefined, limit); const combinedResults = this.combineFileContextResults(claudeCodeResults, desktopMessages); const hasDesktopData = desktopMessages.length > 0; return { source: hasDesktopData ? 'claude-desktop' : 'claude-code', results: combinedResults, enhanced: hasDesktopData, }; } private combineFileContextResults( claudeCodeResults: FileContext[], desktopMessages: CompactMessage[] ): FileContext[] { const desktopFileContexts: FileContext[] = desktopMessages.map((msg) => ({ filePath: 'claude-desktop', lastModified: msg.timestamp, relatedMessages: [msg], operationType: 'read' as const, })); return [...claudeCodeResults, ...desktopFileContexts]; } async findSimilarQueries( query: string, limit?: number ): Promise<{ source: string; results: CompactMessage[]; enhanced: boolean }> { await this.initialize(); const claudeCodeResults = await this.claudeCodeEngine.findSimilarQueries(query, limit); if (!this.claudeDesktopAvailable) { return { source: 'claude-code', results: claudeCodeResults, enhanced: false, }; } const desktopMessages = await this.searchClaudeDesktopConversations(query, undefined, limit); const combinedResults = [...claudeCodeResults, ...desktopMessages]; const hasDesktopData = desktopMessages.length > 0; return { source: hasDesktopData ? 'claude-desktop' : 'claude-code', results: combinedResults, enhanced: hasDesktopData, }; } async getErrorSolutions( errorPattern: string, limit?: number ): Promise<{ source: string; results: ErrorSolution[]; enhanced: boolean }> { await this.initialize(); const claudeCodeResults = await this.claudeCodeEngine.getErrorSolutions(errorPattern, limit); if (!this.claudeDesktopAvailable) { return { source: 'claude-code', results: claudeCodeResults, enhanced: false, }; } const desktopMessages = await this.searchClaudeDesktopConversations( errorPattern, undefined, limit ); const combinedResults = this.combineErrorSolutionResults(claudeCodeResults, desktopMessages); const hasDesktopData = desktopMessages.length > 0; return { source: hasDesktopData ? 'claude-desktop' : 'claude-code', results: combinedResults, enhanced: hasDesktopData, }; } private combineErrorSolutionResults( claudeCodeResults: ErrorSolution[], desktopMessages: CompactMessage[] ): ErrorSolution[] { const desktopErrorSolutions: ErrorSolution[] = desktopMessages.map((msg) => ({ errorPattern: 'claude-desktop-error', solution: [msg], context: msg.content, frequency: 1, })); return [...claudeCodeResults, ...desktopErrorSolutions]; } isClaudeDesktopAvailable(): boolean { return this.claudeDesktopAvailable === true; } getAvailableSources(): string[] { const sources = ['claude-code']; if (this.claudeDesktopAvailable && this.enhancedMode) { sources.push('claude-desktop'); } return sources; } private determineMessageType(data: any): 'user' | 'assistant' | 'tool_use' | 'tool_result' { if (data.type) return data.type; if (data.role === 'user') return 'user'; if (data.role === 'assistant') return 'assistant'; if (data.content && data.content.includes('Tool:')) return 'tool_use'; if (data.content && data.content.includes('Result:')) return 'tool_result'; return 'assistant'; // Default } private extractMessageContent(data: any): string { if (data.content) return data.content; if (data.message) return data.message; if (data.text) return data.text; if (typeof data === 'string') return data; return JSON.stringify(data); } private calculateRelevanceScore(data: any, query: string): number { const content = this.extractMessageContent(data).toLowerCase(); const queryLower = query.toLowerCase(); let score = 0; // Exact match bonus if (content.includes(queryLower)) score += 10; // Word matching const queryWords = queryLower.split(/\s+/); const matchingWords = queryWords.filter((word) => content.includes(word)); score += matchingWords.length * 2; // Special bonuses for Desktop conversations if (content.includes('sketchybar')) score += 5; if (content.includes('analog clock')) score += 5; if (content.includes('script')) score += 3; return score; } private extractFileReferences(data: any): string[] { const content = this.extractMessageContent(data); const fileRefs: string[] = []; // Common file patterns const patterns = [ /\b[\w-]+\.(js|ts|py|json|md|txt|sh|yml|yaml)\b/g, /\/[\w-/]+\.[\w]+/g, /~\/[\w-/]+\.[\w]+/g, ]; patterns.forEach((pattern) => { const matches = content.match(pattern); if (matches) { fileRefs.push(...matches); } }); return [...new Set(fileRefs)]; } private extractToolUsages(data: any): string[] { const content = this.extractMessageContent(data); const tools: string[] = []; // Tool usage patterns const toolPatterns = [ /\[Tool:\s*([^\]]+)\]/g, /execute_command/g, /create_text_file/g, /Tool Result/g, ]; toolPatterns.forEach((pattern) => { const matches = content.match(pattern); if (matches) { tools.push(...matches); } }); return [...new Set(tools)]; } private extractErrorPatterns(data: any): string[] { const content = this.extractMessageContent(data); const errors: string[] = []; // Error patterns const errorPatterns = [/Error:[^\n]*/g, /Exception:[^\n]*/g, /Failed[^\n]*/g, /Cannot[^\n]*/g]; errorPatterns.forEach((pattern) => { const matches = content.match(pattern); if (matches) { errors.push(...matches); } }); return [...new Set(errors)]; } private extractClaudeInsights(data: any): string[] { const content = this.extractMessageContent(data); const insights: string[] = []; // Claude insight patterns const insightPatterns = [/I'll[^\n]*/g, /Let me[^\n]*/g, /Here's[^\n]*/g, /Solution:[^\n]*/g]; insightPatterns.forEach((pattern) => { const matches = content.match(pattern); if (matches) { insights.push(...matches.slice(0, 3)); // Limit to avoid spam } }); return [...new Set(insights)]; } private extractCodeSnippets(data: any): string[] { const content = this.extractMessageContent(data); const snippets: string[] = []; // Code block patterns const codePatterns = [ /```[\s\S]*?```/g, /`[^`\n]+`/g, /function\s+\w+\s*\([^)]*\)/g, /const\s+\w+\s*=/g, ]; codePatterns.forEach((pattern) => { const matches = content.match(pattern); if (matches) { snippets.push(...matches.slice(0, 2)); // Limit to avoid spam } }); return [...new Set(snippets)]; } private extractActionItems(data: any): string[] { const content = this.extractMessageContent(data); const actions: string[] = []; // Action item patterns const actionPatterns = [/TODO:[^\n]*/g, /Next:[^\n]*/g, /Action:[^\n]*/g, /Step \d+:[^\n]*/g]; actionPatterns.forEach((pattern) => { const matches = content.match(pattern); if (matches) { actions.push(...matches); } }); return [...new Set(actions)]; } // Universal methods for all tools async getRecentSessions(limit?: number, project?: string): Promise<UniversalSearchResult> { await this.initialize(); const claudeCodeSessions = await this.claudeCodeEngine.getRecentSessions(limit || 10); if (!this.claudeDesktopAvailable) { return { source: 'claude-code', results: claudeCodeSessions as any, enhanced: false, }; } // For sessions, Desktop doesn't have traditional sessions, so we focus on Code // But we mark as enhanced if Desktop is available for future Desktop session support return { source: 'claude-code', results: claudeCodeSessions as any, enhanced: this.claudeDesktopAvailable, }; } async getToolPatterns(toolName?: string, limit?: number): Promise<UniversalSearchResult> { await this.initialize(); const claudeCodePatterns = await this.claudeCodeEngine.getToolPatterns(toolName, limit || 12); if (!this.claudeDesktopAvailable) { return { source: 'claude-code', results: claudeCodePatterns as any, enhanced: false, }; } // For tool patterns, Desktop doesn't have tool usage data, so we focus on Code // But we mark as enhanced if Desktop is available for future Desktop tool analysis return { source: 'claude-code', results: claudeCodePatterns as any, enhanced: this.claudeDesktopAvailable, }; } async generateCompactSummary( sessionId: string, maxMessages?: number, focus?: string ): Promise<UniversalSearchResult> { await this.initialize(); // Get session data from Claude Code const allSessions = await this.claudeCodeEngine.getRecentSessions(20); // Support "latest" keyword - resolve to most recent session let resolvedSessionId = sessionId; if (sessionId.toLowerCase() === 'latest') { if (allSessions.length > 0) { resolvedSessionId = allSessions[0].session_id; } else { return { source: 'claude-code', results: { session_id: 'latest', end_time: null, start_time: null, duration_minutes: 0, message_count: 0, project_path: null, tools_used: [], files_modified: [], accomplishments: [], key_decisions: [], } as any, enhanced: false, }; } } const sessionData = allSessions.find( (s) => s.session_id === resolvedSessionId || s.session_id.startsWith(resolvedSessionId) || resolvedSessionId.includes(s.session_id) || s.session_id.includes(resolvedSessionId.replace(/^.*\//, '')) ); if (!sessionData) { return { source: 'claude-code', results: { session_id: resolvedSessionId, end_time: null, start_time: null, duration_minutes: 0, message_count: 0, project_path: null, tools_used: [], files_modified: [], accomplishments: [], key_decisions: [], } as any, enhanced: false, }; } const messages = await this.claudeCodeEngine.getSessionMessages( sessionData.project_dir, sessionData.session_id ); const sessionMessages = messages.slice(0, maxMessages || 100); // Increased from 50 to 100 for better extraction // Return rich session object with extracted content const richSummary = { session_id: sessionData.session_id, end_time: sessionData.end_time, start_time: sessionData.start_time, duration_minutes: sessionData.duration_minutes || 0, message_count: sessionMessages.length, project_path: sessionData.project_path, tools_used: this.extractToolsFromMessages(sessionMessages), files_modified: this.extractFilesFromMessages(sessionMessages), accomplishments: this.extractAccomplishmentsFromMessages(sessionMessages), key_decisions: this.extractDecisionsFromMessages(sessionMessages), }; return { source: 'claude-code', results: richSummary as any, enhanced: this.claudeDesktopAvailable === true, }; } async searchPlans( query: string, limit?: number ): Promise<{ source: string; results: PlanResult[]; enhanced: boolean }> { // Plans are local to the machine, no Desktop integration needed const plans = await this.claudeCodeEngine.searchPlans(query, limit || 10); return { source: 'claude-code', results: plans, enhanced: false, }; } private generateSessionSummary(messages: any[], focus: string): string { const insights = { messageCount: messages.length, toolsUsed: new Set<string>(), filesReferenced: new Set<string>(), accomplishments: new Set<string>(), errors: new Set<string>(), solutions: new Set<string>(), }; messages.forEach((msg) => { msg.context?.toolsUsed?.forEach((tool: string) => { if (tool && tool.length > 1) insights.toolsUsed.add(tool); }); msg.context?.filesReferenced?.forEach((file: string) => { if (file && file.length > 3) insights.filesReferenced.add(file); }); const content = msg.content.toLowerCase(); const fullContent = msg.content; // Extract errors with more context if (content.includes('error') || content.includes('failed')) { insights.errors.add(fullContent.substring(0, 200)); } // Extract solutions/fixes with more context if ( content.includes('solution') || content.includes('fixed') || content.includes('resolved') ) { insights.solutions.add(fullContent.substring(0, 200)); } // Extract accomplishments - look for completion indicators if ( content.includes('completed') || content.includes('created') || content.includes('implemented') || content.includes('built') || content.includes('finished') || content.includes('committed') ) { // Extract a meaningful snippet const snippet = fullContent.substring(0, 150); insights.accomplishments.add(snippet); } }); let summary = `Smart Summary (${insights.messageCount} msgs)\n\n`; switch (focus) { case 'solutions': if (insights.solutions.size > 0) { summary += `**Solutions:** ${Array.from(insights.solutions).slice(0, 2).join('\n')}\n`; } break; case 'tools': if (insights.toolsUsed.size > 0) { summary += `**Tools:** ${Array.from(insights.toolsUsed).slice(0, 5).join(', ')}\n`; } break; case 'files': if (insights.filesReferenced.size > 0) { summary += `**Files:** ${Array.from(insights.filesReferenced).slice(0, 5).join(', ')}\n`; } break; default: // All focus - show tools, files, and accomplishments if (insights.toolsUsed.size > 0) { summary += `**Tools:** ${Array.from(insights.toolsUsed).slice(0, 4).join(', ')}\n`; } if (insights.filesReferenced.size > 0) { summary += `**Files:** ${Array.from(insights.filesReferenced).slice(0, 3).join(', ')}\n`; } if (insights.accomplishments.size > 0) { summary += `**Accomplishments:** ${Array.from(insights.accomplishments).slice(0, 2).join(' | ')}\n`; } if (insights.solutions.size > 0) { summary += `**Solutions:** ${Array.from(insights.solutions).slice(0, 1).join('')}\n`; } } return summary; } // Enhanced Desktop content extraction methods private extractCleanDesktopContent(rawSnippet: string, query: string): string | null { try { // Remove binary junk and extract readable sentences const cleaned = rawSnippet.replace(/[^\x20-\x7E\n]/g, ' '); // Extract sentences that contain the query or are near it const sentences = cleaned.split(/[.!?]+/).filter((s) => s.trim().length > 10); const queryLower = query.toLowerCase(); // Find sentences containing the query const relevantSentences = sentences.filter((sentence) => sentence.toLowerCase().includes(queryLower) ); if (relevantSentences.length > 0) { // Get the best sentence and clean it up const bestSentence = relevantSentences[0].trim(); return this.cleanupDesktopSentence(bestSentence, query); } // Fallback: extract text around the query const queryIndex = cleaned.toLowerCase().indexOf(queryLower); if (queryIndex !== -1) { const start = Math.max(0, queryIndex - 50); const end = Math.min(cleaned.length, queryIndex + 150); const contextSnippet = cleaned.substring(start, end).trim(); return this.cleanupDesktopSentence(contextSnippet, query); } return null; } catch (error) { return null; } } private cleanupDesktopSentence(sentence: string, query: string): string { // Remove excessive spaces and cleanup artifacts let cleaned = sentence.replace(/\s+/g, ' ').trim(); // Remove common LevelDB artifacts cleaned = cleaned.replace(/[{}\\'"]+/g, ' '); cleaned = cleaned.replace(/\d{13,}/g, ''); // Remove timestamps cleaned = cleaned.replace(/[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/gi, ''); // Remove UUIDs // Final cleanup cleaned = cleaned.replace(/\s+/g, ' ').trim(); // Ensure the query is preserved and highlighted context is meaningful const queryIndex = cleaned.toLowerCase().indexOf(query.toLowerCase()); if (queryIndex !== -1) { // Extract a meaningful window around the query const start = Math.max(0, queryIndex - 20); const end = Math.min(cleaned.length, queryIndex + query.length + 80); const result = cleaned.substring(start, end).trim(); // Only return if it's a meaningful sentence if (result.length > 15 && !result.match(/^[\s\W]+$/)) { return result; } } return cleaned.length > 15 ? cleaned : ''; } private calculateDesktopRelevanceScore(content: string, query: string): number { let score = 0; const contentLower = content.toLowerCase(); const queryLower = query.toLowerCase(); // Exact query match if (contentLower.includes(queryLower)) { score += 10; } // Word matches const queryWords = queryLower.split(/\s+/); const contentWords = contentLower.split(/\s+/); const matchingWords = queryWords.filter((word) => contentWords.some((cWord) => cWord.includes(word)) ); score += matchingWords.length * 3; // Desktop content gets bonus for being rare/valuable score += 8; // Extra bonus for Desktop content with exact query match if (contentLower.includes(queryLower)) { score += 5; // Desktop exact matches get priority } // Penalize very short or garbled content if (content.length < 30) score -= 5; const nonWordMatches = content.match(/[^\w\s.,!?-]/g); if (nonWordMatches && nonWordMatches.length > content.length * 0.3) score -= 3; return Math.max(0, score); } private extractToolsFromMessages(messages: any[]): string[] { const tools = new Set<string>(); messages.forEach((msg) => { msg.context?.toolsUsed?.forEach((tool: string) => tools.add(tool)); }); return Array.from(tools).slice(0, 8); } private extractFilesFromMessages(messages: any[]): string[] { const files = new Set<string>(); messages.forEach((msg) => { msg.context?.filesReferenced?.forEach((file: string) => { const filename = file.split('/').pop() || file; if (filename.length > 2) files.add(filename); }); }); return Array.from(files).slice(0, 10); } // FIX 5: Extract accomplishments from messages - MINIMUM 15 chars, sentence validation private extractAccomplishmentsFromMessages(messages: any[]): string[] { const rawAccomplishments: string[] = []; // Helper: Validate accomplishment is a coherent phrase (15+ chars, has at least 2 words) const isValidAccomplishment = (text: string): boolean => { const trimmed = text.trim(); if (trimmed.length < 15) return false; // Minimum 15 chars const words = trimmed.split(/\s+/).filter((w) => w.length > 1); if (words.length < 2) return false; // At least 2 words // Reject if it's just file paths or code fragments if (/^[\/\.\w]+$/.test(trimmed)) return false; // Just a path if (/^[\*\`\#]+/.test(trimmed)) return false; // Markdown artifacts return true; }; for (const msg of messages) { if (msg.type !== 'assistant') continue; const content = msg.content; // Pattern 1: Tool completion statements ("I've used X tool to...") const toolCompleteMatch = content.match( /(?:I've|I have|Just|Successfully)\s+(?:used|called|ran|executed)\s+(?:the\s+)?(\w+)\s+tool\s+to\s+([^.]{15,100})/i ); if (toolCompleteMatch) { rawAccomplishments.push(`${toolCompleteMatch[1]}: ${toolCompleteMatch[2].trim()}`); } // Pattern 2: "Done:" or completion markers - 15+ char minimum const doneMatch = content.match(/(?:Done|Complete|Finished)[:.!]\s*([^.\n]{15,100})/i); if (doneMatch) { rawAccomplishments.push(doneMatch[1].trim()); } // Pattern 3: "Now X is..." completion statements - 15+ char combined const nowIsMatch = content.match( /Now\s+(?:the\s+)?(\w+)\s+(?:is|are|has|have|works?)\s+([^.]{10,80})/i ); if (nowIsMatch && nowIsMatch[1].length + nowIsMatch[2].length > 12) { rawAccomplishments.push(`${nowIsMatch[1]} ${nowIsMatch[2].trim()}`); } // Pattern 4: BROADER action verbs - 15+ char minimum const actionMatch = content.match( /(?:Made|Updated|Fixed|Changed|Created|Added|Removed|Refactored|Implemented|Resolved)\s+(?:the\s+)?([^.\n]{15,100})/i ); if (actionMatch) { rawAccomplishments.push(actionMatch[1].trim()); } // Pattern 5: "The X now..." statements const theNowMatch = content.match(/The\s+(\w+)\s+now\s+([^.]{10,80})/i); if (theNowMatch && theNowMatch[1].length + theNowMatch[2].length > 12) { rawAccomplishments.push(`${theNowMatch[1]} now ${theNowMatch[2].trim()}`); } // Git commits - multiple formats (commit messages are usually meaningful) const commitMatch1 = content.match(/git commit -m\s*["']([^"']{10,80})["']/i); if (commitMatch1) { rawAccomplishments.push(`Committed: ${commitMatch1[1]}`); } const commitMatch2 = content.match(/committed:?\s*["']?([^"'\n]{10,60})["']?/i); if (commitMatch2 && !commitMatch1) { rawAccomplishments.push(`Committed: ${commitMatch2[1]}`); } // Expanded patterns for "I've completed", "Successfully", etc. - 15+ chars const accomplishPattern1 = content.match( /(?:I've |I have |Successfully )(?:completed?|implemented?|fixed?|created?|added?|updated?|changed?):?\s*([^.\n]{15,100})/i ); if (accomplishPattern1) { rawAccomplishments.push(accomplishPattern1[1].trim()); } // Pattern for "completed the X" - 15+ chars const accomplishPattern2 = content.match( /(?:completed?|implemented?|fixed?|created?|built?|added?|updated?)\s+(?:the\s+)?([^.\n]{15,100})/i ); if (accomplishPattern2) { rawAccomplishments.push(accomplishPattern2[1].trim()); } // Test outcomes (these are always meaningful) const testCountMatch = content.match(/(\d+)\s*tests?\s*passed/i); if (testCountMatch) { rawAccomplishments.push(`${testCountMatch[1]} tests passed`); } const allTestsMatch = content.match(/all\s*tests?\s*(?:passed|succeeded)/i); if (allTestsMatch) { rawAccomplishments.push('All tests passed'); } // Build outcomes (always meaningful) const buildSuccessMatch = content.match(/build\s*(?:succeeded|completed|passed)/i); if (buildSuccessMatch) { rawAccomplishments.push('Build succeeded'); } const compileSuccessMatch = content.match(/(?:compiled|built)\s*successfully/i); if (compileSuccessMatch) { rawAccomplishments.push('Built successfully'); } // Tool-based fallback for ALL file-editing tools with actual file name const fileTools = ['Edit', 'Write', 'MultiEdit', 'NotebookEdit']; if ( msg.context?.toolsUsed?.some((t: string) => fileTools.includes(t)) && msg.context?.filesReferenced?.length ) { const file = msg.context.filesReferenced[0].split('/').pop(); if (file && file.length > 3) { // Skip short/invalid filenames rawAccomplishments.push(`Modified ${file}`); } } } // Look at tool_result messages for success indicators for (const msg of messages) { if (msg.type === 'tool_result' && msg.content && msg.content.length > 20) { if (msg.content.includes('✨ Done') || msg.content.includes('Successfully compiled')) { rawAccomplishments.push('Build completed'); } if (msg.content.match(/\d+\s+passing|\d+\s+passed|All tests passed/i)) { rawAccomplishments.push('Tests passed'); } // NEW: Extract from tool_result success messages const successMatch = msg.content.match( /(?:successfully|completed|done|finished)[:\s]+([^.\n]{15,80})/i ); if (successMatch) { rawAccomplishments.push(successMatch[1].trim()); } } } // Filter and deduplicate - only keep valid accomplishments const validAccomplishments = rawAccomplishments.filter(isValidAccomplishment); return [...new Set(validAccomplishments)].slice(0, 8); } private extractDecisionsFromMessages(messages: any[]): string[] { const decisions: string[] = []; for (const msg of messages) { if (msg.type !== 'assistant') continue; const content = msg.content; // Decision patterns const decisionPatterns = [ /(?:decided to|chose to|will use|going with|approach is)[\s:]+([^.\n]{20,100})/gi, /(?:best option|recommended|should use)[\s:]+([^.\n]{20,100})/gi, /(?:because|the reason)[\s:]+([^.\n]{20,100})/gi, ]; for (const pattern of decisionPatterns) { let match; while ((match = pattern.exec(content)) !== null) { if (match[1]) decisions.push(match[1].trim()); } } } return [...new Set(decisions)].slice(0, 3); } }

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/Vvkmnn/claude-historian'

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