UNPKG

claude-sesh

Version:

Session explorer for Claude Code - find, search, and resume all your past sessions

302 lines 11.5 kB
import { execSync } from 'child_process'; import { basename } from 'path'; import db from '../db/database.js'; export class SessionService { currentSessionId = null; // Get git info for a directory getGitInfo(cwd) { try { const branch = execSync('git rev-parse --abbrev-ref HEAD', { cwd, encoding: 'utf-8' }).trim(); const commit = execSync('git rev-parse --short HEAD', { cwd, encoding: 'utf-8' }).trim(); return { branch, commit }; } catch { return { branch: null, commit: null }; } } // Start a new session startSession(projectPath) { const gitInfo = this.getGitInfo(projectPath); const session = db.createSession({ id: crypto.randomUUID(), project_path: projectPath, project_name: basename(projectPath), started_at: new Date().toISOString(), ended_at: null, duration_seconds: null, status: 'active', input_tokens: 0, output_tokens: 0, total_tokens: 0, estimated_cost_usd: 0, git_branch: gitInfo.branch, git_commit_start: gitInfo.commit, git_commit_end: null, files_changed: 0, summary: null, key_decisions: null, problems_solved: null, next_steps: null, }); this.currentSessionId = session.id; return session; } // Get or create active session getOrCreateSession(projectPath) { const existing = db.getActiveSession(projectPath); if (existing) { this.currentSessionId = existing.id; return existing; } return this.startSession(projectPath); } // End current session async endSession(sessionId) { const id = sessionId || this.currentSessionId; if (!id) return null; const gitInfo = this.getGitInfo(db.getSession(id)?.project_path || '.'); // Generate summary using Agent SDK (we'll implement this later) const summary = await this.generateSessionSummary(id); db.endSession(id, { summary: summary?.narrative, key_decisions: summary?.key_accomplishments.join('\n'), problems_solved: summary?.knowledge_gained.join('\n'), next_steps: summary?.resume_context, }); if (gitInfo.commit) { db.updateSession(id, { git_commit_end: gitInfo.commit }); } this.currentSessionId = null; return db.getSession(id); } // Record a user prompt recordPrompt(content, tokensUsed = 0) { if (!this.currentSessionId) return null; return db.addEvent({ session_id: this.currentSessionId, timestamp: new Date().toISOString(), event_type: 'prompt', content, metadata: {}, tool_name: null, tool_input: null, tool_output: null, tokens_used: tokensUsed, }); } // Record a tool use recordToolUse(toolName, input, output, tokensUsed = 0) { if (!this.currentSessionId) return null; return db.addEvent({ session_id: this.currentSessionId, timestamp: new Date().toISOString(), event_type: 'tool_use', content: `Used ${toolName}`, metadata: {}, tool_name: toolName, tool_input: input, tool_output: output, tokens_used: tokensUsed, }); } // Record a decision recordDecision(title, description, reasoning, options) { if (!this.currentSessionId) return null; return db.addDecision({ session_id: this.currentSessionId, timestamp: new Date().toISOString(), title, description, context: options?.context || '', alternatives_considered: options?.alternatives || null, reasoning, files_affected: options?.files || [], importance: options?.importance || 'medium', }); } // Record a problem recordProblem(title, description, errorMessage, stackTrace) { if (!this.currentSessionId) return null; return db.addProblem({ session_id: this.currentSessionId, timestamp: new Date().toISOString(), title, description, error_message: errorMessage || null, stack_trace: stackTrace || null, resolution: null, resolved_at: null, time_to_resolve_seconds: null, }); } // Resolve a problem resolveProblem(problemId, resolution) { db.resolveProblem(problemId, resolution); } // Record a file change recordFileChange(filePath, changeType, linesAdded = 0, linesRemoved = 0, reason) { if (!this.currentSessionId) return null; return db.addFileChange({ session_id: this.currentSessionId, timestamp: new Date().toISOString(), file_path: filePath, change_type: changeType, lines_added: linesAdded, lines_removed: linesRemoved, reason: reason || null, }); } // Update token counts updateTokens(inputTokens, outputTokens) { if (!this.currentSessionId) return; const session = db.getSession(this.currentSessionId); if (!session) return; // Claude pricing (approximate) const inputCost = inputTokens * 0.000003; // $3 per 1M input tokens const outputCost = outputTokens * 0.000015; // $15 per 1M output tokens db.updateSession(this.currentSessionId, { input_tokens: (session.input_tokens || 0) + inputTokens, output_tokens: (session.output_tokens || 0) + outputTokens, total_tokens: (session.total_tokens || 0) + inputTokens + outputTokens, estimated_cost_usd: (session.estimated_cost_usd || 0) + inputCost + outputCost, }); } // Generate session summary using Agent SDK async generateSessionSummary(sessionId) { const session = db.getSession(sessionId); if (!session) return null; const events = db.getSessionEvents(sessionId); const decisions = db.getSessionDecisions(sessionId); const problems = db.getSessionProblems(sessionId); const fileChanges = db.getSessionFileChanges(sessionId); // For now, generate a simple summary // TODO: Use Agent SDK for AI-powered summary const accomplishments = decisions.map(d => d.title); const knowledge = problems .filter(p => p.resolution) .map(p => `${p.title}: ${p.resolution}`); const narrative = this.buildNarrative(session, events, decisions, problems, fileChanges); const resumeContext = this.buildResumeContext(session, decisions, problems, fileChanges); return { session, events_count: events.length, decisions, problems, file_changes: fileChanges, narrative, key_accomplishments: accomplishments, knowledge_gained: knowledge, resume_context: resumeContext, }; } buildNarrative(session, events, decisions, problems, fileChanges) { const duration = session.duration_seconds ? `${Math.floor(session.duration_seconds / 60)} minutes` : 'ongoing'; let narrative = `## Session: ${session.project_name}\n\n`; narrative += `**Duration:** ${duration}\n`; narrative += `**Tokens:** ${session.total_tokens.toLocaleString()}\n`; narrative += `**Files Changed:** ${fileChanges.length}\n\n`; if (decisions.length > 0) { narrative += `### Key Decisions\n\n`; decisions.forEach(d => { narrative += `- **${d.title}**: ${d.description}\n`; }); narrative += '\n'; } if (problems.length > 0) { narrative += `### Problems & Solutions\n\n`; problems.forEach(p => { narrative += `- **${p.title}**: ${p.resolution || 'Unresolved'}\n`; }); narrative += '\n'; } if (fileChanges.length > 0) { narrative += `### Files Modified\n\n`; fileChanges.slice(0, 10).forEach(f => { narrative += `- \`${f.file_path}\` (${f.change_type})\n`; }); if (fileChanges.length > 10) { narrative += `- ... and ${fileChanges.length - 10} more\n`; } } return narrative; } buildResumeContext(session, decisions, problems, fileChanges) { let context = `# Resume Context for ${session.project_name}\n\n`; context += `Last session: ${new Date(session.started_at).toLocaleString()}\n`; context += `Branch: ${session.git_branch || 'unknown'}\n\n`; context += `## What Was Done\n\n`; decisions.forEach(d => { context += `- ${d.title}: ${d.reasoning}\n`; }); const unresolvedProblems = problems.filter(p => !p.resolution); if (unresolvedProblems.length > 0) { context += `\n## Open Issues\n\n`; unresolvedProblems.forEach(p => { context += `- ${p.title}: ${p.description}\n`; }); } const recentFiles = fileChanges.slice(-5); if (recentFiles.length > 0) { context += `\n## Recently Modified Files\n\n`; recentFiles.forEach(f => { context += `- ${f.file_path}\n`; }); } return context; } // Get session for resuming getResumableSession(projectPath) { const sessions = db.getSessionsByProject(projectPath, 1); if (sessions.length === 0) return null; return this.generateSessionSummarySync(sessions[0].id); } generateSessionSummarySync(sessionId) { const session = db.getSession(sessionId); if (!session) return null; const events = db.getSessionEvents(sessionId); const decisions = db.getSessionDecisions(sessionId); const problems = db.getSessionProblems(sessionId); const fileChanges = db.getSessionFileChanges(sessionId); const narrative = this.buildNarrative(session, events, decisions, problems, fileChanges); const resumeContext = this.buildResumeContext(session, decisions, problems, fileChanges); return { session, events_count: events.length, decisions, problems, file_changes: fileChanges, narrative, key_accomplishments: decisions.map(d => d.title), knowledge_gained: problems.filter(p => p.resolution).map(p => `${p.title}: ${p.resolution}`), resume_context: resumeContext, }; } // Get all sessions getAllSessions(limit = 50) { return db.getRecentSessions(limit); } // Search sessions searchSessions(query) { return db.searchSessions(query); } // Get stats getStats() { return db.getStats(); } } export const sessionService = new SessionService(); export default sessionService; //# sourceMappingURL=session.js.map