@probelabs/visor 0.1.20 → 0.1.22

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 (107) hide show
  1. package/dist/action-cli-bridge.d.ts +0 -1
  2. package/dist/action-cli-bridge.d.ts.map +1 -1
  3. package/dist/ai-review-service.d.ts +0 -1
  4. package/dist/ai-review-service.d.ts.map +1 -1
  5. package/dist/check-execution-engine.d.ts +0 -1
  6. package/dist/check-execution-engine.d.ts.map +1 -1
  7. package/dist/cli-main.d.ts +0 -2
  8. package/dist/cli-main.d.ts.map +1 -1
  9. package/dist/cli.d.ts +0 -1
  10. package/dist/cli.d.ts.map +1 -1
  11. package/dist/commands.d.ts +0 -1
  12. package/dist/commands.d.ts.map +1 -1
  13. package/dist/config.d.ts +0 -1
  14. package/dist/config.d.ts.map +1 -1
  15. package/dist/dependency-resolver.d.ts +0 -1
  16. package/dist/dependency-resolver.d.ts.map +1 -1
  17. package/dist/event-mapper.d.ts +0 -1
  18. package/dist/event-mapper.d.ts.map +1 -1
  19. package/dist/failure-condition-evaluator.d.ts +0 -1
  20. package/dist/failure-condition-evaluator.d.ts.map +1 -1
  21. package/dist/git-repository-analyzer.d.ts +0 -1
  22. package/dist/git-repository-analyzer.d.ts.map +1 -1
  23. package/dist/github-check-service.d.ts +0 -1
  24. package/dist/github-check-service.d.ts.map +1 -1
  25. package/dist/github-comments.d.ts +0 -1
  26. package/dist/github-comments.d.ts.map +1 -1
  27. package/dist/index.d.ts +0 -1
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +127709 -805
  30. package/dist/output-formatters.d.ts +0 -1
  31. package/dist/output-formatters.d.ts.map +1 -1
  32. package/dist/pr-analyzer.d.ts +0 -1
  33. package/dist/pr-analyzer.d.ts.map +1 -1
  34. package/dist/pr-detector.d.ts +0 -1
  35. package/dist/pr-detector.d.ts.map +1 -1
  36. package/dist/providers/ai-check-provider.d.ts.map +1 -1
  37. package/dist/providers/check-provider-registry.d.ts.map +1 -1
  38. package/dist/providers/check-provider.interface.d.ts.map +1 -1
  39. package/dist/providers/index.d.ts.map +1 -1
  40. package/dist/providers/noop-check-provider.d.ts.map +1 -1
  41. package/dist/providers/tool-check-provider.d.ts.map +1 -1
  42. package/dist/providers/webhook-check-provider.d.ts.map +1 -1
  43. package/dist/reviewer.d.ts +0 -1
  44. package/dist/reviewer.d.ts.map +1 -1
  45. package/dist/session-registry.d.ts +0 -1
  46. package/dist/session-registry.d.ts.map +1 -1
  47. package/dist/types/cli.d.ts.map +1 -1
  48. package/dist/types/config.d.ts.map +1 -1
  49. package/dist/utils/env-resolver.d.ts.map +1 -1
  50. package/package.json +3 -3
  51. package/dist/action-cli-bridge.js +0 -387
  52. package/dist/action-cli-bridge.js.map +0 -1
  53. package/dist/ai-review-service.js +0 -854
  54. package/dist/ai-review-service.js.map +0 -1
  55. package/dist/check-execution-engine.js +0 -1720
  56. package/dist/check-execution-engine.js.map +0 -1
  57. package/dist/cli-main.js +0 -249
  58. package/dist/cli-main.js.map +0 -1
  59. package/dist/cli.js +0 -241
  60. package/dist/cli.js.map +0 -1
  61. package/dist/commands.js +0 -53
  62. package/dist/commands.js.map +0 -1
  63. package/dist/config.js +0 -437
  64. package/dist/config.js.map +0 -1
  65. package/dist/dependency-resolver.js +0 -163
  66. package/dist/dependency-resolver.js.map +0 -1
  67. package/dist/event-mapper.js +0 -316
  68. package/dist/event-mapper.js.map +0 -1
  69. package/dist/failure-condition-evaluator.js +0 -481
  70. package/dist/failure-condition-evaluator.js.map +0 -1
  71. package/dist/git-repository-analyzer.js +0 -285
  72. package/dist/git-repository-analyzer.js.map +0 -1
  73. package/dist/github-check-service.js +0 -369
  74. package/dist/github-check-service.js.map +0 -1
  75. package/dist/github-comments.js +0 -289
  76. package/dist/github-comments.js.map +0 -1
  77. package/dist/index.js.map +0 -1
  78. package/dist/output-formatters.js +0 -624
  79. package/dist/output-formatters.js.map +0 -1
  80. package/dist/pr-analyzer.js +0 -195
  81. package/dist/pr-analyzer.js.map +0 -1
  82. package/dist/pr-detector.js +0 -357
  83. package/dist/pr-detector.js.map +0 -1
  84. package/dist/providers/ai-check-provider.js +0 -437
  85. package/dist/providers/ai-check-provider.js.map +0 -1
  86. package/dist/providers/check-provider-registry.js +0 -138
  87. package/dist/providers/check-provider-registry.js.map +0 -1
  88. package/dist/providers/check-provider.interface.js +0 -11
  89. package/dist/providers/check-provider.interface.js.map +0 -1
  90. package/dist/providers/index.js +0 -19
  91. package/dist/providers/index.js.map +0 -1
  92. package/dist/providers/noop-check-provider.js +0 -55
  93. package/dist/providers/noop-check-provider.js.map +0 -1
  94. package/dist/providers/tool-check-provider.js +0 -174
  95. package/dist/providers/tool-check-provider.js.map +0 -1
  96. package/dist/providers/webhook-check-provider.js +0 -173
  97. package/dist/providers/webhook-check-provider.js.map +0 -1
  98. package/dist/reviewer.js +0 -260
  99. package/dist/reviewer.js.map +0 -1
  100. package/dist/session-registry.js +0 -67
  101. package/dist/session-registry.js.map +0 -1
  102. package/dist/types/cli.js +0 -3
  103. package/dist/types/cli.js.map +0 -1
  104. package/dist/types/config.js +0 -6
  105. package/dist/types/config.js.map +0 -1
  106. package/dist/utils/env-resolver.js +0 -130
  107. package/dist/utils/env-resolver.js.map +0 -1
@@ -1,854 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.AIReviewService = void 0;
4
- const probe_1 = require("@probelabs/probe");
5
- const session_registry_1 = require("./session-registry");
6
- /**
7
- * Helper function to log messages respecting JSON/SARIF output format
8
- * Routes to stderr for JSON/SARIF to avoid contaminating structured output
9
- */
10
- function log(...args) {
11
- const isStructuredOutput = process.env.VISOR_OUTPUT_FORMAT === 'json' || process.env.VISOR_OUTPUT_FORMAT === 'sarif';
12
- const logFn = isStructuredOutput ? console.error : console.log;
13
- logFn(...args);
14
- }
15
- class AIReviewService {
16
- config;
17
- sessionRegistry;
18
- constructor(config = {}) {
19
- this.config = {
20
- timeout: 600000, // Increased timeout to 10 minutes for AI responses
21
- ...config,
22
- };
23
- this.sessionRegistry = session_registry_1.SessionRegistry.getInstance();
24
- // Auto-detect provider and API key from environment
25
- if (!this.config.apiKey) {
26
- if (process.env.GOOGLE_API_KEY) {
27
- this.config.apiKey = process.env.GOOGLE_API_KEY;
28
- this.config.provider = 'google';
29
- }
30
- else if (process.env.ANTHROPIC_API_KEY) {
31
- this.config.apiKey = process.env.ANTHROPIC_API_KEY;
32
- this.config.provider = 'anthropic';
33
- }
34
- else if (process.env.OPENAI_API_KEY) {
35
- this.config.apiKey = process.env.OPENAI_API_KEY;
36
- this.config.provider = 'openai';
37
- }
38
- }
39
- // Auto-detect model from environment
40
- if (!this.config.model && process.env.MODEL_NAME) {
41
- this.config.model = process.env.MODEL_NAME;
42
- }
43
- }
44
- /**
45
- * Execute AI review using probe agent
46
- */
47
- async executeReview(prInfo, customPrompt, schema, _checkName, sessionId) {
48
- const startTime = Date.now();
49
- const timestamp = new Date().toISOString();
50
- // Build prompt from custom instructions
51
- const prompt = await this.buildCustomPrompt(prInfo, customPrompt, schema);
52
- log(`Executing AI review with ${this.config.provider} provider...`);
53
- log(`šŸ”§ Debug: Raw schema parameter: ${JSON.stringify(schema)} (type: ${typeof schema})`);
54
- log(`Schema type: ${schema || 'none (no schema)'}`);
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
- schemaName: schema,
70
- schema: undefined, // Will be populated when schema is loaded
71
- };
72
- }
73
- // Handle mock model/provider first (no API key needed)
74
- if (this.config.model === 'mock' || this.config.provider === 'mock') {
75
- log('šŸŽ­ Using mock AI model/provider for testing - skipping API key validation');
76
- }
77
- else {
78
- // Check if API key is available for real AI models
79
- if (!this.config.apiKey) {
80
- const errorMessage = 'No API key configured. Please set GOOGLE_API_KEY, ANTHROPIC_API_KEY, or OPENAI_API_KEY environment variable.';
81
- // In debug mode, return a review with the error captured
82
- if (debugInfo) {
83
- debugInfo.errors = [errorMessage];
84
- debugInfo.processingTime = Date.now() - startTime;
85
- debugInfo.rawResponse = 'API call not attempted - no API key configured';
86
- return {
87
- issues: [
88
- {
89
- file: 'system',
90
- line: 0,
91
- ruleId: 'system/api-key-missing',
92
- message: errorMessage,
93
- severity: 'error',
94
- category: 'logic',
95
- },
96
- ],
97
- suggestions: [
98
- 'Configure API keys in your GitHub repository secrets or environment variables',
99
- ],
100
- debug: debugInfo,
101
- };
102
- }
103
- throw new Error(errorMessage);
104
- }
105
- }
106
- try {
107
- const { response, effectiveSchema } = await this.callProbeAgent(prompt, schema, debugInfo, _checkName, sessionId);
108
- const processingTime = Date.now() - startTime;
109
- if (debugInfo) {
110
- debugInfo.rawResponse = response;
111
- debugInfo.responseLength = response.length;
112
- debugInfo.processingTime = processingTime;
113
- }
114
- const result = this.parseAIResponse(response, debugInfo, effectiveSchema);
115
- if (debugInfo) {
116
- result.debug = debugInfo;
117
- }
118
- return result;
119
- }
120
- catch (error) {
121
- if (debugInfo) {
122
- debugInfo.errors = [error instanceof Error ? error.message : String(error)];
123
- debugInfo.processingTime = Date.now() - startTime;
124
- // In debug mode, return a review with the error captured
125
- return {
126
- issues: [
127
- {
128
- file: 'system',
129
- line: 0,
130
- ruleId: 'system/ai-execution-error',
131
- message: error instanceof Error ? error.message : String(error),
132
- severity: 'error',
133
- category: 'logic',
134
- },
135
- ],
136
- suggestions: ['Check AI service configuration and API key validity'],
137
- debug: debugInfo,
138
- };
139
- }
140
- throw error;
141
- }
142
- }
143
- /**
144
- * Execute AI review using session reuse - reuses an existing ProbeAgent session
145
- */
146
- async executeReviewWithSessionReuse(prInfo, customPrompt, parentSessionId, schema, checkName) {
147
- const startTime = Date.now();
148
- const timestamp = new Date().toISOString();
149
- // Get the existing session
150
- const existingAgent = this.sessionRegistry.getSession(parentSessionId);
151
- if (!existingAgent) {
152
- throw new Error(`Session not found for reuse: ${parentSessionId}. Ensure the parent check completed successfully.`);
153
- }
154
- // Build prompt from custom instructions
155
- const prompt = await this.buildCustomPrompt(prInfo, customPrompt, schema);
156
- log(`šŸ”„ Reusing AI session ${parentSessionId} for review...`);
157
- log(`šŸ”§ Debug: Raw schema parameter: ${JSON.stringify(schema)} (type: ${typeof schema})`);
158
- log(`Schema type: ${schema || 'none (no schema)'}`);
159
- let debugInfo;
160
- if (this.config.debug) {
161
- debugInfo = {
162
- prompt,
163
- rawResponse: '',
164
- provider: this.config.provider || 'unknown',
165
- model: this.config.model || 'default',
166
- apiKeySource: this.getApiKeySource(),
167
- processingTime: 0,
168
- promptLength: prompt.length,
169
- responseLength: 0,
170
- errors: [],
171
- jsonParseSuccess: false,
172
- timestamp,
173
- schemaName: schema,
174
- schema: undefined, // Will be populated when schema is loaded
175
- };
176
- }
177
- try {
178
- // Use existing agent's answer method instead of creating new agent
179
- const { response, effectiveSchema } = await this.callProbeAgentWithExistingSession(existingAgent, prompt, schema, debugInfo, checkName);
180
- const processingTime = Date.now() - startTime;
181
- if (debugInfo) {
182
- debugInfo.rawResponse = response;
183
- debugInfo.responseLength = response.length;
184
- debugInfo.processingTime = processingTime;
185
- }
186
- const result = this.parseAIResponse(response, debugInfo, effectiveSchema);
187
- if (debugInfo) {
188
- result.debug = debugInfo;
189
- }
190
- return result;
191
- }
192
- catch (error) {
193
- if (debugInfo) {
194
- debugInfo.errors = [error instanceof Error ? error.message : String(error)];
195
- debugInfo.processingTime = Date.now() - startTime;
196
- // In debug mode, return a review with the error captured
197
- return {
198
- issues: [
199
- {
200
- file: 'system',
201
- line: 0,
202
- ruleId: 'system/ai-session-reuse-error',
203
- message: error instanceof Error ? error.message : String(error),
204
- severity: 'error',
205
- category: 'logic',
206
- },
207
- ],
208
- suggestions: [
209
- 'Check session reuse configuration and ensure parent check completed successfully',
210
- ],
211
- debug: debugInfo,
212
- };
213
- }
214
- throw error;
215
- }
216
- }
217
- /**
218
- * Register a new AI session in the session registry
219
- */
220
- registerSession(sessionId, agent) {
221
- this.sessionRegistry.registerSession(sessionId, agent);
222
- }
223
- /**
224
- * Cleanup a session from the registry
225
- */
226
- cleanupSession(sessionId) {
227
- this.sessionRegistry.unregisterSession(sessionId);
228
- }
229
- /**
230
- * Build a custom prompt for AI review with XML-formatted data
231
- */
232
- async buildCustomPrompt(prInfo, customInstructions, _schema) {
233
- const prContext = this.formatPRContext(prInfo);
234
- const analysisType = prInfo.isIncremental ? 'INCREMENTAL' : 'FULL';
235
- return `You are a senior code reviewer.
236
-
237
- ANALYSIS TYPE: ${analysisType}
238
- ${analysisType === 'INCREMENTAL'
239
- ? '- You are analyzing a NEW COMMIT added to an existing PR. Focus on the <commit_diff> section for changes made in this specific commit.'
240
- : '- You are analyzing the COMPLETE PR. Review all changes in the <full_diff> section.'}
241
-
242
- REVIEW INSTRUCTIONS:
243
- ${customInstructions}
244
-
245
- Analyze the following structured pull request data:
246
-
247
- ${prContext}
248
-
249
- XML Data Structure Guide:
250
- - <pull_request>: Root element containing all PR information
251
- - <metadata>: PR metadata (number, title, author, branches, statistics)
252
- - <description>: PR description text if provided
253
- - <full_diff>: Complete unified diff of all changes (for FULL analysis)
254
- - <commit_diff>: Diff of only the latest commit (for INCREMENTAL analysis)
255
- - <files_summary>: List of all files changed with statistics
256
-
257
- IMPORTANT RULES:
258
- 1. Only analyze code that appears with + (additions) or - (deletions) in the diff
259
- 2. Ignore unchanged code unless it's directly relevant to understanding a change
260
- 3. Line numbers in your response should match the actual file line numbers
261
- 4. Focus on real issues, not nitpicks
262
- 5. Provide actionable, specific feedback
263
- 6. For INCREMENTAL analysis, ONLY review changes in <commit_diff>
264
- 7. For FULL analysis, review all changes in <full_diff>`;
265
- }
266
- // REMOVED: Built-in prompts - only use custom prompts from .visor.yaml
267
- // REMOVED: getFocusInstructions - only use custom prompts from .visor.yaml
268
- /**
269
- * Format PR context for the AI using XML structure
270
- */
271
- formatPRContext(prInfo) {
272
- let context = `<pull_request>
273
- <metadata>
274
- <number>${prInfo.number}</number>
275
- <title>${this.escapeXml(prInfo.title)}</title>
276
- <author>${prInfo.author}</author>
277
- <base_branch>${prInfo.base}</base_branch>
278
- <target_branch>${prInfo.head}</target_branch>
279
- <total_additions>${prInfo.totalAdditions}</total_additions>
280
- <total_deletions>${prInfo.totalDeletions}</total_deletions>
281
- <files_changed_count>${prInfo.files.length}</files_changed_count>
282
- </metadata>`;
283
- // Add PR description if available
284
- if (prInfo.body) {
285
- context += `
286
- <description>
287
- ${this.escapeXml(prInfo.body)}
288
- </description>`;
289
- }
290
- // Add full diff if available (for complete PR review)
291
- if (prInfo.fullDiff) {
292
- context += `
293
- <full_diff>
294
- ${this.escapeXml(prInfo.fullDiff)}
295
- </full_diff>`;
296
- }
297
- // Add incremental commit diff if available (for new commit analysis)
298
- if (prInfo.isIncremental) {
299
- if (prInfo.commitDiff && prInfo.commitDiff.length > 0) {
300
- context += `
301
- <commit_diff>
302
- ${this.escapeXml(prInfo.commitDiff)}
303
- </commit_diff>`;
304
- }
305
- else {
306
- context += `
307
- <commit_diff>
308
- <!-- Commit diff could not be retrieved - falling back to full diff analysis -->
309
- ${prInfo.fullDiff ? this.escapeXml(prInfo.fullDiff) : ''}
310
- </commit_diff>`;
311
- }
312
- }
313
- // Add file summary for context
314
- if (prInfo.files.length > 0) {
315
- context += `
316
- <files_summary>`;
317
- prInfo.files.forEach((file, index) => {
318
- context += `
319
- <file index="${index + 1}">
320
- <filename>${this.escapeXml(file.filename)}</filename>
321
- <status>${file.status}</status>
322
- <additions>${file.additions}</additions>
323
- <deletions>${file.deletions}</deletions>
324
- </file>`;
325
- });
326
- context += `
327
- </files_summary>`;
328
- }
329
- context += `
330
- </pull_request>`;
331
- return context;
332
- }
333
- /**
334
- * No longer escaping XML - returning text as-is
335
- */
336
- escapeXml(text) {
337
- return text;
338
- }
339
- /**
340
- * Call ProbeAgent with an existing session
341
- */
342
- async callProbeAgentWithExistingSession(agent, prompt, schema, debugInfo, _checkName) {
343
- // Handle mock model/provider for testing
344
- if (this.config.model === 'mock' || this.config.provider === 'mock') {
345
- log('šŸŽ­ Using mock AI model/provider for testing (session reuse)');
346
- const response = await this.generateMockResponse(prompt);
347
- return { response, effectiveSchema: schema };
348
- }
349
- log('šŸ”„ Reusing existing ProbeAgent session for AI review...');
350
- log(`šŸ“ Prompt length: ${prompt.length} characters`);
351
- log(`āš™ļø Model: ${this.config.model || 'default'}, Provider: ${this.config.provider || 'auto'}`);
352
- try {
353
- log('šŸš€ Calling existing ProbeAgent with answer()...');
354
- // Load and pass the actual schema content if provided (skip for plain schema)
355
- let schemaString = undefined;
356
- let effectiveSchema = schema;
357
- if (schema && schema !== 'plain') {
358
- try {
359
- schemaString = await this.loadSchemaContent(schema);
360
- log(`šŸ“‹ Loaded schema content for: ${schema}`);
361
- log(`šŸ“„ Raw schema JSON:\n${schemaString}`);
362
- }
363
- catch (error) {
364
- log(`āš ļø Failed to load schema ${schema}, proceeding without schema:`, error);
365
- schemaString = undefined;
366
- effectiveSchema = undefined; // Schema loading failed, treat as no schema
367
- if (debugInfo && debugInfo.errors) {
368
- debugInfo.errors.push(`Failed to load schema: ${error}`);
369
- }
370
- }
371
- }
372
- else if (schema === 'plain') {
373
- log(`šŸ“‹ Using plain schema - no JSON validation will be applied`);
374
- }
375
- // Pass schema in options object with 'schema' property
376
- const schemaOptions = schemaString ? { schema: schemaString } : undefined;
377
- // Store the exact schema options being passed to ProbeAgent in debug info
378
- if (debugInfo && schemaOptions) {
379
- debugInfo.schema = JSON.stringify(schemaOptions, null, 2);
380
- }
381
- // Log the schema options being passed to ProbeAgent
382
- if (schemaOptions) {
383
- log(`šŸŽÆ Schema options passed to ProbeAgent.answer() (session reuse):`);
384
- log(JSON.stringify(schemaOptions, null, 2));
385
- }
386
- // Use existing agent's answer method - this reuses the conversation context
387
- const response = await agent.answer(prompt, undefined, schemaOptions);
388
- log('āœ… ProbeAgent session reuse completed successfully');
389
- log(`šŸ“¤ Response length: ${response.length} characters`);
390
- return { response, effectiveSchema };
391
- }
392
- catch (error) {
393
- console.error('āŒ ProbeAgent session reuse failed:', error);
394
- throw new Error(`ProbeAgent session reuse failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
395
- }
396
- }
397
- /**
398
- * Call ProbeAgent SDK with built-in schema validation
399
- */
400
- async callProbeAgent(prompt, schema, debugInfo, _checkName, providedSessionId) {
401
- // Handle mock model/provider for testing
402
- if (this.config.model === 'mock' || this.config.provider === 'mock') {
403
- log('šŸŽ­ Using mock AI model/provider for testing');
404
- const response = await this.generateMockResponse(prompt);
405
- return { response, effectiveSchema: schema };
406
- }
407
- // Create ProbeAgent instance with proper options
408
- const sessionId = providedSessionId ||
409
- (() => {
410
- const timestamp = new Date().toISOString();
411
- return `visor-${timestamp.replace(/[:.]/g, '-')}-${_checkName || 'unknown'}`;
412
- })();
413
- log('šŸ¤– Creating ProbeAgent for AI review...');
414
- log(`šŸ†” Session ID: ${sessionId}`);
415
- log(`šŸ“ Prompt length: ${prompt.length} characters`);
416
- log(`āš™ļø Model: ${this.config.model || 'default'}, Provider: ${this.config.provider || 'auto'}`);
417
- // Store original env vars to restore later
418
- const originalEnv = {
419
- GOOGLE_API_KEY: process.env.GOOGLE_API_KEY,
420
- ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY,
421
- OPENAI_API_KEY: process.env.OPENAI_API_KEY,
422
- };
423
- try {
424
- // Set environment variables for ProbeAgent
425
- // ProbeAgent SDK expects these to be in the environment
426
- if (this.config.provider === 'google' && this.config.apiKey) {
427
- process.env.GOOGLE_API_KEY = this.config.apiKey;
428
- }
429
- else if (this.config.provider === 'anthropic' && this.config.apiKey) {
430
- process.env.ANTHROPIC_API_KEY = this.config.apiKey;
431
- }
432
- else if (this.config.provider === 'openai' && this.config.apiKey) {
433
- process.env.OPENAI_API_KEY = this.config.apiKey;
434
- }
435
- const options = {
436
- sessionId: sessionId,
437
- promptType: schema ? 'code-review-template' : undefined,
438
- allowEdit: false, // We don't want the agent to modify files
439
- debug: this.config.debug || false,
440
- };
441
- // Add provider-specific options if configured
442
- if (this.config.provider) {
443
- options.provider = this.config.provider;
444
- }
445
- if (this.config.model) {
446
- options.model = this.config.model;
447
- }
448
- const agent = new probe_1.ProbeAgent(options);
449
- log('šŸš€ Calling ProbeAgent...');
450
- // Load and pass the actual schema content if provided (skip for plain schema)
451
- let schemaString = undefined;
452
- let effectiveSchema = schema;
453
- if (schema && schema !== 'plain') {
454
- try {
455
- schemaString = await this.loadSchemaContent(schema);
456
- log(`šŸ“‹ Loaded schema content for: ${schema}`);
457
- log(`šŸ“„ Raw schema JSON:\n${schemaString}`);
458
- }
459
- catch (error) {
460
- log(`āš ļø Failed to load schema ${schema}, proceeding without schema:`, error);
461
- schemaString = undefined;
462
- effectiveSchema = undefined; // Schema loading failed, treat as no schema
463
- if (debugInfo && debugInfo.errors) {
464
- debugInfo.errors.push(`Failed to load schema: ${error}`);
465
- }
466
- }
467
- }
468
- else if (schema === 'plain') {
469
- log(`šŸ“‹ Using plain schema - no JSON validation will be applied`);
470
- }
471
- // ProbeAgent now handles schema formatting internally!
472
- // Pass schema in options object with 'schema' property
473
- const schemaOptions = schemaString ? { schema: schemaString } : undefined;
474
- // Store the exact schema options being passed to ProbeAgent in debug info
475
- if (debugInfo && schemaOptions) {
476
- debugInfo.schema = JSON.stringify(schemaOptions, null, 2);
477
- }
478
- // Log the schema options being passed to ProbeAgent
479
- if (schemaOptions) {
480
- log(`šŸŽÆ Schema options passed to ProbeAgent.answer():`);
481
- log(JSON.stringify(schemaOptions, null, 2));
482
- }
483
- // Log the equivalent CLI command for local reproduction
484
- const provider = this.config.provider || 'auto';
485
- const model = this.config.model || 'default';
486
- // Save prompt to a temp file for easier reproduction
487
- try {
488
- const fs = require('fs');
489
- const path = require('path');
490
- const os = require('os');
491
- const tempDir = os.tmpdir();
492
- const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
493
- const promptFile = path.join(tempDir, `visor-prompt-${timestamp}.txt`);
494
- fs.writeFileSync(promptFile, prompt, 'utf-8');
495
- log(`\nšŸ’¾ Prompt saved to: ${promptFile}`);
496
- log(`\nšŸ“ To reproduce locally, run:`);
497
- let cliCommand = `npx @probelabs/probe@latest agent`;
498
- cliCommand += ` --provider ${provider}`;
499
- if (model !== 'default') {
500
- cliCommand += ` --model ${model}`;
501
- }
502
- if (schema) {
503
- cliCommand += ` --schema output/${schema}/schema.json`;
504
- }
505
- cliCommand += ` "${promptFile}"`;
506
- log(`\n$ ${cliCommand}\n`);
507
- }
508
- catch (error) {
509
- log(`āš ļø Could not save prompt file: ${error}`);
510
- }
511
- const response = await agent.answer(prompt, undefined, schemaOptions);
512
- log('āœ… ProbeAgent completed successfully');
513
- log(`šŸ“¤ Response length: ${response.length} characters`);
514
- // Register the session for potential reuse by dependent checks
515
- if (_checkName) {
516
- this.registerSession(sessionId, agent);
517
- log(`šŸ”§ Debug: Registered AI session for potential reuse: ${sessionId}`);
518
- }
519
- return { response, effectiveSchema };
520
- }
521
- catch (error) {
522
- console.error('āŒ ProbeAgent failed:', error);
523
- throw new Error(`ProbeAgent execution failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
524
- }
525
- finally {
526
- // Restore original environment variables
527
- Object.keys(originalEnv).forEach(key => {
528
- if (originalEnv[key] === undefined) {
529
- delete process.env[key];
530
- }
531
- else {
532
- process.env[key] = originalEnv[key];
533
- }
534
- });
535
- }
536
- }
537
- /**
538
- * Load schema content from schema files
539
- */
540
- async loadSchemaContent(schemaName) {
541
- const fs = require('fs').promises;
542
- const path = require('path');
543
- // Sanitize schema name to prevent path traversal attacks
544
- const sanitizedSchemaName = schemaName.replace(/[^a-zA-Z0-9-]/g, '');
545
- if (!sanitizedSchemaName || sanitizedSchemaName !== schemaName) {
546
- throw new Error('Invalid schema name');
547
- }
548
- // Construct path to schema file using sanitized name
549
- const schemaPath = path.join(process.cwd(), 'output', sanitizedSchemaName, 'schema.json');
550
- try {
551
- // Return the schema as a string, not parsed JSON
552
- const schemaContent = await fs.readFile(schemaPath, 'utf-8');
553
- return schemaContent.trim();
554
- }
555
- catch (error) {
556
- throw new Error(`Failed to load schema from ${schemaPath}: ${error instanceof Error ? error.message : 'Unknown error'}`);
557
- }
558
- }
559
- /**
560
- * Parse AI response JSON
561
- */
562
- parseAIResponse(response, debugInfo, _schema) {
563
- log('šŸ” Parsing AI response...');
564
- log(`šŸ“Š Raw response length: ${response.length} characters`);
565
- // Log first and last 200 chars for debugging
566
- if (response.length > 400) {
567
- log('šŸ“‹ Response preview (first 200 chars):', response.substring(0, 200));
568
- log('šŸ“‹ Response preview (last 200 chars):', response.substring(response.length - 200));
569
- }
570
- else {
571
- log('šŸ“‹ Full response preview:', response);
572
- }
573
- try {
574
- // Handle different schema types differently
575
- let reviewData;
576
- // Handle plain schema or no schema - no JSON parsing, return response as-is
577
- if (_schema === 'plain' || !_schema) {
578
- log(`šŸ“‹ ${_schema === 'plain' ? 'Plain' : 'No'} schema detected - returning raw response without JSON parsing`);
579
- return {
580
- issues: [],
581
- suggestions: [response.trim()],
582
- debug: debugInfo,
583
- };
584
- }
585
- {
586
- // For other schemas (code-review, etc.), extract and parse JSON with boundary detection
587
- log('šŸ” Extracting JSON from AI response...');
588
- // Try direct parsing first - if AI returned pure JSON
589
- try {
590
- reviewData = JSON.parse(response.trim());
591
- log('āœ… Successfully parsed direct JSON response');
592
- if (debugInfo)
593
- debugInfo.jsonParseSuccess = true;
594
- }
595
- catch {
596
- log('šŸ” Direct parsing failed, trying to extract JSON from response...');
597
- // If the response starts with "I cannot" or similar, it's likely a refusal
598
- if (response.toLowerCase().includes('i cannot') ||
599
- response.toLowerCase().includes('unable to')) {
600
- console.error('🚫 AI refused to analyze - returning empty result');
601
- return {
602
- issues: [],
603
- suggestions: [
604
- 'AI was unable to analyze this code. Please check the content or try again.',
605
- ],
606
- };
607
- }
608
- // Try to extract JSON using improved method with proper bracket matching
609
- const jsonString = this.extractJsonFromResponse(response);
610
- if (jsonString) {
611
- try {
612
- reviewData = JSON.parse(jsonString);
613
- log('āœ… Successfully parsed extracted JSON');
614
- if (debugInfo)
615
- debugInfo.jsonParseSuccess = true;
616
- }
617
- catch {
618
- log('šŸ”§ Extracted JSON parsing failed, falling back to plain text handling...');
619
- // Check if response is plain text and doesn't contain structured data
620
- if (!response.includes('{') && !response.includes('}')) {
621
- log('šŸ”§ Plain text response detected, creating structured fallback...');
622
- const isNoChanges = response.toLowerCase().includes('no') &&
623
- (response.toLowerCase().includes('changes') ||
624
- response.toLowerCase().includes('code'));
625
- reviewData = {
626
- issues: [],
627
- suggestions: isNoChanges
628
- ? ['No code changes detected in this analysis']
629
- : [
630
- `AI response: ${response.substring(0, 200)}${response.length > 200 ? '...' : ''}`,
631
- ],
632
- };
633
- }
634
- else {
635
- // Fallback: treat the entire response as a suggestion
636
- log('šŸ”§ Creating fallback response from non-JSON content...');
637
- reviewData = {
638
- issues: [],
639
- suggestions: [response.trim()],
640
- };
641
- }
642
- }
643
- }
644
- else {
645
- // No JSON found at all - treat as plain text response
646
- log('šŸ”§ No JSON found in response, treating as plain text...');
647
- reviewData = {
648
- issues: [],
649
- suggestions: [response.trim()],
650
- };
651
- }
652
- }
653
- }
654
- // Standard code-review schema processing
655
- log('šŸ” Validating parsed review data...');
656
- log(`šŸ“Š Overall score: ${0}`);
657
- log(`šŸ“‹ Total issues: ${reviewData.issues?.length || 0}`);
658
- log(`🚨 Critical issues: ${reviewData.issues?.filter((i) => i.severity === 'critical').length || 0}`);
659
- log(`šŸ’” Suggestions count: ${Array.isArray(reviewData.suggestions) ? reviewData.suggestions.length : 0}`);
660
- log(`šŸ’¬ Comments count: ${Array.isArray(reviewData.issues) ? reviewData.issues.length : 0}`);
661
- // Process issues from the simplified format
662
- const processedIssues = Array.isArray(reviewData.issues)
663
- ? reviewData.issues.map((issue, index) => {
664
- log(`šŸ” Processing issue ${index + 1}:`, issue);
665
- return {
666
- file: issue.file || 'unknown',
667
- line: issue.line || 1,
668
- endLine: issue.endLine,
669
- ruleId: issue.ruleId || `${issue.category || 'general'}/unknown`,
670
- message: issue.message || '',
671
- severity: issue.severity,
672
- category: issue.category,
673
- suggestion: issue.suggestion,
674
- replacement: issue.replacement,
675
- };
676
- })
677
- : [];
678
- // Validate and convert to ReviewSummary format
679
- const result = {
680
- issues: processedIssues,
681
- suggestions: Array.isArray(reviewData.suggestions) ? reviewData.suggestions : [],
682
- };
683
- // Log issue counts
684
- const criticalCount = (result.issues || []).filter(i => i.severity === 'critical').length;
685
- if (criticalCount > 0) {
686
- log(`🚨 Found ${criticalCount} critical severity issue(s)`);
687
- }
688
- log(`šŸ“ˆ Total issues: ${(result.issues || []).length}`);
689
- log('āœ… Successfully created ReviewSummary');
690
- return result;
691
- }
692
- catch (error) {
693
- console.error('āŒ Failed to parse AI response:', error);
694
- console.error('šŸ“„ FULL RAW RESPONSE:');
695
- console.error('='.repeat(80));
696
- console.error(response);
697
- console.error('='.repeat(80));
698
- console.error(`šŸ“ Response length: ${response.length} characters`);
699
- // Try to provide more helpful error information
700
- if (error instanceof SyntaxError) {
701
- console.error('šŸ” JSON parsing error - the response may not be valid JSON');
702
- console.error('šŸ” Error details:', error.message);
703
- // Try to identify where the parsing failed
704
- const errorMatch = error.message.match(/position (\d+)/);
705
- if (errorMatch) {
706
- const position = parseInt(errorMatch[1]);
707
- console.error(`šŸ” Error at position ${position}:`);
708
- const start = Math.max(0, position - 50);
709
- const end = Math.min(response.length, position + 50);
710
- console.error(`šŸ” Context: "${response.substring(start, end)}"`);
711
- // Show the first 100 characters to understand what format the AI returned
712
- console.error(`šŸ” Response beginning: "${response.substring(0, 100)}"`);
713
- }
714
- // Check if response contains common non-JSON patterns
715
- if (response.includes('I cannot')) {
716
- console.error('šŸ” Response appears to be a refusal/explanation rather than JSON');
717
- }
718
- if (response.includes('```')) {
719
- console.error('šŸ” Response appears to contain markdown code blocks');
720
- }
721
- if (response.startsWith('<')) {
722
- console.error('šŸ” Response appears to start with XML/HTML');
723
- }
724
- }
725
- throw new Error(`Invalid AI response format: ${error instanceof Error ? error.message : 'Unknown error'}`);
726
- }
727
- }
728
- /**
729
- * Extract JSON from a response that might contain surrounding text
730
- * Uses proper bracket matching to find valid JSON objects or arrays
731
- */
732
- extractJsonFromResponse(response) {
733
- const text = response.trim();
734
- // Try to find JSON objects first (higher priority)
735
- let bestJson = this.findJsonWithBracketMatching(text, '{', '}');
736
- // If no object found, try arrays
737
- if (!bestJson) {
738
- bestJson = this.findJsonWithBracketMatching(text, '[', ']');
739
- }
740
- return bestJson;
741
- }
742
- /**
743
- * Find JSON with proper bracket matching to avoid false positives
744
- */
745
- findJsonWithBracketMatching(text, openChar, closeChar) {
746
- const firstIndex = text.indexOf(openChar);
747
- if (firstIndex === -1)
748
- return null;
749
- let depth = 0;
750
- let inString = false;
751
- let escaping = false;
752
- for (let i = firstIndex; i < text.length; i++) {
753
- const char = text[i];
754
- if (escaping) {
755
- escaping = false;
756
- continue;
757
- }
758
- if (char === '\\' && inString) {
759
- escaping = true;
760
- continue;
761
- }
762
- if (char === '"' && !escaping) {
763
- inString = !inString;
764
- continue;
765
- }
766
- if (!inString) {
767
- if (char === openChar) {
768
- depth++;
769
- }
770
- else if (char === closeChar) {
771
- depth--;
772
- if (depth === 0) {
773
- // Found matching closing bracket
774
- const candidate = text.substring(firstIndex, i + 1);
775
- try {
776
- JSON.parse(candidate); // Validate it's actually valid JSON
777
- return candidate;
778
- }
779
- catch {
780
- // This wasn't valid JSON, keep looking
781
- break;
782
- }
783
- }
784
- }
785
- }
786
- }
787
- return null;
788
- }
789
- /**
790
- * Generate mock response for testing
791
- */
792
- async generateMockResponse(_prompt) {
793
- // Simulate some processing time
794
- await new Promise(resolve => setTimeout(resolve, 500));
795
- // Generate mock response based on prompt content
796
- const mockResponse = {
797
- content: JSON.stringify({
798
- issues: [
799
- {
800
- file: 'test.ts',
801
- line: 7,
802
- endLine: 11,
803
- ruleId: 'security/sql-injection',
804
- message: 'SQL injection vulnerability detected in dynamic query construction',
805
- severity: 'critical',
806
- category: 'security',
807
- suggestion: 'Use parameterized queries or ORM methods to prevent SQL injection',
808
- },
809
- {
810
- file: 'test.ts',
811
- line: 14,
812
- endLine: 23,
813
- ruleId: 'performance/nested-loops',
814
- message: 'Inefficient nested loops with O(n²) complexity',
815
- severity: 'warning',
816
- category: 'performance',
817
- suggestion: 'Consider using more efficient algorithms or caching mechanisms',
818
- },
819
- {
820
- file: 'test.ts',
821
- line: 28,
822
- ruleId: 'style/inconsistent-naming',
823
- message: 'Inconsistent variable naming and formatting',
824
- severity: 'info',
825
- category: 'style',
826
- suggestion: 'Use consistent camelCase naming and proper spacing',
827
- },
828
- ],
829
- summary: {
830
- totalIssues: 3,
831
- criticalIssues: 1,
832
- },
833
- }),
834
- };
835
- return JSON.stringify(mockResponse);
836
- }
837
- /**
838
- * Get the API key source for debugging (without revealing the key)
839
- */
840
- getApiKeySource() {
841
- if (process.env.GOOGLE_API_KEY && this.config.provider === 'google') {
842
- return 'GOOGLE_API_KEY';
843
- }
844
- if (process.env.ANTHROPIC_API_KEY && this.config.provider === 'anthropic') {
845
- return 'ANTHROPIC_API_KEY';
846
- }
847
- if (process.env.OPENAI_API_KEY && this.config.provider === 'openai') {
848
- return 'OPENAI_API_KEY';
849
- }
850
- return 'unknown';
851
- }
852
- }
853
- exports.AIReviewService = AIReviewService;
854
- //# sourceMappingURL=ai-review-service.js.map