commit-analyzer 1.1.4 → 1.1.6

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.
Files changed (55) hide show
  1. package/README.md +164 -82
  2. package/dist/main.ts +0 -0
  3. package/package.json +2 -1
  4. package/.claude/settings.local.json +0 -23
  5. package/commits.csv +0 -2
  6. package/csv-to-report-prompt.md +0 -97
  7. package/eslint.config.mts +0 -45
  8. package/prompt.md +0 -69
  9. package/src/1.domain/analysis.ts +0 -93
  10. package/src/1.domain/analyzed-commit.ts +0 -97
  11. package/src/1.domain/application-error.ts +0 -32
  12. package/src/1.domain/category.ts +0 -52
  13. package/src/1.domain/commit-analysis-service.ts +0 -92
  14. package/src/1.domain/commit-hash.ts +0 -40
  15. package/src/1.domain/commit.ts +0 -99
  16. package/src/1.domain/date-formatting-service.ts +0 -81
  17. package/src/1.domain/date-range.ts +0 -76
  18. package/src/1.domain/report-generation-service.ts +0 -443
  19. package/src/2.application/analyze-commits.usecase.ts +0 -307
  20. package/src/2.application/generate-report.usecase.ts +0 -209
  21. package/src/2.application/llm-service.ts +0 -54
  22. package/src/2.application/resume-analysis.usecase.ts +0 -123
  23. package/src/3.presentation/analysis-repository.interface.ts +0 -27
  24. package/src/3.presentation/analyze-command.ts +0 -128
  25. package/src/3.presentation/cli-application.ts +0 -278
  26. package/src/3.presentation/command-handler.interface.ts +0 -4
  27. package/src/3.presentation/commit-analysis-controller.ts +0 -101
  28. package/src/3.presentation/commit-repository.interface.ts +0 -47
  29. package/src/3.presentation/console-formatter.ts +0 -129
  30. package/src/3.presentation/progress-repository.interface.ts +0 -49
  31. package/src/3.presentation/report-command.ts +0 -50
  32. package/src/3.presentation/resume-command.ts +0 -59
  33. package/src/3.presentation/storage-repository.interface.ts +0 -33
  34. package/src/3.presentation/storage-service.interface.ts +0 -32
  35. package/src/3.presentation/version-control-service.interface.ts +0 -46
  36. package/src/4.infrastructure/cache-service.ts +0 -271
  37. package/src/4.infrastructure/cached-analysis-repository.ts +0 -46
  38. package/src/4.infrastructure/claude-llm-adapter.ts +0 -124
  39. package/src/4.infrastructure/csv-service.ts +0 -252
  40. package/src/4.infrastructure/file-storage-repository.ts +0 -108
  41. package/src/4.infrastructure/file-system-storage-adapter.ts +0 -87
  42. package/src/4.infrastructure/gemini-llm-adapter.ts +0 -46
  43. package/src/4.infrastructure/git-adapter.ts +0 -143
  44. package/src/4.infrastructure/git-commit-repository.ts +0 -85
  45. package/src/4.infrastructure/json-progress-tracker.ts +0 -182
  46. package/src/4.infrastructure/llm-adapter-factory.ts +0 -26
  47. package/src/4.infrastructure/llm-adapter.ts +0 -485
  48. package/src/4.infrastructure/llm-analysis-repository.ts +0 -38
  49. package/src/4.infrastructure/openai-llm-adapter.ts +0 -57
  50. package/src/di.ts +0 -109
  51. package/src/main.ts +0 -63
  52. package/src/utils/app-paths.ts +0 -36
  53. package/src/utils/concurrency.ts +0 -81
  54. package/src/utils.ts +0 -77
  55. package/tsconfig.json +0 -25
@@ -1,46 +0,0 @@
1
- import { Analysis } from "@domain/analysis"
2
- import { Commit } from "@domain/commit"
3
-
4
- import { IAnalysisRepository } from "@presentation/analysis-repository.interface"
5
-
6
- import { CacheService } from "./cache-service"
7
-
8
- /**
9
- * Cached analysis repository that wraps another analysis repository
10
- */
11
- export class CachedAnalysisRepository implements IAnalysisRepository {
12
- constructor(
13
- private readonly baseRepository: IAnalysisRepository,
14
- private readonly cacheService: CacheService,
15
- ) {}
16
-
17
- async analyze(commit: Commit): Promise<Analysis> {
18
- const commitHash = commit.getHash().getValue()
19
-
20
- // Try to get from cache first
21
- const cachedAnalysis = await this.cacheService.get(commitHash)
22
- if (cachedAnalysis) {
23
- return cachedAnalysis
24
- }
25
-
26
- // Cache miss - analyze with base repository
27
- const analysis = await this.baseRepository.analyze(commit)
28
-
29
- // Store in cache for next time
30
- await this.cacheService.set(commitHash, analysis)
31
-
32
- return analysis
33
- }
34
-
35
- async isAvailable(): Promise<boolean> {
36
- return this.baseRepository.isAvailable()
37
- }
38
-
39
- getMaxRetries(): number {
40
- return this.baseRepository.getMaxRetries()
41
- }
42
-
43
- setVerbose(verbose: boolean): void {
44
- this.baseRepository.setVerbose(verbose)
45
- }
46
- }
@@ -1,124 +0,0 @@
1
- import { execSync } from "child_process"
2
-
3
- import { CategoryType } from "@domain/category"
4
-
5
- import { LLMAdapter } from "./llm-adapter"
6
-
7
- export class ClaudeLLMAdapter extends LLMAdapter {
8
- private static readonly MAX_PROMPT_LENGTH = parseInt(
9
- process.env.CLAUDE_MAX_PROMPT_LENGTH || "100000",
10
- 10,
11
- )
12
-
13
- protected getMaxPromptLength(): number {
14
- return ClaudeLLMAdapter.MAX_PROMPT_LENGTH
15
- }
16
-
17
- async detectAvailableModels(): Promise<string[]> {
18
- try {
19
- execSync("command -v claude", { stdio: "ignore" })
20
- return ["claude", "claude --model sonnet", "claude --model haiku"]
21
- } catch {
22
- return []
23
- }
24
- }
25
-
26
- async isAvailable(): Promise<boolean> {
27
- const available = await this.detectAvailableModels()
28
- return available.length > 0
29
- }
30
-
31
- protected async executeModelCommand(prompt: string): Promise<string> {
32
- const truncatedPrompt = this.truncatePrompt(prompt)
33
-
34
- if (this.verbose) {
35
- console.log(` - Prompt length: ${truncatedPrompt.length} characters`)
36
- }
37
-
38
- const modelCommand = this.model || "claude --model sonnet"
39
-
40
- return execSync(modelCommand, {
41
- input: truncatedPrompt,
42
- encoding: "utf8",
43
- stdio: ["pipe", "pipe", "pipe"],
44
- timeout: LLMAdapter.DEFAULT_TIMEOUT,
45
- })
46
- }
47
-
48
- protected parseResponse(response: string): {
49
- category: CategoryType
50
- summary: string
51
- description: string
52
- } {
53
- try {
54
- // First try standard JSON parsing
55
- return super.parseResponse(response)
56
- } catch {
57
- // Claude often responds in natural language format, so try to parse that
58
- if (this.verbose) {
59
- console.log(` - Standard JSON parsing failed, trying Claude natural language parsing...`)
60
- }
61
- return this.parseClaudeNaturalLanguageResponse(response)
62
- }
63
- }
64
-
65
- private parseClaudeNaturalLanguageResponse(response: string): {
66
- category: CategoryType
67
- summary: string
68
- description: string
69
- } {
70
- // Try to extract category from various Claude response patterns
71
- const categoryMatch = response.match(/\*\*?Category\*\*?:?\s*(tweak|feature|process)/i) ||
72
- response.match(/Category:\s*(tweak|feature|process)/i) ||
73
- response.match(/\*\*(tweak|feature|process)\*\*/i) ||
74
- response.match(/(tweak|feature|process)\s*commit/i) ||
75
- response.match(/should be categorized as[:\s]*\*\*?(tweak|feature|process)/i)
76
- const category = categoryMatch?.[1]?.toLowerCase()
77
-
78
- if (!category || !this.isValidCategory(category)) {
79
- if (this.verbose) {
80
- console.log(` - Failed to extract category from Claude response`)
81
- console.log(` - Response snippet: ${response.substring(0, 500)}`)
82
- }
83
- throw new Error(`Could not extract valid category from Claude response`)
84
- }
85
-
86
- // Try to extract summary from patterns
87
- const summaryMatch = response.match(/\*\*?Summary\*\*?:?\s*([^\n\r]+)/i) ||
88
- response.match(/Summary:\s*([^\n\r]+)/i)
89
- let summary = summaryMatch?.[1]?.trim()
90
-
91
- if (!summary) {
92
- // Fallback: try to find a descriptive line
93
- const lines = response.split('\n').filter(line => line.trim())
94
- summary = lines.find(line =>
95
- line.includes('refactor') ||
96
- line.includes('add') ||
97
- line.includes('fix') ||
98
- line.includes('update') ||
99
- line.includes('implement')
100
- )?.trim() || "Code changes"
101
- }
102
-
103
- // Try to extract description
104
- const descMatch = response.match(/\*\*?Description\*\*?:?\s*([\s\S]+?)(?=\n\n|\n\*\*|\n---|\n#|$)/i) ||
105
- response.match(/Description:\s*([\s\S]+?)(?=\n\n|\n\*\*|\n---|\n#|$)/i)
106
- let description = descMatch?.[1]?.trim()
107
-
108
- if (!description) {
109
- // Fallback: extract the longest meaningful sentence
110
- const sentences = response.split(/[.!?]+/).filter(s => s.trim().length > 20)
111
- description = sentences[0]?.trim() || "Commit contains code changes"
112
- }
113
-
114
- // Clean up and truncate
115
- summary = summary.substring(0, 80).replace(/[*"]/g, '').trim()
116
- description = description.replace(/[*"]/g, '').trim()
117
-
118
- return {
119
- category: category as CategoryType,
120
- summary: summary || "Code changes",
121
- description: description || "This commit contains code changes."
122
- }
123
- }
124
- }
@@ -1,252 +0,0 @@
1
- import { Analysis } from "@domain/analysis"
2
- import { AnalyzedCommit } from "@domain/analyzed-commit"
3
- import { Category } from "@domain/category"
4
- import { Commit } from "@domain/commit"
5
- import { CommitHash } from "@domain/commit-hash"
6
-
7
- import { IStorageService } from "@presentation/storage-service.interface"
8
-
9
- export class CSVService {
10
- private static readonly CSV_HEADERS = "timestamp,category,summary,description"
11
- private static readonly CSV_SPECIAL_CHARS = [",", '"', "\n"]
12
-
13
- constructor(private readonly storageService: IStorageService) {}
14
-
15
- async exportToCSV(
16
- commits: AnalyzedCommit[],
17
- filePath: string,
18
- ): Promise<void> {
19
- const csvContent = this.generateCSV(commits)
20
- await this.storageService.writeFile(filePath, csvContent)
21
- }
22
-
23
- async importFromCSV(filePath: string): Promise<AnalyzedCommit[]> {
24
- const content = await this.storageService.readFile(filePath)
25
- return this.parseCSV(content)
26
- }
27
-
28
- private generateCSV(commits: AnalyzedCommit[]): string {
29
- const rows = commits.map((commit) => this.formatRow(commit))
30
- return [CSVService.CSV_HEADERS, ...rows].join("\n")
31
- }
32
-
33
- private formatRow(commit: AnalyzedCommit): string {
34
- const row = commit.toCSVRow()
35
- return this.joinCsvFields(row)
36
- }
37
-
38
- private joinCsvFields(row: {
39
- timestamp: string
40
- category: string
41
- summary: string
42
- description: string
43
- }): string {
44
- return [
45
- row.timestamp,
46
- this.escapeCsvField(row.category),
47
- this.escapeCsvField(row.summary),
48
- this.escapeCsvField(row.description),
49
- ].join(",")
50
- }
51
-
52
- private escapeCsvField(field: string): string {
53
- if (this.needsEscaping(field)) {
54
- return this.escapeAndQuoteField(field)
55
- }
56
- return field
57
- }
58
-
59
- private needsEscaping(field: string): boolean {
60
- return CSVService.CSV_SPECIAL_CHARS.some((char) => field.includes(char))
61
- }
62
-
63
- private escapeAndQuoteField(field: string): string {
64
- return `"${field.replace(/"/g, '""')}"`
65
- }
66
-
67
- /**
68
- * Split CSV content into rows, properly handling quoted fields that may contain newlines
69
- */
70
- private splitCSVIntoRows(content: string): string[] {
71
- const rows: string[] = []
72
- let currentRow = ""
73
- let inQuotes = false
74
- let i = 0
75
-
76
- while (i < content.length) {
77
- const char = content[i]
78
- const nextChar = content[i + 1]
79
-
80
- if (char === '"') {
81
- if (inQuotes && nextChar === '"') {
82
- // Escaped quote inside quoted field
83
- currentRow += '""'
84
- i += 2
85
- } else {
86
- // Start or end of quoted field
87
- inQuotes = !inQuotes
88
- currentRow += '"'
89
- i++
90
- }
91
- } else if (char === "\n" && !inQuotes) {
92
- // Row separator outside quotes
93
- if (currentRow.trim().length > 0) {
94
- rows.push(currentRow)
95
- }
96
- currentRow = ""
97
- i++
98
- } else {
99
- // Regular character (including newlines inside quotes)
100
- currentRow += char
101
- i++
102
- }
103
- }
104
-
105
- // Add the last row if it exists
106
- if (currentRow.trim().length > 0) {
107
- rows.push(currentRow)
108
- }
109
-
110
- return rows
111
- }
112
-
113
- private parseCSV(content: string): AnalyzedCommit[] {
114
- const rows = this.splitCSVIntoRows(content)
115
-
116
- if (rows.length < 2) {
117
- throw new Error("Invalid CSV format: no data rows found")
118
- }
119
-
120
- // Validate header
121
- const header = rows[0].toLowerCase()
122
- const expectedHeader = "timestamp,category,summary,description"
123
- if (header !== expectedHeader) {
124
- throw new Error(
125
- `Invalid CSV format. Expected header: "${expectedHeader}", got: "${header}"`,
126
- )
127
- }
128
-
129
- // Skip header row
130
- const dataRows = rows.slice(1)
131
- const commits: AnalyzedCommit[] = []
132
-
133
- for (let i = 0; i < dataRows.length; i++) {
134
- const row = dataRows[i]
135
- try {
136
- const commit = this.parseCSVRow(row)
137
- commits.push(commit)
138
- } catch (error) {
139
- console.warn(`Warning: Failed to parse CSV row ${i + 2}: ${row}`)
140
- console.warn(`Error: ${error}`)
141
- }
142
- }
143
-
144
- return commits
145
- }
146
-
147
- private parseCSVRow(row: string): AnalyzedCommit {
148
- const fields = this.parseCSVFields(row)
149
-
150
- if (fields.length !== 4) {
151
- throw new Error(
152
- `Expected 4 fields (timestamp,category,summary,description), got ${fields.length}`,
153
- )
154
- }
155
-
156
- const [timestampStr, category, summary, description] = fields
157
-
158
- // Validate timestamp
159
- const timestamp = new Date(timestampStr)
160
- if (isNaN(timestamp.getTime())) {
161
- throw new Error(`Invalid timestamp: ${timestampStr}`)
162
- }
163
-
164
- // Validate category
165
- if (!this.isValidCategory(category)) {
166
- throw new Error(
167
- `Invalid category: ${category}. Must be one of: tweak, feature, process`,
168
- )
169
- }
170
-
171
- // Validate required fields
172
- if (!summary.trim()) {
173
- throw new Error("Summary field cannot be empty")
174
- }
175
-
176
- if (!description.trim()) {
177
- throw new Error("Description field cannot be empty")
178
- }
179
-
180
- // Create a minimal commit object for CSV import
181
- // We'll use placeholder values for hash and diff since they're not in the CSV
182
- const placeholderHash = CommitHash.create(
183
- "0000000000000000000000000000000000000000",
184
- )
185
- const placeholderDiff = "# Placeholder diff for CSV import\n+1\n-0" // Minimal valid diff
186
- const placeholderMessage = summary // Use summary as message
187
-
188
- const commit = new Commit({
189
- hash: placeholderHash,
190
- message: placeholderMessage,
191
- date: timestamp, // Use actual timestamp from CSV
192
- diff: placeholderDiff,
193
- })
194
-
195
- // Create analysis from CSV data
196
- const analysisCategory = Category.create(category)
197
- const analysis = new Analysis({
198
- category: analysisCategory,
199
- summary: summary.trim(),
200
- description: description.trim(),
201
- })
202
-
203
- return new AnalyzedCommit(commit, analysis)
204
- }
205
-
206
- /**
207
- * Parse CSV fields handling quoted fields with commas and escaped quotes
208
- */
209
- private parseCSVFields(line: string): string[] {
210
- const fields: string[] = []
211
- let currentField = ""
212
- let inQuotes = false
213
- let i = 0
214
-
215
- while (i < line.length) {
216
- const char = line[i]
217
- const nextChar = line[i + 1]
218
-
219
- if (char === '"') {
220
- if (inQuotes && nextChar === '"') {
221
- // Escaped quote inside quoted field
222
- currentField += '"'
223
- i += 2
224
- } else {
225
- // Start or end of quoted field
226
- inQuotes = !inQuotes
227
- i++
228
- }
229
- } else if (char === "," && !inQuotes) {
230
- // Field separator outside quotes
231
- fields.push(currentField)
232
- currentField = ""
233
- i++
234
- } else {
235
- // Regular character
236
- currentField += char
237
- i++
238
- }
239
- }
240
-
241
- // Add the last field
242
- fields.push(currentField)
243
-
244
- return fields
245
- }
246
-
247
- private isValidCategory(
248
- category: string,
249
- ): category is "tweak" | "feature" | "process" {
250
- return ["tweak", "feature", "process"].includes(category)
251
- }
252
- }
@@ -1,108 +0,0 @@
1
- import { AnalyzedCommit } from "@domain/analyzed-commit"
2
-
3
- import { IStorageRepository } from "@presentation/storage-repository.interface"
4
- import { IStorageService } from "@presentation/storage-service.interface"
5
-
6
- import { CSVService } from "./csv-service"
7
-
8
- export class FileStorageRepository implements IStorageRepository {
9
- private readonly csvService: CSVService
10
-
11
- constructor(private readonly storageService: IStorageService) {
12
- this.csvService = new CSVService(storageService)
13
- }
14
-
15
- async exportToCSV(
16
- commits: AnalyzedCommit[],
17
- filePath: string,
18
- ): Promise<void> {
19
- await this.csvService.exportToCSV(commits, filePath)
20
- }
21
-
22
- async importFromCSV(filePath: string): Promise<AnalyzedCommit[]> {
23
- return this.csvService.importFromCSV(filePath)
24
- }
25
-
26
- async generateReport(
27
- commits: AnalyzedCommit[],
28
- outputPath: string,
29
- ): Promise<void> {
30
- // Generate markdown report content
31
- const reportContent = this.generateMarkdownReport(commits)
32
- await this.storageService.writeFile(outputPath, reportContent)
33
- }
34
-
35
- async readCommitHashesFromFile(filePath: string): Promise<string[]> {
36
- return this.storageService.readLines(filePath)
37
- }
38
-
39
- async ensureDirectoryExists(directoryPath: string): Promise<void> {
40
- await this.storageService.ensureDirectory(directoryPath)
41
- }
42
-
43
- async writeFile(filePath: string, content: string): Promise<void> {
44
- await this.storageService.writeFile(filePath, content)
45
- }
46
-
47
- private generateMarkdownReport(commits: AnalyzedCommit[]): string {
48
- let content = "# Development Summary Report\n\n"
49
-
50
- // Basic statistics
51
- const totalCommits = commits.length
52
- const years = commits.map((c) => c.getYear())
53
- const minYear = Math.min(...years)
54
- const maxYear = Math.max(...years)
55
-
56
- const categoryBreakdown = commits.reduce(
57
- (acc, commit) => {
58
- const category = commit.getAnalysis().getCategory().getValue()
59
- acc[category] = (acc[category] || 0) + 1
60
- return acc
61
- },
62
- {} as Record<string, number>,
63
- )
64
-
65
- content += `## Analysis Summary\n\n`
66
- content += `**Total Commits Analyzed:** ${totalCommits}\n`
67
- content += `**Time Period:** ${minYear} - ${maxYear}\n\n`
68
-
69
- content += `### Breakdown by Category\n\n`
70
- content += `- **Features:** ${categoryBreakdown.feature || 0} commits\n`
71
- content += `- **Process/Infrastructure:** ${categoryBreakdown.process || 0} commits\n`
72
- content += `- **Tweaks/Fixes:** ${categoryBreakdown.tweak || 0} commits\n\n`
73
-
74
- // Group by year and add yearly summaries
75
- const commitsByYear = new Map<number, AnalyzedCommit[]>()
76
- for (const commit of commits) {
77
- const year = commit.getYear()
78
- if (!commitsByYear.has(year)) {
79
- commitsByYear.set(year, [])
80
- }
81
- commitsByYear.get(year)!.push(commit)
82
- }
83
-
84
- content += `## Yearly Development Highlights\n\n`
85
-
86
- const sortedYears = Array.from(commitsByYear.keys()).sort((a, b) => b - a)
87
- for (const year of sortedYears) {
88
- const yearCommits = commitsByYear.get(year)!
89
- const features = yearCommits.filter((c) =>
90
- c.getAnalysis().isFeatureAnalysis(),
91
- )
92
-
93
- content += `### ${year}\n\n`
94
- content += `${yearCommits.length} commits total, including ${features.length} new features.\n\n`
95
-
96
- // Show top features
97
- if (features.length > 0) {
98
- content += `**Key Features:**\n`
99
- for (const feature of features.slice(0, 5)) {
100
- content += `- ${feature.getAnalysis().getSummary()}\n`
101
- }
102
- content += "\n"
103
- }
104
- }
105
-
106
- return content
107
- }
108
- }
@@ -1,87 +0,0 @@
1
- import {
2
- existsSync,
3
- mkdirSync,
4
- readFileSync,
5
- unlinkSync,
6
- writeFileSync,
7
- } from "fs"
8
- import { dirname, join } from "path"
9
-
10
- import { IStorageService } from "@presentation/storage-service.interface"
11
-
12
- import { getErrorMessage } from "../utils"
13
-
14
- export class FileSystemStorageAdapter implements IStorageService {
15
- private static readonly DEFAULT_ENCODING = "utf8"
16
-
17
- async writeFile(filePath: string, content: string): Promise<void> {
18
- try {
19
- // Ensure directory exists
20
- const dir = dirname(filePath)
21
- if (!existsSync(dir)) {
22
- mkdirSync(dir, { recursive: true })
23
- }
24
-
25
- writeFileSync(
26
- filePath,
27
- content,
28
- FileSystemStorageAdapter.DEFAULT_ENCODING,
29
- )
30
- } catch (error) {
31
- throw new Error(
32
- `Failed to write file ${filePath}: ${getErrorMessage(error)}`,
33
- )
34
- }
35
- }
36
-
37
- async readFile(filePath: string): Promise<string> {
38
- try {
39
- return readFileSync(filePath, FileSystemStorageAdapter.DEFAULT_ENCODING)
40
- } catch (error) {
41
- throw new Error(
42
- `Failed to read file ${filePath}: ${getErrorMessage(error)}`,
43
- )
44
- }
45
- }
46
-
47
- async fileExists(filePath: string): Promise<boolean> {
48
- return existsSync(filePath)
49
- }
50
-
51
- async ensureDirectory(directoryPath: string): Promise<void> {
52
- try {
53
- mkdirSync(directoryPath, { recursive: true })
54
- } catch (error) {
55
- throw new Error(
56
- `Failed to create directory ${directoryPath}: ${getErrorMessage(error)}`,
57
- )
58
- }
59
- }
60
-
61
- async deleteFile(filePath: string): Promise<void> {
62
- try {
63
- if (await this.fileExists(filePath)) {
64
- unlinkSync(filePath)
65
- }
66
- } catch (error) {
67
- throw new Error(
68
- `Failed to delete file ${filePath}: ${getErrorMessage(error)}`,
69
- )
70
- }
71
- }
72
-
73
- async readLines(filePath: string): Promise<string[]> {
74
- const content = await this.readFile(filePath)
75
- return content
76
- .split("\n")
77
- .map((line) => line.trim())
78
- .filter((line) => line.length > 0)
79
- }
80
-
81
- resolveOutputPath(filename: string, outputDir?: string): string {
82
- if (outputDir) {
83
- return join(outputDir, filename)
84
- }
85
- return filename
86
- }
87
- }
@@ -1,46 +0,0 @@
1
- import { execSync } from "child_process"
2
-
3
- import { LLMAdapter } from "./llm-adapter"
4
-
5
- export class GeminiLLMAdapter extends LLMAdapter {
6
- private static readonly MAX_PROMPT_LENGTH = parseInt(
7
- process.env.GEMINI_MAX_PROMPT_LENGTH || "100000",
8
- 10,
9
- )
10
-
11
- protected getMaxPromptLength(): number {
12
- return GeminiLLMAdapter.MAX_PROMPT_LENGTH
13
- }
14
-
15
- async detectAvailableModels(): Promise<string[]> {
16
- try {
17
- execSync("command -v gemini", { stdio: "ignore" })
18
- return ["gemini"]
19
- } catch {
20
- return []
21
- }
22
- }
23
-
24
- async isAvailable(): Promise<boolean> {
25
- const available = await this.detectAvailableModels()
26
- return available.length > 0
27
- }
28
-
29
- protected async executeModelCommand(prompt: string): Promise<string> {
30
- const truncatedPrompt = this.truncatePrompt(prompt)
31
-
32
- if (this.verbose) {
33
- console.log(` - Prompt length: ${truncatedPrompt.length} characters`)
34
- }
35
-
36
- const modelCommand = this.model || "gemini"
37
-
38
- return execSync(modelCommand, {
39
- input: truncatedPrompt,
40
- encoding: "utf8",
41
- stdio: ["pipe", "pipe", "pipe"],
42
- timeout: LLMAdapter.DEFAULT_TIMEOUT,
43
- })
44
- }
45
- }
46
-