// // sessions-store.js — reading Claude Code's own session history. // // Claude Code writes one JSONL transcript per session under // ~/.claude/projects//.jsonl, where the slug is the project // directory with path separators and dots flattened to dashes. Listing that // directory is enough to offer a "resume" picker without shelling out to the // interactive one. // const { log, oneLine } = require("./util.js"); function projectSlug(path) { return String(path).replace(/[/.]/g, "-"); } function projectDirectory(workspacePath) { const home = nova.environment["HOME"]; if (!home || !workspacePath) return null; return nova.path.join(home, ".claude", "projects", projectSlug(workspacePath)); } /** First user prompt in a transcript, used as the picker label. */ function firstPrompt(filePath) { let file = null; try { file = nova.fs.open(filePath, "r"); for (let i = 0; i < 40; i++) { const line = file.readline(); if (!line) break; let record; try { record = JSON.parse(line); } catch (_) { continue; } if (record.type !== "user" || !record.message) continue; if (record.isMeta || record.isSynthetic) continue; const content = record.message.content; if (typeof content === "string") { if (content.trim()) return oneLine(content, 70); } else if (Array.isArray(content)) { for (const block of content) { if (block && block.type === "text" && block.text && block.text.trim()) { return oneLine(block.text, 70); } } } } } catch (err) { log("could not read session transcript:", filePath, err); } finally { if (file) { try { file.close(); } catch (_) { /* already closed */ } } } return null; } /** * Recent sessions for a workspace, newest first. * Returns [{ id, path, modifiedAt, label }]. */ function recentSessions(workspacePath, limit = 15) { const directory = projectDirectory(workspacePath); if (!directory) return []; let names; try { if (!nova.fs.access(directory, nova.fs.R_OK)) return []; names = nova.fs.listdir(directory); } catch (err) { log("no session history:", err); return []; } const sessions = []; for (const name of names) { if (!name.endsWith(".jsonl")) continue; const filePath = nova.path.join(directory, name); const stats = nova.fs.stat(filePath); if (!stats || !stats.isFile() || stats.size === 0) continue; sessions.push({ id: name.slice(0, -".jsonl".length), path: filePath, modifiedAt: stats.mtime, size: stats.size, }); } sessions.sort((a, b) => b.modifiedAt.getTime() - a.modifiedAt.getTime()); const recent = sessions.slice(0, limit); for (const session of recent) { session.label = firstPrompt(session.path) || session.id.slice(0, 8); } return recent; } module.exports = { recentSessions, projectDirectory, projectSlug };