commit-analyzer 1.0.1 → 1.0.3
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/package.json +1 -1
- package/src/cli.ts +47 -20
- package/src/index.ts +169 -57
- package/src/llm.ts +143 -15
- package/src/progress.ts +8 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "commit-analyzer",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "Analyze git commits and generate categories, summaries, and descriptions for each commit. Optionally generate a yearly breakdown report of your commit history.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
package/src/cli.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import { readFileSync } from "fs"
|
|
1
|
+
import { readFileSync, mkdirSync } from "fs"
|
|
2
2
|
import { Command } from "commander"
|
|
3
|
+
import { join, dirname } from "path"
|
|
3
4
|
|
|
4
5
|
export interface CLIOptions {
|
|
5
6
|
output?: string
|
|
7
|
+
outputDir?: string
|
|
6
8
|
file?: string
|
|
7
9
|
commits: string[]
|
|
8
10
|
author?: string
|
|
@@ -13,6 +15,7 @@ export interface CLIOptions {
|
|
|
13
15
|
model?: string
|
|
14
16
|
report?: boolean
|
|
15
17
|
inputCsv?: string
|
|
18
|
+
verbose?: boolean
|
|
16
19
|
}
|
|
17
20
|
|
|
18
21
|
export class CLIService {
|
|
@@ -21,9 +24,15 @@ export class CLIService {
|
|
|
21
24
|
|
|
22
25
|
program
|
|
23
26
|
.name("commit-analyzer")
|
|
24
|
-
.description(
|
|
25
|
-
|
|
26
|
-
|
|
27
|
+
.description(
|
|
28
|
+
"Analyze user authored git commits and generate rich commit descriptions and stakeholder reports from them.",
|
|
29
|
+
)
|
|
30
|
+
.version("1.0.3")
|
|
31
|
+
.option("-o, --output <file>", "Output CSV file (default: commits.csv)")
|
|
32
|
+
.option(
|
|
33
|
+
"--output-dir <dir>",
|
|
34
|
+
"Output directory for CSV and report files (default: current directory)",
|
|
35
|
+
)
|
|
27
36
|
.option(
|
|
28
37
|
"-f, --file <file>",
|
|
29
38
|
"Read commit hashes from file (one per line)",
|
|
@@ -37,18 +46,9 @@ export class CLIService {
|
|
|
37
46
|
"Limit number of commits to analyze",
|
|
38
47
|
parseInt,
|
|
39
48
|
)
|
|
40
|
-
.option(
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
)
|
|
44
|
-
.option(
|
|
45
|
-
"-c, --clear",
|
|
46
|
-
"Clear any existing progress checkpoint",
|
|
47
|
-
)
|
|
48
|
-
.option(
|
|
49
|
-
"-m, --model <model>",
|
|
50
|
-
"LLM model to use (claude, gemini, codex)",
|
|
51
|
-
)
|
|
49
|
+
.option("-r, --resume", "Resume from last checkpoint if available")
|
|
50
|
+
.option("-c, --clear", "Clear any existing progress checkpoint")
|
|
51
|
+
.option("-m, --model <model>", "LLM model to use (claude, gemini, codex)")
|
|
52
52
|
.option(
|
|
53
53
|
"--report",
|
|
54
54
|
"Generate condensed markdown report from existing CSV",
|
|
@@ -57,6 +57,10 @@ export class CLIService {
|
|
|
57
57
|
"--input-csv <file>",
|
|
58
58
|
"Input CSV file to read for report generation",
|
|
59
59
|
)
|
|
60
|
+
.option(
|
|
61
|
+
"-v, --verbose",
|
|
62
|
+
"Enable verbose logging (shows detailed error information)",
|
|
63
|
+
)
|
|
60
64
|
.argument(
|
|
61
65
|
"[commits...]",
|
|
62
66
|
"Commit hashes to analyze (if none provided, uses current user's commits)",
|
|
@@ -78,7 +82,10 @@ export class CLIService {
|
|
|
78
82
|
}
|
|
79
83
|
|
|
80
84
|
return {
|
|
81
|
-
output:
|
|
85
|
+
output:
|
|
86
|
+
options.output ||
|
|
87
|
+
CLIService.resolveOutputPath("commits.csv", options.outputDir),
|
|
88
|
+
outputDir: options.outputDir,
|
|
82
89
|
file: options.file,
|
|
83
90
|
commits,
|
|
84
91
|
author: options.author,
|
|
@@ -89,6 +96,7 @@ export class CLIService {
|
|
|
89
96
|
model: options.model,
|
|
90
97
|
report: options.report,
|
|
91
98
|
inputCsv: options.inputCsv,
|
|
99
|
+
verbose: options.verbose,
|
|
92
100
|
}
|
|
93
101
|
}
|
|
94
102
|
|
|
@@ -106,6 +114,24 @@ export class CLIService {
|
|
|
106
114
|
}
|
|
107
115
|
}
|
|
108
116
|
|
|
117
|
+
/**
|
|
118
|
+
* Resolve the full file path with optional output directory.
|
|
119
|
+
*/
|
|
120
|
+
static resolveOutputPath(filename: string, outputDir?: string): string {
|
|
121
|
+
if (outputDir) {
|
|
122
|
+
// Ensure output directory exists
|
|
123
|
+
try {
|
|
124
|
+
mkdirSync(outputDir, { recursive: true })
|
|
125
|
+
} catch (error) {
|
|
126
|
+
throw new Error(
|
|
127
|
+
`Failed to create output directory ${outputDir}: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
return join(outputDir, filename)
|
|
131
|
+
}
|
|
132
|
+
return filename
|
|
133
|
+
}
|
|
134
|
+
|
|
109
135
|
static showHelp(): void {
|
|
110
136
|
console.log(`
|
|
111
137
|
Usage: commit-analyzer [options] [commits...]
|
|
@@ -114,14 +140,16 @@ Analyze git commits and generate categorized summaries using LLM.
|
|
|
114
140
|
If no commits are specified, analyzes all commits authored by the current user.
|
|
115
141
|
|
|
116
142
|
Options:
|
|
117
|
-
-o, --output <file>
|
|
118
|
-
-
|
|
143
|
+
-o, --output <file> Output file (default: commits.csv for analysis, report.md for reports)
|
|
144
|
+
--output-dir <dir> Output directory for CSV and report files (default: current directory)
|
|
145
|
+
-f, --file <file> Read commit hashes from file (one per line)
|
|
119
146
|
-a, --author <email> Filter commits by author email (defaults to current user)
|
|
120
147
|
-l, --limit <number> Limit number of commits to analyze
|
|
121
148
|
-r, --resume Resume from last checkpoint if available
|
|
122
149
|
-c, --clear Clear any existing progress checkpoint
|
|
123
150
|
--report Generate condensed markdown report from existing CSV
|
|
124
151
|
--input-csv <file> Input CSV file to read for report generation
|
|
152
|
+
-v, --verbose Enable verbose logging (shows detailed error information)
|
|
125
153
|
-h, --help Display help for command
|
|
126
154
|
-V, --version Display version number
|
|
127
155
|
|
|
@@ -140,4 +168,3 @@ Examples:
|
|
|
140
168
|
`)
|
|
141
169
|
}
|
|
142
170
|
}
|
|
143
|
-
|
package/src/index.ts
CHANGED
|
@@ -17,10 +17,13 @@ async function promptResume(): Promise<boolean> {
|
|
|
17
17
|
})
|
|
18
18
|
|
|
19
19
|
return new Promise((resolve) => {
|
|
20
|
-
rl.question(
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
rl.question(
|
|
21
|
+
"\nDo you want to resume from the checkpoint? (y/n): ",
|
|
22
|
+
(answer) => {
|
|
23
|
+
rl.close()
|
|
24
|
+
resolve(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes")
|
|
25
|
+
},
|
|
26
|
+
)
|
|
24
27
|
})
|
|
25
28
|
}
|
|
26
29
|
|
|
@@ -32,50 +35,76 @@ async function main(): Promise<void> {
|
|
|
32
35
|
|
|
33
36
|
const options = CLIService.parseArguments()
|
|
34
37
|
|
|
38
|
+
// Resolve output file with output directory
|
|
39
|
+
if (options.output) {
|
|
40
|
+
options.output = CLIService.resolveOutputPath(
|
|
41
|
+
options.output,
|
|
42
|
+
options.outputDir,
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
35
46
|
// Handle input CSV mode (skip commit analysis, just generate report)
|
|
36
47
|
if (options.inputCsv) {
|
|
37
48
|
console.log("Generating report from existing CSV...")
|
|
38
|
-
|
|
49
|
+
|
|
39
50
|
// Ensure --report flag is set when using --input-csv
|
|
40
51
|
if (!options.report) {
|
|
41
52
|
options.report = true
|
|
42
|
-
console.log(
|
|
53
|
+
console.log(
|
|
54
|
+
"Note: --report flag automatically enabled when using --input-csv",
|
|
55
|
+
)
|
|
43
56
|
}
|
|
44
|
-
|
|
57
|
+
|
|
45
58
|
// Determine output file name for report
|
|
46
|
-
let reportOutput =
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
59
|
+
let reportOutput =
|
|
60
|
+
options.output ||
|
|
61
|
+
CLIService.resolveOutputPath("report.md", options.outputDir)
|
|
62
|
+
if (
|
|
63
|
+
reportOutput.endsWith("commits.csv") ||
|
|
64
|
+
reportOutput.endsWith("/commits.csv")
|
|
65
|
+
) {
|
|
66
|
+
reportOutput = CLIService.resolveOutputPath(
|
|
67
|
+
"report.md",
|
|
68
|
+
options.outputDir,
|
|
69
|
+
)
|
|
70
|
+
} else if (!reportOutput.endsWith(".md")) {
|
|
50
71
|
// If user specified output but it's not .md, append .md
|
|
51
|
-
reportOutput = reportOutput.replace(/\.[^.]+$/,
|
|
72
|
+
reportOutput = reportOutput.replace(/\.[^.]+$/, "") + ".md"
|
|
52
73
|
}
|
|
53
|
-
|
|
54
|
-
await MarkdownReportGenerator.generateReport(
|
|
74
|
+
|
|
75
|
+
await MarkdownReportGenerator.generateReport(
|
|
76
|
+
options.inputCsv,
|
|
77
|
+
reportOutput,
|
|
78
|
+
)
|
|
55
79
|
return
|
|
56
80
|
}
|
|
57
81
|
|
|
58
82
|
// Prompt to select LLM model if not provided
|
|
59
83
|
const availableModels = LLMService.detectAvailableModels()
|
|
60
84
|
if (availableModels.length === 0) {
|
|
61
|
-
throw new Error(
|
|
85
|
+
throw new Error(
|
|
86
|
+
"No supported LLM models found. Please install claude, gemini, or codex.",
|
|
87
|
+
)
|
|
62
88
|
}
|
|
63
89
|
const defaultModel = LLMService.detectDefaultModel()
|
|
64
90
|
let selectedModel = options.model
|
|
65
91
|
if (!selectedModel) {
|
|
66
|
-
const rlModel = readline.createInterface({
|
|
92
|
+
const rlModel = readline.createInterface({
|
|
93
|
+
input: process.stdin,
|
|
94
|
+
output: process.stdout,
|
|
95
|
+
})
|
|
67
96
|
selectedModel = await new Promise<string>((resolve) =>
|
|
68
|
-
rlModel.question(
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
97
|
+
rlModel.question(
|
|
98
|
+
`Select LLM model (${availableModels.join("/")}) [${defaultModel}]: `,
|
|
99
|
+
(answer) => {
|
|
100
|
+
rlModel.close()
|
|
101
|
+
resolve(answer.trim() || defaultModel)
|
|
102
|
+
},
|
|
103
|
+
),
|
|
72
104
|
)
|
|
73
105
|
}
|
|
74
106
|
LLMService.setModel(selectedModel)
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
107
|
+
LLMService.setVerbose(options.verbose || false)
|
|
79
108
|
|
|
80
109
|
// Handle clear flag
|
|
81
110
|
if (options.clear) {
|
|
@@ -100,17 +129,33 @@ async function main(): Promise<void> {
|
|
|
100
129
|
if (progressState) {
|
|
101
130
|
console.log("📂 Found previous session checkpoint")
|
|
102
131
|
console.log(ProgressTracker.formatProgressSummary(progressState))
|
|
103
|
-
|
|
132
|
+
|
|
104
133
|
const resumeChoice = await promptResume()
|
|
105
134
|
if (resumeChoice) {
|
|
106
135
|
commitsToAnalyze = ProgressTracker.getRemainingCommits(progressState)
|
|
107
136
|
analyzedCommits = progressState.analyzedCommits
|
|
108
137
|
processedCommits = progressState.processedCommits
|
|
109
|
-
|
|
138
|
+
|
|
110
139
|
// Use the output file from the previous session
|
|
111
140
|
options.output = progressState.outputFile
|
|
112
|
-
|
|
113
|
-
console.log(
|
|
141
|
+
|
|
142
|
+
console.log(
|
|
143
|
+
`\n▶️ Resuming with ${commitsToAnalyze.length} remaining commits...`,
|
|
144
|
+
)
|
|
145
|
+
console.log(
|
|
146
|
+
`📊 Previous progress: ${processedCommits.length}/${progressState.totalCommits.length} commits processed`,
|
|
147
|
+
)
|
|
148
|
+
if (options.verbose) {
|
|
149
|
+
console.log(
|
|
150
|
+
` Debug: analyzedCommits.length = ${analyzedCommits.length}`,
|
|
151
|
+
)
|
|
152
|
+
console.log(
|
|
153
|
+
` Debug: processedCommits.length = ${processedCommits.length}`,
|
|
154
|
+
)
|
|
155
|
+
console.log(
|
|
156
|
+
` Debug: commitsToAnalyze.length = ${commitsToAnalyze.length}`,
|
|
157
|
+
)
|
|
158
|
+
}
|
|
114
159
|
} else {
|
|
115
160
|
ProgressTracker.clearProgress()
|
|
116
161
|
console.log("Starting fresh analysis...")
|
|
@@ -144,14 +189,17 @@ async function main(): Promise<void> {
|
|
|
144
189
|
}
|
|
145
190
|
}
|
|
146
191
|
|
|
147
|
-
const totalCommitsToProcess =
|
|
148
|
-
|
|
192
|
+
const totalCommitsToProcess =
|
|
193
|
+
processedCommits.length + commitsToAnalyze.length
|
|
194
|
+
console.log(
|
|
195
|
+
`\nAnalyzing ${commitsToAnalyze.length} commits (${totalCommitsToProcess} total)...`,
|
|
196
|
+
)
|
|
149
197
|
|
|
150
198
|
let failedCommits = 0
|
|
151
199
|
|
|
152
200
|
// Keep track of all commits for checkpoint
|
|
153
201
|
const allCommitsToAnalyze = [...processedCommits, ...commitsToAnalyze]
|
|
154
|
-
|
|
202
|
+
|
|
155
203
|
for (const [index, hash] of commitsToAnalyze.entries()) {
|
|
156
204
|
const overallIndex = processedCommits.length + index + 1
|
|
157
205
|
console.log(
|
|
@@ -178,25 +226,39 @@ async function main(): Promise<void> {
|
|
|
178
226
|
...commitInfo,
|
|
179
227
|
analysis,
|
|
180
228
|
})
|
|
181
|
-
|
|
229
|
+
|
|
182
230
|
processedCommits.push(hash)
|
|
183
|
-
|
|
231
|
+
|
|
184
232
|
// Save progress every 10 commits or on failure
|
|
185
|
-
if (
|
|
233
|
+
if (overallIndex % 10 === 0 || index === commitsToAnalyze.length - 1) {
|
|
186
234
|
ProgressTracker.saveProgress(
|
|
187
235
|
allCommitsToAnalyze,
|
|
188
236
|
processedCommits,
|
|
189
237
|
analyzedCommits,
|
|
190
238
|
options.output!,
|
|
191
239
|
)
|
|
192
|
-
console.log(
|
|
240
|
+
console.log(
|
|
241
|
+
` 💾 Progress saved (${overallIndex}/${totalCommitsToProcess})`,
|
|
242
|
+
)
|
|
243
|
+
if (options.verbose) {
|
|
244
|
+
console.log(
|
|
245
|
+
` Debug: Saved ${processedCommits.length} processed, ${analyzedCommits.length} analyzed`,
|
|
246
|
+
)
|
|
247
|
+
}
|
|
193
248
|
}
|
|
194
249
|
} catch (error) {
|
|
195
|
-
const errorMessage =
|
|
250
|
+
const errorMessage =
|
|
251
|
+
error instanceof Error ? error.message : "Unknown error"
|
|
196
252
|
console.error(` ❌ Failed: ${errorMessage}`)
|
|
197
253
|
failedCommits++
|
|
198
254
|
processedCommits.push(hash)
|
|
199
|
-
|
|
255
|
+
|
|
256
|
+
// Check if this was a rate limit error and provide helpful messaging
|
|
257
|
+
const isRateLimitError =
|
|
258
|
+
errorMessage.includes("quota exceeded") ||
|
|
259
|
+
errorMessage.includes("rate limit") ||
|
|
260
|
+
errorMessage.includes("429")
|
|
261
|
+
|
|
200
262
|
// Save progress on failure
|
|
201
263
|
ProgressTracker.saveProgress(
|
|
202
264
|
allCommitsToAnalyze,
|
|
@@ -205,18 +267,43 @@ async function main(): Promise<void> {
|
|
|
205
267
|
options.output!,
|
|
206
268
|
)
|
|
207
269
|
console.log(` 💾 Progress saved after failure`)
|
|
208
|
-
|
|
209
|
-
//
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
270
|
+
|
|
271
|
+
// Provide specific guidance based on error type
|
|
272
|
+
if (isRateLimitError) {
|
|
273
|
+
console.error(`\n⛔ Stopping due to rate limit/quota exceeded`)
|
|
274
|
+
console.log(`💡 Suggestions:`)
|
|
275
|
+
console.log(
|
|
276
|
+
` • Wait for quota to reset (daily limits typically reset at midnight Pacific Time)`,
|
|
277
|
+
)
|
|
278
|
+
console.log(
|
|
279
|
+
` • Switch to a different model: --model claude or --model codex`,
|
|
280
|
+
)
|
|
281
|
+
console.log(` • Resume later with: --resume`)
|
|
282
|
+
} else {
|
|
283
|
+
console.error(
|
|
284
|
+
`\n⛔ Stopping due to failure (after ${LLMService.getMaxRetries()} retry attempts)`,
|
|
285
|
+
)
|
|
286
|
+
console.log(`💡 Suggestions:`)
|
|
287
|
+
console.log(` • Check your LLM model configuration and credentials`)
|
|
288
|
+
console.log(
|
|
289
|
+
` • Run with --verbose flag for detailed error information`,
|
|
290
|
+
)
|
|
291
|
+
console.log(` • Resume later with: --resume`)
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
console.log(
|
|
295
|
+
`✅ Successfully analyzed ${analyzedCommits.length} commits before failure`,
|
|
296
|
+
)
|
|
297
|
+
console.log(
|
|
298
|
+
`📁 Progress saved. Use --resume to continue from commit ${overallIndex + 1}`,
|
|
299
|
+
)
|
|
300
|
+
|
|
214
301
|
// Export what we have so far
|
|
215
302
|
if (analyzedCommits.length > 0) {
|
|
216
303
|
CSVService.exportToFile(analyzedCommits, options.output!)
|
|
217
304
|
console.log(`📊 Partial results exported to ${options.output}`)
|
|
218
305
|
}
|
|
219
|
-
|
|
306
|
+
|
|
220
307
|
process.exit(1)
|
|
221
308
|
}
|
|
222
309
|
}
|
|
@@ -230,32 +317,58 @@ async function main(): Promise<void> {
|
|
|
230
317
|
console.log(
|
|
231
318
|
`Successfully analyzed ${analyzedCommits.length}/${totalCommitsToProcess} commits`,
|
|
232
319
|
)
|
|
233
|
-
|
|
320
|
+
|
|
234
321
|
if (failedCommits > 0) {
|
|
235
|
-
console.log(
|
|
322
|
+
console.log(
|
|
323
|
+
`⚠️ Failed to analyze ${failedCommits} commits (see errors above)`,
|
|
324
|
+
)
|
|
236
325
|
}
|
|
237
|
-
|
|
326
|
+
|
|
238
327
|
// Generate report if --report flag is provided
|
|
239
328
|
if (options.report) {
|
|
240
329
|
console.log("\nGenerating condensed markdown report...")
|
|
241
|
-
|
|
330
|
+
|
|
242
331
|
// Determine report output filename
|
|
243
332
|
let reportOutput: string
|
|
244
|
-
if (options.output!.endsWith(
|
|
245
|
-
reportOutput = options.output!.replace(
|
|
333
|
+
if (options.output!.endsWith(".csv")) {
|
|
334
|
+
reportOutput = options.output!.replace(".csv", ".md")
|
|
246
335
|
} else {
|
|
247
|
-
reportOutput = options.output! +
|
|
336
|
+
reportOutput = options.output! + ".md"
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Handle default case - if output is commits.csv, make report report.md
|
|
340
|
+
if (reportOutput.endsWith("commits.md")) {
|
|
341
|
+
reportOutput = reportOutput.replace("commits.md", "report.md")
|
|
248
342
|
}
|
|
249
|
-
|
|
343
|
+
|
|
344
|
+
// If output directory is specified but report output is just a filename, use the output directory
|
|
345
|
+
if (
|
|
346
|
+
options.outputDir &&
|
|
347
|
+
!reportOutput.includes("/") &&
|
|
348
|
+
!reportOutput.includes("\\")
|
|
349
|
+
) {
|
|
350
|
+
reportOutput = CLIService.resolveOutputPath(
|
|
351
|
+
reportOutput.split("/").pop() || reportOutput,
|
|
352
|
+
options.outputDir,
|
|
353
|
+
)
|
|
354
|
+
}
|
|
355
|
+
|
|
250
356
|
try {
|
|
251
|
-
await MarkdownReportGenerator.generateReport(
|
|
357
|
+
await MarkdownReportGenerator.generateReport(
|
|
358
|
+
options.output!,
|
|
359
|
+
reportOutput,
|
|
360
|
+
)
|
|
252
361
|
console.log(`📊 Report generated: ${reportOutput}`)
|
|
253
362
|
} catch (error) {
|
|
254
|
-
console.error(
|
|
255
|
-
|
|
363
|
+
console.error(
|
|
364
|
+
`⚠️ Failed to generate report: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
365
|
+
)
|
|
366
|
+
console.log(
|
|
367
|
+
"CSV analysis was successful, but report generation failed.",
|
|
368
|
+
)
|
|
256
369
|
}
|
|
257
370
|
}
|
|
258
|
-
|
|
371
|
+
|
|
259
372
|
// Clear checkpoint on successful completion
|
|
260
373
|
ProgressTracker.clearProgress()
|
|
261
374
|
console.log("✓ Progress checkpoint cleared (analysis complete)")
|
|
@@ -280,4 +393,3 @@ async function main(): Promise<void> {
|
|
|
280
393
|
if (require.main === module) {
|
|
281
394
|
main()
|
|
282
395
|
}
|
|
283
|
-
|
package/src/llm.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { CommitInfo, LLMAnalysis } from "./types"
|
|
|
3
3
|
|
|
4
4
|
export class LLMService {
|
|
5
5
|
private static model: string
|
|
6
|
+
private static verbose: boolean = false
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Detect available LLM models by checking CLI commands.
|
|
@@ -43,6 +44,13 @@ export class LLMService {
|
|
|
43
44
|
this.model = model
|
|
44
45
|
}
|
|
45
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Set verbose mode for detailed error logging.
|
|
49
|
+
*/
|
|
50
|
+
static setVerbose(verbose: boolean): void {
|
|
51
|
+
this.verbose = verbose
|
|
52
|
+
}
|
|
53
|
+
|
|
46
54
|
/**
|
|
47
55
|
* Get the configured LLM model or detect default.
|
|
48
56
|
*/
|
|
@@ -52,6 +60,20 @@ export class LLMService {
|
|
|
52
60
|
}
|
|
53
61
|
return this.model
|
|
54
62
|
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Get the model command with appropriate flags.
|
|
66
|
+
*/
|
|
67
|
+
static getModelCommand(): string {
|
|
68
|
+
const model = this.getModel()
|
|
69
|
+
|
|
70
|
+
// Append -q flag for codex model
|
|
71
|
+
if (model === 'codex') {
|
|
72
|
+
return 'codex -q'
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return model
|
|
76
|
+
}
|
|
55
77
|
private static readonly MAX_RETRIES = parseInt(
|
|
56
78
|
process.env.LLM_MAX_RETRIES || "3",
|
|
57
79
|
10,
|
|
@@ -79,6 +101,7 @@ export class LLMService {
|
|
|
79
101
|
|
|
80
102
|
static async analyzeCommit(commit: CommitInfo): Promise<LLMAnalysis> {
|
|
81
103
|
const currentModel = this.getModel()
|
|
104
|
+
const currentModelCommand = this.getModelCommand()
|
|
82
105
|
const prompt = this.buildPrompt(commit.message, commit.diff, currentModel)
|
|
83
106
|
|
|
84
107
|
// Log prompt length for debugging - only for Claude models
|
|
@@ -93,7 +116,7 @@ export class LLMService {
|
|
|
93
116
|
|
|
94
117
|
for (let attempt = 1; attempt <= this.MAX_RETRIES; attempt++) {
|
|
95
118
|
try {
|
|
96
|
-
const output = execSync(
|
|
119
|
+
const output = execSync(currentModelCommand, {
|
|
97
120
|
input: prompt,
|
|
98
121
|
encoding: "utf8",
|
|
99
122
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -104,21 +127,62 @@ export class LLMService {
|
|
|
104
127
|
} catch (error) {
|
|
105
128
|
lastError = error instanceof Error ? error : new Error("Unknown error")
|
|
106
129
|
|
|
107
|
-
//
|
|
108
|
-
|
|
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
|
-
}
|
|
130
|
+
// Check if this is a rate limit error
|
|
131
|
+
const rateLimitInfo = this.isRateLimitError(error)
|
|
114
132
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
const
|
|
118
|
-
console.log(`
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
133
|
+
if (rateLimitInfo.isRateLimit) {
|
|
134
|
+
// For rate limits, show user-friendly message immediately
|
|
135
|
+
const friendlyMessage = this.getRateLimitMessage(rateLimitInfo.service, rateLimitInfo.limitType)
|
|
136
|
+
console.log(` - ${friendlyMessage}`)
|
|
137
|
+
|
|
138
|
+
// Show detailed error info only in verbose mode
|
|
139
|
+
if (this.verbose) {
|
|
140
|
+
console.log(` - Verbose error details for commit ${commit.hash.substring(0, 8)}:`)
|
|
141
|
+
console.log(` Command: ${currentModelCommand}`)
|
|
142
|
+
console.log(` Error message: ${lastError.message}`)
|
|
143
|
+
if (this.isClaudeModel(currentModel)) {
|
|
144
|
+
console.log(` Prompt length: ${prompt.length} characters`)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// If it's an exec error, log additional details
|
|
148
|
+
if (error && typeof error === 'object' && 'stderr' in error) {
|
|
149
|
+
const execError = error as any
|
|
150
|
+
console.log(` Exit code: ${execError.status || 'unknown'}`)
|
|
151
|
+
console.log(` Signal: ${execError.signal || 'none'}`)
|
|
152
|
+
if (execError.stderr) {
|
|
153
|
+
console.log(` Stderr: ${execError.stderr.substring(0, 1000)}${execError.stderr.length > 1000 ? '...' : ''}`)
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// If it's a daily quota error, don't retry - fail immediately
|
|
159
|
+
if (rateLimitInfo.limitType === "daily quota") {
|
|
160
|
+
throw new Error(
|
|
161
|
+
`Daily quota exceeded for ${rateLimitInfo.service || 'LLM service'}. Retrying will not help until quota resets.`,
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
} else {
|
|
165
|
+
// For non-rate-limit errors, show detailed info based on verbose mode
|
|
166
|
+
if (this.verbose) {
|
|
167
|
+
console.log(` - Error details for commit ${commit.hash.substring(0, 8)}:`)
|
|
168
|
+
console.log(` Command: ${currentModelCommand}`)
|
|
169
|
+
console.log(` Error message: ${lastError.message}`)
|
|
170
|
+
if (this.isClaudeModel(currentModel)) {
|
|
171
|
+
console.log(` Prompt length: ${prompt.length} characters`)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// If it's an exec error, log additional details
|
|
175
|
+
if (error && typeof error === 'object' && 'stderr' in error) {
|
|
176
|
+
const execError = error as any
|
|
177
|
+
console.log(` Exit code: ${execError.status || 'unknown'}`)
|
|
178
|
+
console.log(` Signal: ${execError.signal || 'none'}`)
|
|
179
|
+
console.log(` Stderr: ${execError.stderr || 'none'}`)
|
|
180
|
+
console.log(` Stdout: ${execError.stdout || 'none'}`)
|
|
181
|
+
}
|
|
182
|
+
} else {
|
|
183
|
+
// In non-verbose mode, show concise error message
|
|
184
|
+
console.log(` - Analysis failed: ${lastError.message}`)
|
|
185
|
+
}
|
|
122
186
|
}
|
|
123
187
|
|
|
124
188
|
if (attempt < this.MAX_RETRIES) {
|
|
@@ -280,4 +344,68 @@ Format as JSON:
|
|
|
280
344
|
): category is "tweak" | "feature" | "process" {
|
|
281
345
|
return ["tweak", "feature", "process"].includes(category)
|
|
282
346
|
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Check if an error is related to rate limiting or quota exceeded.
|
|
350
|
+
*/
|
|
351
|
+
private static isRateLimitError(error: any): { isRateLimit: boolean; service?: string; limitType?: string } {
|
|
352
|
+
const errorMessage = error?.message?.toLowerCase() || ""
|
|
353
|
+
const stderr = error?.stderr?.toLowerCase() || ""
|
|
354
|
+
const stdout = error?.stdout?.toLowerCase() || ""
|
|
355
|
+
const combinedOutput = `${errorMessage} ${stderr} ${stdout}`
|
|
356
|
+
|
|
357
|
+
// Check for Gemini rate limit patterns
|
|
358
|
+
if (combinedOutput.includes("quota exceeded") && combinedOutput.includes("gemini")) {
|
|
359
|
+
if (combinedOutput.includes("requests per day")) {
|
|
360
|
+
return { isRateLimit: true, service: "Gemini", limitType: "daily quota" }
|
|
361
|
+
}
|
|
362
|
+
if (combinedOutput.includes("requests per minute")) {
|
|
363
|
+
return { isRateLimit: true, service: "Gemini", limitType: "per-minute rate limit" }
|
|
364
|
+
}
|
|
365
|
+
return { isRateLimit: true, service: "Gemini", limitType: "quota limit" }
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Check for Claude rate limit patterns
|
|
369
|
+
if (combinedOutput.includes("rate limit") && combinedOutput.includes("claude")) {
|
|
370
|
+
return { isRateLimit: true, service: "Claude", limitType: "rate limit" }
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Check for generic rate limit indicators
|
|
374
|
+
if (combinedOutput.includes("429") ||
|
|
375
|
+
combinedOutput.includes("too many requests") ||
|
|
376
|
+
combinedOutput.includes("rate limit") ||
|
|
377
|
+
combinedOutput.includes("quota exceeded")) {
|
|
378
|
+
// Try to determine service from model name
|
|
379
|
+
const currentModel = this.getModel().toLowerCase()
|
|
380
|
+
if (currentModel.includes("gemini")) {
|
|
381
|
+
return { isRateLimit: true, service: "Gemini", limitType: "rate/quota limit" }
|
|
382
|
+
}
|
|
383
|
+
if (currentModel.includes("claude")) {
|
|
384
|
+
return { isRateLimit: true, service: "Claude", limitType: "rate limit" }
|
|
385
|
+
}
|
|
386
|
+
return { isRateLimit: true, limitType: "rate/quota limit" }
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return { isRateLimit: false }
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Get user-friendly error message for rate limit errors.
|
|
394
|
+
*/
|
|
395
|
+
private static getRateLimitMessage(service?: string, limitType?: string): string {
|
|
396
|
+
if (service === "Gemini" && limitType === "daily quota") {
|
|
397
|
+
return "⚠️ Gemini daily quota exceeded. The limit resets at midnight Pacific Time. Consider switching to a different model or resuming tomorrow."
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (service === "Gemini" && limitType === "per-minute rate limit") {
|
|
401
|
+
return "⚠️ Gemini rate limit exceeded. Wait a minute before retrying, or consider switching to a different model."
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (service === "Claude") {
|
|
405
|
+
return "⚠️ Claude rate limit exceeded. Wait a moment before retrying, or consider switching to a different model."
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const serviceMsg = service ? `${service} ` : ""
|
|
409
|
+
return `⚠️ ${serviceMsg}rate limit exceeded. Consider switching models or waiting before retrying.`
|
|
410
|
+
}
|
|
283
411
|
}
|
package/src/progress.ts
CHANGED
|
@@ -19,12 +19,19 @@ export class ProgressTracker {
|
|
|
19
19
|
analyzedCommits: AnalyzedCommit[],
|
|
20
20
|
outputFile: string,
|
|
21
21
|
): void {
|
|
22
|
+
// Preserve the original start time if this is an update to existing progress
|
|
23
|
+
let startTime = new Date().toISOString()
|
|
24
|
+
const existingState = this.loadProgress()
|
|
25
|
+
if (existingState) {
|
|
26
|
+
startTime = existingState.startTime
|
|
27
|
+
}
|
|
28
|
+
|
|
22
29
|
const state: ProgressState = {
|
|
23
30
|
totalCommits,
|
|
24
31
|
processedCommits,
|
|
25
32
|
analyzedCommits,
|
|
26
33
|
lastProcessedIndex: processedCommits.length - 1,
|
|
27
|
-
startTime
|
|
34
|
+
startTime,
|
|
28
35
|
outputFile,
|
|
29
36
|
}
|
|
30
37
|
|