mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-15 20:20:09 +00:00
051805074e
* fix(cliproxy): migrate deprecated gemini-claude-* model names to upstream claude-* names (#515) * fix(cliproxy): migrate deprecated gemini-claude-* model names to upstream claude-* names CLIProxyAPI registry no longer recognizes the gemini-claude-* prefix convention. Model names in catalog, base config, and user settings are migrated to upstream claude-* names. Auto-migration in env-builder rewrites existing user settings on load and persists the change. Closes #513 * fix: address code review feedback — sync UI layer and add migration tests - Sync UI isNativeGeminiModel() with backend (remove gemini-claude- exclusion) - Update UI model catalog agy entries from gemini-claude-* to claude-* - Update CI/CD workflow and code-reviewer default model names - Add unit tests for migrateDeprecatedModelNames() logic * fix(hooks): isolate image type check before error-prone processing (#514) * fix(hooks): isolate image type check before error-prone processing Restructure processHook() into two phases so non-image Read calls never see hook error messages. Phase 1 defensively checks tool name and file extension, exiting 0 silently on any failure. Phase 2 only runs for confirmed image/PDF files where errors are relevant. Closes #511 * fix(hooks): sync image analyzer hook file on every profile launch Add installImageAnalyzerHook() call to cliproxy executor, matching the existing installWebSearchHook() pattern. This ensures the .cjs file in ~/.ccs/hooks/ gets refreshed from the npm package on every launch, so users receive hook updates after npm update. * chore(release): 7.41.0-dev.1 [skip ci] * fix(cliproxy): add fork:true for Claude model aliases in config generator (#523) Config generator now outputs fork:true for Claude model alias entries, ensuring both upstream (claude-*) and aliased (gemini-claude-*) model names appear in /v1/models listings. Also preserves fork flag when parsing user-added aliases during config regeneration. Bumps config version to v7 to trigger regeneration on next ccs doctor. Closes #522 * chore(release): 7.41.0-dev.2 [skip ci] * feat(cliproxy): add account safety guards to prevent Google account bans (#516) * feat(cliproxy): add account safety guards to prevent Google account bans Implements cross-provider isolation to prevent Google from flagging concurrent OAuth usage across different client IDs (ref: #509, #512). Three pillars: 1. Auto-pause enforcement at session launch — conflicting accounts in other Google OAuth providers are paused so CLIProxyAPI can't use them, restored on session exit with crash recovery via auto-paused.json 2. Ban/disable detection — error responses matching Google ban patterns auto-pause the affected account to prevent further damage 3. Cross-provider conflict warnings during OAuth registration Key design decisions: - PID-based session tracking for crash recovery (dead PID = restore) - Timestamp comparison prevents restoring ban-paused accounts on exit - Schema validation on auto-paused.json prevents corrupted state - Falls back to warn-only when another session is managing isolation * fix(cliproxy): address code review feedback (attempt 1/5) - Re-read auto-paused.json before write in enforceProviderIsolation to reduce concurrent write race window - Use actual email from registry for display instead of raw accountId - Export maskEmail for testability - Add 27 unit tests covering ban detection, email masking, cross-provider duplicate detection, enforcement lifecycle, crash recovery, and timestamp-guarded restore * fix(cliproxy): address remaining review feedback (attempt 2/5) - Add handleBanDetection test verifying account pause on ban error - Add warnCrossProviderDuplicates tests (true/false/non-Google) - Document PID reuse limitation in isPidAlive JSDoc comment * chore(release): 7.41.0-dev.3 [skip ci] * feat(cliproxy): runtime quota monitoring during active sessions (#529) * feat(cliproxy): add runtime quota monitoring during active sessions Adds adaptive background quota polling to detect and respond to quota exhaustion during active CLIProxy sessions. Prevents rate-limit-driven account bans by auto-cooling exhausted accounts and switching defaults. - Adaptive polling: 300s normal, 60s at 20% threshold, stops at 0% - Stderr warnings at 20%, boxed exhaustion alerts at 0% - Cooldown + default switch on exhaustion (existing patterns) - Configurable via quota_management.runtime_monitor in config.yaml - Timer.unref() prevents blocking process exit - monitorStopped guard for in-flight poll safety Closes #524 * fix: address code review feedback (attempt 1/5) - M1: Round quotaPercent display with Math.round() to avoid ugly floats - M2: Rename exhaust_threshold -> exhaustion_threshold for consistency with existing auto.exhaustion_threshold config field - M3: Replace async not.toThrow() with direct await assertion pattern * fix: address code review feedback (attempt 2/5) - Remove .claude/agent-memory/ from tracking and add to .gitignore - Unify cooldown_minutes default to 5 (was 10 in runtime_monitor, 5 in auto) - Add threshold validation in startQuotaMonitor (warn > exhaustion) - Document intentional post-switch monitoring gap in code comment * chore(release): 7.41.0-dev.4 [skip ci] * fix(cliproxy): mask email in ban detection and fix JSDoc default - Use maskEmail() in handleBanDetection output for consistency - Fix cooldown_minutes JSDoc: default is 5, not 10 * chore(release): 7.41.0-dev.5 [skip ci] * fix(cliproxy): address all review feedback (Low + informational) - Add sync constraint comment on process.exit handler (executor) - Add TOCTOU race acceptability comment (account-safety) - Mask email in handleQuotaExhaustion reason string - Use realistic exhaustion_threshold (5) in test configs * chore(release): 7.41.0-dev.6 [skip ci] --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
218 lines
6.6 KiB
TypeScript
Executable File
218 lines
6.6 KiB
TypeScript
Executable File
#!/usr/bin/env bun
|
|
/**
|
|
* AI Code Reviewer for CCS CLI
|
|
*
|
|
* Fetches PR diff, calls Claude via CLIProxyAPI, posts review as comment.
|
|
* Runs on self-hosted runner with localhost access to CLIProxyAPI:8317.
|
|
* Posts as ccs-agy-reviewer[bot] via GitHub App token.
|
|
*
|
|
* Usage: bun run scripts/code-reviewer.ts <PR_NUMBER>
|
|
* Env: CLIPROXY_API_KEY, GITHUB_REPOSITORY, GH_TOKEN
|
|
*/
|
|
|
|
import { $ } from 'bun';
|
|
|
|
// Types
|
|
interface PRContext {
|
|
number: number;
|
|
title: string;
|
|
body: string;
|
|
baseRef: string;
|
|
headRef: string;
|
|
files: Array<{ path: string; additions: number; deletions: number }>;
|
|
diff: string;
|
|
}
|
|
|
|
// Config
|
|
const MAX_DIFF_LINES = 10000;
|
|
const CLIPROXY_URL = process.env.CLIPROXY_URL || 'http://localhost:8317';
|
|
const MODEL = process.env.REVIEW_MODEL || 'claude-opus-4-6-thinking';
|
|
|
|
// System prompt for code review - new style
|
|
const CODE_REVIEWER_SYSTEM_PROMPT = `You are the CCS AGY Code Reviewer, an expert AI assistant reviewing pull requests for the CCS CLI project.
|
|
|
|
## Review Guidelines
|
|
- Focus ONLY on changes in this PR - don't suggest unrelated improvements
|
|
- Be concise - no fluff, no excessive praise
|
|
- Provide specific file:line references for issues
|
|
- Verify claims before making them (check if patterns exist, check actual code)
|
|
- Avoid over-engineering suggestions for simple fixes
|
|
|
|
## Check For
|
|
1. **Bugs**: Logic errors, edge cases, null handling, race conditions
|
|
2. **Security**: Injection, auth bypass, secrets exposure, data leaks
|
|
3. **Performance**: N+1 queries, missing indexes, inefficient algorithms
|
|
4. **TypeScript**: Proper typing, no \`any\`, null safety
|
|
5. **Consistency**: Similar patterns exist elsewhere that need same fix?
|
|
|
|
## Output Format
|
|
Structure your response EXACTLY like this (no code fences, render as markdown):
|
|
|
|
## 🔍 Code Review
|
|
|
|
**Verdict**: [✅ Approve | ✅ Approve with suggestions | ⚠️ Request changes]
|
|
|
|
### Summary
|
|
[1-2 sentences on what the PR does and if it's correct]
|
|
|
|
### ✅ What's Good
|
|
- [Bullet points, 2-4 items max]
|
|
|
|
### ⚠️ Issues Found
|
|
| File:Line | Issue | Severity |
|
|
|-----------|-------|----------|
|
|
| \`file.ts:123\` | Description | 🔴 High / 🟡 Medium / 🟢 Low |
|
|
|
|
(If no issues, write "None - LGTM")
|
|
|
|
### 💡 Suggestions (Optional)
|
|
- [Only if truly valuable, max 2 items]
|
|
|
|
IMPORTANT: Output ONLY the markdown review. No JSON, no code blocks wrapping the review.`;
|
|
|
|
// Fetch PR context
|
|
async function getPRContext(prNumber: number, repo: string): Promise<PRContext> {
|
|
$.throws(true);
|
|
|
|
// Get PR metadata
|
|
const prJson =
|
|
await $`gh pr view ${prNumber} --repo ${repo} --json number,title,body,baseRefName,headRefName,files`.text();
|
|
const pr = JSON.parse(prJson);
|
|
|
|
// Get diff
|
|
let diff = await $`gh pr diff ${prNumber} --repo ${repo}`.text();
|
|
|
|
// Truncate if too large
|
|
const lines = diff.split('\n');
|
|
if (lines.length > MAX_DIFF_LINES) {
|
|
diff = lines.slice(0, MAX_DIFF_LINES).join('\n') + '\n\n[DIFF TRUNCATED - exceeded 10k lines]';
|
|
}
|
|
|
|
return {
|
|
number: pr.number,
|
|
title: pr.title,
|
|
body: pr.body || '',
|
|
baseRef: pr.baseRefName,
|
|
headRef: pr.headRefName,
|
|
files: pr.files || [],
|
|
diff,
|
|
};
|
|
}
|
|
|
|
// Call Claude via CLIProxyAPI
|
|
async function callClaude(context: PRContext, repo: string): Promise<string> {
|
|
const apiKey = process.env.CLIPROXY_API_KEY;
|
|
if (!apiKey) throw new Error('CLIPROXY_API_KEY not set');
|
|
|
|
const userMessage = `REPO: ${repo}
|
|
PR NUMBER: ${context.number}
|
|
|
|
## Pull Request: ${context.title}
|
|
|
|
### Description
|
|
${context.body || '(No description provided)'}
|
|
|
|
### Changed Files
|
|
${context.files.map((f) => `- ${f.path} (+${f.additions}/-${f.deletions})`).join('\n')}
|
|
|
|
### Diff
|
|
\`\`\`diff
|
|
${context.diff}
|
|
\`\`\`
|
|
|
|
Review this PR following the guidelines. Refer to the project's CLAUDE.md and docs/ folder for conventions.`;
|
|
|
|
const response = await fetch(`${CLIPROXY_URL}/v1/messages`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'x-api-key': apiKey,
|
|
'anthropic-version': '2023-06-01',
|
|
'content-type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
model: MODEL,
|
|
max_tokens: 4096,
|
|
system: CODE_REVIEWER_SYSTEM_PROMPT,
|
|
messages: [{ role: 'user', content: userMessage }],
|
|
}),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const error = await response.text();
|
|
throw new Error(`CLIProxyAPI error: ${response.status} - ${error}`);
|
|
}
|
|
|
|
const data = (await response.json()) as { content: Array<{ text: string }> };
|
|
const content = data.content[0]?.text;
|
|
|
|
if (!content) {
|
|
throw new Error('Empty response from Claude');
|
|
}
|
|
|
|
return content;
|
|
}
|
|
|
|
// Post review as PR comment
|
|
async function postReview(prNumber: number, repo: string, reviewContent: string): Promise<void> {
|
|
// Use gh pr comment to post the review
|
|
await $`gh pr comment ${prNumber} --repo ${repo} --body ${reviewContent}`;
|
|
}
|
|
|
|
// Check if already reviewed this PR (avoid spam)
|
|
async function hasRecentReview(prNumber: number, repo: string): Promise<boolean> {
|
|
try {
|
|
const comments =
|
|
await $`gh api repos/${repo}/issues/${prNumber}/comments --jq '[.[] | select(.body | contains("🔍 Code Review"))] | length'`.text();
|
|
return parseInt(comments.trim(), 10) > 0;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Main
|
|
async function main() {
|
|
const prNumber = parseInt(process.argv[2], 10);
|
|
const repo = process.env.GITHUB_REPOSITORY || 'kaitranntt/ccs';
|
|
const forceReview = process.argv.includes('--force');
|
|
|
|
if (!prNumber || isNaN(prNumber)) {
|
|
console.error('Usage: bun run scripts/code-reviewer.ts <PR_NUMBER> [--force]');
|
|
process.exit(1);
|
|
}
|
|
|
|
console.log(`[i] Reviewing PR #${prNumber} in ${repo}`);
|
|
|
|
try {
|
|
// Check for existing review (avoid spam)
|
|
if (!forceReview && (await hasRecentReview(prNumber, repo))) {
|
|
console.log('[i] Already reviewed this PR. Use --force to review again.');
|
|
process.exit(0);
|
|
}
|
|
|
|
// 1. Get PR context
|
|
console.log('[i] Fetching PR context...');
|
|
const context = await getPRContext(prNumber, repo);
|
|
console.log(`[i] PR: "${context.title}" (${context.files.length} files changed)`);
|
|
|
|
const diffLines = context.diff.split('\n').length;
|
|
if (diffLines > MAX_DIFF_LINES) {
|
|
console.log(`[!] Diff too large (${diffLines} lines), truncated to ${MAX_DIFF_LINES}`);
|
|
}
|
|
|
|
// 2. Call Claude
|
|
console.log(`[i] Calling Claude (${MODEL}) for review...`);
|
|
const reviewContent = await callClaude(context, repo);
|
|
console.log('[i] Review generated');
|
|
|
|
// 3. Post review as comment
|
|
console.log('[i] Posting review to PR...');
|
|
await postReview(prNumber, repo, reviewContent);
|
|
console.log('[OK] Review posted successfully');
|
|
} catch (error) {
|
|
console.error('[X] Review failed:', error);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
main();
|