UNPKG

cc-inspect-mcp

Version:

MCP server for inspecting and searching Claude Code conversation history with boolean search support

240 lines (239 loc) 9.55 kB
import { BooleanQueryParser, BooleanQueryEvaluator } from './booleanParser.js'; export class SearchEngine { booleanParser = new BooleanQueryParser(); booleanEvaluator = new BooleanQueryEvaluator(); searchProjects(projects, params) { let filtered = [...projects]; // Filter by query if (params.query) { // Try boolean search first let useBooleanSearch = false; let booleanQuery; try { if (/\b(AND|OR|NOT)\b|[()"-]/.test(params.query)) { booleanQuery = this.booleanParser.parse(params.query); useBooleanSearch = true; } } catch (e) { // Fall back to simple search } if (useBooleanSearch && booleanQuery) { filtered = filtered.filter(p => { const searchText = `${p.name} ${p.path}`; return this.booleanEvaluator.evaluate(booleanQuery, searchText); }); } else { // Simple search const query = params.query.toLowerCase(); filtered = filtered.filter(p => p.name.toLowerCase().includes(query) || p.path.toLowerCase().includes(query)); } } // Filter by time range if (params.startTime || params.endTime) { filtered = filtered.filter(p => { if (params.startTime && p.lastMessageTime < params.startTime) return false; if (params.endTime && p.firstMessageTime > params.endTime) return false; return true; }); } // Sort const sortBy = params.sortBy || 'recent'; filtered.sort((a, b) => { switch (sortBy) { case 'recent': return b.lastMessageTime.localeCompare(a.lastMessageTime); case 'messageCount': return b.messageCount - a.messageCount; case 'name': return a.name.localeCompare(b.name); default: return 0; } }); // Paginate const limit = params.limit || 50; const offset = params.offset || 0; const paged = filtered.slice(offset, offset + limit); return { items: paged, totalCount: filtered.length, hasMore: offset + limit < filtered.length, nextOffset: offset + limit < filtered.length ? offset + limit : undefined }; } searchMessages(messages, sessions, params) { let filtered = [...messages]; // Handle session-specific search with index range if (params.sessionId) { const sessionMessages = sessions.get(params.sessionId); if (sessionMessages) { if (params.sessionStartIndex !== undefined || params.sessionEndIndex !== undefined) { const start = params.sessionStartIndex || 0; const end = params.sessionEndIndex || sessionMessages.length - 1; filtered = sessionMessages.slice(start, end + 1); } else { filtered = sessionMessages; } } else { filtered = []; } } // Filter by project if (params.projectPath) { filtered = filtered.filter(m => m.projectPath === params.projectPath); } // Filter by message type if (params.messageType && params.messageType !== 'all') { filtered = filtered.filter(m => m.type === params.messageType); } // Filter by time range if (params.startTime || params.endTime) { filtered = filtered.filter(m => { if (params.startTime && m.timestamp < params.startTime) return false; if (params.endTime && m.timestamp > params.endTime) return false; return true; }); } // Search query if (params.query) { // Try to parse as boolean query first let useBooleanSearch = false; let booleanQuery; let queryTerms = []; try { // Check if query contains boolean operators if (/\b(AND|OR|NOT)\b|[()"-]/.test(params.query)) { booleanQuery = this.booleanParser.parse(params.query); queryTerms = this.booleanEvaluator.extractTerms(booleanQuery); useBooleanSearch = true; } } catch (e) { // Fall back to simple search if parsing fails console.error('Boolean query parse error:', e); } if (useBooleanSearch && booleanQuery) { // Boolean search filtered = filtered.filter(m => { return this.booleanEvaluator.evaluate(booleanQuery, m.content); }); // Sort by relevance using extracted terms if (queryTerms.length > 0) { filtered.sort((a, b) => { const scoreA = this.scoreMessage(a, queryTerms); const scoreB = this.scoreMessage(b, queryTerms); return scoreB - scoreA; }); } } else { // Simple search (backward compatible) const query = params.query.toLowerCase(); const queryWords = query.split(/\s+/).filter(w => w.length > 0); filtered = filtered.filter(m => { const content = m.content.toLowerCase(); // Match if all query words are found return queryWords.every(word => content.includes(word)); }); // Sort by relevance filtered.sort((a, b) => { const scoreA = this.scoreMessage(a, queryWords); const scoreB = this.scoreMessage(b, queryWords); return scoreB - scoreA; }); } } else { // Sort by timestamp if no query filtered.sort((a, b) => { if (!a.timestamp || !b.timestamp) return 0; return b.timestamp.localeCompare(a.timestamp); }); } // Paginate const limit = params.limit || 50; const offset = params.offset || 0; const paged = filtered.slice(offset, offset + limit); return { items: paged, totalCount: filtered.length, hasMore: offset + limit < filtered.length, nextOffset: offset + limit < filtered.length ? offset + limit : undefined }; } scoreMessage(message, queryWords) { const content = message.content.toLowerCase(); let score = 0; for (const word of queryWords) { // Count occurrences const regex = new RegExp(`\\b${word}\\b`, 'g'); const matches = content.match(regex); if (matches) { score += matches.length; } } // Boost for exact phrase match const fullQuery = queryWords.join(' '); if (content.includes(fullQuery)) { score += queryWords.length * 2; } // Slight boost for user messages (questions often more relevant) if (message.type === 'user') { score *= 1.1; } return score; } getActivitySummary(messages, params) { // Filter by time range const filtered = messages.filter(m => m.timestamp >= params.startTime && m.timestamp <= params.endTime); const breakdown = new Map(); for (const message of filtered) { let key; switch (params.groupBy) { case 'project': key = message.projectName; break; case 'session': key = message.sessionId; break; case 'hour': const date = new Date(message.timestamp); key = `${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2, '0')}-${String(date.getUTCDate()).padStart(2, '0')} ${String(date.getUTCHours()).padStart(2, '0')}:00`; break; } const existing = breakdown.get(key) || { messageCount: 0, userMessageCount: 0, tools: new Set() }; existing.messageCount++; if (message.type === 'user') { existing.userMessageCount++; } if (message.toolsUsed) { message.toolsUsed.forEach(tool => existing.tools.add(tool)); } breakdown.set(key, existing); } return { totalMessages: filtered.length, breakdown: Array.from(breakdown.entries()).map(([key, data]) => ({ key, messageCount: data.messageCount, userMessageCount: data.userMessageCount, toolsUsed: data.tools.size > 0 ? Array.from(data.tools) : undefined })) }; } }