this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add SQLite database module

- Schema for session_summaries, daily_summaries, processed_files
- Query functions for days list, day detail, stats
- File processing tracking with hash-based change detection

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

alice 4ef7ca29 d8f9203d

+281
+281
src/core/db.ts
··· 1 + import { Database } from 'bun:sqlite'; 2 + import { join, dirname } from 'path'; 3 + import { mkdirSync, existsSync } from 'fs'; 4 + import type { 5 + DBSessionSummary, 6 + DBDailySummary, 7 + DBProcessedFile, 8 + SessionSummary, 9 + ParsedSession, 10 + SessionStats, 11 + DayListItem, 12 + DayDetail, 13 + ProjectDetail, 14 + SessionDetail, 15 + } from '../types'; 16 + 17 + const DATA_DIR = join(import.meta.dir, '../../data'); 18 + const DB_PATH = join(DATA_DIR, 'worklog.db'); 19 + 20 + let db: Database | null = null; 21 + 22 + export function getDb(): Database { 23 + if (!db) { 24 + if (!existsSync(DATA_DIR)) { 25 + mkdirSync(DATA_DIR, { recursive: true }); 26 + } 27 + db = new Database(DB_PATH); 28 + initSchema(); 29 + } 30 + return db; 31 + } 32 + 33 + function initSchema() { 34 + const database = db!; 35 + 36 + database.exec(` 37 + CREATE TABLE IF NOT EXISTS session_summaries ( 38 + id INTEGER PRIMARY KEY, 39 + session_id TEXT UNIQUE NOT NULL, 40 + project_path TEXT NOT NULL, 41 + project_name TEXT, 42 + git_branch TEXT, 43 + start_time TEXT NOT NULL, 44 + end_time TEXT NOT NULL, 45 + date TEXT NOT NULL, 46 + short_summary TEXT, 47 + accomplishments TEXT, 48 + tools_used TEXT, 49 + files_changed TEXT, 50 + stats TEXT, 51 + processed_at TEXT NOT NULL 52 + ); 53 + 54 + CREATE INDEX IF NOT EXISTS idx_session_date ON session_summaries(date); 55 + CREATE INDEX IF NOT EXISTS idx_session_project ON session_summaries(project_path); 56 + 57 + CREATE TABLE IF NOT EXISTS daily_summaries ( 58 + date TEXT PRIMARY KEY, 59 + brag_summary TEXT, 60 + projects_worked TEXT, 61 + total_sessions INTEGER, 62 + generated_at TEXT NOT NULL 63 + ); 64 + 65 + CREATE TABLE IF NOT EXISTS processed_files ( 66 + file_path TEXT PRIMARY KEY, 67 + file_hash TEXT NOT NULL, 68 + processed_at TEXT NOT NULL 69 + ); 70 + `); 71 + } 72 + 73 + // Processed files tracking 74 + export function isFileProcessed(filePath: string, fileHash: string): boolean { 75 + const database = getDb(); 76 + const row = database.query<DBProcessedFile, [string]>( 77 + 'SELECT * FROM processed_files WHERE file_path = ?' 78 + ).get(filePath); 79 + 80 + return row !== null && row.file_hash === fileHash; 81 + } 82 + 83 + export function markFileProcessed(filePath: string, fileHash: string): void { 84 + const database = getDb(); 85 + database.run( 86 + `INSERT OR REPLACE INTO processed_files (file_path, file_hash, processed_at) 87 + VALUES (?, ?, ?)`, 88 + [filePath, fileHash, new Date().toISOString()] 89 + ); 90 + } 91 + 92 + // Session summaries 93 + export function saveSessionSummary( 94 + session: ParsedSession, 95 + summary: SessionSummary 96 + ): void { 97 + const database = getDb(); 98 + database.run( 99 + `INSERT OR REPLACE INTO session_summaries 100 + (session_id, project_path, project_name, git_branch, start_time, end_time, date, 101 + short_summary, accomplishments, tools_used, files_changed, stats, processed_at) 102 + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, 103 + [ 104 + session.sessionId, 105 + session.projectPath, 106 + session.projectName, 107 + session.gitBranch, 108 + session.startTime, 109 + session.endTime, 110 + session.date, 111 + summary.shortSummary, 112 + JSON.stringify(summary.accomplishments), 113 + JSON.stringify(summary.toolsUsed), 114 + JSON.stringify(summary.filesChanged), 115 + JSON.stringify(session.stats), 116 + new Date().toISOString(), 117 + ] 118 + ); 119 + } 120 + 121 + // Daily summaries 122 + export function saveDailySummary( 123 + date: string, 124 + bragSummary: string, 125 + projectsWorked: string[], 126 + totalSessions: number 127 + ): void { 128 + const database = getDb(); 129 + database.run( 130 + `INSERT OR REPLACE INTO daily_summaries 131 + (date, brag_summary, projects_worked, total_sessions, generated_at) 132 + VALUES (?, ?, ?, ?, ?)`, 133 + [ 134 + date, 135 + bragSummary, 136 + JSON.stringify(projectsWorked), 137 + totalSessions, 138 + new Date().toISOString(), 139 + ] 140 + ); 141 + } 142 + 143 + // Query functions for API 144 + export function getDays(limit = 30): DayListItem[] { 145 + const database = getDb(); 146 + const rows = database.query< 147 + { date: string; project_count: number; session_count: number }, 148 + [number] 149 + >(` 150 + SELECT 151 + date, 152 + COUNT(DISTINCT project_path) as project_count, 153 + COUNT(*) as session_count 154 + FROM session_summaries 155 + GROUP BY date 156 + ORDER BY date DESC 157 + LIMIT ? 158 + `).all(limit); 159 + 160 + const dailySummaries = new Map<string, string>(); 161 + const summaryRows = database.query<{ date: string; brag_summary: string }, []>( 162 + 'SELECT date, brag_summary FROM daily_summaries' 163 + ).all(); 164 + for (const row of summaryRows) { 165 + dailySummaries.set(row.date, row.brag_summary); 166 + } 167 + 168 + return rows.map((row) => ({ 169 + date: row.date, 170 + projectCount: row.project_count, 171 + sessionCount: row.session_count, 172 + bragSummary: dailySummaries.get(row.date), 173 + })); 174 + } 175 + 176 + export function getDayDetail(date: string): DayDetail | null { 177 + const database = getDb(); 178 + const sessions = database.query<DBSessionSummary, [string]>( 179 + 'SELECT * FROM session_summaries WHERE date = ? ORDER BY start_time' 180 + ).all(date); 181 + 182 + if (sessions.length === 0) return null; 183 + 184 + const dailySummary = database.query<DBDailySummary, [string]>( 185 + 'SELECT * FROM daily_summaries WHERE date = ?' 186 + ).get(date); 187 + 188 + // Group by project 189 + const projectMap = new Map<string, SessionDetail[]>(); 190 + let totalTokens = 0; 191 + 192 + for (const session of sessions) { 193 + const stats: SessionStats = JSON.parse(session.stats || '{}'); 194 + totalTokens += (stats.totalInputTokens || 0) + (stats.totalOutputTokens || 0); 195 + 196 + const sessionDetail: SessionDetail = { 197 + sessionId: session.session_id, 198 + startTime: session.start_time, 199 + endTime: session.end_time, 200 + shortSummary: session.short_summary, 201 + accomplishments: JSON.parse(session.accomplishments || '[]'), 202 + filesChanged: JSON.parse(session.files_changed || '[]'), 203 + toolsUsed: JSON.parse(session.tools_used || '[]'), 204 + stats, 205 + }; 206 + 207 + const existing = projectMap.get(session.project_path) || []; 208 + existing.push(sessionDetail); 209 + projectMap.set(session.project_path, existing); 210 + } 211 + 212 + const projects: ProjectDetail[] = Array.from(projectMap.entries()).map( 213 + ([path, sessions]) => ({ 214 + name: sessions[0]?.sessionId ? path.split('/').pop() || path : path, 215 + path, 216 + sessions, 217 + }) 218 + ); 219 + 220 + // Get project name from first session 221 + for (const project of projects) { 222 + const firstSession = sessions.find((s) => s.project_path === project.path); 223 + if (firstSession?.project_name) { 224 + project.name = firstSession.project_name; 225 + } 226 + } 227 + 228 + return { 229 + date, 230 + bragSummary: dailySummary?.brag_summary, 231 + projects, 232 + stats: { 233 + totalSessions: sessions.length, 234 + totalTokens, 235 + }, 236 + }; 237 + } 238 + 239 + export function getStats(): { 240 + totalSessions: number; 241 + totalDays: number; 242 + totalProjects: number; 243 + } { 244 + const database = getDb(); 245 + const stats = database.query< 246 + { total_sessions: number; total_days: number; total_projects: number }, 247 + [] 248 + >(` 249 + SELECT 250 + COUNT(*) as total_sessions, 251 + COUNT(DISTINCT date) as total_days, 252 + COUNT(DISTINCT project_path) as total_projects 253 + FROM session_summaries 254 + `).get(); 255 + 256 + return { 257 + totalSessions: stats?.total_sessions || 0, 258 + totalDays: stats?.total_days || 0, 259 + totalProjects: stats?.total_projects || 0, 260 + }; 261 + } 262 + 263 + export function getSessionsForDate(date: string): DBSessionSummary[] { 264 + const database = getDb(); 265 + return database.query<DBSessionSummary, [string]>( 266 + 'SELECT * FROM session_summaries WHERE date = ? ORDER BY start_time' 267 + ).all(date); 268 + } 269 + 270 + export function getDatesWithoutBragSummary(): string[] { 271 + const database = getDb(); 272 + const rows = database.query<{ date: string }, []>(` 273 + SELECT DISTINCT s.date 274 + FROM session_summaries s 275 + LEFT JOIN daily_summaries d ON s.date = d.date 276 + WHERE d.date IS NULL 277 + ORDER BY s.date DESC 278 + `).all(); 279 + 280 + return rows.map((r) => r.date); 281 + }