mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-15 22:21:20 +00:00
* fix(teams): propagate CLAUDE_CONFIG_DIR to tmux session for agent teammates
When CCS launches Claude with CLAUDE_CONFIG_DIR pointing to the isolated
instance path, agent team teammates spawned via tmux split-window don't
inherit it because tmux creates new panes from its own session environment.
This causes teammates to use ~/.claude/ instead of ~/.ccs/instances/{profile}/,
creating a split-brain in shared state (tasks, teams, mailbox).
Sets key CCS env vars in the tmux session environment via `tmux setenv` so
all new panes inherit the correct config directory.
* refactor(teams): use spawnSync array args instead of execSync string
Avoids shell interpretation of env values by passing args as array
to spawnSync instead of interpolating into a shell command string.
* chore(release): 7.38.0-dev.1 [skip ci]
* feat(cliproxy): add hybrid catalog sync with CLIProxyAPI (#485)
* chore(release): 7.38.0 [skip ci]
## [7.38.0](https://github.com/kaitranntt/ccs/compare/v7.37.1...v7.38.0) (2026-02-07)
### Features
* **release:** v7.38.0 - Extended Context, Qwen Models, Bug Fixes ([#480](https://github.com/kaitranntt/ccs/issues/480)) ([b454834](https://github.com/kaitranntt/ccs/commit/b4548341750804339c87c48259dd8741425d2acf)), closes [#472](https://github.com/kaitranntt/ccs/issues/472) [#474](https://github.com/kaitranntt/ccs/issues/474) [#103](https://github.com/kaitranntt/ccs/issues/103) [#478](https://github.com/kaitranntt/ccs/issues/478) [#482](https://github.com/kaitranntt/ccs/issues/482)
* feat(cliproxy): add hybrid catalog sync with CLIProxyAPI
Add `ccs cliproxy catalog` command family for syncing model catalogs
from CLIProxyAPI's model-definitions endpoint. Cached locally with
24h TTL, merges remote models with static catalog preserving
broken/deprecated flags. Dashboard API endpoint at /api/cliproxy/catalog.
Closes #477
---------
Co-authored-by: semantic-release-bot <semantic-release-bot@martynus.net>
* chore(reviewer): upgrade review model from opus 4.5 to opus 4.6
- update default model in code-reviewer.ts
- update REVIEW_MODEL, ANTHROPIC_MODEL, ANTHROPIC_DEFAULT_OPUS_MODEL
in ai-review.yml workflow
- aligns reviewer with AGY model catalog default
* chore(release): 7.38.0-dev.2 [skip ci]
* fix(hooks): add image analysis env vars for settings-based profiles (#484)
* chore(release): 7.38.0 [skip ci]
## [7.38.0](https://github.com/kaitranntt/ccs/compare/v7.37.1...v7.38.0) (2026-02-07)
### Features
* **release:** v7.38.0 - Extended Context, Qwen Models, Bug Fixes ([#480](https://github.com/kaitranntt/ccs/issues/480)) ([b454834](https://github.com/kaitranntt/ccs/commit/b4548341750804339c87c48259dd8741425d2acf)), closes [#472](https://github.com/kaitranntt/ccs/issues/472) [#474](https://github.com/kaitranntt/ccs/issues/474) [#103](https://github.com/kaitranntt/ccs/issues/103) [#478](https://github.com/kaitranntt/ccs/issues/478) [#482](https://github.com/kaitranntt/ccs/issues/482)
* fix(hooks): add image analysis env vars for settings-based profiles
Image analysis hook was injected into settings files for API profiles
but the CCS_IMAGE_ANALYSIS_* env vars were never set, causing the hook
to skip. Add getImageAnalysisHookEnv() call to both settings-based and
GLMT proxy execution paths, matching CLIProxy profile behavior.
Closes #440
* fix(hooks): add hook env vars to copilot executor
Copilot profile had same gap as settings-based profiles: hooks installed
but webSearch/imageAnalysis env vars and CCS_PROFILE_TYPE never set.
Add missing env var injection matching other profile flows.
---------
Co-authored-by: semantic-release-bot <semantic-release-bot@martynus.net>
* chore(release): 7.38.0-dev.3 [skip ci]
---------
Co-authored-by: Carlos Umanzor <cumanzor@gmail.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: semantic-release-bot <semantic-release-bot@martynus.net>
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 || 'gemini-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();
|