this repo has no description
0
fork

Configure Feed

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

at 4e2e1998a4d99cb7c857981cb6e80ca1dab55d5e 361 lines 11 kB view raw
1import { findUnprocessedSessions, type SessionFile } from '../core/session-detector'; 2import { parseSessionFile } from '../core/session-reader'; 3import { summarizeSession, generateDailyBragSummary } from '../core/summarizer'; 4import { 5 markFileProcessed, 6 saveSessionSummary, 7 saveDailySummary, 8 getDatesWithoutBragSummary, 9 getSessionsForDate, 10} from '../core/db'; 11 12interface ProcessOptions { 13 force: boolean; 14 verbose: boolean; 15 date?: string; 16 week?: string; 17} 18 19// Get start and end of week (Monday-Sunday) for a given date 20function getWeekBounds(date: Date): { start: string; end: string } { 21 const d = new Date(date); 22 const day = d.getDay(); 23 const diffToMonday = d.getDate() - day + (day === 0 ? -6 : 1); 24 const monday = new Date(d.setDate(diffToMonday)); 25 const sunday = new Date(monday); 26 sunday.setDate(monday.getDate() + 6); 27 28 return { 29 start: monday.toISOString().split('T')[0], 30 end: sunday.toISOString().split('T')[0], 31 }; 32} 33 34// Parse date string, handling shortcuts like "today", "yesterday" 35function parseDate(dateStr: string): string { 36 const today = new Date(); 37 38 switch (dateStr.toLowerCase()) { 39 case 'today': 40 return today.toISOString().split('T')[0]; 41 case 'yesterday': { 42 const yesterday = new Date(today); 43 yesterday.setDate(yesterday.getDate() - 1); 44 return yesterday.toISOString().split('T')[0]; 45 } 46 default: 47 // Assume YYYY-MM-DD format 48 return dateStr; 49 } 50} 51 52// Parse week string, handling shortcuts like "thisweek", "lastweek" 53function parseWeek(weekStr: string): { start: string; end: string } { 54 const today = new Date(); 55 56 switch (weekStr.toLowerCase()) { 57 case 'thisweek': 58 case 'this': 59 return getWeekBounds(today); 60 case 'lastweek': 61 case 'last': { 62 const lastWeek = new Date(today); 63 lastWeek.setDate(lastWeek.getDate() - 7); 64 return getWeekBounds(lastWeek); 65 } 66 default: 67 // Assume YYYY-MM-DD format, get the week containing that date 68 return getWeekBounds(new Date(weekStr + 'T12:00:00')); 69 } 70} 71 72// Check if a date falls within a range 73function isDateInRange(date: string, start: string, end: string): boolean { 74 return date >= start && date <= end; 75} 76 77export async function processCommand(options: ProcessOptions): Promise<{ 78 sessionsProcessed: number; 79 errors: number; 80}> { 81 const { force, verbose, date, week } = options; 82 83 // Build date filter 84 let dateFilter: { type: 'date'; value: string } | { type: 'range'; start: string; end: string } | null = null; 85 86 if (date) { 87 const targetDate = parseDate(date); 88 dateFilter = { type: 'date', value: targetDate }; 89 console.log(`\n📅 Filtering to date: ${targetDate}\n`); 90 } else if (week) { 91 const { start, end } = parseWeek(week); 92 dateFilter = { type: 'range', start, end }; 93 console.log(`\n📅 Filtering to week: ${start} to ${end}\n`); 94 } else { 95 console.log('\n🔍 Scanning for sessions...\n'); 96 } 97 98 let sessions = await findUnprocessedSessions(force); 99 100 // Pre-filter by file modification time if date filter is set 101 // This avoids parsing thousands of files just to check their dates 102 if (dateFilter && sessions.length > 0) { 103 const originalCount = sessions.length; 104 const bufferDays = 2; // Allow some buffer for timezone/edge cases 105 106 let startDate: Date, endDate: Date; 107 if (dateFilter.type === 'date') { 108 startDate = new Date(dateFilter.value + 'T00:00:00'); 109 endDate = new Date(dateFilter.value + 'T23:59:59'); 110 } else { 111 startDate = new Date(dateFilter.start + 'T00:00:00'); 112 endDate = new Date(dateFilter.end + 'T23:59:59'); 113 } 114 115 // Expand range by buffer 116 startDate.setDate(startDate.getDate() - bufferDays); 117 endDate.setDate(endDate.getDate() + bufferDays); 118 119 sessions = sessions.filter(s => 120 s.modifiedAt >= startDate && s.modifiedAt <= endDate 121 ); 122 123 console.log(`Pre-filtered ${originalCount}${sessions.length} sessions by modification time\n`); 124 } 125 126 if (sessions.length === 0) { 127 console.log('✅ No new sessions to process.\n'); 128 return { sessionsProcessed: 0, errors: 0 }; 129 } 130 131 console.log(`Found ${sessions.length} session(s) to check\n`); 132 133 // Group by project for display 134 const byProject = groupByProject(sessions); 135 let processed = 0; 136 let errors = 0; 137 const datesProcessed = new Set<string>(); 138 139 for (const [projectName, projectSessions] of Object.entries(byProject)) { 140 console.log(`📁 ${projectName} (${projectSessions.length} sessions)`); 141 142 let skipped = 0; 143 let filtered = 0; 144 for (const session of projectSessions) { 145 try { 146 const result = await processSession(session, verbose, dateFilter); 147 148 if (result.filtered) { 149 filtered++; 150 continue; 151 } 152 153 if (result.skipped) { 154 skipped++; 155 if (verbose) { 156 console.log(`${session.sessionId.slice(0, 8)}... (skipped - no work)`); 157 } 158 continue; 159 } 160 161 if (result.date) { 162 datesProcessed.add(result.date); 163 } 164 processed++; 165 166 const duration = formatDuration(result.startTime, result.endTime); 167 const summary = result.summary.slice(0, 60); 168 console.log(`${session.sessionId.slice(0, 8)}... (${duration}) → "${summary}..."`); 169 } catch (error) { 170 errors++; 171 console.log(`${session.sessionId.slice(0, 8)}... - Error: ${error}`); 172 if (verbose) { 173 console.error(error); 174 } 175 } 176 } 177 const notes = []; 178 if (skipped > 0) notes.push(`${skipped} empty`); 179 if (filtered > 0) notes.push(`${filtered} outside date range`); 180 if (notes.length > 0) { 181 console.log(` (${notes.join(', ')} skipped)`); 182 } 183 console.log(''); 184 } 185 186 // Generate brag summaries for new dates 187 console.log('📝 Generating daily summaries...\n'); 188 await generateMissingBragSummaries(verbose); 189 190 console.log(`\n✅ Done! Processed ${processed} sessions (${errors} errors)\n`); 191 console.log('Run `bun cli serve` to view your worklog.\n'); 192 193 return { sessionsProcessed: processed, errors }; 194} 195 196type DateFilter = { type: 'date'; value: string } | { type: 'range'; start: string; end: string } | null; 197 198async function processSession( 199 sessionFile: SessionFile, 200 verbose: boolean, 201 dateFilter: DateFilter 202): Promise<{ 203 date: string; 204 startTime: string; 205 endTime: string; 206 summary: string; 207 skipped: boolean; 208 filtered: boolean; 209}> { 210 // Parse the session file 211 const parsed = await parseSessionFile( 212 sessionFile.path, 213 sessionFile.projectPath, 214 sessionFile.projectName 215 ); 216 217 if (verbose) { 218 console.log(` Parsed: ${parsed.messages.length} messages, ${Object.keys(parsed.stats.toolCalls).length} tool types`); 219 } 220 221 // Check date filter BEFORE expensive LLM summarization 222 if (dateFilter) { 223 const matchesFilter = 224 dateFilter.type === 'date' 225 ? parsed.date === dateFilter.value 226 : isDateInRange(parsed.date, dateFilter.start, dateFilter.end); 227 228 if (!matchesFilter) { 229 // Don't mark as processed - we're just skipping for this run 230 return { 231 date: parsed.date, 232 startTime: parsed.startTime, 233 endTime: parsed.endTime, 234 summary: '', 235 skipped: false, 236 filtered: true, 237 }; 238 } 239 } 240 241 // Skip sessions with no meaningful work 242 // But be careful not to filter out quick fixes! 243 const tools = parsed.stats.toolCalls; 244 const toolCallCount = Object.values(tools).reduce((a, b) => a + b, 0); 245 246 // Tools that indicate actual code changes happened 247 const codeChangeTools = ['Edit', 'Write', 'NotebookEdit', 'MultiEdit']; 248 const hasCodeChanges = codeChangeTools.some(tool => (tools[tool] || 0) > 0); 249 250 // If code was changed, always keep it (even a 1-line quickfix) 251 // Otherwise require substantial exploration/conversation 252 const hasSubstantialWork = toolCallCount >= 3; 253 const hasLongConversation = parsed.stats.assistantMessages >= 5; 254 255 if (!hasCodeChanges && !hasSubstantialWork && !hasLongConversation) { 256 // Mark as processed but don't save to DB 257 markFileProcessed(sessionFile.path, sessionFile.fileHash); 258 return { 259 date: parsed.date, 260 startTime: parsed.startTime, 261 endTime: parsed.endTime, 262 summary: '', 263 skipped: true, 264 filtered: false, 265 }; 266 } 267 268 // Generate summary via LLM 269 const summary = await summarizeSession(parsed); 270 271 if (verbose) { 272 console.log(` Summary: ${summary.shortSummary}`); 273 console.log(` Accomplishments: ${summary.accomplishments.length}`); 274 } 275 276 // Filter out sessions that the LLM determined had no real work 277 const noWorkPhrases = [ 278 'no work', 'no coding', 'was interrupted', 'no substantive', 279 'minimal progress', 'minimal activity', 'no significant', 'nothing was accomplished' 280 ]; 281 const summaryLower = summary.shortSummary.toLowerCase(); 282 if (noWorkPhrases.some(phrase => summaryLower.includes(phrase))) { 283 markFileProcessed(sessionFile.path, sessionFile.fileHash); 284 return { 285 date: parsed.date, 286 startTime: parsed.startTime, 287 endTime: parsed.endTime, 288 summary: '', 289 skipped: true, 290 filtered: false, 291 }; 292 } 293 294 // Save to database 295 saveSessionSummary(parsed, summary); 296 markFileProcessed(sessionFile.path, sessionFile.fileHash); 297 298 return { 299 date: parsed.date, 300 startTime: parsed.startTime, 301 endTime: parsed.endTime, 302 summary: summary.shortSummary, 303 skipped: false, 304 filtered: false, 305 }; 306} 307 308async function generateMissingBragSummaries(verbose: boolean): Promise<void> { 309 const datesWithoutSummary = getDatesWithoutBragSummary(); 310 311 for (const date of datesWithoutSummary) { 312 try { 313 const sessions = getSessionsForDate(date); 314 if (sessions.length === 0) continue; 315 316 if (verbose) { 317 console.log(` Generating brag summary for ${date} (${sessions.length} sessions)`); 318 } 319 320 const bragSummary = await generateDailyBragSummary(date, sessions); 321 const projectNames = [...new Set(sessions.map((s) => s.project_name))]; 322 323 saveDailySummary(date, bragSummary, projectNames, sessions.length); 324 325 console.log(` 📣 ${date}: "${bragSummary.slice(0, 80)}..."`); 326 } catch (error) { 327 console.error(` Failed to generate brag for ${date}:`, error); 328 } 329 } 330} 331 332function groupByProject( 333 sessions: SessionFile[] 334): Record<string, SessionFile[]> { 335 const grouped: Record<string, SessionFile[]> = {}; 336 337 for (const session of sessions) { 338 const key = session.projectName; 339 if (!grouped[key]) { 340 grouped[key] = []; 341 } 342 grouped[key].push(session); 343 } 344 345 return grouped; 346} 347 348function formatDuration(start: string, end: string): string { 349 if (!start || !end) return '?'; 350 351 const startDate = new Date(start); 352 const endDate = new Date(end); 353 const diffMs = endDate.getTime() - startDate.getTime(); 354 355 const minutes = Math.floor(diffMs / 60000); 356 if (minutes < 60) return `${minutes}m`; 357 358 const hours = Math.floor(minutes / 60); 359 const remainingMinutes = minutes % 60; 360 return `${hours}h${remainingMinutes}m`; 361}