@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.
- package/README.md +1240 -0
- package/action.yml +142 -0
- package/defaults/.visor.yaml +184 -0
- package/dist/action-cli-bridge.d.ts +104 -0
- package/dist/action-cli-bridge.d.ts.map +1 -0
- package/dist/action-cli-bridge.js +372 -0
- package/dist/action-cli-bridge.js.map +1 -0
- package/dist/ai-review-service.d.ts +84 -0
- package/dist/ai-review-service.d.ts.map +1 -0
- package/dist/ai-review-service.js +674 -0
- package/dist/ai-review-service.js.map +1 -0
- package/dist/check-execution-engine.d.ts +165 -0
- package/dist/check-execution-engine.d.ts.map +1 -0
- package/dist/check-execution-engine.js +1172 -0
- package/dist/check-execution-engine.js.map +1 -0
- package/dist/cli-main.d.ts +6 -0
- package/dist/cli-main.d.ts.map +1 -0
- package/dist/cli-main.js +247 -0
- package/dist/cli-main.js.map +1 -0
- package/dist/cli.d.ts +47 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +224 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands.d.ts +10 -0
- package/dist/commands.d.ts.map +1 -0
- package/dist/commands.js +53 -0
- package/dist/commands.js.map +1 -0
- package/dist/config.d.ts +63 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +369 -0
- package/dist/config.js.map +1 -0
- package/dist/dependency-resolver.d.ts +54 -0
- package/dist/dependency-resolver.d.ts.map +1 -0
- package/dist/dependency-resolver.js +163 -0
- package/dist/dependency-resolver.js.map +1 -0
- package/dist/event-mapper.d.ts +125 -0
- package/dist/event-mapper.d.ts.map +1 -0
- package/dist/event-mapper.js +311 -0
- package/dist/event-mapper.js.map +1 -0
- package/dist/failure-condition-evaluator.d.ts +81 -0
- package/dist/failure-condition-evaluator.d.ts.map +1 -0
- package/dist/failure-condition-evaluator.js +445 -0
- package/dist/failure-condition-evaluator.js.map +1 -0
- package/dist/git-repository-analyzer.d.ts +45 -0
- package/dist/git-repository-analyzer.d.ts.map +1 -0
- package/dist/git-repository-analyzer.js +285 -0
- package/dist/git-repository-analyzer.js.map +1 -0
- package/dist/github-check-service.d.ts +104 -0
- package/dist/github-check-service.d.ts.map +1 -0
- package/dist/github-check-service.js +382 -0
- package/dist/github-check-service.js.map +1 -0
- package/dist/github-comments.d.ts +109 -0
- package/dist/github-comments.d.ts.map +1 -0
- package/dist/github-comments.js +289 -0
- package/dist/github-comments.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1265 -0
- package/dist/index.js.map +1 -0
- package/dist/output-formatters.d.ts +66 -0
- package/dist/output-formatters.d.ts.map +1 -0
- package/dist/output-formatters.js +624 -0
- package/dist/output-formatters.js.map +1 -0
- package/dist/pr-analyzer.d.ts +47 -0
- package/dist/pr-analyzer.d.ts.map +1 -0
- package/dist/pr-analyzer.js +194 -0
- package/dist/pr-analyzer.js.map +1 -0
- package/dist/pr-detector.d.ts +78 -0
- package/dist/pr-detector.d.ts.map +1 -0
- package/dist/pr-detector.js +357 -0
- package/dist/pr-detector.js.map +1 -0
- package/dist/providers/ai-check-provider.d.ts +40 -0
- package/dist/providers/ai-check-provider.d.ts.map +1 -0
- package/dist/providers/ai-check-provider.js +416 -0
- package/dist/providers/ai-check-provider.js.map +1 -0
- package/dist/providers/check-provider-registry.d.ts +67 -0
- package/dist/providers/check-provider-registry.d.ts.map +1 -0
- package/dist/providers/check-provider-registry.js +138 -0
- package/dist/providers/check-provider-registry.js.map +1 -0
- package/dist/providers/check-provider.interface.d.ts +78 -0
- package/dist/providers/check-provider.interface.d.ts.map +1 -0
- package/dist/providers/check-provider.interface.js +11 -0
- package/dist/providers/check-provider.interface.js.map +1 -0
- package/dist/providers/index.d.ts +10 -0
- package/dist/providers/index.d.ts.map +1 -0
- package/dist/providers/index.js +19 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/providers/script-check-provider.d.ts +20 -0
- package/dist/providers/script-check-provider.d.ts.map +1 -0
- package/dist/providers/script-check-provider.js +163 -0
- package/dist/providers/script-check-provider.js.map +1 -0
- package/dist/providers/tool-check-provider.d.ts +19 -0
- package/dist/providers/tool-check-provider.d.ts.map +1 -0
- package/dist/providers/tool-check-provider.js +125 -0
- package/dist/providers/tool-check-provider.js.map +1 -0
- package/dist/providers/webhook-check-provider.d.ts +21 -0
- package/dist/providers/webhook-check-provider.d.ts.map +1 -0
- package/dist/providers/webhook-check-provider.js +173 -0
- package/dist/providers/webhook-check-provider.js.map +1 -0
- package/dist/reviewer.d.ts +88 -0
- package/dist/reviewer.d.ts.map +1 -0
- package/dist/reviewer.js +760 -0
- package/dist/reviewer.js.map +1 -0
- package/dist/types/cli.d.ts +41 -0
- package/dist/types/cli.d.ts.map +1 -0
- package/dist/types/cli.js +3 -0
- package/dist/types/cli.js.map +1 -0
- package/dist/types/config.d.ts +315 -0
- package/dist/types/config.d.ts.map +1 -0
- package/dist/types/config.js +6 -0
- package/dist/types/config.js.map +1 -0
- package/dist/utils/env-resolver.d.ts +38 -0
- package/dist/utils/env-resolver.d.ts.map +1 -0
- package/dist/utils/env-resolver.js +130 -0
- package/dist/utils/env-resolver.js.map +1 -0
- 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, '&')
|
|
251
|
+
.replace(/</g, '<')
|
|
252
|
+
.replace(/>/g, '>')
|
|
253
|
+
.replace(/"/g, '"')
|
|
254
|
+
.replace(/'/g, ''');
|
|
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
|