claude-sesh
Version:
Session explorer for Claude Code - find, search, and resume all your past sessions
325 lines • 12.2 kB
JavaScript
import { parser, formatModelName } from './parser.js';
class CardsService {
formatTokens(tokens) {
if (tokens < 1000)
return tokens.toString();
if (tokens < 1000000)
return `${(tokens / 1000).toFixed(1)}K`;
return `${(tokens / 1000000).toFixed(2)}M`;
}
formatDuration(seconds) {
const hours = Math.floor(seconds / 3600);
const mins = Math.floor((seconds % 3600) / 60);
if (hours === 0)
return `${mins}m`;
return `${hours}h ${mins}m`;
}
formatHours(seconds) {
const hours = Math.floor(seconds / 3600);
if (hours === 0)
return '<1h';
return `${hours}h`;
}
getSessionsInPeriod(period) {
const sessions = parser.getAllSessions(10000);
const now = new Date();
if (period === 'all')
return sessions;
const cutoff = new Date();
if (period === 'week')
cutoff.setDate(now.getDate() - 7);
if (period === 'month')
cutoff.setMonth(now.getMonth() - 1);
if (period === 'year')
cutoff.setFullYear(now.getFullYear() - 1);
return sessions.filter(s => s.startedAt >= cutoff);
}
getPeriodLabel(period) {
const now = new Date();
if (period === 'week')
return 'This Week';
if (period === 'month')
return now.toLocaleDateString('en-US', { month: 'long', year: 'numeric' });
if (period === 'year')
return now.getFullYear().toString();
return 'All Time';
}
getSummaryCard(period = 'month') {
const sessions = this.getSessionsInPeriod(period);
const totalTokens = sessions.reduce((sum, s) => sum + s.totalTokens, 0);
const totalCost = sessions.reduce((sum, s) => sum + s.estimatedCostUsd, 0);
const totalDuration = sessions.reduce((sum, s) => sum + s.durationSeconds, 0);
const projects = new Set(sessions.map(s => s.projectName));
const files = new Set([
...sessions.flatMap(s => s.filesWritten || []),
...sessions.flatMap(s => s.filesEdited || [])
]);
return {
type: 'summary',
title: 'CLAUDE CODE STATS',
subtitle: `${sessions.length} coding sessions`,
period: this.getPeriodLabel(period),
stats: {
totalSessions: sessions.length,
totalTokens: this.formatTokens(totalTokens),
totalCost: `$${totalCost.toFixed(2)}`,
totalHours: this.formatHours(totalDuration),
projectCount: projects.size,
filesChanged: files.size,
},
};
}
getLanguagesCard(period = 'month') {
const sessions = this.getSessionsInPeriod(period);
// Extract languages from file extensions
const langCounts = new Map();
const extToLang = {
'.ts': 'TypeScript',
'.tsx': 'TypeScript',
'.js': 'JavaScript',
'.jsx': 'JavaScript',
'.py': 'Python',
'.rs': 'Rust',
'.go': 'Go',
'.rb': 'Ruby',
'.java': 'Java',
'.kt': 'Kotlin',
'.swift': 'Swift',
'.cpp': 'C++',
'.c': 'C',
'.cs': 'C#',
'.php': 'PHP',
'.vue': 'Vue',
'.svelte': 'Svelte',
'.html': 'HTML',
'.css': 'CSS',
'.scss': 'SCSS',
'.json': 'JSON',
'.yaml': 'YAML',
'.yml': 'YAML',
'.md': 'Markdown',
'.sql': 'SQL',
'.sh': 'Shell',
'.bash': 'Shell',
};
const langColors = {
'TypeScript': '#3178C6',
'JavaScript': '#F7DF1E',
'Python': '#3776AB',
'Rust': '#DEA584',
'Go': '#00ADD8',
'Ruby': '#CC342D',
'Java': '#ED8B00',
'Kotlin': '#7F52FF',
'Swift': '#F05138',
'C++': '#00599C',
'C': '#A8B9CC',
'C#': '#512BD4',
'PHP': '#777BB4',
'Vue': '#4FC08D',
'Svelte': '#FF3E00',
'HTML': '#E34F26',
'CSS': '#1572B6',
'SCSS': '#CC6699',
'JSON': '#292929',
'YAML': '#CB171E',
'Markdown': '#083FA1',
'SQL': '#4479A1',
'Shell': '#89E051',
};
const allFiles = [
...sessions.flatMap(s => s.filesWritten || []),
...sessions.flatMap(s => s.filesEdited || [])
];
for (const file of allFiles) {
const ext = '.' + file.split('.').pop()?.toLowerCase();
const lang = extToLang[ext] || 'Other';
langCounts.set(lang, (langCounts.get(lang) || 0) + 1);
}
const total = Array.from(langCounts.values()).reduce((sum, n) => sum + n, 0);
const sorted = Array.from(langCounts.entries())
.sort((a, b) => b[1] - a[1])
.slice(0, 6)
.map(([name, count]) => ({
name,
percentage: Math.round((count / total) * 100),
color: langColors[name] || '#888888',
}));
return {
type: 'languages',
title: 'POLYGLOT DEVELOPER',
subtitle: `${langCounts.size} languages used`,
period: this.getPeriodLabel(period),
stats: {
languageCount: langCounts.size,
},
extras: {
languages: sorted,
},
};
}
getRhythmCard(period = 'month') {
const sessions = this.getSessionsInPeriod(period);
// Calculate hourly activity (0-23)
const hourlyActivity = new Array(24).fill(0);
// Calculate daily activity (0-6, Sunday = 0)
const dailyActivity = new Array(7).fill(0);
for (const session of sessions) {
const hour = session.startedAt.getHours();
const day = session.startedAt.getDay();
hourlyActivity[hour] += session.totalTokens;
dailyActivity[day] += session.totalTokens;
}
// Find peak hour
const peakHourIndex = hourlyActivity.indexOf(Math.max(...hourlyActivity));
const peakHour = this.formatHourRange(peakHourIndex);
// Find peak day
const dayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
const peakDayIndex = dailyActivity.indexOf(Math.max(...dailyActivity));
const peakDay = dayNames[peakDayIndex];
// Determine archetype based on peak hour
let archetype;
let archetypeEmoji;
if (peakHourIndex >= 5 && peakHourIndex < 9) {
archetype = 'Early Bird';
archetypeEmoji = '🐦';
}
else if (peakHourIndex >= 9 && peakHourIndex < 17) {
archetype = 'Day Worker';
archetypeEmoji = '☀️';
}
else if (peakHourIndex >= 17 && peakHourIndex < 21) {
archetype = 'Evening Coder';
archetypeEmoji = '🌆';
}
else {
archetype = 'Night Owl';
archetypeEmoji = '🦉';
}
return {
type: 'rhythm',
title: 'YOUR CODING RHYTHM',
subtitle: `Peak productivity patterns`,
period: this.getPeriodLabel(period),
stats: {
peakHour,
peakDay,
archetype,
archetypeEmoji,
},
extras: {
hourlyActivity,
dailyActivity,
},
};
}
formatHourRange(hour) {
const formatHour = (h) => {
if (h === 0 || h === 12)
return h === 0 ? '12am' : '12pm';
return h < 12 ? `${h}am` : `${h - 12}pm`;
};
return `${formatHour(hour)} - ${formatHour((hour + 1) % 24)}`;
}
getAchievementCard() {
const allSessions = parser.getAllSessions(10000);
const totalTokens = allSessions.reduce((sum, s) => sum + s.totalTokens, 0);
const projects = new Set(allSessions.map(s => s.projectName));
// Calculate percentile (placeholder - would need actual comparison data)
// For now, use a simple heuristic based on sessions
let percentile;
if (allSessions.length >= 500)
percentile = 1;
else if (allSessions.length >= 200)
percentile = 5;
else if (allSessions.length >= 100)
percentile = 10;
else if (allSessions.length >= 50)
percentile = 20;
else if (allSessions.length >= 20)
percentile = 40;
else
percentile = 60;
return {
type: 'achievement',
title: `TOP ${percentile}% POWER USER`,
subtitle: `More productive than ${100 - percentile}% of Claude Code developers`,
period: 'All Time',
stats: {
percentile,
totalSessions: allSessions.length,
totalProjects: projects.size,
totalTokens: this.formatTokens(totalTokens),
},
};
}
getWrappedCard(period = 'month') {
const sessions = this.getSessionsInPeriod(period);
const totalTokens = sessions.reduce((sum, s) => sum + s.totalTokens, 0);
const totalCost = sessions.reduce((sum, s) => sum + s.estimatedCostUsd, 0);
const totalDuration = sessions.reduce((sum, s) => sum + s.durationSeconds, 0);
const projects = new Set(sessions.map(s => s.projectName));
const files = new Set([
...sessions.flatMap(s => s.filesWritten || []),
...sessions.flatMap(s => s.filesEdited || [])
]);
// Find top project by session count
const projectCounts = new Map();
for (const s of sessions) {
projectCounts.set(s.projectName, (projectCounts.get(s.projectName) || 0) + 1);
}
const topProject = Array.from(projectCounts.entries())
.sort((a, b) => b[1] - a[1])[0]?.[0] || 'N/A';
// Find top model
const modelCounts = new Map();
for (const s of sessions) {
const model = formatModelName(s.model);
modelCounts.set(model, (modelCounts.get(model) || 0) + 1);
}
const topModel = Array.from(modelCounts.entries())
.sort((a, b) => b[1] - a[1])[0]?.[0] || 'N/A';
// Achievement title
const hours = totalDuration / 3600;
let achievementTitle;
if (hours > 100)
achievementTitle = 'Code Legend';
else if (hours > 50)
achievementTitle = 'AI Power User';
else if (sessions.length > 100)
achievementTitle = 'Prolific Coder';
else if (projects.size > 10)
achievementTitle = 'Project Juggler';
else if (files.size > 500)
achievementTitle = 'Refactoring Champion';
else
achievementTitle = 'Rising Star';
return {
type: 'wrapped',
title: `${this.getPeriodLabel(period).toUpperCase()} WRAPPED`,
subtitle: 'Your Claude Code journey',
period: this.getPeriodLabel(period),
stats: {
sessions: sessions.length,
hours: this.formatHours(totalDuration),
tokens: this.formatTokens(totalTokens),
cost: `$${totalCost.toFixed(2)}`,
projects: projects.size,
files: files.size,
topProject,
topModel,
achievementTitle,
},
};
}
getAllCards(period = 'month') {
return {
summary: this.getSummaryCard(period),
languages: this.getLanguagesCard(period),
rhythm: this.getRhythmCard(period),
achievement: this.getAchievementCard(),
wrapped: this.getWrappedCard(period),
};
}
}
export const cardsService = new CardsService();
//# sourceMappingURL=cards.js.map