this repo has no description
0
fork

Configure Feed

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

Use Vercel AI SDK structured outputs with Zod schemas

- Replace generateText + JSON parsing with generateObject
- Add Zod schemas for session and daily summaries
- Daily summaries now return structured JSON for rendering
- More reliable than regex parsing of LLM text output

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

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

alice 29be652b 4e2e1998

+69 -67
+69 -67
src/core/summarizer.ts
··· 1 1 import { createAnthropic } from '@ai-sdk/anthropic'; 2 - import { generateText } from 'ai'; 2 + import { generateObject, generateText } from 'ai'; 3 + import { z } from 'zod'; 3 4 import type { ParsedSession, SessionSummary, DBSessionSummary } from '../types'; 4 5 import { createCondensedTranscript } from './session-reader'; 5 6 ··· 13 14 14 15 const MODEL = process.env.SUMMARIZER_MODEL || 'claude-haiku-4-5-20251001'; 15 16 17 + // Zod schema for session summaries - using .describe() for better LLM understanding 18 + const sessionSummarySchema = z.object({ 19 + shortSummary: z 20 + .string() 21 + .describe('1-2 sentence summary focusing on what was accomplished, built, or fixed'), 22 + accomplishments: z 23 + .array(z.string()) 24 + .describe('List of specific outcomes: features built, bugs fixed, problems solved'), 25 + filesChanged: z 26 + .array(z.string()) 27 + .describe('List of files that were modified or created'), 28 + toolsUsed: z 29 + .array(z.string()) 30 + .describe('List of tools used like Edit, Bash, Read, Write, Grep'), 31 + }); 32 + 16 33 /** 17 - * Generate a summary for a session using Claude 34 + * Generate a summary for a session using Claude with structured output 18 35 */ 19 36 export async function summarizeSession( 20 37 session: ParsedSession 21 38 ): Promise<SessionSummary> { 22 39 const transcript = createCondensedTranscript(session); 23 40 24 - const systemPrompt = `You are summarizing a Claude Code coding session for a daily worklog. 25 - Focus on: what was accomplished, problems solved, features built or bugs fixed. 26 - Be concise but specific about the actual work done. 27 - 28 - Output valid JSON with this exact structure: 29 - { 30 - "shortSummary": "1-2 sentence summary of what was accomplished", 31 - "accomplishments": ["bullet point 1", "bullet point 2", ...], 32 - "filesChanged": ["file1.ts", "file2.ts", ...], 33 - "toolsUsed": ["Bash", "Edit", ...] 34 - } 35 - 36 - Keep accomplishments focused on outcomes, not process. If very little was done (just exploration or questions), say so briefly.`; 41 + const systemPrompt = `You summarize Claude Code sessions for a worklog. 42 + Focus on outcomes: features built, bugs fixed, problems solved. 43 + If little was done (exploration/questions only), say so briefly in the summary.`; 37 44 38 45 const userPrompt = `Summarize this Claude Code session:\n\n${transcript}`; 39 46 40 47 try { 41 - const { text } = await generateText({ 48 + const { object } = await generateObject({ 42 49 model: anthropic(MODEL), 50 + schema: sessionSummarySchema, 43 51 system: systemPrompt, 44 52 prompt: userPrompt, 45 53 maxTokens: 1024, 46 54 }); 47 55 48 - // Parse JSON from response (handle markdown code blocks) 49 - const jsonMatch = text.match(/```json\n?([\s\S]*?)\n?```/) || 50 - text.match(/\{[\s\S]*\}/); 51 - 52 - if (!jsonMatch) { 53 - throw new Error('No JSON found in response'); 54 - } 55 - 56 - const jsonStr = jsonMatch[1] || jsonMatch[0]; 57 - const summary = JSON.parse(jsonStr) as SessionSummary; 58 - 59 - // Validate and provide defaults 60 56 return { 61 - shortSummary: summary.shortSummary || 'Session completed', 62 - accomplishments: Array.isArray(summary.accomplishments) 63 - ? summary.accomplishments 64 - : [], 65 - filesChanged: Array.isArray(summary.filesChanged) 66 - ? summary.filesChanged 67 - : [], 68 - toolsUsed: Array.isArray(summary.toolsUsed) 69 - ? summary.toolsUsed 57 + shortSummary: object.shortSummary || 'Session completed', 58 + accomplishments: object.accomplishments || [], 59 + filesChanged: object.filesChanged || [], 60 + toolsUsed: object.toolsUsed.length > 0 61 + ? object.toolsUsed 70 62 : Object.keys(session.stats.toolCalls), 71 63 }; 72 64 } catch (error) { ··· 82 74 } 83 75 } 84 76 77 + // Schema for daily summary - structured for easy rendering 78 + const dailySummarySchema = z.object({ 79 + projects: z 80 + .array(z.object({ 81 + name: z.string().describe('Project name'), 82 + summary: z.string().describe('1-2 sentence summary of work done on this project'), 83 + })) 84 + .describe('List of projects worked on with summaries'), 85 + }); 86 + 87 + export type DailySummary = z.infer<typeof dailySummarySchema>; 88 + 85 89 /** 86 - * Generate a daily brag summary from multiple session summaries 90 + * Generate a daily summary from multiple session summaries 91 + * Returns structured data for easy rendering 87 92 */ 88 93 export async function generateDailyBragSummary( 89 94 date: string, 90 95 sessions: DBSessionSummary[] 91 96 ): Promise<string> { 92 97 if (sessions.length === 0) { 93 - return 'No sessions recorded'; 98 + return JSON.stringify({ projects: [] }); 94 99 } 95 100 96 - // Collect all accomplishments 97 - const allAccomplishments: string[] = []; 98 - const projectNames = new Set<string>(); 99 - 101 + // Group accomplishments by project 102 + const accomplishmentsByProject = new Map<string, string[]>(); 100 103 for (const session of sessions) { 101 - projectNames.add(session.project_name); 104 + const project = session.project_name; 105 + if (!accomplishmentsByProject.has(project)) { 106 + accomplishmentsByProject.set(project, []); 107 + } 102 108 try { 103 - const accomplishments = JSON.parse(session.accomplishments || '[]'); 104 - allAccomplishments.push(...accomplishments); 105 - } catch { 106 - // Skip invalid JSON 107 - } 109 + const acc = JSON.parse(session.accomplishments || '[]'); 110 + accomplishmentsByProject.get(project)!.push(...acc); 111 + } catch {} 108 112 } 109 113 110 - const systemPrompt = `You are writing a brief, impressive summary of a developer's daily work for sharing on social media. 111 - Keep it short (1-3 sentences), punchy, and focused on impact. 112 - Don't use hashtags or emojis unless they really fit. 113 - Make it sound natural, not like AI-generated content. 114 - Focus on the most impressive or interesting accomplishments.`; 114 + const projectSummaries = Array.from(accomplishmentsByProject.entries()) 115 + .map(([project, accs]) => `${project}: ${accs.join('; ')}`) 116 + .join('\n'); 115 117 116 - const userPrompt = `Summarize this developer's day (${date}): 117 - 118 - Projects worked on: ${Array.from(projectNames).join(', ')} 119 - Number of sessions: ${sessions.length} 120 - 121 - Accomplishments: 122 - ${allAccomplishments.map((a) => `- ${a}`).join('\n')} 118 + const systemPrompt = `Summarize a developer's daily work by project. 119 + Be concise but specific. No hype, no buzzwords, just straightforward description. 120 + Combine related accomplishments per project into 1-2 sentences.`; 123 121 124 - Write a brief, impressive summary for social media.`; 122 + const userPrompt = `Summarize this developer's day (${date}):\n\n${projectSummaries}`; 125 123 126 124 try { 127 - const { text } = await generateText({ 125 + const { object } = await generateObject({ 128 126 model: anthropic(MODEL), 127 + schema: dailySummarySchema, 129 128 system: systemPrompt, 130 129 prompt: userPrompt, 131 - maxTokens: 256, 130 + maxTokens: 512, 132 131 }); 133 132 134 - return text.trim() || 'Productive coding day!'; 133 + return JSON.stringify(object); 135 134 } catch (error) { 136 135 console.error('Brag summary error:', error); 137 136 138 137 // Generate a basic summary on failure 139 - const projectList = Array.from(projectNames).slice(0, 3).join(', '); 140 - return `Worked on ${projectList}. ${sessions.length} coding sessions, making solid progress.`; 138 + const projects = Array.from(accomplishmentsByProject.keys()).map(name => ({ 139 + name, 140 + summary: 'Session details unavailable', 141 + })); 142 + return JSON.stringify({ projects }); 141 143 } 142 144 }