Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesPull RequestsActionsSecurityInsightsSettings
✨ AI
More
Blame · Line-by-line history

ai-review.ts

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

ai-review.tsBlame210 lines · 1 contributor
e883329Claude1/**
2 * AI-powered code review using Claude.
3 *
4 * Generates inline review comments on pull request diffs.
5 * Reviews are posted as PR comments with isAiReview=true.
6 */
7
8import Anthropic from "@anthropic-ai/sdk";
9import { config } from "./config";
eb3b81aClaude10import { getRepoPath } from "../git/repository";
e883329Claude11
12interface ReviewComment {
13 filePath: string;
14 lineNumber: number | null;
15 body: string;
16}
17
18interface ReviewResult {
19 summary: string;
20 comments: ReviewComment[];
21 approved: boolean;
22}
23
24let _client: Anthropic | null = null;
25
26function getClient(): Anthropic {
27 if (!_client) {
28 if (!config.anthropicApiKey) {
29 throw new Error("ANTHROPIC_API_KEY is not set");
30 }
31 _client = new Anthropic({ apiKey: config.anthropicApiKey });
32 }
33 return _client;
34}
35
36/**
37 * Run AI code review on a PR diff.
38 */
39export async function reviewDiff(
40 repoFullName: string,
41 prTitle: string,
42 prBody: string | null,
43 baseBranch: string,
44 headBranch: string,
45 diffText: string
46): Promise<ReviewResult> {
eb3b81aClaude47 if (!isAiReviewEnabled()) {
48 return { summary: "AI review unavailable: ANTHROPIC_API_KEY not configured.", comments: [], approved: true };
49 }
e883329Claude50 const client = getClient();
51
eb3b81aClaude52 let text = "";
53 try {
54 const message = await client.messages.create({
55 model: "claude-sonnet-4-20250514",
56 max_tokens: 4096,
57 messages: [
58 {
59 role: "user",
60 content: `You are reviewing a pull request on the repository "${repoFullName}".
e883329Claude61
62**PR Title:** ${prTitle}
63**PR Description:** ${prBody || "(none)"}
64**Base branch:** ${baseBranch}
65**Head branch:** ${headBranch}
66
67Review the following diff. Look for:
68- Bugs, logic errors, or potential runtime failures
69- Security vulnerabilities (injection, XSS, auth bypasses, secrets in code)
70- Performance issues (N+1 queries, unnecessary allocations, blocking I/O)
71- Missing error handling at system boundaries
72- Breaking changes or API contract violations
73
74Do NOT comment on style, formatting, naming, missing docs, or minor nitpicks. Only flag issues that could cause real problems.
75
76Respond in JSON format:
77{
78 "summary": "1-3 sentence overall assessment",
79 "approved": true/false,
80 "comments": [
81 {
82 "filePath": "path/to/file.ts",
83 "lineNumber": 42,
84 "body": "Explain the issue and suggest a fix"
85 }
86 ]
87}
88
89If the diff looks clean, return approved: true with an empty comments array.
90
91\`\`\`diff
92${diffText.slice(0, 100000)}
93\`\`\``,
eb3b81aClaude94 },
95 ],
96 });
97 text = message.content[0].type === "text" ? message.content[0].text : "";
98 } catch (err) {
99 console.error("[ai-review] reviewDiff API call failed:", err);
100 return { summary: "AI review failed due to an API error.", comments: [], approved: true };
101 }
e883329Claude102
103 try {
104 // Extract JSON from response (may be wrapped in markdown code block)
105 const jsonMatch = text.match(/\{[\s\S]*\}/);
106 if (!jsonMatch) {
107 return {
108 summary: "AI review completed but could not parse structured output.",
109 comments: [],
110 approved: true,
111 };
112 }
113 const parsed = JSON.parse(jsonMatch[0]);
114 return {
115 summary: parsed.summary || "Review complete.",
116 comments: Array.isArray(parsed.comments) ? parsed.comments : [],
117 approved: parsed.approved !== false,
118 };
119 } catch {
120 return {
121 summary: text.slice(0, 500),
122 comments: [],
123 approved: true,
124 };
125 }
126}
127
128/**
129 * Check if AI review is available (API key configured).
130 */
131export function isAiReviewEnabled(): boolean {
132 return !!config.anthropicApiKey;
133}
0316dbbClaude134
135/**
eb3b81aClaude136 * Fire-and-forget AI review trigger. Gets the branch diff, runs Claude review,
137 * and posts the results as pr_comments with isAiReview=true.
0316dbbClaude138 */
139export async function triggerAiReview(
140 ownerName: string,
141 repoName: string,
eb3b81aClaude142 prId: string,
143 title: string,
144 body: string,
145 baseBranch: string,
146 headBranch: string,
0316dbbClaude147): Promise<void> {
148 if (!isAiReviewEnabled()) return;
eb3b81aClaude149 try {
150 // Get branch diff
151 const repoDir = getRepoPath(ownerName, repoName);
152 const proc = Bun.spawn(
153 ["git", "diff", `${baseBranch}...${headBranch}`],
154 { cwd: repoDir, stdout: "pipe", stderr: "pipe" }
155 );
156 const diffRaw = await new Response(proc.stdout).text();
157 await proc.exited;
158
159 if (!diffRaw.trim()) return;
160
161 const result = await reviewDiff(
162 `${ownerName}/${repoName}`,
163 title,
164 body,
165 baseBranch,
166 headBranch,
167 diffRaw
168 );
169
170 // Load DB + schema lazily to avoid circular imports at module load time
171 const { db } = await import("../db");
172 const { pullRequests, prComments } = await import("../db/schema");
173 const { eq } = await import("drizzle-orm");
174
175 const [pr] = await db
176 .select({ repositoryId: pullRequests.repositoryId, authorId: pullRequests.authorId })
177 .from(pullRequests)
178 .where(eq(pullRequests.id, prId))
179 .limit(1);
180
181 if (!pr) return;
182
183 // Post summary comment
184 await db.insert(prComments).values({
185 pullRequestId: prId,
186 authorId: pr.authorId,
187 body: `**AI Code Review** ${result.approved ? "✓ Approved" : "⚠ Changes requested"}\n\n${result.summary}`,
188 isAiReview: true,
189 });
190
191 // Post inline comments
192 for (const comment of result.comments) {
193 if (!comment.body) continue;
194 await db.insert(prComments).values({
195 pullRequestId: prId,
196 authorId: pr.authorId,
197 body: comment.body,
198 isAiReview: true,
199 filePath: comment.filePath || null,
200 lineNumber: comment.lineNumber || null,
201 });
202 }
203
204 if (process.env.DEBUG_AI_REVIEW === "1") {
205 console.log("[ai-review] posted", 1 + result.comments.length, "comments on", ownerName, repoName, prId);
206 }
207 } catch (err) {
208 console.error("[ai-review] triggerAiReview failed:", err);
0316dbbClaude209 }
210}