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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "commit-analyzer",
3
- "version": "1.0.1",
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("Analyze git commits and generate categorized summaries")
25
- .version("1.0.0")
26
- .option("-o, --output <file>", "Output CSV file (default: output.csv)")
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
- "-r, --resume",
42
- "Resume from last checkpoint if available",
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: options.output || "output.csv",
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> Output file (default: output.csv for analysis, summary-report.md for reports)
118
- -f, --file <file> Read commit hashes from file (one per line)
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("\nDo you want to resume from the checkpoint? (y/n): ", (answer) => {
21
- rl.close()
22
- resolve(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes")
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("Note: --report flag automatically enabled when using --input-csv")
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 = options.output || "summary-report.md"
47
- if (reportOutput === "output.csv") {
48
- reportOutput = "summary-report.md"
49
- } else if (!reportOutput.endsWith('.md')) {
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(/\.[^.]+$/, '') + '.md'
72
+ reportOutput = reportOutput.replace(/\.[^.]+$/, "") + ".md"
52
73
  }
53
-
54
- await MarkdownReportGenerator.generateReport(options.inputCsv, reportOutput)
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("No supported LLM models found. Please install claude, gemini, or codex.")
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({ input: process.stdin, output: process.stdout })
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(`Select LLM model (${availableModels.join("/")}) [${defaultModel}]: `, (answer) => {
69
- rlModel.close()
70
- resolve(answer.trim() || defaultModel)
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(`\n▶️ Resuming with ${commitsToAnalyze.length} remaining commits...`)
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 = processedCommits.length + commitsToAnalyze.length
148
- console.log(`\nAnalyzing ${commitsToAnalyze.length} commits (${totalCommitsToProcess} total)...`)
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 ((overallIndex % 10 === 0) || index === commitsToAnalyze.length - 1) {
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(` 💾 Progress saved (${overallIndex}/${totalCommitsToProcess})`)
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 = error instanceof Error ? error.message : "Unknown error"
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
- // Always stop on failure after max retries
210
- console.error(`\n⛔ Stopping due to failure (after ${LLMService.getMaxRetries()} retry attempts)`)
211
- console.log(`✅ Successfully analyzed ${analyzedCommits.length} commits before failure`)
212
- console.log(`📁 Progress saved. Use --resume to continue from commit ${overallIndex + 1}`)
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(`⚠️ Failed to analyze ${failedCommits} commits (see errors above)`)
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('.csv')) {
245
- reportOutput = options.output!.replace('.csv', '-report.md')
333
+ if (options.output!.endsWith(".csv")) {
334
+ reportOutput = options.output!.replace(".csv", ".md")
246
335
  } else {
247
- reportOutput = options.output! + '-report.md'
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(options.output!, reportOutput)
357
+ await MarkdownReportGenerator.generateReport(
358
+ options.output!,
359
+ reportOutput,
360
+ )
252
361
  console.log(`📊 Report generated: ${reportOutput}`)
253
362
  } catch (error) {
254
- console.error(`⚠️ Failed to generate report: ${error instanceof Error ? error.message : "Unknown error"}`)
255
- console.log("CSV analysis was successful, but report generation failed.")
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(currentModel, {
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
- // 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
- }
130
+ // Check if this is a rate limit error
131
+ const rateLimitInfo = this.isRateLimitError(error)
114
132
 
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'}`)
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: new Date().toISOString(),
34
+ startTime,
28
35
  outputFile,
29
36
  }
30
37