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
JavaScript
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('/');
}