@kaitranntt/ccs 7.15.0-dev.1 → 7.15.0-dev.3
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/package.json +1 -1
- package/scripts/code-reviewer.ts +61 -152
package/package.json
CHANGED
package/scripts/code-reviewer.ts
CHANGED
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* AI Code Reviewer for CCS CLI
|
|
4
4
|
*
|
|
5
|
-
* Fetches PR diff, calls Claude via CLIProxyAPI, posts review
|
|
5
|
+
* Fetches PR diff, calls Claude via CLIProxyAPI, posts review as comment.
|
|
6
6
|
* Runs on self-hosted runner with localhost access to CLIProxyAPI:8317.
|
|
7
|
+
* Posts as ccs-agy-reviewer[bot] via GitHub App token.
|
|
7
8
|
*
|
|
8
9
|
* Usage: bun run scripts/code-reviewer.ts <PR_NUMBER>
|
|
9
|
-
* Env: CLIPROXY_API_KEY, GITHUB_REPOSITORY
|
|
10
|
+
* Env: CLIPROXY_API_KEY, GITHUB_REPOSITORY, GH_TOKEN
|
|
10
11
|
*/
|
|
11
12
|
|
|
12
13
|
import { $ } from 'bun';
|
|
@@ -22,64 +23,52 @@ interface PRContext {
|
|
|
22
23
|
diff: string;
|
|
23
24
|
}
|
|
24
25
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
severity: 'critical' | 'warning' | 'suggestion' | 'nitpick';
|
|
30
|
-
}
|
|
26
|
+
// Config
|
|
27
|
+
const MAX_DIFF_LINES = 10000;
|
|
28
|
+
const CLIPROXY_URL = process.env.CLIPROXY_URL || 'http://localhost:8317';
|
|
29
|
+
const MODEL = process.env.REVIEW_MODEL || 'gemini-claude-opus-4-5-thinking';
|
|
31
30
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
approvalStatus: 'APPROVE' | 'REQUEST_CHANGES' | 'COMMENT';
|
|
35
|
-
comments: ReviewComment[];
|
|
36
|
-
securityIssues: string[];
|
|
37
|
-
suggestions: string[];
|
|
38
|
-
}
|
|
31
|
+
// System prompt for code review - new style
|
|
32
|
+
const CODE_REVIEWER_SYSTEM_PROMPT = `You are the CCS AGY Code Reviewer, an expert AI assistant reviewing pull requests for the CCS CLI project.
|
|
39
33
|
|
|
40
|
-
|
|
41
|
-
|
|
34
|
+
## Review Guidelines
|
|
35
|
+
- Focus ONLY on changes in this PR - don't suggest unrelated improvements
|
|
36
|
+
- Be concise - no fluff, no excessive praise
|
|
37
|
+
- Provide specific file:line references for issues
|
|
38
|
+
- Verify claims before making them (check if patterns exist, check actual code)
|
|
39
|
+
- Avoid over-engineering suggestions for simple fixes
|
|
42
40
|
|
|
43
|
-
##
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
5. **CCS Conventions** - ASCII only (no emojis), conventional commits
|
|
41
|
+
## Check For
|
|
42
|
+
1. **Bugs**: Logic errors, edge cases, null handling, race conditions
|
|
43
|
+
2. **Security**: Injection, auth bypass, secrets exposure, data leaks
|
|
44
|
+
3. **Performance**: N+1 queries, missing indexes, inefficient algorithms
|
|
45
|
+
4. **TypeScript**: Proper typing, no \`any\`, null safety
|
|
46
|
+
5. **Consistency**: Similar patterns exist elsewhere that need same fix?
|
|
50
47
|
|
|
51
48
|
## Output Format
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
\`\`\`json
|
|
55
|
-
{
|
|
56
|
-
"summary": "2-3 sentence overall assessment",
|
|
57
|
-
"approvalStatus": "APPROVE" | "REQUEST_CHANGES" | "COMMENT",
|
|
58
|
-
"comments": [
|
|
59
|
-
{
|
|
60
|
-
"path": "relative/path/to/file.ts",
|
|
61
|
-
"line": 42,
|
|
62
|
-
"body": "Specific feedback for this line",
|
|
63
|
-
"severity": "critical" | "warning" | "suggestion" | "nitpick"
|
|
64
|
-
}
|
|
65
|
-
],
|
|
66
|
-
"securityIssues": ["List of security concerns if any"],
|
|
67
|
-
"suggestions": ["General improvement suggestions"]
|
|
68
|
-
}
|
|
69
|
-
\`\`\`
|
|
49
|
+
Structure your response EXACTLY like this (no code fences, render as markdown):
|
|
70
50
|
|
|
71
|
-
##
|
|
72
|
-
- Be constructive, not harsh
|
|
73
|
-
- Prioritize critical issues over nitpicks
|
|
74
|
-
- Reference specific lines and provide fix suggestions
|
|
75
|
-
- If no issues: approvalStatus = "APPROVE", comments = []
|
|
76
|
-
- Max 10 inline comments (focus on most important)`;
|
|
51
|
+
## 🔍 Code Review
|
|
77
52
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
53
|
+
**Verdict**: [✅ Approve | ✅ Approve with suggestions | ⚠️ Request changes]
|
|
54
|
+
|
|
55
|
+
### Summary
|
|
56
|
+
[1-2 sentences on what the PR does and if it's correct]
|
|
57
|
+
|
|
58
|
+
### ✅ What's Good
|
|
59
|
+
- [Bullet points, 2-4 items max]
|
|
60
|
+
|
|
61
|
+
### ⚠️ Issues Found
|
|
62
|
+
| File:Line | Issue | Severity |
|
|
63
|
+
|-----------|-------|----------|
|
|
64
|
+
| \`file.ts:123\` | Description | 🔴 High / 🟡 Medium / 🟢 Low |
|
|
65
|
+
|
|
66
|
+
(If no issues, write "None - LGTM")
|
|
67
|
+
|
|
68
|
+
### 💡 Suggestions (Optional)
|
|
69
|
+
- [Only if truly valuable, max 2 items]
|
|
70
|
+
|
|
71
|
+
IMPORTANT: Output ONLY the markdown review. No JSON, no code blocks wrapping the review.`;
|
|
83
72
|
|
|
84
73
|
// Fetch PR context
|
|
85
74
|
async function getPRContext(prNumber: number, repo: string): Promise<PRContext> {
|
|
@@ -111,11 +100,14 @@ async function getPRContext(prNumber: number, repo: string): Promise<PRContext>
|
|
|
111
100
|
}
|
|
112
101
|
|
|
113
102
|
// Call Claude via CLIProxyAPI
|
|
114
|
-
async function callClaude(context: PRContext): Promise<
|
|
103
|
+
async function callClaude(context: PRContext, repo: string): Promise<string> {
|
|
115
104
|
const apiKey = process.env.CLIPROXY_API_KEY;
|
|
116
105
|
if (!apiKey) throw new Error('CLIPROXY_API_KEY not set');
|
|
117
106
|
|
|
118
|
-
const userMessage =
|
|
107
|
+
const userMessage = `REPO: ${repo}
|
|
108
|
+
PR NUMBER: ${context.number}
|
|
109
|
+
|
|
110
|
+
## Pull Request: ${context.title}
|
|
119
111
|
|
|
120
112
|
### Description
|
|
121
113
|
${context.body || '(No description provided)'}
|
|
@@ -128,7 +120,7 @@ ${context.files.map((f) => `- ${f.path} (+${f.additions}/-${f.deletions})`).join
|
|
|
128
120
|
${context.diff}
|
|
129
121
|
\`\`\`
|
|
130
122
|
|
|
131
|
-
|
|
123
|
+
Review this PR following the guidelines. Refer to the project's CLAUDE.md and docs/ folder for conventions.`;
|
|
132
124
|
|
|
133
125
|
const response = await fetch(`${CLIPROXY_URL}/v1/messages`, {
|
|
134
126
|
method: 'POST',
|
|
@@ -139,7 +131,7 @@ Please review this pull request and provide your assessment in the specified JSO
|
|
|
139
131
|
},
|
|
140
132
|
body: JSON.stringify({
|
|
141
133
|
model: MODEL,
|
|
142
|
-
max_tokens:
|
|
134
|
+
max_tokens: 4096,
|
|
143
135
|
system: CODE_REVIEWER_SYSTEM_PROMPT,
|
|
144
136
|
messages: [{ role: 'user', content: userMessage }],
|
|
145
137
|
}),
|
|
@@ -157,103 +149,21 @@ Please review this pull request and provide your assessment in the specified JSO
|
|
|
157
149
|
throw new Error('Empty response from Claude');
|
|
158
150
|
}
|
|
159
151
|
|
|
160
|
-
|
|
161
|
-
const jsonMatch = content.match(/```json\n?([\s\S]*?)\n?```/);
|
|
162
|
-
const jsonStr = jsonMatch ? jsonMatch[1].trim() : content.trim();
|
|
163
|
-
|
|
164
|
-
try {
|
|
165
|
-
return JSON.parse(jsonStr) as ReviewResult;
|
|
166
|
-
} catch {
|
|
167
|
-
// Fallback: create basic review from raw text
|
|
168
|
-
console.warn('[!] Failed to parse JSON, using fallback');
|
|
169
|
-
return {
|
|
170
|
-
summary: content.slice(0, 500),
|
|
171
|
-
approvalStatus: 'COMMENT',
|
|
172
|
-
comments: [],
|
|
173
|
-
securityIssues: [],
|
|
174
|
-
suggestions: [],
|
|
175
|
-
};
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// Format inline comment with severity badge
|
|
180
|
-
function formatComment(comment: ReviewComment): string {
|
|
181
|
-
const badge: Record<string, string> = {
|
|
182
|
-
critical: '[!] **Critical**',
|
|
183
|
-
warning: '[~] **Warning**',
|
|
184
|
-
suggestion: '[i] **Suggestion**',
|
|
185
|
-
nitpick: '[ ] Nitpick',
|
|
186
|
-
};
|
|
187
|
-
return `${badge[comment.severity] || '[i]'}\n\n${comment.body}`;
|
|
152
|
+
return content;
|
|
188
153
|
}
|
|
189
154
|
|
|
190
|
-
// Post review
|
|
191
|
-
async function postReview(prNumber: number, repo: string,
|
|
192
|
-
//
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
if (review.securityIssues.length > 0) {
|
|
196
|
-
body += `\n### Security Issues\n${review.securityIssues.map((i) => `- ${i}`).join('\n')}\n`;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
if (review.suggestions.length > 0) {
|
|
200
|
-
body += `\n### Suggestions\n${review.suggestions.map((s) => `- ${s}`).join('\n')}\n`;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
if (review.comments.length > 0) {
|
|
204
|
-
body += `\n### Inline Comments\n${review.comments.length} comment(s) posted on specific lines.\n`;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
body += '\n---\n*Automated review by CCS AGY Code Reviewer*';
|
|
208
|
-
|
|
209
|
-
// Map approval status to gh flag
|
|
210
|
-
const eventFlag: Record<string, string> = {
|
|
211
|
-
APPROVE: '--approve',
|
|
212
|
-
REQUEST_CHANGES: '--request-changes',
|
|
213
|
-
COMMENT: '--comment',
|
|
214
|
-
};
|
|
215
|
-
|
|
216
|
-
// Post main review (try APPROVE/REQUEST_CHANGES, fallback to COMMENT if self-PR)
|
|
217
|
-
const flag = eventFlag[review.approvalStatus] || '--comment';
|
|
218
|
-
const reviewResult = await $`gh pr review ${prNumber} --repo ${repo} ${flag} --body ${body}`.nothrow();
|
|
219
|
-
|
|
220
|
-
if (reviewResult.exitCode !== 0) {
|
|
221
|
-
const stderr = reviewResult.stderr.toString();
|
|
222
|
-
// GitHub doesn't allow self-approval or self-request-changes - fallback to comment
|
|
223
|
-
if (stderr.includes('your own pull request')) {
|
|
224
|
-
console.log('[i] Self-PR detected, falling back to COMMENT');
|
|
225
|
-
await $`gh pr review ${prNumber} --repo ${repo} --comment --body ${body}`;
|
|
226
|
-
} else {
|
|
227
|
-
throw new Error(`Failed to post review: ${stderr}`);
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
// Post inline comments via REST API
|
|
232
|
-
for (const comment of review.comments.slice(0, MAX_INLINE_COMMENTS)) {
|
|
233
|
-
try {
|
|
234
|
-
const commentBody = formatComment(comment);
|
|
235
|
-
// Get the PR head SHA for the comment
|
|
236
|
-
const prInfo = await $`gh pr view ${prNumber} --repo ${repo} --json headRefOid --jq .headRefOid`.text();
|
|
237
|
-
const commitId = prInfo.trim();
|
|
238
|
-
|
|
239
|
-
await $`gh api repos/${repo}/pulls/${prNumber}/comments --method POST \
|
|
240
|
-
-f body=${commentBody} \
|
|
241
|
-
-f path=${comment.path} \
|
|
242
|
-
-F line=${comment.line} \
|
|
243
|
-
-f side=RIGHT \
|
|
244
|
-
-f commit_id=${commitId}`;
|
|
245
|
-
} catch (err) {
|
|
246
|
-
console.error(`[!] Failed to post inline comment on ${comment.path}:${comment.line}`, err);
|
|
247
|
-
}
|
|
248
|
-
}
|
|
155
|
+
// Post review as PR comment
|
|
156
|
+
async function postReview(prNumber: number, repo: string, reviewContent: string): Promise<void> {
|
|
157
|
+
// Use gh pr comment to post the review
|
|
158
|
+
await $`gh pr comment ${prNumber} --repo ${repo} --body ${reviewContent}`;
|
|
249
159
|
}
|
|
250
160
|
|
|
251
161
|
// Check if already reviewed this PR (avoid spam)
|
|
252
162
|
async function hasRecentReview(prNumber: number, repo: string): Promise<boolean> {
|
|
253
163
|
try {
|
|
254
|
-
const
|
|
255
|
-
await $`gh api repos/${repo}/
|
|
256
|
-
return parseInt(
|
|
164
|
+
const comments =
|
|
165
|
+
await $`gh api repos/${repo}/issues/${prNumber}/comments --jq '[.[] | select(.body | contains("🔍 Code Review"))] | length'`.text();
|
|
166
|
+
return parseInt(comments.trim(), 10) > 0;
|
|
257
167
|
} catch {
|
|
258
168
|
return false;
|
|
259
169
|
}
|
|
@@ -291,13 +201,12 @@ async function main() {
|
|
|
291
201
|
|
|
292
202
|
// 2. Call Claude
|
|
293
203
|
console.log(`[i] Calling Claude (${MODEL}) for review...`);
|
|
294
|
-
const
|
|
295
|
-
console.log(
|
|
296
|
-
console.log(`[i] Comments: ${review.comments.length}, Security issues: ${review.securityIssues.length}`);
|
|
204
|
+
const reviewContent = await callClaude(context, repo);
|
|
205
|
+
console.log('[i] Review generated');
|
|
297
206
|
|
|
298
|
-
// 3. Post review
|
|
207
|
+
// 3. Post review as comment
|
|
299
208
|
console.log('[i] Posting review to PR...');
|
|
300
|
-
await postReview(prNumber, repo,
|
|
209
|
+
await postReview(prNumber, repo, reviewContent);
|
|
301
210
|
console.log('[OK] Review posted successfully');
|
|
302
211
|
} catch (error) {
|
|
303
212
|
console.error('[X] Review failed:', error);
|