@probelabs/visor 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 (116) hide show
  1. package/README.md +1240 -0
  2. package/action.yml +142 -0
  3. package/defaults/.visor.yaml +184 -0
  4. package/dist/action-cli-bridge.d.ts +104 -0
  5. package/dist/action-cli-bridge.d.ts.map +1 -0
  6. package/dist/action-cli-bridge.js +372 -0
  7. package/dist/action-cli-bridge.js.map +1 -0
  8. package/dist/ai-review-service.d.ts +84 -0
  9. package/dist/ai-review-service.d.ts.map +1 -0
  10. package/dist/ai-review-service.js +674 -0
  11. package/dist/ai-review-service.js.map +1 -0
  12. package/dist/check-execution-engine.d.ts +165 -0
  13. package/dist/check-execution-engine.d.ts.map +1 -0
  14. package/dist/check-execution-engine.js +1172 -0
  15. package/dist/check-execution-engine.js.map +1 -0
  16. package/dist/cli-main.d.ts +6 -0
  17. package/dist/cli-main.d.ts.map +1 -0
  18. package/dist/cli-main.js +247 -0
  19. package/dist/cli-main.js.map +1 -0
  20. package/dist/cli.d.ts +47 -0
  21. package/dist/cli.d.ts.map +1 -0
  22. package/dist/cli.js +224 -0
  23. package/dist/cli.js.map +1 -0
  24. package/dist/commands.d.ts +10 -0
  25. package/dist/commands.d.ts.map +1 -0
  26. package/dist/commands.js +53 -0
  27. package/dist/commands.js.map +1 -0
  28. package/dist/config.d.ts +63 -0
  29. package/dist/config.d.ts.map +1 -0
  30. package/dist/config.js +369 -0
  31. package/dist/config.js.map +1 -0
  32. package/dist/dependency-resolver.d.ts +54 -0
  33. package/dist/dependency-resolver.d.ts.map +1 -0
  34. package/dist/dependency-resolver.js +163 -0
  35. package/dist/dependency-resolver.js.map +1 -0
  36. package/dist/event-mapper.d.ts +125 -0
  37. package/dist/event-mapper.d.ts.map +1 -0
  38. package/dist/event-mapper.js +311 -0
  39. package/dist/event-mapper.js.map +1 -0
  40. package/dist/failure-condition-evaluator.d.ts +81 -0
  41. package/dist/failure-condition-evaluator.d.ts.map +1 -0
  42. package/dist/failure-condition-evaluator.js +445 -0
  43. package/dist/failure-condition-evaluator.js.map +1 -0
  44. package/dist/git-repository-analyzer.d.ts +45 -0
  45. package/dist/git-repository-analyzer.d.ts.map +1 -0
  46. package/dist/git-repository-analyzer.js +285 -0
  47. package/dist/git-repository-analyzer.js.map +1 -0
  48. package/dist/github-check-service.d.ts +104 -0
  49. package/dist/github-check-service.d.ts.map +1 -0
  50. package/dist/github-check-service.js +382 -0
  51. package/dist/github-check-service.js.map +1 -0
  52. package/dist/github-comments.d.ts +109 -0
  53. package/dist/github-comments.d.ts.map +1 -0
  54. package/dist/github-comments.js +289 -0
  55. package/dist/github-comments.js.map +1 -0
  56. package/dist/index.d.ts +2 -0
  57. package/dist/index.d.ts.map +1 -0
  58. package/dist/index.js +1265 -0
  59. package/dist/index.js.map +1 -0
  60. package/dist/output-formatters.d.ts +66 -0
  61. package/dist/output-formatters.d.ts.map +1 -0
  62. package/dist/output-formatters.js +624 -0
  63. package/dist/output-formatters.js.map +1 -0
  64. package/dist/pr-analyzer.d.ts +47 -0
  65. package/dist/pr-analyzer.d.ts.map +1 -0
  66. package/dist/pr-analyzer.js +194 -0
  67. package/dist/pr-analyzer.js.map +1 -0
  68. package/dist/pr-detector.d.ts +78 -0
  69. package/dist/pr-detector.d.ts.map +1 -0
  70. package/dist/pr-detector.js +357 -0
  71. package/dist/pr-detector.js.map +1 -0
  72. package/dist/providers/ai-check-provider.d.ts +40 -0
  73. package/dist/providers/ai-check-provider.d.ts.map +1 -0
  74. package/dist/providers/ai-check-provider.js +416 -0
  75. package/dist/providers/ai-check-provider.js.map +1 -0
  76. package/dist/providers/check-provider-registry.d.ts +67 -0
  77. package/dist/providers/check-provider-registry.d.ts.map +1 -0
  78. package/dist/providers/check-provider-registry.js +138 -0
  79. package/dist/providers/check-provider-registry.js.map +1 -0
  80. package/dist/providers/check-provider.interface.d.ts +78 -0
  81. package/dist/providers/check-provider.interface.d.ts.map +1 -0
  82. package/dist/providers/check-provider.interface.js +11 -0
  83. package/dist/providers/check-provider.interface.js.map +1 -0
  84. package/dist/providers/index.d.ts +10 -0
  85. package/dist/providers/index.d.ts.map +1 -0
  86. package/dist/providers/index.js +19 -0
  87. package/dist/providers/index.js.map +1 -0
  88. package/dist/providers/script-check-provider.d.ts +20 -0
  89. package/dist/providers/script-check-provider.d.ts.map +1 -0
  90. package/dist/providers/script-check-provider.js +163 -0
  91. package/dist/providers/script-check-provider.js.map +1 -0
  92. package/dist/providers/tool-check-provider.d.ts +19 -0
  93. package/dist/providers/tool-check-provider.d.ts.map +1 -0
  94. package/dist/providers/tool-check-provider.js +125 -0
  95. package/dist/providers/tool-check-provider.js.map +1 -0
  96. package/dist/providers/webhook-check-provider.d.ts +21 -0
  97. package/dist/providers/webhook-check-provider.d.ts.map +1 -0
  98. package/dist/providers/webhook-check-provider.js +173 -0
  99. package/dist/providers/webhook-check-provider.js.map +1 -0
  100. package/dist/reviewer.d.ts +88 -0
  101. package/dist/reviewer.d.ts.map +1 -0
  102. package/dist/reviewer.js +760 -0
  103. package/dist/reviewer.js.map +1 -0
  104. package/dist/types/cli.d.ts +41 -0
  105. package/dist/types/cli.d.ts.map +1 -0
  106. package/dist/types/cli.js +3 -0
  107. package/dist/types/cli.js.map +1 -0
  108. package/dist/types/config.d.ts +315 -0
  109. package/dist/types/config.d.ts.map +1 -0
  110. package/dist/types/config.js +6 -0
  111. package/dist/types/config.js.map +1 -0
  112. package/dist/utils/env-resolver.d.ts +38 -0
  113. package/dist/utils/env-resolver.d.ts.map +1 -0
  114. package/dist/utils/env-resolver.js +130 -0
  115. package/dist/utils/env-resolver.js.map +1 -0
  116. package/package.json +116 -0
@@ -0,0 +1,674 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AIReviewService = void 0;
4
+ const probe_1 = require("@probelabs/probe");
5
+ /**
6
+ * Helper function to log messages respecting JSON/SARIF output format
7
+ * Routes to stderr for JSON/SARIF to avoid contaminating structured output
8
+ */
9
+ function log(...args) {
10
+ const isStructuredOutput = process.env.VISOR_OUTPUT_FORMAT === 'json' || process.env.VISOR_OUTPUT_FORMAT === 'sarif';
11
+ const logFn = isStructuredOutput ? console.error : console.log;
12
+ logFn(...args);
13
+ }
14
+ class AIReviewService {
15
+ config;
16
+ constructor(config = {}) {
17
+ this.config = {
18
+ timeout: 600000, // Increased timeout to 10 minutes for AI responses
19
+ ...config,
20
+ };
21
+ // Auto-detect provider and API key from environment
22
+ if (!this.config.apiKey) {
23
+ if (process.env.GOOGLE_API_KEY) {
24
+ this.config.apiKey = process.env.GOOGLE_API_KEY;
25
+ this.config.provider = 'google';
26
+ }
27
+ else if (process.env.ANTHROPIC_API_KEY) {
28
+ this.config.apiKey = process.env.ANTHROPIC_API_KEY;
29
+ this.config.provider = 'anthropic';
30
+ }
31
+ else if (process.env.OPENAI_API_KEY) {
32
+ this.config.apiKey = process.env.OPENAI_API_KEY;
33
+ this.config.provider = 'openai';
34
+ }
35
+ }
36
+ // Auto-detect model from environment
37
+ if (!this.config.model && process.env.MODEL_NAME) {
38
+ this.config.model = process.env.MODEL_NAME;
39
+ }
40
+ }
41
+ /**
42
+ * Execute AI review using probe agent
43
+ */
44
+ async executeReview(prInfo, customPrompt, schema) {
45
+ const startTime = Date.now();
46
+ const timestamp = new Date().toISOString();
47
+ // Build prompt from custom instructions
48
+ const prompt = await this.buildCustomPrompt(prInfo, customPrompt, schema);
49
+ log(`Executing AI review with ${this.config.provider} provider...`);
50
+ log(`šŸ”§ Debug: Raw schema parameter: ${JSON.stringify(schema)} (type: ${typeof schema})`);
51
+ log(`Schema type: ${schema || 'default (code-review)'}`);
52
+ if (schema === 'plain') {
53
+ log('Using plain schema - expecting JSON with content field');
54
+ }
55
+ let debugInfo;
56
+ if (this.config.debug) {
57
+ debugInfo = {
58
+ prompt,
59
+ rawResponse: '',
60
+ provider: this.config.provider || 'unknown',
61
+ model: this.config.model || 'default',
62
+ apiKeySource: this.getApiKeySource(),
63
+ processingTime: 0,
64
+ promptLength: prompt.length,
65
+ responseLength: 0,
66
+ errors: [],
67
+ jsonParseSuccess: false,
68
+ timestamp,
69
+ };
70
+ }
71
+ // Handle mock model/provider first (no API key needed)
72
+ if (this.config.model === 'mock' || this.config.provider === 'mock') {
73
+ log('šŸŽ­ Using mock AI model/provider for testing - skipping API key validation');
74
+ }
75
+ else {
76
+ // Check if API key is available for real AI models
77
+ if (!this.config.apiKey) {
78
+ const errorMessage = 'No API key configured. Please set GOOGLE_API_KEY, ANTHROPIC_API_KEY, or OPENAI_API_KEY environment variable.';
79
+ // In debug mode, return a review with the error captured
80
+ if (debugInfo) {
81
+ debugInfo.errors = [errorMessage];
82
+ debugInfo.processingTime = Date.now() - startTime;
83
+ debugInfo.rawResponse = 'API call not attempted - no API key configured';
84
+ return {
85
+ issues: [
86
+ {
87
+ file: 'system',
88
+ line: 0,
89
+ ruleId: 'system/api-key-missing',
90
+ message: errorMessage,
91
+ severity: 'error',
92
+ category: 'logic',
93
+ },
94
+ ],
95
+ suggestions: [
96
+ 'Configure API keys in your GitHub repository secrets or environment variables',
97
+ ],
98
+ debug: debugInfo,
99
+ };
100
+ }
101
+ throw new Error(errorMessage);
102
+ }
103
+ }
104
+ try {
105
+ const response = await this.callProbeAgent(prompt, schema);
106
+ const processingTime = Date.now() - startTime;
107
+ if (debugInfo) {
108
+ debugInfo.rawResponse = response;
109
+ debugInfo.responseLength = response.length;
110
+ debugInfo.processingTime = processingTime;
111
+ }
112
+ const result = this.parseAIResponse(response, debugInfo, schema);
113
+ if (debugInfo) {
114
+ result.debug = debugInfo;
115
+ }
116
+ return result;
117
+ }
118
+ catch (error) {
119
+ if (debugInfo) {
120
+ debugInfo.errors = [error instanceof Error ? error.message : String(error)];
121
+ debugInfo.processingTime = Date.now() - startTime;
122
+ // In debug mode, return a review with the error captured
123
+ return {
124
+ issues: [
125
+ {
126
+ file: 'system',
127
+ line: 0,
128
+ ruleId: 'system/ai-execution-error',
129
+ message: error instanceof Error ? error.message : String(error),
130
+ severity: 'error',
131
+ category: 'logic',
132
+ },
133
+ ],
134
+ suggestions: ['Check AI service configuration and API key validity'],
135
+ debug: debugInfo,
136
+ };
137
+ }
138
+ throw error;
139
+ }
140
+ }
141
+ /**
142
+ * Build a custom prompt for AI review with XML-formatted data
143
+ */
144
+ async buildCustomPrompt(prInfo, customInstructions, _schema) {
145
+ const prContext = this.formatPRContext(prInfo);
146
+ const analysisType = prInfo.isIncremental ? 'INCREMENTAL' : 'FULL';
147
+ return `You are a senior code reviewer.
148
+
149
+ ANALYSIS TYPE: ${analysisType}
150
+ ${analysisType === 'INCREMENTAL'
151
+ ? '- You are analyzing a NEW COMMIT added to an existing PR. Focus on the <commit_diff> section for changes made in this specific commit.'
152
+ : '- You are analyzing the COMPLETE PR. Review all changes in the <full_diff> section.'}
153
+
154
+ REVIEW INSTRUCTIONS:
155
+ ${customInstructions}
156
+
157
+ Analyze the following structured pull request data:
158
+
159
+ ${prContext}
160
+
161
+ XML Data Structure Guide:
162
+ - <pull_request>: Root element containing all PR information
163
+ - <metadata>: PR metadata (number, title, author, branches, statistics)
164
+ - <description>: PR description text if provided
165
+ - <full_diff>: Complete unified diff of all changes (for FULL analysis)
166
+ - <commit_diff>: Diff of only the latest commit (for INCREMENTAL analysis)
167
+ - <files_summary>: List of all files changed with statistics
168
+
169
+ IMPORTANT RULES:
170
+ 1. Only analyze code that appears with + (additions) or - (deletions) in the diff
171
+ 2. Ignore unchanged code unless it's directly relevant to understanding a change
172
+ 3. Line numbers in your response should match the actual file line numbers
173
+ 4. Focus on real issues, not nitpicks
174
+ 5. Provide actionable, specific feedback
175
+ 6. For INCREMENTAL analysis, ONLY review changes in <commit_diff>
176
+ 7. For FULL analysis, review all changes in <full_diff>`;
177
+ }
178
+ // REMOVED: Built-in prompts - only use custom prompts from .visor.yaml
179
+ // REMOVED: getFocusInstructions - only use custom prompts from .visor.yaml
180
+ /**
181
+ * Format PR context for the AI using XML structure
182
+ */
183
+ formatPRContext(prInfo) {
184
+ let context = `<pull_request>
185
+ <metadata>
186
+ <number>${prInfo.number}</number>
187
+ <title>${this.escapeXml(prInfo.title)}</title>
188
+ <author>${prInfo.author}</author>
189
+ <base_branch>${prInfo.base}</base_branch>
190
+ <target_branch>${prInfo.head}</target_branch>
191
+ <total_additions>${prInfo.totalAdditions}</total_additions>
192
+ <total_deletions>${prInfo.totalDeletions}</total_deletions>
193
+ <files_changed_count>${prInfo.files.length}</files_changed_count>
194
+ </metadata>`;
195
+ // Add PR description if available
196
+ if (prInfo.body) {
197
+ context += `
198
+ <description>
199
+ ${this.escapeXml(prInfo.body)}
200
+ </description>`;
201
+ }
202
+ // Add full diff if available (for complete PR review)
203
+ if (prInfo.fullDiff) {
204
+ context += `
205
+ <full_diff>
206
+ ${this.escapeXml(prInfo.fullDiff)}
207
+ </full_diff>`;
208
+ }
209
+ // Add incremental commit diff if available (for new commit analysis)
210
+ if (prInfo.isIncremental) {
211
+ if (prInfo.commitDiff && prInfo.commitDiff.length > 0) {
212
+ context += `
213
+ <commit_diff>
214
+ ${this.escapeXml(prInfo.commitDiff)}
215
+ </commit_diff>`;
216
+ }
217
+ else {
218
+ context += `
219
+ <commit_diff>
220
+ <!-- Commit diff could not be retrieved - falling back to full diff analysis -->
221
+ ${prInfo.fullDiff ? this.escapeXml(prInfo.fullDiff) : ''}
222
+ </commit_diff>`;
223
+ }
224
+ }
225
+ // Add file summary for context
226
+ if (prInfo.files.length > 0) {
227
+ context += `
228
+ <files_summary>`;
229
+ prInfo.files.forEach((file, index) => {
230
+ context += `
231
+ <file index="${index + 1}">
232
+ <filename>${this.escapeXml(file.filename)}</filename>
233
+ <status>${file.status}</status>
234
+ <additions>${file.additions}</additions>
235
+ <deletions>${file.deletions}</deletions>
236
+ </file>`;
237
+ });
238
+ context += `
239
+ </files_summary>`;
240
+ }
241
+ context += `
242
+ </pull_request>`;
243
+ return context;
244
+ }
245
+ /**
246
+ * Escape XML special characters
247
+ */
248
+ escapeXml(text) {
249
+ return text
250
+ .replace(/&/g, '&amp;')
251
+ .replace(/</g, '&lt;')
252
+ .replace(/>/g, '&gt;')
253
+ .replace(/"/g, '&quot;')
254
+ .replace(/'/g, '&apos;');
255
+ }
256
+ /**
257
+ * Call ProbeAgent SDK with built-in schema validation
258
+ */
259
+ async callProbeAgent(prompt, schema) {
260
+ // Handle mock model/provider for testing
261
+ if (this.config.model === 'mock' || this.config.provider === 'mock') {
262
+ log('šŸŽ­ Using mock AI model/provider for testing');
263
+ return this.generateMockResponse(prompt);
264
+ }
265
+ log('šŸ¤– Creating ProbeAgent for AI review...');
266
+ log(`šŸ“ Prompt length: ${prompt.length} characters`);
267
+ log(`āš™ļø Model: ${this.config.model || 'default'}, Provider: ${this.config.provider || 'auto'}`);
268
+ // Store original env vars to restore later
269
+ const originalEnv = {
270
+ GOOGLE_API_KEY: process.env.GOOGLE_API_KEY,
271
+ ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY,
272
+ OPENAI_API_KEY: process.env.OPENAI_API_KEY,
273
+ };
274
+ try {
275
+ // Set environment variables for ProbeAgent
276
+ // ProbeAgent SDK expects these to be in the environment
277
+ if (this.config.provider === 'google' && this.config.apiKey) {
278
+ process.env.GOOGLE_API_KEY = this.config.apiKey;
279
+ }
280
+ else if (this.config.provider === 'anthropic' && this.config.apiKey) {
281
+ process.env.ANTHROPIC_API_KEY = this.config.apiKey;
282
+ }
283
+ else if (this.config.provider === 'openai' && this.config.apiKey) {
284
+ process.env.OPENAI_API_KEY = this.config.apiKey;
285
+ }
286
+ // Create ProbeAgent instance with proper options
287
+ // For plain schema, use a simpler approach without tools
288
+ const options = {
289
+ promptType: schema === 'plain' ? undefined : 'code-review-template',
290
+ customPrompt: schema === 'plain'
291
+ ? 'You are a helpful AI assistant. Respond only with valid JSON matching the provided schema. Do not use any tools or commands.'
292
+ : undefined,
293
+ allowEdit: false, // We don't want the agent to modify files
294
+ debug: this.config.debug || false,
295
+ };
296
+ // Add provider-specific options if configured
297
+ if (this.config.provider) {
298
+ options.provider = this.config.provider;
299
+ }
300
+ if (this.config.model) {
301
+ options.model = this.config.model;
302
+ }
303
+ const agent = new probe_1.ProbeAgent(options);
304
+ log('šŸš€ Calling ProbeAgent...');
305
+ // Load and pass the actual schema content if provided
306
+ let schemaString = undefined;
307
+ if (schema) {
308
+ try {
309
+ schemaString = await this.loadSchemaContent(schema);
310
+ log(`šŸ“‹ Loaded schema content for: ${schema}`);
311
+ }
312
+ catch (error) {
313
+ log(`āš ļø Failed to load schema ${schema}, proceeding without schema:`, error);
314
+ schemaString = undefined;
315
+ }
316
+ }
317
+ // ProbeAgent now handles schema formatting internally!
318
+ const response = await agent.answer(prompt, undefined, schemaString ? { schema: schemaString } : undefined);
319
+ log('āœ… ProbeAgent completed successfully');
320
+ log(`šŸ“¤ Response length: ${response.length} characters`);
321
+ return response;
322
+ }
323
+ catch (error) {
324
+ console.error('āŒ ProbeAgent failed:', error);
325
+ throw new Error(`ProbeAgent execution failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
326
+ }
327
+ finally {
328
+ // Restore original environment variables
329
+ Object.keys(originalEnv).forEach(key => {
330
+ if (originalEnv[key] === undefined) {
331
+ delete process.env[key];
332
+ }
333
+ else {
334
+ process.env[key] = originalEnv[key];
335
+ }
336
+ });
337
+ }
338
+ }
339
+ /**
340
+ * Load schema content from schema files
341
+ */
342
+ async loadSchemaContent(schemaName) {
343
+ const fs = require('fs').promises;
344
+ const path = require('path');
345
+ // Sanitize schema name to prevent path traversal attacks
346
+ const sanitizedSchemaName = schemaName.replace(/[^a-zA-Z0-9-]/g, '');
347
+ if (!sanitizedSchemaName || sanitizedSchemaName !== schemaName) {
348
+ throw new Error('Invalid schema name');
349
+ }
350
+ // Construct path to schema file using sanitized name
351
+ const schemaPath = path.join(process.cwd(), 'output', sanitizedSchemaName, 'schema.json');
352
+ try {
353
+ // Return the schema as a string, not parsed JSON
354
+ const schemaContent = await fs.readFile(schemaPath, 'utf-8');
355
+ return schemaContent.trim();
356
+ }
357
+ catch (error) {
358
+ throw new Error(`Failed to load schema from ${schemaPath}: ${error instanceof Error ? error.message : 'Unknown error'}`);
359
+ }
360
+ }
361
+ /**
362
+ * Parse AI response JSON
363
+ */
364
+ parseAIResponse(response, debugInfo, schema) {
365
+ log('šŸ” Parsing AI response...');
366
+ log(`šŸ“Š Raw response length: ${response.length} characters`);
367
+ // Log first and last 200 chars for debugging
368
+ if (response.length > 400) {
369
+ log('šŸ“‹ Response preview (first 200 chars):', response.substring(0, 200));
370
+ log('šŸ“‹ Response preview (last 200 chars):', response.substring(response.length - 200));
371
+ }
372
+ else {
373
+ log('šŸ“‹ Full response preview:', response);
374
+ }
375
+ try {
376
+ // Handle different schema types differently
377
+ let reviewData;
378
+ if (schema === 'plain') {
379
+ // For plain schema, ProbeAgent returns JSON with a content field
380
+ log('šŸ“ Processing plain schema response (expect JSON with content field)');
381
+ // Extract JSON using the same logic as other schemas
382
+ // ProbeAgent's cleanSchemaResponse now strips code blocks, so we need to find JSON boundaries
383
+ const trimmed = response.trim();
384
+ const firstBrace = trimmed.indexOf('{');
385
+ const firstBracket = trimmed.indexOf('[');
386
+ const lastBrace = trimmed.lastIndexOf('}');
387
+ const lastBracket = trimmed.lastIndexOf(']');
388
+ let jsonStr = trimmed;
389
+ let startIdx = -1;
390
+ let endIdx = -1;
391
+ // Prioritize {} if both exist
392
+ if (firstBrace !== -1 && lastBrace !== -1) {
393
+ if (firstBracket === -1 ||
394
+ firstBrace < firstBracket ||
395
+ (firstBrace < firstBracket && lastBrace > lastBracket)) {
396
+ startIdx = firstBrace;
397
+ endIdx = lastBrace;
398
+ }
399
+ }
400
+ // Fall back to [] if no valid {} or [] is better
401
+ if (startIdx === -1 && firstBracket !== -1 && lastBracket !== -1) {
402
+ startIdx = firstBracket;
403
+ endIdx = lastBracket;
404
+ }
405
+ // If we found valid JSON boundaries, extract it
406
+ if (startIdx !== -1 && endIdx !== -1 && startIdx < endIdx) {
407
+ jsonStr = trimmed.substring(startIdx, endIdx + 1);
408
+ log(`šŸ” Extracted JSON from response (chars ${startIdx} to ${endIdx + 1})`);
409
+ }
410
+ try {
411
+ reviewData = JSON.parse(jsonStr);
412
+ log('āœ… Successfully parsed plain schema JSON response');
413
+ if (debugInfo)
414
+ debugInfo.jsonParseSuccess = true;
415
+ }
416
+ catch {
417
+ // If JSON parsing fails, treat the entire response as content
418
+ log('šŸ”§ Plain schema fallback - treating entire response as content');
419
+ reviewData = {
420
+ content: response.trim(),
421
+ };
422
+ if (debugInfo)
423
+ debugInfo.jsonParseSuccess = true;
424
+ }
425
+ }
426
+ else {
427
+ // For other schemas (code-review, etc.), extract and parse JSON with boundary detection
428
+ log('šŸ” Extracting JSON from AI response...');
429
+ // Simple JSON extraction: find first { or [ and last } or ], with {} taking priority
430
+ let jsonString = response.trim();
431
+ // Find the first occurrence of { or [
432
+ const firstBrace = jsonString.indexOf('{');
433
+ const firstBracket = jsonString.indexOf('[');
434
+ let startIndex = -1;
435
+ let endChar = '';
436
+ // Determine which comes first (or if only one exists), {} takes priority
437
+ if (firstBrace !== -1 && (firstBracket === -1 || firstBrace < firstBracket)) {
438
+ // Object comes first or only objects exist
439
+ startIndex = firstBrace;
440
+ endChar = '}';
441
+ }
442
+ else if (firstBracket !== -1) {
443
+ // Array comes first or only arrays exist
444
+ startIndex = firstBracket;
445
+ endChar = ']';
446
+ }
447
+ if (startIndex !== -1) {
448
+ // Find the last occurrence of the matching end character
449
+ const lastEndIndex = jsonString.lastIndexOf(endChar);
450
+ if (lastEndIndex !== -1 && lastEndIndex > startIndex) {
451
+ jsonString = jsonString.substring(startIndex, lastEndIndex + 1);
452
+ }
453
+ }
454
+ // Parse the extracted JSON
455
+ try {
456
+ reviewData = JSON.parse(jsonString);
457
+ log('āœ… Successfully parsed probe agent JSON response');
458
+ if (debugInfo)
459
+ debugInfo.jsonParseSuccess = true;
460
+ }
461
+ catch (initialError) {
462
+ log('šŸ” Initial parsing failed, trying to extract JSON from response...');
463
+ // If the response starts with "I cannot" or similar, it's likely a refusal
464
+ if (response.toLowerCase().includes('i cannot') ||
465
+ response.toLowerCase().includes('unable to')) {
466
+ console.error('🚫 AI refused to analyze - returning empty result');
467
+ return {
468
+ issues: [],
469
+ suggestions: [
470
+ 'AI was unable to analyze this code. Please check the content or try again.',
471
+ ],
472
+ };
473
+ }
474
+ // Try to find JSON within the response
475
+ const jsonMatches = response.match(/\{[\s\S]*\}/g);
476
+ if (jsonMatches && jsonMatches.length > 0) {
477
+ log('šŸ”§ Found potential JSON in response, attempting to parse...');
478
+ // Try the largest JSON-like string (likely the complete response)
479
+ const largestJson = jsonMatches.reduce((a, b) => (a.length > b.length ? a : b));
480
+ log('šŸ”§ Attempting to parse extracted JSON...');
481
+ reviewData = JSON.parse(largestJson);
482
+ log('āœ… Successfully parsed extracted JSON');
483
+ if (debugInfo)
484
+ debugInfo.jsonParseSuccess = true;
485
+ }
486
+ else {
487
+ // Check if response is plain text and doesn't contain structured data
488
+ if (!response.includes('{') && !response.includes('}')) {
489
+ log('šŸ”§ Plain text response detected, creating structured fallback...');
490
+ const isNoChanges = response.toLowerCase().includes('no') &&
491
+ (response.toLowerCase().includes('changes') ||
492
+ response.toLowerCase().includes('code'));
493
+ reviewData = {
494
+ issues: [],
495
+ suggestions: isNoChanges
496
+ ? ['No code changes detected in this analysis']
497
+ : [
498
+ `AI response: ${response.substring(0, 200)}${response.length > 200 ? '...' : ''}`,
499
+ ],
500
+ };
501
+ }
502
+ else {
503
+ throw initialError;
504
+ }
505
+ }
506
+ }
507
+ }
508
+ // Handle different schemas
509
+ if (schema === 'plain') {
510
+ // For plain schema, we expect a content field with text (usually markdown)
511
+ log('šŸ“ Processing plain schema response');
512
+ if (!reviewData.content) {
513
+ console.error('āŒ Plain schema response missing content field');
514
+ console.error('šŸ” Available fields:', Object.keys(reviewData));
515
+ throw new Error('Invalid plain response: missing content field');
516
+ }
517
+ // Return a single "issue" that contains the text content
518
+ // This will be rendered using the text template
519
+ const result = {
520
+ issues: [
521
+ {
522
+ file: 'PR',
523
+ line: 1,
524
+ ruleId: 'full-review/overview',
525
+ message: reviewData.content,
526
+ severity: 'info',
527
+ category: 'documentation',
528
+ },
529
+ ],
530
+ suggestions: [],
531
+ };
532
+ log('āœ… Successfully created text ReviewSummary');
533
+ return result;
534
+ }
535
+ // Standard code-review schema processing
536
+ log('šŸ” Validating parsed review data...');
537
+ log(`šŸ“Š Overall score: ${0}`);
538
+ log(`šŸ“‹ Total issues: ${reviewData.issues?.length || 0}`);
539
+ log(`🚨 Critical issues: ${reviewData.issues?.filter((i) => i.severity === 'critical').length || 0}`);
540
+ log(`šŸ’” Suggestions count: ${Array.isArray(reviewData.suggestions) ? reviewData.suggestions.length : 0}`);
541
+ log(`šŸ’¬ Comments count: ${Array.isArray(reviewData.issues) ? reviewData.issues.length : 0}`);
542
+ // Process issues from the simplified format
543
+ const processedIssues = Array.isArray(reviewData.issues)
544
+ ? reviewData.issues.map((issue, index) => {
545
+ log(`šŸ” Processing issue ${index + 1}:`, issue);
546
+ return {
547
+ file: issue.file || 'unknown',
548
+ line: issue.line || 1,
549
+ endLine: issue.endLine,
550
+ ruleId: issue.ruleId || `${issue.category || 'general'}/unknown`,
551
+ message: issue.message || '',
552
+ severity: issue.severity,
553
+ category: issue.category,
554
+ suggestion: issue.suggestion,
555
+ replacement: issue.replacement,
556
+ };
557
+ })
558
+ : [];
559
+ // Validate and convert to ReviewSummary format
560
+ const result = {
561
+ issues: processedIssues,
562
+ suggestions: Array.isArray(reviewData.suggestions) ? reviewData.suggestions : [],
563
+ };
564
+ // Log issue counts
565
+ const criticalCount = result.issues.filter(i => i.severity === 'critical').length;
566
+ if (criticalCount > 0) {
567
+ log(`🚨 Found ${criticalCount} critical severity issue(s)`);
568
+ }
569
+ log(`šŸ“ˆ Total issues: ${result.issues.length}`);
570
+ log('āœ… Successfully created ReviewSummary');
571
+ return result;
572
+ }
573
+ catch (error) {
574
+ console.error('āŒ Failed to parse AI response:', error);
575
+ console.error('šŸ“„ FULL RAW RESPONSE:');
576
+ console.error('='.repeat(80));
577
+ console.error(response);
578
+ console.error('='.repeat(80));
579
+ console.error(`šŸ“ Response length: ${response.length} characters`);
580
+ // Try to provide more helpful error information
581
+ if (error instanceof SyntaxError) {
582
+ console.error('šŸ” JSON parsing error - the response may not be valid JSON');
583
+ console.error('šŸ” Error details:', error.message);
584
+ // Try to identify where the parsing failed
585
+ const errorMatch = error.message.match(/position (\d+)/);
586
+ if (errorMatch) {
587
+ const position = parseInt(errorMatch[1]);
588
+ console.error(`šŸ” Error at position ${position}:`);
589
+ const start = Math.max(0, position - 50);
590
+ const end = Math.min(response.length, position + 50);
591
+ console.error(`šŸ” Context: "${response.substring(start, end)}"`);
592
+ // Show the first 100 characters to understand what format the AI returned
593
+ console.error(`šŸ” Response beginning: "${response.substring(0, 100)}"`);
594
+ }
595
+ // Check if response contains common non-JSON patterns
596
+ if (response.includes('I cannot')) {
597
+ console.error('šŸ” Response appears to be a refusal/explanation rather than JSON');
598
+ }
599
+ if (response.includes('```')) {
600
+ console.error('šŸ” Response appears to contain markdown code blocks');
601
+ }
602
+ if (response.startsWith('<')) {
603
+ console.error('šŸ” Response appears to start with XML/HTML');
604
+ }
605
+ }
606
+ throw new Error(`Invalid AI response format: ${error instanceof Error ? error.message : 'Unknown error'}`);
607
+ }
608
+ }
609
+ /**
610
+ * Generate mock response for testing
611
+ */
612
+ async generateMockResponse(_prompt) {
613
+ // Simulate some processing time
614
+ await new Promise(resolve => setTimeout(resolve, 500));
615
+ // Generate mock response based on prompt content
616
+ const mockResponse = {
617
+ content: JSON.stringify({
618
+ issues: [
619
+ {
620
+ file: 'test.ts',
621
+ line: 7,
622
+ endLine: 11,
623
+ ruleId: 'security/sql-injection',
624
+ message: 'SQL injection vulnerability detected in dynamic query construction',
625
+ severity: 'critical',
626
+ category: 'security',
627
+ suggestion: 'Use parameterized queries or ORM methods to prevent SQL injection',
628
+ },
629
+ {
630
+ file: 'test.ts',
631
+ line: 14,
632
+ endLine: 23,
633
+ ruleId: 'performance/nested-loops',
634
+ message: 'Inefficient nested loops with O(n²) complexity',
635
+ severity: 'warning',
636
+ category: 'performance',
637
+ suggestion: 'Consider using more efficient algorithms or caching mechanisms',
638
+ },
639
+ {
640
+ file: 'test.ts',
641
+ line: 28,
642
+ ruleId: 'style/inconsistent-naming',
643
+ message: 'Inconsistent variable naming and formatting',
644
+ severity: 'info',
645
+ category: 'style',
646
+ suggestion: 'Use consistent camelCase naming and proper spacing',
647
+ },
648
+ ],
649
+ summary: {
650
+ totalIssues: 3,
651
+ criticalIssues: 1,
652
+ },
653
+ }),
654
+ };
655
+ return JSON.stringify(mockResponse);
656
+ }
657
+ /**
658
+ * Get the API key source for debugging (without revealing the key)
659
+ */
660
+ getApiKeySource() {
661
+ if (process.env.GOOGLE_API_KEY && this.config.provider === 'google') {
662
+ return 'GOOGLE_API_KEY';
663
+ }
664
+ if (process.env.ANTHROPIC_API_KEY && this.config.provider === 'anthropic') {
665
+ return 'ANTHROPIC_API_KEY';
666
+ }
667
+ if (process.env.OPENAI_API_KEY && this.config.provider === 'openai') {
668
+ return 'OPENAI_API_KEY';
669
+ }
670
+ return 'unknown';
671
+ }
672
+ }
673
+ exports.AIReviewService = AIReviewService;
674
+ //# sourceMappingURL=ai-review-service.js.map