claude-sesh
Version:
Session explorer for Claude Code - find, search, and resume all your past sessions
302 lines • 11.5 kB
JavaScript
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