coderev-cli 1.0.0

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/.env.example ADDED
@@ -0,0 +1,14 @@
1
+ # coderev Configuration
2
+ #
3
+ # Copy this file and rename to .env
4
+ # Then fill in your API keys.
5
+ #
6
+ # How to get keys:
7
+ # - DeepSeek: https://platform.deepseek.com/api_keys
8
+ # - GitHub: https://github.com/settings/tokens (fine-grained token with repo permissions)
9
+
10
+ # Required
11
+ DEEPSEEK_API_KEY=sk-your-deepseek-api-key
12
+
13
+ # GitHub (for --post and private repos)
14
+ GITHUB_TOKEN=github_pat_your_token
package/README.md ADDED
@@ -0,0 +1,188 @@
1
+ # coderev — AI Code Review Agent 🚀
2
+
3
+ **coderev** 是一个 AI 驱动的一站式代码审查工具。采用**多智能体并行审查架构**——3 个专业 Agent 同时从安全、Bug、质量维度审查代码,每个 Issue 附带置信度评分,自动过滤误报。支持 GitHub PR 集成、自动修复、git hooks、缓存、自定义规则、多语言专项审查、统计看板。
4
+
5
+ ## 🧠 架构:多智能体并行审查 (v0.3.0)
6
+
7
+ ```
8
+ 你的代码 (git diff)
9
+
10
+ ┌──────┼──────┐
11
+ ▼ ▼ ▼
12
+ ┌──────┐┌──────┐┌──────┐
13
+ │ 🔒 ││ 🐛 ││ 📐 │
14
+ │Security││ Bug ││Quality│
15
+ │Auditor││Detector││Check │
16
+ └──┬───┘└──┬───┘└──┬───┘
17
+ │ │ │
18
+ └───────┼───────┘
19
+
20
+ ┌──────────┐
21
+ │ 合并 & │
22
+ │ 置信度评分 │
23
+ │ (0-100) │
24
+ └────┬─────┘
25
+
26
+ 结构化审查报告
27
+ ```
28
+
29
+ **3 个专业 Agent 并行工作**,从不同角度审查同一份代码:
30
+
31
+ | Agent | 专注领域 |
32
+ |-------|---------|
33
+ | 🔒 Security Auditor | SQL注入、XSS、SSRF、硬编码密钥、认证缺陷 |
34
+ | 🐛 Bug Detector | 空指针、竞态条件、异步问题、逻辑错误 |
35
+ | 📐 Code Quality | 代码复杂度、DRY、命名规范、异常处理 |
36
+
37
+ 每个 issue 都会计算**置信度评分 (0-100)**,低于阈值(默认 60)的自动过滤。同一问题被多个 Agent 发现时自动去重。
38
+
39
+ ## ⚡ 快速上手
40
+
41
+ ```bash
42
+ # 1. 安装
43
+ npm install -g @lishihao2749/coderev
44
+
45
+ # 2. 初始化项目配置
46
+ coderev init
47
+
48
+ # 3. 设置 API Key(二选一)
49
+ export DEEPSEEK_API_KEY="sk-your-key-here" # DeepSeek
50
+ export OPENAI_API_KEY="sk-your-key-here" # OpenAI
51
+
52
+ # 4. 审查当前仓库暂存区
53
+ coderev review
54
+
55
+ # 5. 或审查两个分支间的差异
56
+ coderev review --repo . --base main --head feature
57
+
58
+ # 6. 或使用管道
59
+ git diff main | coderev review
60
+ ```
61
+
62
+ ## 🎯 CLI 选项详解
63
+
64
+ ### 审查命令
65
+
66
+ ```bash
67
+ # 多智能体并行审查(默认,推荐)
68
+ coderev review
69
+
70
+ # 提高置信度阈值(更少但更可靠的结果)
71
+ coderev review --min-confidence 80
72
+
73
+ # 降低阈值(更多结果,含一些误报)
74
+ coderev review --min-confidence 40
75
+
76
+ # 单 Agent 模式(v0.2.x 传统模式,消费更低)
77
+ coderev review --single
78
+
79
+ # 安全审计模式(注入 OWASP 级审查)
80
+ coderev review --audit
81
+
82
+ # 跳过缓存强刷
83
+ coderev review --no-cache
84
+
85
+ # 输出 JSON
86
+ coderev review --format json
87
+ ```
88
+
89
+ ### 其他命令
90
+
91
+ ```bash
92
+ coderev fix --file changes.diff # 自动修复
93
+ coderev fix --file changes.diff --apply # 生成并应用补丁
94
+ coderev hook install # 安装 git hook
95
+ coderev hook install pre-commit --min-score 70
96
+ coderev stats # 统计看板
97
+ coderev stats week # 本周统计
98
+ coderev cache status # 缓存状态
99
+ coderev cache clear # 清空缓存
100
+ coderev config show # 查看配置
101
+ ```
102
+
103
+ ## 🔗 GitHub PR 审查
104
+
105
+ ```bash
106
+ coderev review --pr owner/repo#42 # 审查 PR
107
+ coderev review --pr 42 # 自动检测当前仓库
108
+ coderev review --pr owner/repo#42 --post # 审查 + 贴评论
109
+ coderev review --pr owner/repo#42 --inline # 行内评论
110
+ coderev review --pr owner/repo#42 --format json # JSON 输出
111
+ ```
112
+
113
+ `--inline` 模式将每条 issue 贴在 PR 的具体代码行上,像人工 review 一样直观。
114
+
115
+ ## 📝 项目上下文(.coderevhint)
116
+
117
+ 在项目根目录创建 `.coderevhint` 文件,描述项目概况、架构和规范。AI 审查时会自动加载并据此调整审查重点。
118
+
119
+ coderev 也兼容 `CLAUDE.md` 文件,与 Claude Code 项目规范互通。
120
+
121
+ ## 🌐 多语言专项规则
122
+
123
+ coderev 自动检测 diff 中的编程语言,为不同语言添加专项检查规则:
124
+
125
+ | 语言 | 检查重点 |
126
+ |---|---|
127
+ | JavaScript | async/await 链、== vs ===、内存泄漏、import 循环依赖 |
128
+ | TypeScript | strict 模式、avoid any、泛型、类型断言 |
129
+ | Python | PEP 8、except 类型、mutable 默认参数、async 用法 |
130
+ | Rust | unsafe 审计、unwrap/expect、生命周期、ownership |
131
+ | Go | error handling、goroutine 安全、context 传播、data race |
132
+ | Java | null 处理、checked exception、== vs .equals()、线程安全 |
133
+ | SQL | 注入防护、N+1 查询、索引缺失、大 IN-clause |
134
+
135
+ ## 🗂 配置管理
136
+
137
+ ```json
138
+ {
139
+ "ai": {
140
+ "provider": "deepseek",
141
+ "model": "deepseek-chat",
142
+ "temperature": 0.3
143
+ },
144
+ "rules": {
145
+ "maxLineLength": 100,
146
+ "predefined": ["security", "performance", "style"],
147
+ "custom": [
148
+ {
149
+ "name": "no-console-log",
150
+ "severity": "warning",
151
+ "message": "避免在生产代码中使用 console.log"
152
+ }
153
+ ]
154
+ }
155
+ }
156
+ ```
157
+
158
+ 团队共享:将 `.coderevrc.json` 放入仓库根目录,全组自动读取。
159
+
160
+ ## 🔄 GitHub Actions 自动审查
161
+
162
+ 在工作流中使用 `coderev-review.yml`,PR 创建时自动审查并贴评论。详见 `.github/workflows/`。
163
+
164
+ ## 📁 项目结构
165
+
166
+ ```
167
+ coderev/
168
+ ├── src/
169
+ │ ├── cli.js # CLI 入口(review/fix/hook/stats 等子命令)
170
+ │ ├── reviewer.js # AI 审查核心(多智能体并行 + 置信度评分)
171
+ │ ├── config.js # 配置加载
172
+ │ ├── github.js # GitHub API
173
+ │ ├── gitlab.js # GitLab API
174
+ │ ├── gitee.js # Gitee API
175
+ │ ├── bitbucket.js # Bitbucket API
176
+ │ ├── cache.js # 缓存系统
177
+ │ ├── rules.js # 规则引擎(8 套预定义 + 7 种语言 + 自定义)
178
+ │ ├── stats.js # 统计看板
179
+ │ └── coderev.test.js # 20 个单元测试
180
+ ├── .github/workflows/ # GitHub Actions
181
+ ├── .coderevrc.json # 配置模板
182
+ ├── .coderevignore # 忽略规则
183
+ ├── .coderevhint # 项目上下文
184
+ └── ROADMAP.md # 路线图
185
+
186
+ ## License
187
+
188
+ MIT
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "coderev-cli",
3
+ "version": "1.0.0",
4
+ "description": "Multi-agent AI code review for git -- parallel agents with confidence scoring",
5
+ "main": "src/index.js",
6
+ "bin": {
7
+ "coderev": "src/cli.js"
8
+ },
9
+ "scripts": {
10
+ "start": "node src/cli.js",
11
+ "test": "node --test src/**/*.test.js",
12
+ "prepublishOnly": "node --test src/**/*.test.js"
13
+ },
14
+ "keywords": [
15
+ "code-review",
16
+ "ai",
17
+ "cli",
18
+ "github",
19
+ "pr-review",
20
+ "deepseek"
21
+ ],
22
+ "author": "Annie-Bot <2457643059@qq.com>",
23
+ "license": "MIT",
24
+ "type": "commonjs",
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/jishuanjimingtian/coderev.git"
28
+ },
29
+ "bugs": {
30
+ "url": "https://github.com/jishuanjimingtian/coderev/issues"
31
+ },
32
+ "homepage": "https://github.com/jishuanjimingtian/coderev",
33
+ "engines": {
34
+ "node": ">=18"
35
+ },
36
+ "files": [
37
+ "src/",
38
+ "README.md",
39
+ ".env.example"
40
+ ],
41
+ "dependencies": {
42
+ "chalk": "^4.1.2",
43
+ "commander": "^12.0.0",
44
+ "diff": "^5.2.0",
45
+ "openai": "^4.0.0"
46
+ }
47
+ }
@@ -0,0 +1,130 @@
1
+ const https = require('https');
2
+
3
+ /**
4
+ * Parse a Bitbucket PR reference.
5
+ * Supported formats:
6
+ * - owner/repo#42
7
+ * - https://bitbucket.org/owner/repo/pull-requests/42
8
+ * - 42 (requires --repo with git remote)
9
+ */
10
+ function parsePrRef(ref) {
11
+ const shorthand = ref.match(/^([\w.-]+)\/([\w.-]+)#(\d+)$/);
12
+ if (shorthand) {
13
+ return { owner: shorthand[1], repo: shorthand[2], pr: parseInt(shorthand[3], 10), workspace: shorthand[1] };
14
+ }
15
+
16
+ const fullUrl = ref.match(/bitbucket\.org\/([\w.-]+)\/([\w.-]+)\/pull-requests\/(\d+)/);
17
+ if (fullUrl) {
18
+ return { workspace: fullUrl[1], owner: fullUrl[1], repo: fullUrl[2], pr: parseInt(fullUrl[3], 10) };
19
+ }
20
+
21
+ const justNumber = ref.match(/^(\d+)$/);
22
+ if (justNumber) return { owner: null, repo: null, pr: parseInt(justNumber[1], 10) };
23
+
24
+ throw new Error(
25
+ `Invalid Bitbucket PR reference: "${ref}". Use:\n` +
26
+ ` coderev review --bb owner/repo#42\n` +
27
+ ` coderev review --bb https://bitbucket.org/owner/repo/pull-requests/42`
28
+ );
29
+ }
30
+
31
+ function resolvePrRef(ref, repoPath) {
32
+ const parsed = parsePrRef(ref);
33
+ if (parsed.owner && parsed.repo) return parsed;
34
+
35
+ try {
36
+ const { execSync } = require('child_process');
37
+ const remote = execSync('git config --get remote.origin.url', {
38
+ cwd: repoPath || process.cwd(), encoding: 'utf-8', timeout: 5000,
39
+ }).trim();
40
+ const match = remote.match(/bitbucket\.org[\/:]([\w.-]+)\/([\w.-]+?)(?:\.git)?$/);
41
+ if (match) return { workspace: match[1], owner: match[1], repo: match[2], pr: parsed.pr };
42
+ } catch {}
43
+ throw new Error(`Could not detect Bitbucket repo. Use full format like owner/repo#${parsed.pr}.`);
44
+ }
45
+
46
+ /**
47
+ * Fetch a pull request diff from Bitbucket Cloud.
48
+ * Bitbucket API 2.0: GET /2.0/repositories/{workspace}/{repo}/pullrequests/{number}
49
+ */
50
+ function fetchPrDiff(ref, token) {
51
+ const { workspace, repo, pr } = ref;
52
+ return new Promise((resolve, reject) => {
53
+ const username = process.env.BITBUCKET_USERNAME || '';
54
+ const headers = { 'User-Agent': 'coderev-agent', 'Accept': 'application/json' };
55
+
56
+ let auth = '';
57
+ if (token && username) {
58
+ const encoded = Buffer.from(username + ':' + token).toString('base64');
59
+ headers['Authorization'] = 'Basic ' + encoded;
60
+ }
61
+
62
+ https.get({
63
+ hostname: 'api.bitbucket.org',
64
+ path: `/2.0/repositories/${encodeURIComponent(workspace)}/${encodeURIComponent(repo)}/pullrequests/${pr}`,
65
+ headers,
66
+ }, (res) => {
67
+ let body = '';
68
+ res.on('data', (c) => (body += c));
69
+ res.on('end', () => {
70
+ if (res.statusCode === 200) {
71
+ try {
72
+ const prData = JSON.parse(body);
73
+ // Bitbucket doesn't give diff directly, get it from links
74
+ const diffUrl = prData.links?.diff?.href;
75
+ if (diffUrl) {
76
+ https.get(diffUrl, { headers }, (res2) => {
77
+ let d = ''; res2.on('data', (c) => (d += c));
78
+ res2.on('end', () => resolve(d));
79
+ }).on('error', reject);
80
+ } else {
81
+ reject(new Error('Could not find diff URL in Bitbucket response'));
82
+ }
83
+ } catch { reject(new Error('Failed to parse Bitbucket response')); }
84
+ } else if (res.statusCode === 404) {
85
+ reject(new Error(`PR not found: ${workspace}/${repo}#${pr}`));
86
+ } else {
87
+ reject(new Error(`Bitbucket API error (${res.statusCode}): ${body.slice(0, 200)}`));
88
+ }
89
+ });
90
+ }).on('error', reject);
91
+ });
92
+ }
93
+
94
+ /**
95
+ * Post a comment on a Bitbucket PR.
96
+ * API: POST /2.0/repositories/{workspace}/{repo}/pullrequests/{number}/comments
97
+ */
98
+ function postPrComment(ref, body, token) {
99
+ const { workspace, repo, pr } = ref;
100
+ return new Promise((resolve, reject) => {
101
+ const username = process.env.BITBUCKET_USERNAME || '';
102
+ const postData = JSON.stringify({ content: { raw: body } });
103
+ const headers = {
104
+ 'User-Agent': 'coderev-agent',
105
+ 'Content-Type': 'application/json',
106
+ 'Content-Length': Buffer.byteLength(postData),
107
+ };
108
+ if (token && username) {
109
+ headers['Authorization'] = 'Basic ' + Buffer.from(username + ':' + token).toString('base64');
110
+ }
111
+
112
+ const options = {
113
+ hostname: 'api.bitbucket.org',
114
+ path: `/2.0/repositories/${encodeURIComponent(workspace)}/${encodeURIComponent(repo)}/pullrequests/${pr}/comments`,
115
+ method: 'POST',
116
+ headers,
117
+ };
118
+
119
+ const req = https.request(options, (res) => {
120
+ let rb = ''; res.on('data', (c) => (rb += c));
121
+ res.on('end', () => {
122
+ if (res.statusCode === 201) { try { resolve(JSON.parse(rb)); } catch { resolve(rb); } }
123
+ else reject(new Error(`Failed to post comment (${res.statusCode}): ${rb.slice(0, 200)}`));
124
+ });
125
+ });
126
+ req.on('error', reject); req.write(postData); req.end();
127
+ });
128
+ }
129
+
130
+ module.exports = { parsePrRef, resolvePrRef, fetchPrDiff, postPrComment };
package/src/cache.js ADDED
@@ -0,0 +1,71 @@
1
+ const crypto = require('crypto');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+
5
+ const CACHE_DIR = path.join(require('os').homedir(), '.coderev', 'cache');
6
+
7
+ /**
8
+ * Generate a cache key from a diff string.
9
+ */
10
+ function cacheKey(diff) {
11
+ return crypto.createHash('sha256').update(diff).digest('hex');
12
+ }
13
+
14
+ /**
15
+ * Get cached review result if available and not expired.
16
+ * @param {string} key - Cache key (diff hash)
17
+ * @param {number} ttlMs - Time-to-live in ms (default 24h)
18
+ * @returns {object|null} Cached result or null
19
+ */
20
+ function getCached(key, ttlMs = 24 * 60 * 60 * 1000) {
21
+ const cachePath = path.join(CACHE_DIR, `${key}.json`);
22
+ try {
23
+ if (!fs.existsSync(cachePath)) return null;
24
+ const stat = fs.statSync(cachePath);
25
+ if (Date.now() - stat.mtimeMs > ttlMs) {
26
+ // Expired
27
+ fs.unlinkSync(cachePath);
28
+ return null;
29
+ }
30
+ return JSON.parse(fs.readFileSync(cachePath, 'utf-8'));
31
+ } catch {
32
+ return null;
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Store a review result in cache.
38
+ */
39
+ function setCached(key, result) {
40
+ try {
41
+ if (!fs.existsSync(CACHE_DIR)) {
42
+ fs.mkdirSync(CACHE_DIR, { recursive: true });
43
+ }
44
+ fs.writeFileSync(path.join(CACHE_DIR, `${key}.json`), JSON.stringify(result), 'utf-8');
45
+ } catch {
46
+ // Cache write failure is non-fatal
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Clear expired cache entries.
52
+ */
53
+ function cleanCache(ttlMs = 24 * 60 * 60 * 1000) {
54
+ try {
55
+ if (!fs.existsSync(CACHE_DIR)) return 0;
56
+ let cleared = 0;
57
+ for (const file of fs.readdirSync(CACHE_DIR)) {
58
+ const fullPath = path.join(CACHE_DIR, file);
59
+ const stat = fs.statSync(fullPath);
60
+ if (Date.now() - stat.mtimeMs > ttlMs) {
61
+ fs.unlinkSync(fullPath);
62
+ cleared++;
63
+ }
64
+ }
65
+ return cleared;
66
+ } catch {
67
+ return 0;
68
+ }
69
+ }
70
+
71
+ module.exports = { cacheKey, getCached, setCached, cleanCache };