this repo has no description
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}