PRIX / lib /index.js
Rachit-Tw's picture
feat: implement autonomous Auto-PR and optimize Docker image
5cb7295
Raw
History Blame Contribute Delete
26.7 kB
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const options_1 = require("./src/options");
const prompts_1 = require("./src/prompts");
const review_1 = require("./src/review");
const review_comment_1 = require("./src/review-comment");
const context_1 = require("./src/context");
const octokit_1 = require("./src/octokit");
const checkout_1 = require("./src/checkout");
const promises_1 = require("fs/promises");
const path_1 = require("path");
const os_1 = require("os");
const feedback_capture_1 = require("./src/feedback-capture");
const feedback_tracker_1 = require("./src/feedback-tracker");
const lru_cache_1 = require("lru-cache");
const pino_1 = __importDefault(require("pino"));
const zod_1 = require("zod");
const prom_client_1 = __importDefault(require("prom-client"));
const bot_1 = require("./src/bot");
const options_2 = require("./src/options");
const issue_handler_1 = require("./src/issue-handler");
// Environment variable validation schema
const envSchema = zod_1.z.object({
GROQ_API_KEY: zod_1.z.string().min(1, 'GROQ_API_KEY is required'),
AI_API_KEY: zod_1.z.string().optional(),
DEBUG: zod_1.z.enum(['true', 'false']).default('false'),
DISABLE_REVIEW: zod_1.z.enum(['true', 'false']).default('false'),
MAX_FILES: zod_1.z.coerce.number().min(0).max(1000).default(0),
REVIEW_SIMPLE_CHANGES: zod_1.z.enum(['true', 'false']).default('false'),
REVIEW_COMMENT_LGTM: zod_1.z.enum(['true', 'false']).default('false'),
PATH_FILTERS: zod_1.z.string().optional(),
SYSTEM_MESSAGE: zod_1.z.string().default(''),
LIGHT_MODEL: zod_1.z.string().default('meta-llama/llama-4-scout-17b-16e-instruct'),
HEAVY_MODEL: zod_1.z.string().default('llama-3.3-70b-versatile'),
MODEL_TEMPERATURE: zod_1.z.coerce.number().min(0).max(2).default(0),
RETRIES: zod_1.z.coerce.number().min(0).max(10).default(3),
TIMEOUT_MS: zod_1.z.coerce.number().min(1000).max(300000).default(120000),
CONCURRENCY_LIMIT: zod_1.z.coerce.number().min(1).max(10).default(1),
GITHUB_CONCURRENCY_LIMIT: zod_1.z.coerce.number().min(1).max(10).default(1),
API_BASE_URL: zod_1.z.string().url().default('https://api.groq.com/openai/v1'),
LANGUAGE: zod_1.z.string().default('en-US'),
MINIMUM_SEVERITY: zod_1.z.enum(['critical', 'major', 'minor', 'info']).default('minor'),
MIN_CONFIDENCE: zod_1.z.coerce.number().min(0).max(100).default(0),
DENSE_MODE: zod_1.z.enum(['true', 'false']).default('false'),
SHOW_IMPACT_ANALYSIS: zod_1.z.enum(['true', 'false']).default('true'),
LOG_LEVEL: zod_1.z.enum(['trace', 'debug', 'info', 'warn', 'error', 'fatal']).default('info')
});
// Validate env vars on startup
try {
envSchema.parse(process.env);
}
catch (error) {
console.error('Invalid environment variables:', error);
process.exit(1);
}
const logger = (0, pino_1.default)({ level: process.env.LOG_LEVEL || 'info' });
// Webhook rate limiter: max 30 requests per repo per minute
const webhookRateLimiter = new lru_cache_1.LRUCache({
max: 1000, // Track 1000 repos
ttl: 60000, // 1 minute window
updateAgeOnGet: false
});
function checkRateLimit(repo) {
const current = webhookRateLimiter.get(repo) || 0;
if (current >= 30) {
logger.warn({ repo, count: current }, 'Webhook rate limit exceeded');
return false;
}
webhookRateLimiter.set(repo, current + 1);
return true;
}
/**
* PRIX (AI PR Reviewer and Auditor)
* Probot Service Entry Point
*/
exports.default = (app, { getRouter }) => {
app.log.info('PRIX module loading...');
// 0. Health Check for Hugging Face
if (getRouter) {
const router = getRouter();
router.get('/', (_req, res) => {
res.status(200).send(`
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PRIX | AI Code Auditor</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600&display=swap');
:root {
--bg: #09090b;
--primary: #6366f1;
--secondary: #a855f7;
--accent: #ec4899;
--card: rgba(24, 24, 27, 0.8);
--border: rgba(63, 63, 70, 0.5);
--text: #fafafa;
}
body {
margin: 0;
padding: 0;
background-color: var(--bg);
color: var(--text);
font-family: 'Outfit', sans-serif;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
/* Animated background glow */
.glow {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 500px;
height: 500px;
background: radial-gradient(circle, var(--primary) 0%, transparent 70%);
opacity: 0.15;
filter: blur(80px);
z-index: 0;
animation: pulse 8s infinite alternate;
}
@keyframes pulse {
0% { opacity: 0.1; transform: translate(-50%, -50%) scale(1); }
100% { opacity: 0.2; transform: translate(-50%, -50%) scale(1.2); }
}
.container {
position: relative;
z-index: 10;
padding: 3rem;
background: var(--card);
backdrop-filter: blur(12px);
border: 1px solid var(--border);
border-radius: 24px;
text-align: center;
max-width: 500px;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
}
.logo-icon {
width: 80px;
height: 80px;
background: linear-gradient(135deg, var(--primary), var(--secondary));
margin: 0 auto 1.5rem;
border-radius: 20px;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 0 30px rgba(99, 102, 241, 0.4);
}
.logo-icon svg {
width: 45px;
height: 45px;
fill: white;
}
h1 {
font-size: 2.5rem;
margin: 0;
background: linear-gradient(to right, #fff, #94a3b8);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
letter-spacing: -0.02em;
}
p {
color: #94a3b8;
font-size: 1.125rem;
margin: 1rem 0 2rem;
}
.status-badge {
display: inline-flex;
align-items: center;
gap: 0.5rem;
background: rgba(16, 185, 129, 0.1);
color: #10b981;
padding: 0.5rem 1rem;
border-radius: 100px;
font-weight: 600;
font-size: 0.875rem;
border: 1px solid rgba(16, 185, 129, 0.2);
}
.status-dot {
width: 8px;
height: 8px;
background: #10b981;
border-radius: 50%;
box-shadow: 0 0 10px #10b981;
animation: blink 2s infinite;
}
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
</style>
</head>
<body>
<div class="glow"></div>
<div class="container">
<div class="logo-icon">
<svg viewBox="0 0 24 24"><path d="M12,1L3,5V11C3,16.55 6.84,21.74 12,23C17.16,21.74 21,16.55 21,11V5L12,1M12,7C13.4,7 14.8,7.3 16,7.9V11C16,14.7 14.2,18.1 11.5,20.3L12,20.6L12.5,20.3C9.8,18.1 8,14.7 8,11V7.9C9.2,7.3 10.6,7 12,7Z"/></svg>
</div>
<h1>PRIX AUDITOR</h1>
<p>Enterprise-grade AI Code Security & Symbol Engine is humming.</p>
<div class="status-badge">
<div class="status-dot"></div>
SERVICE ACTIVE
</div>
</div>
</body>
</html>
`);
});
router.get('/ping', (_req, res) => {
res.status(200).send('pong');
});
// Deep health check with external service validation
router.get('/health', async (_req, res) => {
const checks = {};
// 1. Check GitHub App configuration
try {
const appId = process.env.APP_ID;
checks.github = { ok: !!appId };
}
catch (error) {
checks.github = { ok: false, error: error.message };
}
// 2. Check AI API connectivity
try {
const apiKey = process.env.GROQ_API_KEY || process.env.AI_API_KEY;
if (!apiKey) {
checks.ai = { ok: false, error: 'No API key configured' };
}
else {
// Simple check - verify key exists (don't make actual API call to avoid costs)
checks.ai = { ok: true };
}
}
catch (error) {
checks.ai = { ok: false, error: error.message };
}
// 3. Check disk space (basic)
try {
const tempDir = (0, os_1.tmpdir)();
checks.disk = { ok: !!tempDir };
}
catch (error) {
checks.disk = { ok: false, error: error.message };
}
// 4. Check memory usage
const memUsage = process.memoryUsage();
checks.memory = {
ok: memUsage.heapUsed < 1024 * 1024 * 1024, // Less than 1GB
};
const allOk = Object.values(checks).every(c => c.ok);
res.status(allOk ? 200 : 503).json({
status: allOk ? 'healthy' : 'unhealthy',
checks,
timestamp: new Date().toISOString(),
version: process.env.npm_package_version || '1.1.0'
});
});
// Prometheus metrics endpoint
const register = new prom_client_1.default.Registry();
prom_client_1.default.collectDefaultMetrics({ register });
// Custom metrics
const prReviewCounter = new prom_client_1.default.Counter({
name: 'prix_pr_reviews_total',
help: 'Total number of PRs reviewed',
labelNames: ['status'],
registers: [register]
});
const reviewDurationHistogram = new prom_client_1.default.Histogram({
name: 'prix_review_duration_seconds',
help: 'Time spent reviewing PRs',
buckets: [10, 30, 60, 120, 300, 600],
registers: [register]
});
router.get('/metrics', async (_req, res) => {
try {
res.set('Content-Type', register.contentType);
res.end(await register.metrics());
}
catch (ex) {
res.status(500).end(ex.message);
}
});
}
app.log.info('PRIX Service Started');
function parseSeverityLevel(value) {
const validLevels = [
'critical',
'major',
'minor',
'info'
];
if (value && validLevels.includes(value)) {
return value;
}
return 'minor';
}
// Helper to initialize options from environment variables
const getOptions = () => {
return new options_1.Options(process.env.DEBUG === 'true', process.env.DISABLE_REVIEW === 'true', process.env.DISABLE_RELEASE_NOTES === 'true', process.env.MAX_FILES || '0', process.env.REVIEW_SIMPLE_CHANGES === 'true', process.env.REVIEW_COMMENT_LGTM === 'true', process.env.PATH_FILTERS ? process.env.PATH_FILTERS.split(',') : null, process.env.SYSTEM_MESSAGE || '', process.env.LIGHT_MODEL || 'meta-llama/llama-4-scout-17b-16e-instruct', process.env.HEAVY_MODEL || 'llama-3.3-70b-versatile', process.env.MODEL_TEMPERATURE || '0.0', process.env.RETRIES || '3', process.env.TIMEOUT_MS || '120000', process.env.CONCURRENCY_LIMIT || '1', process.env.GITHUB_CONCURRENCY_LIMIT || '1', process.env.API_BASE_URL || 'https://api.groq.com/openai/v1', process.env.LANGUAGE || 'en-US', true, parseSeverityLevel(process.env.MINIMUM_SEVERITY), process.env.MIN_CONFIDENCE || '0', process.env.DENSE_MODE || 'false', process.env.SHOW_IMPACT_ANALYSIS || 'true', process.env.ENABLE_AUTO_PR === 'true');
};
// 1. Event Listeners for PR Audit
app.on([
'pull_request.opened',
'pull_request.synchronize',
'pull_request.reopened'
], async (context) => {
const { payload } = context;
// Skip audits for PRs opened by bots to prevent recursive loops
const user = payload.pull_request.user;
if (user?.type === 'Bot') {
app.log.info(`Skipping audit for PR #${payload.pull_request.number} because it was opened by a bot (${user.login})`);
return;
}
// Rate limiting: max 30 webhooks per repo per minute
const repoKey = `${context.repo().owner}/${context.repo().repo}`;
if (!checkRateLimit(repoKey)) {
logger.warn({ repo: repoKey, pr: payload.pull_request.number }, 'Rate limit exceeded, skipping audit');
return;
}
app.log.info(`Received ${context.name}.${payload.action} for PR #${payload.pull_request.number}`);
try {
const options = getOptions();
const prompts = new prompts_1.Prompts();
// Create a unique temporary directory for this audit in the system temp folder
const tempDir = (0, path_1.join)((0, os_1.tmpdir)(), `prix_tmp_${payload.pull_request.id}_${Date.now()}`);
// Create a safe, isolated execution context
await context_1.als.run({
probotContext: context,
octokit: context.octokit,
repo: context.repo(),
workingDir: tempDir
}, async () => {
try {
// Clone the repo locally so Git commands work
await (0, checkout_1.checkoutRepo)(tempDir);
// Initialize shims
(0, octokit_1.setOctokit)(context.octokit);
// Trigger the main PRIX Audit
await (0, review_1.run)(context, options, prompts);
}
finally {
// Cleanup temp directory (async non-blocking)
try {
await (0, promises_1.rm)(tempDir, { recursive: true, force: true });
logger.debug({ tempDir }, 'Temp directory cleaned up');
}
catch (cleanupErr) {
logger.error({ tempDir, error: cleanupErr }, 'Failed to cleanup temp directory');
}
}
});
app.log.info(`Successfully processed audit for PR #${payload.pull_request.number}`);
}
catch (err) {
app.log.error(`Failed to process audit for PR #${payload.pull_request.number}: ${err.message}`);
// Log specific reason without crashing as per requirements
}
});
// 1b. Event Listener for Autonomous PR (Push to default branch)
app.on('push', async (context) => {
const { payload } = context;
// Check if push is to default branch
const defaultBranch = payload.repository.default_branch;
const refBranch = payload.ref.replace('refs/heads/', '');
if (refBranch !== defaultBranch) {
app.log.info(`Skipping autonomous audit on non-default branch: ${refBranch}`);
return;
}
app.log.info(`Received push event on default branch ${defaultBranch}, triggering autonomous audit...`);
try {
const options = getOptions();
if (!options.enableAutoPR) {
app.log.info('Auto PR is disabled, skipping autonomous audit.');
return;
}
const prompts = new prompts_1.Prompts();
const tempDir = (0, path_1.join)((0, os_1.tmpdir)(), `prix_auto_pr_${payload.after}_${Date.now()}`);
await context_1.als.run({
probotContext: context,
octokit: context.octokit,
repo: context.repo(),
workingDir: tempDir
}, async () => {
try {
await (0, checkout_1.checkoutRepo)(tempDir, { ref: payload.after });
(0, octokit_1.setOctokit)(context.octokit);
// Assuming autonomousAudit takes (context, options, prompts)
const { autonomousAudit } = await Promise.resolve().then(() => __importStar(require('./src/review')));
await autonomousAudit(context, options, prompts);
}
finally {
try {
await (0, promises_1.rm)(tempDir, { recursive: true, force: true });
}
catch (e) {
logger.error({ error: e }, 'Failed to cleanup temp directory');
}
}
});
app.log.info('Successfully processed autonomous audit.');
}
catch (err) {
app.log.error(`Failed to process autonomous audit: ${err.message}`);
}
});
// 2. Event Listener for "Chat with Bot" (Reply to comments) and Reaction Feedback
// Handles both chat replies AND reaction capture for PR comments
app.on('pull_request_review_comment.created', async (context) => {
const { payload } = context;
app.log.info(`Received ${context.name}.${payload.action} for comment ${payload.comment.id}`);
const isBotComment = payload.comment?.user?.type === 'Bot';
try {
// 2a. Chat with Bot handler (skip bot's own comments)
if (!isBotComment) {
const options = getOptions();
const prompts = new prompts_1.Prompts();
await context_1.als.run({
probotContext: context,
octokit: context.octokit,
repo: context.repo()
}, async () => {
(0, octokit_1.setOctokit)(context.octokit);
await (0, review_comment_1.run)(context, options, prompts);
});
app.log.info(`Successfully processed chat reply for comment ${payload.comment.id}`);
}
// 2b. Reaction Feedback handler (captures 👍/❤️ as acceptance, 👀/-1 as rejection)
if (!isBotComment) {
await context_1.als.run({
probotContext: context,
octokit: context.octokit,
repo: context.repo()
}, async () => {
(0, octokit_1.setOctokit)(context.octokit);
const pullNumber = payload.pull_request?.number;
if (pullNumber) {
await feedback_capture_1.feedbackCapture.checkReactionFeedback(pullNumber);
app.log.info(`Processed reaction feedback for PR #${pullNumber}`);
}
});
}
}
catch (err) {
app.log.error(`Failed to process pull_request_review_comment: ${err.message}`);
}
});
// 4. Event Listener for PR Review Submission
// Tracks when a review is submitted to monitor suggestion acceptance
app.on('pull_request_review.submitted', async (context) => {
const { payload } = context;
app.log.info(`Received review submitted for PR #${payload.pull_request?.number}, review ID: ${payload.review?.id}`);
try {
await context_1.als.run({
probotContext: context,
octokit: context.octokit,
repo: context.repo()
}, async () => {
(0, octokit_1.setOctokit)(context.octokit);
const pullNumber = payload.pull_request?.number;
const reviewId = payload.review?.id;
const commitId = payload.review?.commit_id;
if (pullNumber && reviewId && commitId) {
await feedback_capture_1.feedbackCapture.trackReviewSubmission(pullNumber, reviewId, commitId);
app.log.info(`Tracked review submission: PR #${pullNumber}`);
}
});
}
catch (err) {
app.log.error(`Failed to track review submission: ${err.message}`);
}
});
// 5. Event Listener for Reaction on Issues (for standalone issue comments)
app.on('issue_comment.created', async (context) => {
const { payload } = context;
// Only process user comments (not bot comments)
if (payload.comment?.user?.type === 'Bot') {
return;
}
try {
await context_1.als.run({
probotContext: context,
octokit: context.octokit,
repo: context.repo()
}, async () => {
(0, octokit_1.setOctokit)(context.octokit);
// Check for feedback reactions on issue comments
const { data: reactions } = await context.octokit.rest.reactions.listForIssueComment({
owner: context.repo().owner,
repo: context.repo().repo,
// eslint-disable-next-line camelcase
comment_id: payload.comment.id
});
for (const reaction of reactions) {
if (reaction.content === '+1' || reaction.content === 'heart') {
feedback_tracker_1.feedbackTracker.recordFeedback({
patternType: 'general',
severity: 'info',
filenamePattern: 'issue',
repoSlug: context.repo().repo,
teamId: context.repo().owner,
confidence: 50,
feedbackType: 'accepted',
commentId: String(payload.comment.id)
});
}
else if (reaction.content === '-1' ||
reaction.content === 'eyes') {
feedback_tracker_1.feedbackTracker.recordFeedback({
patternType: 'general',
severity: 'info',
filenamePattern: 'issue',
repoSlug: context.repo().repo,
teamId: context.repo().owner,
confidence: 50,
feedbackType: 'rejected',
commentId: String(payload.comment.id)
});
}
}
});
}
catch (err) {
app.log.error(`Failed to process issue reaction feedback: ${err.message}`);
}
});
// 6. Event Listener for @prix mention in Issue Comments
// When someone mentions @prix in an issue comment, analyze the issue and generate a fix plan
app.on('issue_comment.created', async (context) => {
const { payload } = context;
const comment = payload.comment;
if (!comment || comment.user?.type === 'Bot') {
return;
}
if (!comment.body.includes('@prix')) {
return;
}
app.log.info(`Received @prix mention in issue #${payload.issue?.number} from @${comment.user?.login}`);
try {
const options = getOptions();
const prompts = new prompts_1.Prompts();
const heavyAIOptions = new options_2.AIOptions(options.heavyModel, options.heavyTokenLimits);
const heavyBot = new bot_1.Bot(options, heavyAIOptions);
await context_1.als.run({
probotContext: context,
octokit: context.octokit,
repo: context.repo()
}, async () => {
(0, octokit_1.setOctokit)(context.octokit);
await (0, issue_handler_1.handleIssueComment)(heavyBot, options, prompts);
});
app.log.info(`Successfully processed @prix mention in issue #${payload.issue?.number}`);
}
catch (err) {
app.log.error(`Failed to process @prix mention in issue #${payload.issue?.number}: ${err.message}`);
}
});
};
//# sourceMappingURL=index.js.map