codereview-aia 0.1.0

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 (153) hide show
  1. package/.cr-aia.yml +23 -0
  2. package/.crignore +0 -0
  3. package/dist/index.js +27 -0
  4. package/package.json +85 -0
  5. package/src/analysis/FindingsExtractor.ts +431 -0
  6. package/src/analysis/ai-detection/analyzers/BaseAnalyzer.ts +267 -0
  7. package/src/analysis/ai-detection/analyzers/DocumentationAnalyzer.ts +622 -0
  8. package/src/analysis/ai-detection/analyzers/GitHistoryAnalyzer.ts +430 -0
  9. package/src/analysis/ai-detection/core/AIDetectionEngine.ts +467 -0
  10. package/src/analysis/ai-detection/types/DetectionTypes.ts +406 -0
  11. package/src/analysis/ai-detection/utils/SubmissionConverter.ts +390 -0
  12. package/src/analysis/context/ReviewContext.ts +378 -0
  13. package/src/analysis/context/index.ts +7 -0
  14. package/src/analysis/index.ts +8 -0
  15. package/src/analysis/tokens/TokenAnalysisFormatter.ts +154 -0
  16. package/src/analysis/tokens/TokenAnalyzer.ts +747 -0
  17. package/src/analysis/tokens/index.ts +8 -0
  18. package/src/clients/base/abstractClient.ts +190 -0
  19. package/src/clients/base/httpClient.ts +160 -0
  20. package/src/clients/base/index.ts +12 -0
  21. package/src/clients/base/modelDetection.ts +107 -0
  22. package/src/clients/base/responseProcessor.ts +586 -0
  23. package/src/clients/factory/clientFactory.ts +55 -0
  24. package/src/clients/factory/index.ts +8 -0
  25. package/src/clients/implementations/index.ts +8 -0
  26. package/src/clients/implementations/openRouterClient.ts +411 -0
  27. package/src/clients/openRouterClient.ts +863 -0
  28. package/src/clients/openRouterClientWrapper.ts +44 -0
  29. package/src/clients/utils/directoryStructure.ts +52 -0
  30. package/src/clients/utils/index.ts +11 -0
  31. package/src/clients/utils/languageDetection.ts +44 -0
  32. package/src/clients/utils/promptFormatter.ts +105 -0
  33. package/src/clients/utils/promptLoader.ts +53 -0
  34. package/src/clients/utils/tokenCounter.ts +297 -0
  35. package/src/core/ApiClientSelector.ts +37 -0
  36. package/src/core/ConfigurationService.ts +591 -0
  37. package/src/core/ConsolidationService.ts +423 -0
  38. package/src/core/InteractiveDisplayManager.ts +81 -0
  39. package/src/core/OutputManager.ts +275 -0
  40. package/src/core/ReviewGenerator.ts +140 -0
  41. package/src/core/fileDiscovery.ts +237 -0
  42. package/src/core/handlers/EstimationHandler.ts +104 -0
  43. package/src/core/handlers/FileProcessingHandler.ts +204 -0
  44. package/src/core/handlers/OutputHandler.ts +125 -0
  45. package/src/core/handlers/ReviewExecutor.ts +104 -0
  46. package/src/core/reviewOrchestrator.ts +333 -0
  47. package/src/core/utils/ModelInfoUtils.ts +56 -0
  48. package/src/formatters/outputFormatter.ts +62 -0
  49. package/src/formatters/utils/IssueFormatters.ts +83 -0
  50. package/src/formatters/utils/JsonFormatter.ts +77 -0
  51. package/src/formatters/utils/MarkdownFormatters.ts +609 -0
  52. package/src/formatters/utils/MetadataFormatter.ts +269 -0
  53. package/src/formatters/utils/ModelInfoExtractor.ts +115 -0
  54. package/src/index.ts +27 -0
  55. package/src/plugins/PluginInterface.ts +50 -0
  56. package/src/plugins/PluginManager.ts +126 -0
  57. package/src/prompts/PromptManager.ts +69 -0
  58. package/src/prompts/cache/PromptCache.ts +50 -0
  59. package/src/prompts/promptText/common/variables/css-frameworks.json +33 -0
  60. package/src/prompts/promptText/common/variables/framework-versions.json +45 -0
  61. package/src/prompts/promptText/frameworks/react/comprehensive.hbs +19 -0
  62. package/src/prompts/promptText/languages/css/comprehensive.hbs +18 -0
  63. package/src/prompts/promptText/languages/generic/comprehensive.hbs +20 -0
  64. package/src/prompts/promptText/languages/html/comprehensive.hbs +18 -0
  65. package/src/prompts/promptText/languages/javascript/comprehensive.hbs +18 -0
  66. package/src/prompts/promptText/languages/python/comprehensive.hbs +18 -0
  67. package/src/prompts/promptText/languages/typescript/comprehensive.hbs +18 -0
  68. package/src/runtime/auth/service.ts +58 -0
  69. package/src/runtime/auth/session.ts +103 -0
  70. package/src/runtime/auth/types.ts +11 -0
  71. package/src/runtime/cliEntry.ts +196 -0
  72. package/src/runtime/errors.ts +13 -0
  73. package/src/runtime/fileCollector.ts +188 -0
  74. package/src/runtime/manifest.ts +64 -0
  75. package/src/runtime/openrouterProxy.ts +45 -0
  76. package/src/runtime/proxyConfig.ts +94 -0
  77. package/src/runtime/proxyEnvironment.ts +71 -0
  78. package/src/runtime/reportMerge.ts +102 -0
  79. package/src/runtime/reporting/markdownReportBuilder.ts +138 -0
  80. package/src/runtime/reporting/reportDataCollector.ts +234 -0
  81. package/src/runtime/reporting/summaryGenerator.ts +86 -0
  82. package/src/runtime/reviewPipeline.ts +155 -0
  83. package/src/runtime/runAiCodeReview.ts +153 -0
  84. package/src/runtime/runtimeConfig.ts +5 -0
  85. package/src/runtime/ui/Layout.tsx +57 -0
  86. package/src/runtime/ui/RuntimeApp.tsx +150 -0
  87. package/src/runtime/ui/inkModules.ts +73 -0
  88. package/src/runtime/ui/screens/AuthScreen.tsx +128 -0
  89. package/src/runtime/ui/screens/ModeSelection.tsx +52 -0
  90. package/src/runtime/ui/screens/ProgressScreen.tsx +55 -0
  91. package/src/runtime/ui/screens/ResultsScreen.tsx +76 -0
  92. package/src/strategies/ArchitecturalReviewStrategy.ts +54 -0
  93. package/src/strategies/CodingTestReviewStrategy.ts +920 -0
  94. package/src/strategies/ConsolidatedReviewStrategy.ts +59 -0
  95. package/src/strategies/ExtractPatternsReviewStrategy.ts +64 -0
  96. package/src/strategies/MultiPassReviewStrategy.ts +785 -0
  97. package/src/strategies/ReviewStrategy.ts +64 -0
  98. package/src/strategies/StrategyFactory.ts +79 -0
  99. package/src/strategies/index.ts +14 -0
  100. package/src/tokenizers/baseTokenizer.ts +61 -0
  101. package/src/tokenizers/gptTokenizer.ts +27 -0
  102. package/src/tokenizers/index.ts +8 -0
  103. package/src/types/apiResponses.ts +40 -0
  104. package/src/types/cli.ts +24 -0
  105. package/src/types/common.ts +39 -0
  106. package/src/types/configuration.ts +201 -0
  107. package/src/types/handlebars.d.ts +5 -0
  108. package/src/types/patch.d.ts +25 -0
  109. package/src/types/review.ts +294 -0
  110. package/src/types/reviewContext.d.ts +65 -0
  111. package/src/types/reviewSchema.ts +181 -0
  112. package/src/types/structuredReview.ts +167 -0
  113. package/src/types/tokenAnalysis.ts +56 -0
  114. package/src/utils/FileReader.ts +93 -0
  115. package/src/utils/FileWriter.ts +76 -0
  116. package/src/utils/PathGenerator.ts +97 -0
  117. package/src/utils/api/apiUtils.ts +14 -0
  118. package/src/utils/api/index.ts +1 -0
  119. package/src/utils/apiErrorHandler.ts +287 -0
  120. package/src/utils/ciDataCollector.ts +252 -0
  121. package/src/utils/codingTestConfigLoader.ts +466 -0
  122. package/src/utils/dependencies/aiDependencyAnalyzer.ts +454 -0
  123. package/src/utils/detection/frameworkDetector.ts +879 -0
  124. package/src/utils/detection/index.ts +10 -0
  125. package/src/utils/detection/projectTypeDetector.ts +518 -0
  126. package/src/utils/diagramGenerator.ts +206 -0
  127. package/src/utils/errorLogger.ts +60 -0
  128. package/src/utils/estimationUtils.ts +407 -0
  129. package/src/utils/fileFilters.ts +373 -0
  130. package/src/utils/fileSystem.ts +57 -0
  131. package/src/utils/index.ts +36 -0
  132. package/src/utils/logger.ts +240 -0
  133. package/src/utils/pathValidator.ts +98 -0
  134. package/src/utils/priorityFilter.ts +59 -0
  135. package/src/utils/projectDocs.ts +189 -0
  136. package/src/utils/promptPaths.ts +29 -0
  137. package/src/utils/promptTemplateManager.ts +157 -0
  138. package/src/utils/review/consolidateReview.ts +553 -0
  139. package/src/utils/review/fixDisplay.ts +100 -0
  140. package/src/utils/review/fixImplementation.ts +61 -0
  141. package/src/utils/review/index.ts +36 -0
  142. package/src/utils/review/interactiveProcessing.ts +294 -0
  143. package/src/utils/review/progressTracker.ts +296 -0
  144. package/src/utils/review/reviewExtraction.ts +382 -0
  145. package/src/utils/review/types.ts +46 -0
  146. package/src/utils/reviewActionHandler.ts +18 -0
  147. package/src/utils/reviewParser.ts +253 -0
  148. package/src/utils/sanitizer.ts +238 -0
  149. package/src/utils/smartFileSelector.ts +255 -0
  150. package/src/utils/templateLoader.ts +514 -0
  151. package/src/utils/treeGenerator.ts +153 -0
  152. package/tsconfig.build.json +14 -0
  153. package/tsconfig.json +59 -0
@@ -0,0 +1,586 @@
1
+ /**
2
+ * @fileoverview Common response processing and error handling for AI API clients.
3
+ *
4
+ * This module provides shared functionality for processing API responses,
5
+ * extracting structured data, handling errors, and standardizing output formats
6
+ * across different AI providers.
7
+ */
8
+
9
+ import type { AIJsonResponse } from '../../types/apiResponses';
10
+ import type { CostInfo, ReviewResult, ReviewType } from '../../types/review';
11
+ import type { StructuredReview } from '../../types/structuredReview';
12
+ import { ApiError } from '../../utils/apiErrorHandler';
13
+ import { configurationService } from '../../core/ConfigurationService';
14
+ import logger from '../../utils/logger';
15
+ import { getCostInfoFromText } from '../utils/tokenCounter';
16
+
17
+ /**
18
+ * Attempt to recover JSON from malformed responses using various strategies
19
+ * @param content The malformed response content
20
+ * @returns Parsed StructuredReview object or null if recovery fails
21
+ */
22
+ function attemptJsonRecovery(content: string): StructuredReview | null {
23
+ const strategies = [
24
+ // Strategy 1: Remove leading language identifiers (e.g., "typescript\n{...}")
25
+ (text: string) => {
26
+ const match = text.match(/^(?:typescript|javascript|json|ts|js)\s*\n?\s*({[\s\S]*})$/i);
27
+ return match ? match[1] : null;
28
+ },
29
+
30
+ // Strategy 2: Extract JSON from markdown code blocks (most common case)
31
+ (text: string) => {
32
+ // Match various code block formats
33
+ const patterns = [
34
+ /```(?:json)?\s*([\s\S]*?)\s*```/,
35
+ /```(?:typescript|javascript|ts|js)?\s*([\s\S]*?)\s*```/,
36
+ /`([\s\S]*?)`/, // Single backtick blocks
37
+ ];
38
+
39
+ for (const pattern of patterns) {
40
+ const match = text.match(pattern);
41
+ if (match && match[1]) {
42
+ const content = match[1].trim();
43
+ // Check if it looks like JSON
44
+ if (content.startsWith('{') && content.endsWith('}')) {
45
+ return content;
46
+ }
47
+ }
48
+ }
49
+ return null;
50
+ },
51
+
52
+ // Strategy 3: Extract JSON from mixed content (find first complete JSON object)
53
+ (text: string) => {
54
+ // Find balanced braces
55
+ const startIdx = text.indexOf('{');
56
+ if (startIdx === -1) return null;
57
+
58
+ let depth = 0;
59
+ let inString = false;
60
+ let escapeNext = false;
61
+
62
+ for (let i = startIdx; i < text.length; i++) {
63
+ const char = text[i];
64
+
65
+ if (escapeNext) {
66
+ escapeNext = false;
67
+ continue;
68
+ }
69
+
70
+ if (char === '\\') {
71
+ escapeNext = true;
72
+ continue;
73
+ }
74
+
75
+ if (char === '"' && !escapeNext) {
76
+ inString = !inString;
77
+ continue;
78
+ }
79
+
80
+ if (!inString) {
81
+ if (char === '{') depth++;
82
+ else if (char === '}') {
83
+ depth--;
84
+ if (depth === 0) {
85
+ return text.substring(startIdx, i + 1);
86
+ }
87
+ }
88
+ }
89
+ }
90
+
91
+ return null;
92
+ },
93
+
94
+ // Strategy 4: Look for JSON between quotes (e.g., "typescript\n{...}")
95
+ (text: string) => {
96
+ const match = text.match(/"[^"]*"\s*\n?\s*({[\s\S]*})/);
97
+ return match ? match[1] : null;
98
+ },
99
+
100
+ // Strategy 5: Remove everything before the first opening brace
101
+ (text: string) => {
102
+ const braceIndex = text.indexOf('{');
103
+ if (braceIndex === -1) return null;
104
+ return text.substring(braceIndex);
105
+ },
106
+
107
+ // Strategy 6: Fix unterminated strings by closing them
108
+ (text: string) => {
109
+ // If JSON parse fails due to unterminated string, try to fix it
110
+ const braceIndex = text.indexOf('{');
111
+ if (braceIndex === -1) return null;
112
+
113
+ let jsonStr = text.substring(braceIndex);
114
+
115
+ // Count open and closing quotes
116
+ let inString = false;
117
+ let escapeNext = false;
118
+ let lastQuoteIndex = -1;
119
+
120
+ for (let i = 0; i < jsonStr.length; i++) {
121
+ if (escapeNext) {
122
+ escapeNext = false;
123
+ continue;
124
+ }
125
+
126
+ if (jsonStr[i] === '\\') {
127
+ escapeNext = true;
128
+ continue;
129
+ }
130
+
131
+ if (jsonStr[i] === '"') {
132
+ inString = !inString;
133
+ if (inString) {
134
+ lastQuoteIndex = i;
135
+ }
136
+ }
137
+ }
138
+
139
+ // If we're still in a string at the end, close it
140
+ if (inString && lastQuoteIndex !== -1) {
141
+ // Find a reasonable place to close the string
142
+ // Look for common JSON structure patterns
143
+ const patterns = ['}', ']', ','];
144
+ let closeIndex = jsonStr.length;
145
+
146
+ for (const pattern of patterns) {
147
+ const idx = jsonStr.lastIndexOf(pattern);
148
+ if (idx > lastQuoteIndex) {
149
+ closeIndex = idx;
150
+ break;
151
+ }
152
+ }
153
+
154
+ // Insert closing quote before the structural character
155
+ jsonStr = jsonStr.substring(0, closeIndex) + '"' + jsonStr.substring(closeIndex);
156
+ }
157
+
158
+ return jsonStr;
159
+ },
160
+ ];
161
+
162
+ for (const strategy of strategies) {
163
+ try {
164
+ const extracted = strategy(content.trim());
165
+ if (extracted) {
166
+ const parsed = JSON.parse(extracted) as StructuredReview;
167
+ // Basic validation
168
+ if (typeof parsed === 'object' && parsed !== null) {
169
+ logger.debug('Successfully recovered JSON using recovery strategy');
170
+ return parsed;
171
+ }
172
+ }
173
+ } catch (_error) {}
174
+ }
175
+
176
+ return null;
177
+ }
178
+
179
+ /**
180
+ * Process API response content and extract structured data if possible
181
+ * @param content The API response content
182
+ * @returns Structured data object or null if not valid JSON
183
+ */
184
+ export function extractStructuredData(content: string): StructuredReview | undefined {
185
+ // Declare these outside try block so they're accessible in catch block
186
+ let jsonBlockMatch: RegExpMatchArray | null = null;
187
+ let anyCodeBlockMatch: RegExpMatchArray | null = null;
188
+
189
+ try {
190
+ // Check if the response is wrapped in any code block with improved language marker handling
191
+ // Handle various formats:
192
+ // 1. ```json {...}```
193
+ // 2. ```typescript {...}``` or other language markers
194
+ // 3. ```{...}```
195
+ // 4. Plain JSON without code blocks
196
+
197
+ // Enhanced regex to handle various language markers
198
+ // First try to find explicit JSON blocks
199
+ jsonBlockMatch = content.match(/```(?:json)\s*([\s\S]*?)\s*```/) || null;
200
+
201
+ // If no explicit JSON block, look for any code block
202
+ if (!jsonBlockMatch) {
203
+ // Try different code block patterns
204
+ const codeBlockPatterns = [
205
+ /```(?:typescript|ts)\s*([\s\S]*?)\s*```/,
206
+ /```(?:javascript|js)\s*([\s\S]*?)\s*```/,
207
+ /```\s*([\s\S]*?)\s*```/, // Code block without language
208
+ ];
209
+
210
+ for (const pattern of codeBlockPatterns) {
211
+ anyCodeBlockMatch = content.match(pattern);
212
+ if (anyCodeBlockMatch) {
213
+ logger.debug(`Found code block with pattern: ${pattern.source}`);
214
+ break;
215
+ }
216
+ }
217
+
218
+ // Additional check for typescript blocks specifically
219
+ if (anyCodeBlockMatch && (content.includes('```typescript') || content.includes('```ts'))) {
220
+ logger.debug('Detected typescript code block, will check if it contains valid JSON');
221
+ }
222
+ }
223
+
224
+ let jsonContent = '';
225
+
226
+ if (jsonBlockMatch) {
227
+ // If we have a JSON code block, use its content
228
+ jsonContent = jsonBlockMatch[1] || '';
229
+ logger.debug('Found JSON code block, extracting content');
230
+ } else if (anyCodeBlockMatch) {
231
+ // If we have any other code block, use its content but check if it starts with {
232
+ const blockContent = (anyCodeBlockMatch[1] || '').trim();
233
+
234
+ // Special handling for TypeScript blocks that might contain JSON objects
235
+ if (content.includes('```typescript') || content.includes('```ts')) {
236
+ logger.debug('Analyzing TypeScript code block for JSON content');
237
+
238
+ // Check if the TypeScript block contains a JSON object literal
239
+ if (blockContent.includes('{') && blockContent.includes('}')) {
240
+ // Look for a properly formatted object within the TypeScript code
241
+ const objectMatch = blockContent.match(/(\{[\s\S]*\})/);
242
+ if (objectMatch?.[1]) {
243
+ const potentialJson = objectMatch[1].trim();
244
+ try {
245
+ // Try to verify it's parsable JSON
246
+ JSON.parse(potentialJson);
247
+ jsonContent = potentialJson;
248
+ logger.debug('Successfully extracted JSON object from TypeScript code block');
249
+ } catch (_parseError) {
250
+ // Not valid JSON, continue with regular processing
251
+ logger.debug('TypeScript block contains object syntax but not valid JSON');
252
+ if (blockContent.startsWith('{') && blockContent.endsWith('}')) {
253
+ jsonContent = blockContent;
254
+ logger.debug('Using TypeScript block content for potential parsing');
255
+ } else {
256
+ logger.debug(
257
+ 'TypeScript code block is not JSON-compatible, falling back to raw content',
258
+ );
259
+ jsonContent = content;
260
+ }
261
+ }
262
+ } else {
263
+ logger.debug('No valid object literal found in TypeScript code block');
264
+ jsonContent = content;
265
+ }
266
+ } else {
267
+ logger.debug(
268
+ "TypeScript code block doesn't contain object literals, falling back to raw content",
269
+ );
270
+ jsonContent = content;
271
+ }
272
+ } else if (blockContent.startsWith('{') && blockContent.endsWith('}')) {
273
+ // For non-TypeScript blocks, check if the content looks like JSON
274
+ jsonContent = blockContent;
275
+ logger.debug('Found code block with JSON-like content, attempting to parse');
276
+ } else {
277
+ // If the code block doesn't look like JSON, use the raw content
278
+ logger.debug("Code block found but doesn't appear to be JSON, falling back to raw content");
279
+ jsonContent = content;
280
+ }
281
+ } else {
282
+ // No code block, use the raw content
283
+ jsonContent = content;
284
+ logger.debug('No code blocks found, attempting to parse raw content');
285
+ }
286
+
287
+ // Parse the JSON content
288
+ const parsedData = JSON.parse(jsonContent) as AIJsonResponse | StructuredReview;
289
+
290
+ // Attempt to determine which format we have (AIJsonResponse or direct StructuredReview)
291
+ if ('review' in parsedData) {
292
+ // We have an AIJsonResponse, convert it to StructuredReview
293
+ const aiResponse = parsedData as AIJsonResponse;
294
+
295
+ if (!aiResponse.review) {
296
+ logger.warn('Response is valid JSON but missing "review" property');
297
+ return undefined;
298
+ }
299
+
300
+ // Convert the AIJsonResponse format to StructuredReview format
301
+ const review: StructuredReview = {
302
+ summary: aiResponse.review.summary
303
+ ? typeof aiResponse.review.summary === 'string'
304
+ ? aiResponse.review.summary
305
+ : JSON.stringify(aiResponse.review.summary)
306
+ : 'No summary provided',
307
+ issues: [],
308
+ };
309
+
310
+ // Process issues if available
311
+ if (aiResponse.review.files && Array.isArray(aiResponse.review.files)) {
312
+ // Collect issues from all files
313
+ aiResponse.review.files.forEach((file) => {
314
+ if (file.issues && Array.isArray(file.issues)) {
315
+ file.issues.forEach((issue) => {
316
+ if (issue.id && issue.description) {
317
+ review.issues.push({
318
+ title: issue.id,
319
+ description: issue.description,
320
+ priority: mapPriority(issue.priority),
321
+ type: 'other',
322
+ filePath: file.filePath || '',
323
+ lineNumbers:
324
+ issue.location && (issue.location.startLine || issue.location.endLine)
325
+ ? `${issue.location.startLine || ''}-${issue.location.endLine || ''}`
326
+ : undefined,
327
+ codeSnippet: issue.currentCode,
328
+ suggestedFix: issue.suggestedCode,
329
+ impact: issue.explanation,
330
+ });
331
+ }
332
+ });
333
+ }
334
+ });
335
+ }
336
+
337
+ // Add recommendations and positive aspects if available
338
+ if (aiResponse.review.recommendations) {
339
+ review.recommendations = aiResponse.review.recommendations;
340
+ }
341
+
342
+ if (aiResponse.review.positiveAspects) {
343
+ review.positiveAspects = aiResponse.review.positiveAspects;
344
+ }
345
+
346
+ return review;
347
+ }
348
+ // We assume it's already a StructuredReview format
349
+ const structuredData = parsedData as StructuredReview;
350
+
351
+ // Validate basic structure
352
+ if (typeof structuredData.summary === 'undefined' || !Array.isArray(structuredData.issues)) {
353
+ logger.warn(
354
+ 'Response is valid JSON but does not have the expected StructuredReview structure',
355
+ );
356
+ return undefined;
357
+ }
358
+
359
+ return structuredData;
360
+ } catch (parseError) {
361
+ const errorMsg = parseError instanceof Error ? parseError.message : String(parseError);
362
+ logger.warn(`Response is not valid JSON: ${errorMsg}`);
363
+
364
+ // Always provide basic info regardless of log level
365
+ const contentPreview = content.substring(0, 50).replace(/\n/g, ' ');
366
+ logger.info(`JSON parse error with content starting with: "${contentPreview}..."`);
367
+
368
+ // Try additional recovery strategies for common malformed response patterns
369
+ try {
370
+ const recoveredJson = attemptJsonRecovery(content);
371
+ if (recoveredJson) {
372
+ logger.info('Successfully recovered JSON from malformed response');
373
+ return recoveredJson;
374
+ }
375
+ } catch (recoveryError) {
376
+ logger.debug('JSON recovery attempt failed:', recoveryError);
377
+ }
378
+
379
+ // Check for common patterns that might be causing issues
380
+ if (content.includes('```typescript') || content.includes('```ts')) {
381
+ logger.info('Content contains TypeScript code blocks that may be causing parsing issues');
382
+ }
383
+
384
+ if (content.includes('```json')) {
385
+ logger.info('Content contains JSON code blocks that could not be parsed properly');
386
+ }
387
+
388
+ // Check for language identifier patterns that might indicate Gemini response issues
389
+ if (content.match(/^(?:typescript|javascript|json|ts|js)\s*\n/i)) {
390
+ logger.info(
391
+ 'Content starts with language identifier, which may indicate a Gemini response formatting issue',
392
+ );
393
+ }
394
+
395
+ // Check for quoted language identifiers
396
+ if (content.match(/"(?:typescript|javascript|json|ts|js)"/i)) {
397
+ logger.info(
398
+ 'Content contains quoted language identifiers, which may indicate a parsing issue',
399
+ );
400
+ }
401
+
402
+ // In debug mode, log additional details to help diagnose the issue
403
+ if (configurationService.getApplicationConfig().logLevel.value === 'debug') {
404
+ const snippet =
405
+ content.length > 200
406
+ ? `${content.substring(0, 100)}...${content.substring(content.length - 100)}`
407
+ : content;
408
+ logger.debug(`Content snippet causing JSON parse error: ${snippet}`);
409
+
410
+ // Also log if we found code blocks but couldn't parse the content
411
+ if (jsonBlockMatch) {
412
+ if (jsonBlockMatch?.[1]) {
413
+ logger.debug(
414
+ `Found JSON code block but content couldn't be parsed as JSON: ${jsonBlockMatch[1].substring(0, 100)}`,
415
+ );
416
+ }
417
+ } else if (anyCodeBlockMatch) {
418
+ if (anyCodeBlockMatch?.[1]) {
419
+ logger.debug(
420
+ `Found non-JSON code block but content couldn't be parsed as JSON: ${anyCodeBlockMatch[1].substring(0, 100)}`,
421
+ );
422
+ }
423
+ }
424
+ }
425
+
426
+ // Try to create a basic structured response as a fallback
427
+ try {
428
+ // If we can't parse JSON but have a content string, create a simple summary
429
+ return {
430
+ summary: "AI generated a response that couldn't be parsed as structured data",
431
+ issues: [
432
+ {
433
+ title: 'Response format issue',
434
+ description:
435
+ "The response couldn't be parsed into structured format. Please see the full text for details.",
436
+ priority: 'medium',
437
+ type: 'other',
438
+ filePath: 'unknown', // Required field in ReviewIssue
439
+ },
440
+ ],
441
+ };
442
+ } catch (_fallbackError) {
443
+ logger.debug('Failed to create fallback structured response');
444
+ return undefined;
445
+ }
446
+ }
447
+ }
448
+
449
+ /**
450
+ * Map priority string to IssuePriority type
451
+ * @param priority String priority value (may be undefined or in different case)
452
+ * @returns Normalized IssuePriority value
453
+ */
454
+ function mapPriority(priority: string | undefined): 'high' | 'medium' | 'low' {
455
+ if (!priority) return 'medium';
456
+
457
+ const normalizedPriority = priority.toLowerCase();
458
+
459
+ if (normalizedPriority.includes('high')) return 'high';
460
+ if (normalizedPriority.includes('low')) return 'low';
461
+
462
+ return 'medium';
463
+ }
464
+
465
+ /**
466
+ * Create a standardized review result object
467
+ * @param content The review content
468
+ * @param prompt The original prompt (for cost calculation)
469
+ * @param modelName The full model name
470
+ * @param filePath The file path or identifier
471
+ * @param reviewType The review type
472
+ * @param options Optional review options to determine expected output format
473
+ * @returns Standardized review result
474
+ */
475
+ export function createStandardReviewResult(
476
+ content: string,
477
+ prompt: string,
478
+ modelName: string,
479
+ filePath: string,
480
+ reviewType: ReviewType,
481
+ options?: { interactive?: boolean; output?: string },
482
+ ): ReviewResult {
483
+ // Only attempt to extract structured data if we expect JSON output
484
+ const expectsJsonOutput = options?.interactive === true || options?.output === 'json';
485
+
486
+ // Extract structured data only when appropriate
487
+ const structuredData = expectsJsonOutput ? extractStructuredData(content) : undefined;
488
+
489
+ // Calculate cost information
490
+ let cost: CostInfo | undefined;
491
+ try {
492
+ cost = getCostInfoFromText(prompt, content, modelName);
493
+ } catch (error) {
494
+ logger.warn(
495
+ `Failed to calculate cost information: ${
496
+ error instanceof Error ? error.message : String(error)
497
+ }`,
498
+ );
499
+ }
500
+
501
+ // Return standardized review result
502
+ return {
503
+ content,
504
+ cost,
505
+ modelUsed: modelName,
506
+ filePath,
507
+ reviewType,
508
+ timestamp: new Date().toISOString(),
509
+ structuredData: structuredData as StructuredReview | undefined,
510
+ };
511
+ }
512
+
513
+ /**
514
+ * Handle API errors with standardized logging and wrapping
515
+ * @param error The original error
516
+ * @param operation Description of the operation that failed
517
+ * @param modelName The model being used
518
+ * @param context Additional context for debugging
519
+ * @returns Wrapped ApiError
520
+ */
521
+ export function handleApiError(
522
+ error: unknown,
523
+ operation: string,
524
+ modelName: string,
525
+ context?: {
526
+ endpoint?: string;
527
+ statusCode?: number;
528
+ requestId?: string;
529
+ filePath?: string;
530
+ additionalInfo?: Record<string, any>;
531
+ },
532
+ ): ApiError {
533
+ const errorMessage = error instanceof Error ? error.message : String(error);
534
+
535
+ // Build detailed error message with context
536
+ let formattedError = `Failed to ${operation} with ${modelName}`;
537
+
538
+ if (context) {
539
+ const contextParts: string[] = [];
540
+
541
+ if (context.endpoint) {
542
+ contextParts.push(`Endpoint: ${context.endpoint}`);
543
+ }
544
+
545
+ if (context.statusCode) {
546
+ contextParts.push(`Status: ${context.statusCode}`);
547
+ }
548
+
549
+ if (context.requestId) {
550
+ contextParts.push(`Request ID: ${context.requestId}`);
551
+ }
552
+
553
+ if (context.filePath) {
554
+ contextParts.push(`File: ${context.filePath}`);
555
+ }
556
+
557
+ if (context.additionalInfo) {
558
+ Object.entries(context.additionalInfo).forEach(([key, value]) => {
559
+ contextParts.push(`${key}: ${value}`);
560
+ });
561
+ }
562
+
563
+ if (contextParts.length > 0) {
564
+ formattedError += `\n Context: ${contextParts.join(', ')}`;
565
+ }
566
+ }
567
+
568
+ formattedError += `\n Error: ${errorMessage}`;
569
+
570
+ logger.error(formattedError);
571
+
572
+ // Log stack trace in debug mode
573
+ if (
574
+ error instanceof Error &&
575
+ error.stack &&
576
+ configurationService.getApplicationConfig().logLevel.value === 'debug'
577
+ ) {
578
+ logger.debug(`Stack trace: ${error.stack}`);
579
+ }
580
+
581
+ if (error instanceof ApiError) {
582
+ return error;
583
+ }
584
+
585
+ return new ApiError(formattedError);
586
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * @fileoverview Factory for creating API clients based on model selection.
3
+ *
4
+ * This module provides a factory for creating the appropriate API client
5
+ * based on the selected model. It handles client instantiation, model detection,
6
+ * and initialization.
7
+ *
8
+ * The factory includes robust model name cleaning to handle malformed input
9
+ * such as trailing quotes or backticks that can occur during configuration
10
+ * or environment variable parsing.
11
+ */
12
+
13
+ import { getConfig } from '../../core/ConfigurationService';
14
+ import logger from '../../utils/logger';
15
+ import type { AbstractClient } from '../base';
16
+ import { OpenRouterClient } from '../implementations';
17
+
18
+ /**
19
+ * Factory for creating API clients
20
+ */
21
+ export class ClientFactory {
22
+ /**
23
+ * Create an appropriate client instance based on the selected model
24
+ * @param overrideModel Optional model to use instead of the configured one
25
+ * @returns The client instance
26
+ */
27
+ public static createClient(overrideModel?: string): AbstractClient {
28
+ const config = getConfig();
29
+ const selectedModel = overrideModel || config.model || '';
30
+
31
+ // Clean the model name early to handle malformed quotes and backticks
32
+ const cleanedModel = selectedModel.replace(/['"``]/g, '').trim();
33
+
34
+ logger.debug(
35
+ `[ClientFactory] Creating client for model: ${selectedModel} (cleaned: ${cleanedModel})`,
36
+ );
37
+
38
+ const [adapter, modelName] = cleanedModel.includes(':')
39
+ ? cleanedModel.split(':', 2)
40
+ : ['openrouter', cleanedModel];
41
+
42
+ if (adapter.toLowerCase() !== 'openrouter') {
43
+ logger.warn(
44
+ `[ClientFactory] Non-OpenRouter adapter "${adapter}" detected for model "${selectedModel}". Forcing OpenRouter client.`,
45
+ );
46
+ }
47
+
48
+ if (!modelName) {
49
+ throw new Error('A valid OpenRouter model must be specified (e.g. openrouter:meta-llama/...).');
50
+ }
51
+
52
+ logger.info(`Creating OpenRouter client for model: ${modelName}`);
53
+ return new OpenRouterClient();
54
+ }
55
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * @fileoverview Factory for creating API clients.
3
+ *
4
+ * This module serves as the main entry point for the client factory functionality,
5
+ * providing a unified interface for client instantiation.
6
+ */
7
+
8
+ export * from './clientFactory';
@@ -0,0 +1,8 @@
1
+ /**
2
+ * @fileoverview Client implementations for various AI providers.
3
+ *
4
+ * This module serves as the main entry point for client implementations,
5
+ * exporting all client classes.
6
+ */
7
+
8
+ export * from './openRouterClient';