import fs from "fs/promises"; import path from "path"; import os from "os"; import type { SessionEntry } from "../../shared/types.js"; interface IndexEntry { sessionId: string; summary?: string; firstPrompt?: string; created?: string; modified?: string; messageCount?: number; fullPath?: string; projectPath?: string; } const CLAUDE_PROJECTS_DIR = path.join(os.homedir(), ".claude", "projects"); export async function discoverSessions( projectsDir: string = CLAUDE_PROJECTS_DIR ): Promise { const sessions: SessionEntry[] = []; let projectDirs: string[]; try { projectDirs = await fs.readdir(projectsDir); } catch { return sessions; } // Parallel I/O: stat + readFile for all project dirs concurrently const results = await Promise.all( projectDirs.map(async (projectDir) => { const projectPath = path.join(projectsDir, projectDir); const entries: SessionEntry[] = []; let stat; try { stat = await fs.stat(projectPath); } catch { return entries; } if (!stat.isDirectory()) return entries; const indexPath = path.join(projectPath, "sessions-index.json"); try { const content = await fs.readFile(indexPath, "utf-8"); const parsed = JSON.parse(content); // Handle both formats: raw array or { version, entries: [...] } const rawEntries: IndexEntry[] = Array.isArray(parsed) ? parsed : parsed.entries ?? []; for (const entry of rawEntries) { const sessionPath = entry.fullPath || path.join(projectPath, `${entry.sessionId}.jsonl`); // Validate: reject paths with traversal segments or non-JSONL extensions. // Check the raw path for ".." before resolving (resolve normalizes them away). if (sessionPath.includes("..") || !sessionPath.endsWith(".jsonl")) { continue; } const resolved = path.resolve(sessionPath); // Containment check: reject paths that escape the projects directory. // A corrupted or malicious index could set fullPath to an arbitrary // absolute path like "/etc/shadow.jsonl". if (!resolved.startsWith(projectsDir + path.sep) && resolved !== projectsDir) { continue; } entries.push({ id: entry.sessionId, summary: entry.summary || "", firstPrompt: entry.firstPrompt || "", project: projectDir, created: entry.created || "", modified: entry.modified || "", messageCount: entry.messageCount || 0, path: resolved, }); } } catch { // Missing or corrupt index - skip } return entries; }) ); for (const entries of results) { sessions.push(...entries); } sessions.sort((a, b) => { const dateA = new Date(a.modified || a.created || 0).getTime() || 0; const dateB = new Date(b.modified || b.created || 0).getTime() || 0; return dateB - dateA; }); return sessions; }