Skip to main content
Glama
universal-engine.ts53.4 kB
import { HistorySearchEngine } from './search.js'; import { SearchResult, FileContext, ErrorSolution, CompactMessage } 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); const sessionData = allSessions.find(s => s.session_id === sessionId || s.session_id.startsWith(sessionId) || sessionId.includes(s.session_id) || s.session_id.includes(sessionId.replace(/^.*\//, '')) ); if (!sessionData) { return { source: 'claude-code', results: { sessionId, summary: `No session found for ID: ${sessionId}`, messageCount: 0, focus: focus || 'all' } as any, enhanced: false }; } const messages = await this.claudeCodeEngine.getSessionMessages(sessionData.project_dir, sessionData.session_id); const sessionMessages = messages.slice(0, maxMessages || 10); const summary = { sessionId, summary: this.generateSessionSummary(sessionMessages, focus || 'all'), messageCount: sessionMessages.length, focus: focus || 'all' }; if (!this.claudeDesktopAvailable) { return { source: 'claude-code', results: summary as any, enhanced: false }; } // For summaries, Desktop could provide additional context in the future return { source: 'claude-code', results: summary as any, enhanced: this.claudeDesktopAvailable }; } private generateSessionSummary(messages: any[], focus: string): string { const insights = { messageCount: messages.length, toolsUsed: new Set<string>(), filesReferenced: new Set<string>(), outcomes: 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(); if (content.includes('error') || content.includes('failed')) { insights.errors.add(msg.content.substring(0, 100)); } if (content.includes('solution') || content.includes('fixed')) { insights.solutions.add(msg.content.substring(0, 100)); } }); 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`; } break; case 'tools': if (insights.toolsUsed.size > 0) { summary += `**Tools:** ${Array.from(insights.toolsUsed).slice(0, 4).join(', ')}\n`; } break; case 'files': if (insights.filesReferenced.size > 0) { summary += `**Files:** ${Array.from(insights.filesReferenced).slice(0, 3).join(', ')}\n`; } break; default: if (insights.toolsUsed.size > 0) { summary += `**Tools:** ${Array.from(insights.toolsUsed).slice(0, 3).join(', ')}\n`; } if (insights.filesReferenced.size > 0) { summary += `**Files:** ${Array.from(insights.filesReferenced).slice(0, 2).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 let 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); } }

Implementation Reference

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