UNPKG

cc-inspect-mcp

Version:

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

166 lines (165 loc) 6.21 kB
import { readFileSync } from 'fs'; import { join, basename } from 'path'; import { homedir } from 'os'; import { glob } from 'glob'; export class SessionLoader { claudeDir; messages = []; projects = new Map(); sessions = new Map(); excludedProjects; allowedProjects; enableLogging; constructor(claudeDir, excludedProjects, allowedProjects, enableLogging = true) { this.claudeDir = claudeDir || join(homedir(), '.claude', 'projects'); this.excludedProjects = new Set(excludedProjects || []); this.allowedProjects = allowedProjects ? new Set(allowedProjects) : null; this.enableLogging = enableLogging; } async loadSessions() { if (this.enableLogging) { console.error('Loading Claude Code sessions...'); } // Find all JSONL files const pattern = join(this.claudeDir, '**/*.jsonl'); const files = await glob(pattern); if (this.enableLogging) { console.error(`Found ${files.length} session files`); } for (const file of files) { try { await this.loadSessionFile(file); } catch (error) { if (this.enableLogging) { console.error(`Error loading ${file}:`, error); } } } if (this.enableLogging) { console.error(`Loaded ${this.messages.length} messages from ${this.projects.size} projects`); } } async loadSessionFile(filePath) { const projectFolder = basename(dirname(filePath)); const sessionId = basename(filePath, '.jsonl'); const content = readFileSync(filePath, 'utf-8'); const lines = content.split('\n').filter(line => line.trim()); const sessionMessages = []; for (let i = 0; i < lines.length; i++) { try { const rawMessage = JSON.parse(lines[i]); const message = this.parseMessage(rawMessage, projectFolder, i, lines.length); this.messages.push(message); sessionMessages.push(message); } catch (error) { if (this.enableLogging) { console.error(`Error parsing message in ${filePath} line ${i + 1}:`, error); } } } if (sessionMessages.length > 0) { this.sessions.set(sessionId, sessionMessages); this.updateProjectInfo(projectFolder, sessionMessages); } } parseMessage(raw, projectFolder, index, totalInSession) { const projectPath = raw.cwd || ''; const projectName = this.decodeProjectName(projectFolder); // Extract content let content = ''; const toolsUsed = []; // Ensure raw.message exists if (!raw.message) { content = '[No message content]'; } else if (raw.type === 'user') { content = typeof raw.message.content === 'string' ? raw.message.content : JSON.stringify(raw.message.content || ''); } else if (raw.type === 'assistant' && raw.message.content && Array.isArray(raw.message.content)) { const textParts = []; for (const item of raw.message.content) { if (item.type === 'text' && item.text) { textParts.push(item.text); } else if (item.type === 'tool_use' && item.name) { toolsUsed.push(item.name); textParts.push(`[Tool: ${item.name}]`); } } content = textParts.join(' '); } else if (raw.message && typeof raw.message.content === 'string') { content = raw.message.content; } return { uuid: raw.uuid, sessionId: raw.sessionId, sessionIndex: index, sessionMessageCount: totalInSession, timestamp: raw.timestamp, type: raw.type, content, projectPath, projectName, toolsUsed: toolsUsed.length > 0 ? toolsUsed : undefined, parentUuid: raw.parentUuid || undefined }; } decodeProjectName(folderName) { // Convert encoded folder name back to readable format // e.g., "-Users-x-Documents-code-project" -> "project" const parts = folderName.split('-').filter(p => p); return parts[parts.length - 1] || folderName; } updateProjectInfo(projectFolder, messages) { const projectName = this.decodeProjectName(projectFolder); const projectPath = messages[0]?.projectPath || ''; // Skip excluded projects if (this.excludedProjects.has(projectName)) { return; } // Skip if not in allowed list (when specified) if (this.allowedProjects && !this.allowedProjects.has(projectName)) { return; } const existing = this.projects.get(projectName) || { name: projectName, path: projectPath, messageCount: 0, sessionCount: 0, firstMessageTime: messages[0]?.timestamp || '', lastMessageTime: messages[0]?.timestamp || '' }; existing.messageCount += messages.length; existing.sessionCount += 1; // Update time range for (const msg of messages) { if (msg.timestamp < existing.firstMessageTime) { existing.firstMessageTime = msg.timestamp; } if (msg.timestamp > existing.lastMessageTime) { existing.lastMessageTime = msg.timestamp; } } this.projects.set(projectName, existing); } getMessages() { return this.messages; } getProjects() { return Array.from(this.projects.values()); } getSession(sessionId) { return this.sessions.get(sessionId) || []; } } // Helper to get dirname (not available in ES modules by default) function dirname(filePath) { const parts = filePath.split(/[/\\]/); parts.pop(); return parts.join('/'); }