this repo has no description
0
fork

Configure Feed

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

Improve session filtering and add date range support

- Pre-filter sessions by file modification time for faster processing
- Smart empty session detection: keep if Edit/Write used (quick fixes)
- Post-LLM filtering for "no work" summaries
- Require 3+ tool calls OR 5+ messages for non-edit sessions

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

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

alice 4e2e1998 e4f6fd7b

+195 -10
+195 -10
src/cli/process.ts
··· 13 13 force: boolean; 14 14 verbose: boolean; 15 15 date?: string; 16 + week?: string; 17 + } 18 + 19 + // Get start and end of week (Monday-Sunday) for a given date 20 + function 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" 35 + function 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" 53 + function 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 73 + function isDateInRange(date: string, start: string, end: string): boolean { 74 + return date >= start && date <= end; 16 75 } 17 76 18 77 export async function processCommand(options: ProcessOptions): Promise<{ 19 78 sessionsProcessed: number; 20 79 errors: number; 21 80 }> { 22 - const { force, verbose, date } = options; 81 + const { force, verbose, date, week } = options; 23 82 24 - console.log('\n🔍 Scanning for sessions...\n'); 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 + } 25 97 26 98 let sessions = await findUnprocessedSessions(force); 27 99 28 - // Filter by date if specified 29 - if (date) { 30 - // We need to peek into files to filter by date 31 - // For efficiency, we'll do a rough filter first 32 - console.log(`Filtering to date: ${date}`); 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`); 33 124 } 34 125 35 126 if (sessions.length === 0) { ··· 37 128 return { sessionsProcessed: 0, errors: 0 }; 38 129 } 39 130 40 - console.log(`Found ${sessions.length} session(s) to process\n`); 131 + console.log(`Found ${sessions.length} session(s) to check\n`); 41 132 42 133 // Group by project for display 43 134 const byProject = groupByProject(sessions); ··· 48 139 for (const [projectName, projectSessions] of Object.entries(byProject)) { 49 140 console.log(`📁 ${projectName} (${projectSessions.length} sessions)`); 50 141 142 + let skipped = 0; 143 + let filtered = 0; 51 144 for (const session of projectSessions) { 52 145 try { 53 - const result = await processSession(session, verbose); 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 + 54 161 if (result.date) { 55 162 datesProcessed.add(result.date); 56 163 } ··· 67 174 } 68 175 } 69 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 + } 70 183 console.log(''); 71 184 } 72 185 ··· 80 193 return { sessionsProcessed: processed, errors }; 81 194 } 82 195 196 + type DateFilter = { type: 'date'; value: string } | { type: 'range'; start: string; end: string } | null; 197 + 83 198 async function processSession( 84 199 sessionFile: SessionFile, 85 - verbose: boolean 200 + verbose: boolean, 201 + dateFilter: DateFilter 86 202 ): Promise<{ 87 203 date: string; 88 204 startTime: string; 89 205 endTime: string; 90 206 summary: string; 207 + skipped: boolean; 208 + filtered: boolean; 91 209 }> { 92 210 // Parse the session file 93 211 const parsed = await parseSessionFile( ··· 100 218 console.log(` Parsed: ${parsed.messages.length} messages, ${Object.keys(parsed.stats.toolCalls).length} tool types`); 101 219 } 102 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 + 103 268 // Generate summary via LLM 104 269 const summary = await summarizeSession(parsed); 105 270 ··· 108 273 console.log(` Accomplishments: ${summary.accomplishments.length}`); 109 274 } 110 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 + 111 294 // Save to database 112 295 saveSessionSummary(parsed, summary); 113 296 markFileProcessed(sessionFile.path, sessionFile.fileHash); ··· 117 300 startTime: parsed.startTime, 118 301 endTime: parsed.endTime, 119 302 summary: summary.shortSummary, 303 + skipped: false, 304 + filtered: false, 120 305 }; 121 306 } 122 307