commit-analyzer 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/llm.ts ADDED
@@ -0,0 +1,283 @@
1
+ import { execSync } from "child_process"
2
+ import { CommitInfo, LLMAnalysis } from "./types"
3
+
4
+ export class LLMService {
5
+ private static model: string
6
+
7
+ /**
8
+ * Detect available LLM models by checking CLI commands.
9
+ */
10
+ static detectAvailableModels(): string[] {
11
+ const models = ["claude", "gemini", "codex"]
12
+ return models.filter((model) => {
13
+ try {
14
+ execSync(`command -v ${model}`, { stdio: "ignore" })
15
+ return true
16
+ } catch {
17
+ return false
18
+ }
19
+ })
20
+ }
21
+
22
+ /**
23
+ * Determine default LLM model based on availability.
24
+ */
25
+ static detectDefaultModel(): string {
26
+ const available = this.detectAvailableModels()
27
+ if (available.length === 0) {
28
+ throw new Error(
29
+ "No supported LLM models found. Please install claude, gemini, or codex.",
30
+ )
31
+ }
32
+ // Default to sonnet if claude is available
33
+ if (available.includes('claude')) {
34
+ return 'claude --model sonnet'
35
+ }
36
+ return available[0]
37
+ }
38
+
39
+ /**
40
+ * Set the LLM model command to use.
41
+ */
42
+ static setModel(model: string): void {
43
+ this.model = model
44
+ }
45
+
46
+ /**
47
+ * Get the configured LLM model or detect default.
48
+ */
49
+ static getModel(): string {
50
+ if (!this.model) {
51
+ this.model = this.detectDefaultModel()
52
+ }
53
+ return this.model
54
+ }
55
+ private static readonly MAX_RETRIES = parseInt(
56
+ process.env.LLM_MAX_RETRIES || "3",
57
+ 10,
58
+ )
59
+ private static readonly INITIAL_RETRY_DELAY = parseInt(
60
+ process.env.LLM_INITIAL_RETRY_DELAY || "5000",
61
+ 10,
62
+ )
63
+ private static readonly MAX_RETRY_DELAY = parseInt(
64
+ process.env.LLM_MAX_RETRY_DELAY || "30000",
65
+ 10,
66
+ )
67
+ private static readonly RETRY_MULTIPLIER = parseFloat(
68
+ process.env.LLM_RETRY_MULTIPLIER || "2",
69
+ )
70
+ // Claude-specific configuration with backward compatibility
71
+ private static readonly CLAUDE_MAX_PROMPT_LENGTH = parseInt(
72
+ process.env.CLAUDE_MAX_PROMPT_LENGTH || process.env.LLM_MAX_PROMPT_LENGTH || "100000",
73
+ 10,
74
+ )
75
+ private static readonly CLAUDE_MAX_DIFF_LENGTH = parseInt(
76
+ process.env.CLAUDE_MAX_DIFF_LENGTH || process.env.LLM_MAX_DIFF_LENGTH || "80000",
77
+ 10,
78
+ )
79
+
80
+ static async analyzeCommit(commit: CommitInfo): Promise<LLMAnalysis> {
81
+ const currentModel = this.getModel()
82
+ const prompt = this.buildPrompt(commit.message, commit.diff, currentModel)
83
+
84
+ // Log prompt length for debugging - only for Claude models
85
+ if (this.isClaudeModel(currentModel)) {
86
+ console.log(` - Prompt length: ${prompt.length} characters`)
87
+ if (prompt.length > this.CLAUDE_MAX_PROMPT_LENGTH) {
88
+ console.log(` - Warning: Prompt exceeds Claude max length (${this.CLAUDE_MAX_PROMPT_LENGTH})`)
89
+ }
90
+ }
91
+
92
+ let lastError: Error | null = null
93
+
94
+ for (let attempt = 1; attempt <= this.MAX_RETRIES; attempt++) {
95
+ try {
96
+ const output = execSync(currentModel, {
97
+ input: prompt,
98
+ encoding: "utf8",
99
+ stdio: ["pipe", "pipe", "pipe"],
100
+ timeout: 60000,
101
+ })
102
+
103
+ return this.parseResponse(output)
104
+ } catch (error) {
105
+ lastError = error instanceof Error ? error : new Error("Unknown error")
106
+
107
+ // Log detailed error information for debugging
108
+ console.log(` - Error details for commit ${commit.hash.substring(0, 8)}:`)
109
+ console.log(` Command: ${currentModel}`)
110
+ console.log(` Error message: ${lastError.message}`)
111
+ if (this.isClaudeModel(currentModel)) {
112
+ console.log(` Prompt length: ${prompt.length} characters`)
113
+ }
114
+
115
+ // If it's an exec error, log additional details
116
+ if (error && typeof error === 'object' && 'stderr' in error) {
117
+ const execError = error as any
118
+ console.log(` Exit code: ${execError.status || 'unknown'}`)
119
+ console.log(` Signal: ${execError.signal || 'none'}`)
120
+ console.log(` Stderr: ${execError.stderr || 'none'}`)
121
+ console.log(` Stdout: ${execError.stdout || 'none'}`)
122
+ }
123
+
124
+ if (attempt < this.MAX_RETRIES) {
125
+ const delay = Math.min(
126
+ this.INITIAL_RETRY_DELAY *
127
+ Math.pow(this.RETRY_MULTIPLIER, attempt - 1),
128
+ this.MAX_RETRY_DELAY,
129
+ )
130
+
131
+ console.log(
132
+ ` - Attempt ${attempt}/${this.MAX_RETRIES} failed for commit ${commit.hash.substring(0, 8)}. Retrying in ${delay / 1000}s...`,
133
+ )
134
+
135
+ await this.sleep(delay)
136
+ }
137
+ }
138
+ }
139
+
140
+ throw new Error(
141
+ `Failed to analyze commit ${commit.hash} after ${this.MAX_RETRIES} attempts: ${lastError?.message || "Unknown error"}`,
142
+ )
143
+ }
144
+
145
+ private static sleep(ms: number): Promise<void> {
146
+ return new Promise((resolve) => setTimeout(resolve, ms))
147
+ }
148
+
149
+
150
+ static getMaxRetries(): number {
151
+ return this.MAX_RETRIES
152
+ }
153
+
154
+ /**
155
+ * Check if the current model is Claude-based
156
+ */
157
+ private static isClaudeModel(model?: string): boolean {
158
+ const currentModel = model || this.getModel()
159
+ return currentModel.toLowerCase().includes('claude')
160
+ }
161
+
162
+ private static buildPrompt(commitMessage: string, diff: string, model: string): string {
163
+ // Only truncate for Claude models
164
+ let truncatedDiff = diff
165
+ let diffTruncated = false
166
+
167
+ if (this.isClaudeModel(model) && diff.length > this.CLAUDE_MAX_DIFF_LENGTH) {
168
+ truncatedDiff = diff.substring(0, this.CLAUDE_MAX_DIFF_LENGTH) + "\n\n[DIFF TRUNCATED - Original length: " + diff.length + " characters]"
169
+ diffTruncated = true
170
+ }
171
+
172
+ const basePrompt = `Analyze this git commit and provide a categorization:
173
+
174
+ COMMIT MESSAGE:
175
+ ${commitMessage}
176
+
177
+ COMMIT DIFF:
178
+ ${truncatedDiff}
179
+
180
+ Based on the commit message and code changes, categorize this commit as one of:
181
+ - "tweak": Minor adjustments, bug fixes, small improvements
182
+ - "feature": New functionality, major additions
183
+ - "process": Build system, CI/CD, tooling, configuration changes
184
+
185
+ Provide:
186
+ 1. Category: [tweak|feature|process]
187
+ 2. Summary: One-line description (max 80 chars)
188
+ 3. Description: Detailed explanation (2-3 sentences)
189
+
190
+ Format as JSON:
191
+ \`\`\`json
192
+ {
193
+ "category": "...",
194
+ "summary": "...",
195
+ "description": "..."
196
+ }
197
+ \`\`\``
198
+
199
+ // Final length check - only for Claude models
200
+ if (this.isClaudeModel(model) && basePrompt.length > this.CLAUDE_MAX_PROMPT_LENGTH) {
201
+ // Further truncate the diff if needed
202
+ const overhead = basePrompt.length - this.CLAUDE_MAX_PROMPT_LENGTH
203
+ const newDiffLength = Math.max(1000, this.CLAUDE_MAX_DIFF_LENGTH - overhead - 200) // Keep at least 1000 chars, subtract extra for safety
204
+ truncatedDiff = diff.substring(0, newDiffLength) + "\n\n[DIFF HEAVILY TRUNCATED - Original length: " + diff.length + " characters]"
205
+
206
+ return `Analyze this git commit and provide a categorization:
207
+
208
+ COMMIT MESSAGE:
209
+ ${commitMessage}
210
+
211
+ COMMIT DIFF:
212
+ ${truncatedDiff}
213
+
214
+ Based on the commit message and code changes, categorize this commit as one of:
215
+ - "tweak": Minor adjustments, bug fixes, small improvements
216
+ - "feature": New functionality, major additions
217
+ - "process": Build system, CI/CD, tooling, configuration changes
218
+
219
+ Provide:
220
+ 1. Category: [tweak|feature|process]
221
+ 2. Summary: One-line description (max 80 chars)
222
+ 3. Description: Detailed explanation (2-3 sentences)
223
+
224
+ Format as JSON:
225
+ \`\`\`json
226
+ {
227
+ "category": "...",
228
+ "summary": "...",
229
+ "description": "..."
230
+ }
231
+ \`\`\``
232
+ }
233
+
234
+ return basePrompt
235
+ }
236
+
237
+ private static parseResponse(response: string): LLMAnalysis {
238
+ try {
239
+ const jsonMatch = response.match(/```json\s*([\s\S]*?)\s*```/)
240
+ if (!jsonMatch) {
241
+ throw new Error("No JSON block found in response")
242
+ }
243
+
244
+ const parsed = JSON.parse(jsonMatch[1])
245
+
246
+ if (!this.isValidCategory(parsed.category)) {
247
+ throw new Error(`Invalid category: ${parsed.category}`)
248
+ }
249
+
250
+ if (!parsed.summary || !parsed.description) {
251
+ throw new Error("Missing required fields in response")
252
+ }
253
+
254
+ return {
255
+ category: parsed.category,
256
+ summary: parsed.summary.substring(0, 80),
257
+ description: parsed.description,
258
+ }
259
+ } catch (error) {
260
+ // Log the raw response for debugging
261
+ console.log(` - Raw LLM response (first 1000 chars): ${response.substring(0, 1000)}`)
262
+ if (response.length > 1000) {
263
+ console.log(` - Response truncated (total length: ${response.length} chars)`)
264
+ }
265
+
266
+ // Try to extract and log the JSON block if it exists but is malformed
267
+ const jsonMatch = response.match(/```json\s*([\s\S]*?)\s*```/)
268
+ if (jsonMatch) {
269
+ console.log(` - Extracted JSON block: ${jsonMatch[1]}`)
270
+ }
271
+
272
+ throw new Error(
273
+ `Failed to parse LLM response: ${error instanceof Error ? error.message : "Unknown error"}`,
274
+ )
275
+ }
276
+ }
277
+
278
+ private static isValidCategory(
279
+ category: string,
280
+ ): category is "tweak" | "feature" | "process" {
281
+ return ["tweak", "feature", "process"].includes(category)
282
+ }
283
+ }
@@ -0,0 +1,77 @@
1
+ import { writeFileSync, readFileSync, existsSync, unlinkSync } from "fs"
2
+ import { AnalyzedCommit } from "./types"
3
+
4
+ interface ProgressState {
5
+ totalCommits: string[]
6
+ processedCommits: string[]
7
+ analyzedCommits: AnalyzedCommit[]
8
+ lastProcessedIndex: number
9
+ startTime: string
10
+ outputFile: string
11
+ }
12
+
13
+ export class ProgressTracker {
14
+ private static readonly CHECKPOINT_FILE = ".commit-analyzer-progress.json"
15
+
16
+ static saveProgress(
17
+ totalCommits: string[],
18
+ processedCommits: string[],
19
+ analyzedCommits: AnalyzedCommit[],
20
+ outputFile: string,
21
+ ): void {
22
+ const state: ProgressState = {
23
+ totalCommits,
24
+ processedCommits,
25
+ analyzedCommits,
26
+ lastProcessedIndex: processedCommits.length - 1,
27
+ startTime: new Date().toISOString(),
28
+ outputFile,
29
+ }
30
+
31
+ writeFileSync(this.CHECKPOINT_FILE, JSON.stringify(state, null, 2))
32
+ }
33
+
34
+ static loadProgress(): ProgressState | null {
35
+ if (!existsSync(this.CHECKPOINT_FILE)) {
36
+ return null
37
+ }
38
+
39
+ try {
40
+ const content = readFileSync(this.CHECKPOINT_FILE, "utf8")
41
+ return JSON.parse(content)
42
+ } catch (error) {
43
+ console.error("Failed to load progress file:", error)
44
+ return null
45
+ }
46
+ }
47
+
48
+ static hasProgress(): boolean {
49
+ return existsSync(this.CHECKPOINT_FILE)
50
+ }
51
+
52
+ static clearProgress(): void {
53
+ if (existsSync(this.CHECKPOINT_FILE)) {
54
+ unlinkSync(this.CHECKPOINT_FILE)
55
+ }
56
+ }
57
+
58
+ static getRemainingCommits(state: ProgressState): string[] {
59
+ const processedSet = new Set(state.processedCommits)
60
+ return state.totalCommits.filter(hash => !processedSet.has(hash))
61
+ }
62
+
63
+ static formatProgressSummary(state: ProgressState): string {
64
+ const processed = state.processedCommits.length
65
+ const total = state.totalCommits.length
66
+ const remaining = total - processed
67
+ const percentComplete = Math.round((processed / total) * 100)
68
+
69
+ return `
70
+ Previous session:
71
+ - Started: ${new Date(state.startTime).toLocaleString()}
72
+ - Progress: ${processed}/${total} commits (${percentComplete}%)
73
+ - Remaining: ${remaining} commits
74
+ - Output file: ${state.outputFile}
75
+ `.trim()
76
+ }
77
+ }
@@ -0,0 +1,286 @@
1
+ import { writeFileSync } from "fs"
2
+ import { CSVReaderService, ParsedCSVRow } from "./csv-reader"
3
+ import { LLMService } from "./llm"
4
+
5
+ export class MarkdownReportGenerator {
6
+ /**
7
+ * Generate a markdown report from CSV data
8
+ */
9
+ static async generateReport(
10
+ csvFilePath: string,
11
+ outputPath: string,
12
+ ): Promise<void> {
13
+ console.log(`Reading CSV data from ${csvFilePath}...`)
14
+
15
+ // Read and parse CSV data
16
+ const csvData = CSVReaderService.readCSV(csvFilePath)
17
+
18
+ if (csvData.length === 0) {
19
+ throw new Error("No data found in CSV file")
20
+ }
21
+
22
+ // Get statistics for logging
23
+ const stats = CSVReaderService.getStatistics(csvData)
24
+ console.log(
25
+ `Found ${stats.totalRows} commits spanning ${stats.yearRange.min}-${stats.yearRange.max}`,
26
+ )
27
+ console.log(
28
+ `Categories: ${stats.categoryBreakdown.feature} features, ${stats.categoryBreakdown.process} process, ${stats.categoryBreakdown.tweak} tweaks`,
29
+ )
30
+
31
+ console.log("Generating condensed report...")
32
+
33
+ // Generate commit analysis section programmatically
34
+ const analysisSection = this.generateAnalysisSection(csvData)
35
+
36
+ // Convert CSV data to string format for LLM (for content summarization only)
37
+ const csvContent = this.convertToCSVString(csvData)
38
+
39
+ // Generate detailed yearly summaries using LLM
40
+ const yearlyContent = await this.generateYearlySummariesWithLLM(csvContent)
41
+
42
+ // Combine analysis section with LLM-generated content
43
+ const reportContent = `# Development Summary Report\n\n${analysisSection}\n\n${yearlyContent}`
44
+
45
+ // Write to output file
46
+ writeFileSync(outputPath, reportContent, "utf8")
47
+
48
+ console.log(`Report generated: ${outputPath}`)
49
+ }
50
+
51
+ /**
52
+ * Convert parsed CSV data back to CSV string format for LLM consumption
53
+ */
54
+ private static convertToCSVString(data: ParsedCSVRow[]): string {
55
+ const header = "year,category,summary,description"
56
+ const rows = data.map((row) =>
57
+ [
58
+ row.year,
59
+ this.escapeCsvField(row.category),
60
+ this.escapeCsvField(row.summary),
61
+ this.escapeCsvField(row.description),
62
+ ].join(","),
63
+ )
64
+
65
+ return [header, ...rows].join("\n")
66
+ }
67
+
68
+ /**
69
+ * Escape CSV fields that contain commas, quotes, or newlines
70
+ */
71
+ private static escapeCsvField(field: string): string {
72
+ if (field.includes(",") || field.includes('"') || field.includes("\n")) {
73
+ return `"${field.replace(/"/g, '""')}"`
74
+ }
75
+ return field
76
+ }
77
+
78
+ /**
79
+ * Generate commit analysis section with accurate counts
80
+ */
81
+ private static generateAnalysisSection(csvData: ParsedCSVRow[]): string {
82
+ const stats = CSVReaderService.getStatistics(csvData)
83
+
84
+ // Group data by year for detailed breakdown
85
+ const yearlyStats = csvData.reduce(
86
+ (acc, row) => {
87
+ if (!acc[row.year]) {
88
+ acc[row.year] = { tweak: 0, feature: 0, process: 0, total: 0 }
89
+ }
90
+ acc[row.year][row.category]++
91
+ acc[row.year].total++
92
+ return acc
93
+ },
94
+ {} as Record<
95
+ number,
96
+ { tweak: number; feature: number; process: number; total: number }
97
+ >,
98
+ )
99
+
100
+ // Sort years in descending order
101
+ const sortedYears = Object.keys(yearlyStats)
102
+ .map(Number)
103
+ .sort((a, b) => b - a)
104
+
105
+ let analysisContent = `## Commit Analysis\n`
106
+ analysisContent += `- **Total Commits**: ${stats.totalRows} commits across ${stats.yearRange.min}-${stats.yearRange.max}\n`
107
+
108
+ // Add year-by-year breakdown
109
+ for (const year of sortedYears) {
110
+ const yearData = yearlyStats[year]
111
+ analysisContent += `- **${year}**: ${yearData.total} commits (${yearData.feature} features, ${yearData.process} process, ${yearData.tweak} tweaks)\n`
112
+ }
113
+
114
+ return analysisContent
115
+ }
116
+
117
+ /**
118
+ * Generate yearly summaries using LLM service
119
+ */
120
+ private static async generateYearlySummariesWithLLM(
121
+ csvContent: string,
122
+ ): Promise<string> {
123
+ const prompt = this.buildReportPrompt(csvContent)
124
+
125
+ try {
126
+ // Use the same retry logic as commit analysis
127
+ const response = await this.callLLMWithRetry(prompt)
128
+ return this.parseReportResponse(response)
129
+ } catch (error) {
130
+ throw new Error(
131
+ `Failed to generate report: ${error instanceof Error ? error.message : "Unknown error"}`,
132
+ )
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Build the prompt for report generation based on the template
138
+ */
139
+ private static buildReportPrompt(csvContent: string): string {
140
+ return `Analyze the following CSV data containing git commit analysis results and generate a condensed markdown development summary report.
141
+
142
+ CSV DATA:
143
+ ${csvContent}
144
+
145
+ INSTRUCTIONS:
146
+ 1. Group the data by year (descending order, most recent first)
147
+ 2. Within each year, group by category: Features, Process Improvements, and Tweaks & Bug Fixes
148
+ 3. Consolidate similar items within each category to create readable summaries
149
+ 4. Focus on what was accomplished rather than individual commit details
150
+ 5. Use clear, professional language appropriate for stakeholders
151
+
152
+ CATEGORY MAPPING:
153
+ - "feature" → "Features" section
154
+ - "process" → "Processes" section
155
+ - "tweak" → "Tweaks & Bug Fixes" section
156
+
157
+ CONSOLIDATION GUIDELINES:
158
+ - Group similar features together (e.g., "authentication system improvements")
159
+ - Combine related bug fixes (e.g., "resolved 8 authentication issues")
160
+ - Summarize process changes by theme (e.g., "CI/CD pipeline enhancements")
161
+ - Use bullet points for individual items within categories
162
+ - Aim for 3-7 bullet points per category per year
163
+ - Include specific numbers when relevant (e.g., "15 bug fixes", "3 new features")
164
+
165
+ OUTPUT FORMAT:
166
+ Generate yearly summary sections with this exact structure (DO NOT include the main title or commit analysis section):
167
+
168
+ \`\`\`markdown
169
+ ## [YEAR]
170
+ ### Features
171
+ - [Consolidated feature summary 1]
172
+ - [Consolidated feature summary 2]
173
+ - [Additional features as needed]
174
+
175
+ ### Processes
176
+ - [Consolidated process improvement 1]
177
+ - [Consolidated process improvement 2]
178
+ - [Additional process items as needed]
179
+
180
+ ### Tweaks & Bug Fixes
181
+ - [Consolidated tweak/fix summary 1]
182
+ - [Consolidated tweak/fix summary 2]
183
+ - [Additional tweaks/fixes as needed]
184
+
185
+ ## [PREVIOUS YEAR]
186
+ [Repeat structure for each year in the data]
187
+ \`\`\`
188
+
189
+ QUALITY REQUIREMENTS:
190
+ - Keep summaries concise but informative
191
+ - Use active voice and clear language
192
+ - Avoid technical jargon where possible
193
+ - Ensure each bullet point represents meaningful work
194
+ - Make the report valuable for both technical and non-technical readers
195
+
196
+ Generate the markdown report now:`
197
+ }
198
+
199
+ /**
200
+ * Call LLM with retry logic similar to commit analysis
201
+ */
202
+ private static async callLLMWithRetry(prompt: string): Promise<string> {
203
+ const maxRetries = LLMService.getMaxRetries()
204
+ let lastError: Error | null = null
205
+
206
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
207
+ try {
208
+ // Create a mock commit object for the LLM service
209
+ const mockCommit = {
210
+ hash: "report-generation",
211
+ message: "Generate report from CSV data",
212
+ date: new Date(),
213
+ diff: prompt,
214
+ year: new Date().getFullYear(),
215
+ }
216
+
217
+ // Use the existing LLM service but intercept the response
218
+ const { execSync } = require("child_process")
219
+ const currentModel = LLMService.getModel()
220
+
221
+ console.log(` - Using model: ${currentModel}`)
222
+ console.log(
223
+ ` - Processing ${prompt.split("\n").length} lines of CSV data`,
224
+ )
225
+
226
+ const output = execSync(currentModel, {
227
+ input: prompt,
228
+ encoding: "utf8",
229
+ stdio: ["pipe", "pipe", "pipe"],
230
+ timeout: 120000, // Longer timeout for report generation
231
+ })
232
+
233
+ return output.trim()
234
+ } catch (error) {
235
+ lastError = error instanceof Error ? error : new Error("Unknown error")
236
+
237
+ console.log(` - Error generating report:`)
238
+ console.log(` Attempt: ${attempt}/${maxRetries}`)
239
+ console.log(` Error: ${lastError.message}`)
240
+
241
+ if (attempt < maxRetries) {
242
+ const delay = Math.min(5000 * Math.pow(2, attempt - 1), 30000)
243
+ console.log(` Retrying in ${delay / 1000}s...`)
244
+ await this.sleep(delay)
245
+ }
246
+ }
247
+ }
248
+
249
+ throw new Error(
250
+ `Failed to generate report after ${maxRetries} attempts: ${lastError?.message || "Unknown error"}`,
251
+ )
252
+ }
253
+
254
+ /**
255
+ * Parse the LLM response to extract the yearly summary content
256
+ */
257
+ private static parseReportResponse(response: string): string {
258
+ // Look for markdown block first
259
+ const markdownMatch = response.match(/```markdown\s*([\s\S]*?)\s*```/)
260
+ if (markdownMatch) {
261
+ return markdownMatch[1].trim()
262
+ }
263
+
264
+ // If no markdown block, look for content starting with "##" (yearly sections)
265
+ const yearSectionMatch = response.match(/^(##\s+\d{4}[\s\S]*)/m)
266
+ if (yearSectionMatch) {
267
+ return yearSectionMatch[1].trim()
268
+ }
269
+
270
+ // If no clear structure found, return the entire response but log a warning
271
+ console.log(
272
+ " - Warning: Could not find structured yearly sections in LLM response",
273
+ )
274
+ console.log(` - Response preview: ${response.substring(0, 200)}...`)
275
+
276
+ return response.trim()
277
+ }
278
+
279
+ /**
280
+ * Sleep utility for retry delays
281
+ */
282
+ private static sleep(ms: number): Promise<void> {
283
+ return new Promise((resolve) => setTimeout(resolve, ms))
284
+ }
285
+ }
286
+
package/src/types.ts ADDED
@@ -0,0 +1,24 @@
1
+ export interface CommitInfo {
2
+ hash: string
3
+ message: string
4
+ date: Date
5
+ diff: string
6
+ year: number
7
+ }
8
+
9
+ export interface LLMAnalysis {
10
+ category: "tweak" | "feature" | "process"
11
+ summary: string
12
+ description: string
13
+ }
14
+
15
+ export interface AnalyzedCommit extends CommitInfo {
16
+ analysis: LLMAnalysis
17
+ }
18
+
19
+ export interface CSVRow {
20
+ year: number
21
+ category: string
22
+ summary: string
23
+ description: string
24
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "lib": ["ES2020"],
5
+ "module": "commonjs",
6
+ "outDir": "./dist",
7
+ "rootDir": "./src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "resolveJsonModule": true,
13
+ "declaration": true,
14
+ "declarationMap": true,
15
+ "sourceMap": true
16
+ },
17
+ "include": ["src/**/*"],
18
+ "exclude": ["node_modules", "dist"]
19
+ }