@raolin2025/claude-code-node 2.1.0 → 2.2.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/README.md +117 -0
- package/package.json +1 -1
- package/src/__tests__/git-tool.integration.test.js +89 -0
- package/src/__tests__/git-tool.test.js +144 -0
- package/src/git/github-api.js +360 -0
- package/src/git/index.js +37 -0
- package/src/git/llm-assistant.js +83 -0
- package/src/git/pr-merge-policy.js +367 -0
- package/src/git/pr-reviewer.js +533 -0
- package/src/git/utils/diff-parser.js +330 -0
- package/src/tools/git-tool.js +308 -0
- package/src/tools/index.js +2 -0
package/README.md
CHANGED
|
@@ -111,6 +111,7 @@ cc-node --resume session-1747000000000-abc123
|
|
|
111
111
|
| **Grep** | 内容搜索(rg/grep) | `always-allow` | — |
|
|
112
112
|
| **WebFetch** | 抓取网页内容 | `ask` | ✅ SSRF 防护 |
|
|
113
113
|
| **WebSearch** | 网页搜索 | `ask` | 需要 API Key |
|
|
114
|
+
| **GitTool** | GitHub PR 自动化(审查/合并/评论) | `high` | ✅ 预检查 |
|
|
114
115
|
| **AskUserQuestion** | 向用户提问 | `always-allow` | — |
|
|
115
116
|
|
|
116
117
|
[](https://www.npmjs.com/package/@raolin2025/claude-code-node) [](https://github.com/bg1avd/claude-code-node) [](https://opensource.org/licenses/MIT)
|
|
@@ -429,3 +430,119 @@ sudo systemctl start cc-notify
|
|
|
429
430
|
# 查看日志
|
|
430
431
|
journalctl -u cc-notify -f
|
|
431
432
|
```
|
|
433
|
+
|
|
434
|
+
## 🔧 GitTool - PR 自动化管理
|
|
435
|
+
|
|
436
|
+
GitTool 是 GitHub PR 自动化管理工具,支持查看、审查、合并 PR,以及批量操作和智能分析。
|
|
437
|
+
|
|
438
|
+
### 前置配置
|
|
439
|
+
|
|
440
|
+
```bash
|
|
441
|
+
# 1. 创建 GitHub Personal Access Token (classic, 有 repo 权限)
|
|
442
|
+
export GITHUB_TOKEN=ghp_xxx
|
|
443
|
+
|
|
444
|
+
# 2. 设置仓库信息
|
|
445
|
+
export GITHUB_OWNER=your_org
|
|
446
|
+
export GITHUB_REPO=your_repo
|
|
447
|
+
|
|
448
|
+
# 可选:启用 LLM 智能分析
|
|
449
|
+
export DEEPSEEK_API_KEY=sk-xxx
|
|
450
|
+
export DEEPSEEK_API_BASE=https://api.deepseek.com/v1
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
或在 `~/.claude-code/config.json` 中配置:
|
|
454
|
+
|
|
455
|
+
```json
|
|
456
|
+
{
|
|
457
|
+
"github": {
|
|
458
|
+
"owner": "your_org",
|
|
459
|
+
"repo": "your_repo"
|
|
460
|
+
},
|
|
461
|
+
"llm": {
|
|
462
|
+
"apiKey": "sk-xxx",
|
|
463
|
+
"apiBase": "https://api.deepseek.com/v1",
|
|
464
|
+
"model": "deepseek-chat"
|
|
465
|
+
},
|
|
466
|
+
"reviewRules": {
|
|
467
|
+
"checks": {
|
|
468
|
+
"codeQuality": true,
|
|
469
|
+
"security": true,
|
|
470
|
+
"tests": true,
|
|
471
|
+
"docs": true
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
### 使用示例
|
|
478
|
+
|
|
479
|
+
在 REPL 中调用 GitTool:
|
|
480
|
+
|
|
481
|
+
```
|
|
482
|
+
/GitTool list-prs
|
|
483
|
+
/GitTool review-pr 123 --auto-comment false
|
|
484
|
+
/GitTool check-mergeable 123
|
|
485
|
+
/GitTool approve 123 "Looks good"
|
|
486
|
+
/GitTool merge-pr 123 --method squash
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
命令行脚本(`test-git-tool.mjs`):
|
|
490
|
+
|
|
491
|
+
```bash
|
|
492
|
+
# 列出 PR
|
|
493
|
+
node test-git-tool.mjs list
|
|
494
|
+
|
|
495
|
+
# 审查 PR(规则检查,无 LLM)
|
|
496
|
+
node test-git-tool.mjs review 123
|
|
497
|
+
|
|
498
|
+
# 使用 LLM 智能分析
|
|
499
|
+
DEEPSEEK_API_KEY=sk-xxx node test-git-tool.mjs review-llm 123
|
|
500
|
+
|
|
501
|
+
# 检查可合并性
|
|
502
|
+
node test-git-tool.mjs check-mergeable 123
|
|
503
|
+
|
|
504
|
+
# Approve PR
|
|
505
|
+
node test-git-tool.mjs approve 123 "Approved"
|
|
506
|
+
|
|
507
|
+
# 提交审查意见
|
|
508
|
+
node test-git-tool.mjs comment 123 "Please add tests"
|
|
509
|
+
|
|
510
|
+
# 行级评论(自动计算 diff position)
|
|
511
|
+
node test-git-tool.mjs comment 123 "Fix needed" --path src/index.js --line 42
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
### 自动化工作流
|
|
515
|
+
|
|
516
|
+
- **每日自动审查**: 设置 cron 运行 `auto-review-all`
|
|
517
|
+
- **自动合并**: 为满足条件的 PR 添加 `auto-merge` 标签,运行 `auto-merge-eligible`
|
|
518
|
+
- **集成 OpenClaw Heartbeat**: 通过 `cron` 工具定期执行
|
|
519
|
+
|
|
520
|
+
```bash
|
|
521
|
+
# 每天 10:00 自动审查所有 PR
|
|
522
|
+
cron add "0 10 * * *" "session:git-automation" \
|
|
523
|
+
--payload '{"kind":"agentTurn","message":"/GitTool auto-review-all"}'
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
### 合并策略
|
|
527
|
+
|
|
528
|
+
PRMergePolicy 支持以下检查(可配置):
|
|
529
|
+
|
|
530
|
+
- ✅ 最少 Approvals 数量
|
|
531
|
+
- ✅ CI 状态全部通过
|
|
532
|
+
- ✅ 无 `changes_requested`
|
|
533
|
+
- ✅ 分支保护规则
|
|
534
|
+
- ✅ 自动合并标签(如 `auto-merge`)
|
|
535
|
+
- ✅ 合并冲突检测
|
|
536
|
+
|
|
537
|
+
### 安全与权限
|
|
538
|
+
|
|
539
|
+
- GitTool 工具注册为 `high` 权限级别(合并操作需要确认)
|
|
540
|
+
- 操作会被记录在审计日志中
|
|
541
|
+
- 禁止合并到受保护分支(main/master)
|
|
542
|
+
|
|
543
|
+
### 与 OpenClaw 集成
|
|
544
|
+
|
|
545
|
+
- GitTool 已内置到 cc-node 工具集中
|
|
546
|
+
- 可通过 `/tools` 查看
|
|
547
|
+
- 审查结果可与 QQ/Telegram 通道集成,发送通知
|
|
548
|
+
|
package/package.json
CHANGED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitTool 集成测试
|
|
3
|
+
* 测试工具在真实环境中的集成情况
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { test, describe, beforeEach } from 'node:test'
|
|
7
|
+
import assert from 'node:assert'
|
|
8
|
+
|
|
9
|
+
// 模拟 GitHub API 响应
|
|
10
|
+
function mockGitHubAPI() {
|
|
11
|
+
return {
|
|
12
|
+
token: 'mock-token',
|
|
13
|
+
owner: 'test-owner',
|
|
14
|
+
repo: 'test-repo',
|
|
15
|
+
baseUrl: 'https://api.github.com',
|
|
16
|
+
request: async (endpoint, options = {}) => {
|
|
17
|
+
if (endpoint.includes('/pulls')) {
|
|
18
|
+
return [{ number: 1, title: 'Test PR', state: 'open', user: { login: 'tester' } }]
|
|
19
|
+
}
|
|
20
|
+
if (endpoint.includes('/pulls/1')) {
|
|
21
|
+
return {
|
|
22
|
+
number: 1,
|
|
23
|
+
title: 'Test PR',
|
|
24
|
+
body: 'Test',
|
|
25
|
+
user: { login: 'tester' },
|
|
26
|
+
head: { ref: 'feature', sha: 'abc123' },
|
|
27
|
+
base: { ref: 'main' },
|
|
28
|
+
mergeable: true,
|
|
29
|
+
changed_files: 1,
|
|
30
|
+
additions: 1,
|
|
31
|
+
deletions: 0,
|
|
32
|
+
labels: []
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return {}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// 导入 GitTool 类
|
|
41
|
+
import { GitTool } from '../tools/git-tool.js'
|
|
42
|
+
|
|
43
|
+
describe('GitTool Integration', () => {
|
|
44
|
+
let tool
|
|
45
|
+
|
|
46
|
+
beforeEach(() => {
|
|
47
|
+
// 创建 GitTool 实例,使用 mock 配置
|
|
48
|
+
tool = new GitTool({
|
|
49
|
+
owner: 'test-owner',
|
|
50
|
+
repo: 'test-repo',
|
|
51
|
+
token: 'mock-token'
|
|
52
|
+
})
|
|
53
|
+
// 注入 mock 的 GitHub API
|
|
54
|
+
tool.github = mockGitHubAPI()
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test('should list PRs', async () => {
|
|
58
|
+
const result = await tool.execute({ action: 'list-prs', limit: 10 })
|
|
59
|
+
assert.ok(result.count >= 0)
|
|
60
|
+
assert.ok(Array.isArray(result.prs))
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
test('should get PR details', async () => {
|
|
64
|
+
const result = await tool.execute({ action: 'get-pr', prNumber: 1 })
|
|
65
|
+
assert.strictEqual(result.number, 1)
|
|
66
|
+
assert.ok(result.title.length > 0)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
test('should check mergeable', async () => {
|
|
70
|
+
const result = await tool.execute({ action: 'check-mergeable', prNumber: 1 })
|
|
71
|
+
assert.ok('mergeable' in result)
|
|
72
|
+
assert.ok('checks' in result)
|
|
73
|
+
assert.ok('violations' in result)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
test('should validate parameters before execution', async () => {
|
|
77
|
+
await assert.rejects(
|
|
78
|
+
tool.execute({ action: 'list-prs', state: 'invalid' }),
|
|
79
|
+
/Invalid enum value/
|
|
80
|
+
)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
test('should handle unknown action', async () => {
|
|
84
|
+
await assert.rejects(
|
|
85
|
+
tool.execute({ action: 'unknown-action' }),
|
|
86
|
+
/Unknown action/
|
|
87
|
+
)
|
|
88
|
+
})
|
|
89
|
+
})
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitTool 单元测试
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { test, describe } from 'node:test'
|
|
6
|
+
import assert from 'node:assert'
|
|
7
|
+
|
|
8
|
+
import { gitTool, createToolDef, GitTool } from '../tools/git-tool.js'
|
|
9
|
+
import { parseDiff, splitDiffByFile, getPositionInDiff } from '../git/utils/diff-parser.js'
|
|
10
|
+
|
|
11
|
+
// ============================================
|
|
12
|
+
// Diff Parser Tests
|
|
13
|
+
// ============================================
|
|
14
|
+
describe('Diff Parser', () => {
|
|
15
|
+
const sampleDiff = `diff --git a/src/index.js b/src/index.js
|
|
16
|
+
--- a/src/index.js
|
|
17
|
+
+++ b/src/index.js
|
|
18
|
+
@@ -1,5 +1,5 @@
|
|
19
|
+
function add(a, b) {
|
|
20
|
+
- return a + b
|
|
21
|
+
+ return a + b + 0
|
|
22
|
+
}
|
|
23
|
+
module.exports = { add }
|
|
24
|
+
`
|
|
25
|
+
|
|
26
|
+
test('parseDiff should parse hunk headers', () => {
|
|
27
|
+
const hunks = parseDiff(sampleDiff)
|
|
28
|
+
assert.strictEqual(hunks.length, 1)
|
|
29
|
+
assert.strictEqual(hunks[0].oldStart, 1)
|
|
30
|
+
assert.strictEqual(hunks[0].newStart, 1)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
test('splitDiffByFile should return file map with correct file', () => {
|
|
34
|
+
const fileMap = splitDiffByFile(sampleDiff)
|
|
35
|
+
assert.ok(fileMap.has('src/index.js'), 'Missing src/index.js in fileMap')
|
|
36
|
+
const hunks = fileMap.get('src/index.js')
|
|
37
|
+
assert.ok(Array.isArray(hunks))
|
|
38
|
+
assert.ok(hunks.length >= 1, `Expected at least 1 hunk, got ${hunks.length}`)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test('getPositionInDiff should calculate position for added line', () => {
|
|
42
|
+
const fileMap = splitDiffByFile(sampleDiff)
|
|
43
|
+
const hunks = fileMap.get('src/index.js')
|
|
44
|
+
|
|
45
|
+
// The line " return a + b + 0" is at new line 2
|
|
46
|
+
const position = getPositionInDiff(hunks, 2)
|
|
47
|
+
assert.ok(position !== null, 'Position should not be null for existing line')
|
|
48
|
+
assert.ok(position > 1, `Position ${position} should be > 1`)
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
// ============================================
|
|
53
|
+
// GitTool ToolDef Tests
|
|
54
|
+
// ============================================
|
|
55
|
+
describe('GitTool ToolDef', () => {
|
|
56
|
+
test('should have correct name', () => {
|
|
57
|
+
assert.strictEqual(gitTool.name, 'GitTool')
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
test('should have description', () => {
|
|
61
|
+
assert.ok(typeof gitTool.description === 'string' && gitTool.description.length > 10)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
test('should have parameters schema', () => {
|
|
65
|
+
const params = gitTool.parameters
|
|
66
|
+
assert.strictEqual(params.type, 'object')
|
|
67
|
+
assert.ok('action' in params.properties)
|
|
68
|
+
assert.ok('prNumber' in params.properties)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
test('should require action parameter', () => {
|
|
72
|
+
const required = gitTool.parameters.required
|
|
73
|
+
assert.ok(Array.isArray(required))
|
|
74
|
+
assert.ok(required.includes('action'))
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
test('should support all expected actions', () => {
|
|
78
|
+
const actions = gitTool.parameters.properties.action.enum
|
|
79
|
+
const expected = [
|
|
80
|
+
'list-prs', 'get-pr', 'review-pr', 'merge-pr',
|
|
81
|
+
'comment', 'approve', 'request-changes',
|
|
82
|
+
'check-mergeable', 'auto-review-all', 'auto-merge-eligible'
|
|
83
|
+
]
|
|
84
|
+
expected.forEach(a => assert.ok(actions.includes(a), `Missing action: ${a}`))
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
test('should have high permission level', () => {
|
|
88
|
+
assert.strictEqual(gitTool.permissionLevel, 'high')
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
test('should have handler function', () => {
|
|
92
|
+
assert.ok(typeof gitTool.handler === 'function')
|
|
93
|
+
})
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
// ============================================
|
|
97
|
+
// GitTool Factory Tests
|
|
98
|
+
// ============================================
|
|
99
|
+
describe('GitTool Factory', () => {
|
|
100
|
+
test('createToolDef should return ToolDef instance with handler', () => {
|
|
101
|
+
const toolDef = createToolDef({})
|
|
102
|
+
assert.ok(toolDef instanceof Object)
|
|
103
|
+
assert.ok(typeof toolDef.handler === 'function')
|
|
104
|
+
assert.strictEqual(toolDef.name, 'GitTool')
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
test('GitTool class should be instantiable with config', () => {
|
|
108
|
+
const instance = new GitTool({ owner: 'test', repo: 'test' })
|
|
109
|
+
assert.ok(instance instanceof GitTool)
|
|
110
|
+
assert.ok(typeof instance.execute === 'function')
|
|
111
|
+
})
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
// ============================================
|
|
115
|
+
// Parameter Validation Tests
|
|
116
|
+
// ============================================
|
|
117
|
+
describe('Parameter Validation', () => {
|
|
118
|
+
test('comment action should validate prNumber and body before GitHub init', async () => {
|
|
119
|
+
// Pass dummy token to bypass GitHub token check early
|
|
120
|
+
const tool = new GitTool({
|
|
121
|
+
owner: 'test',
|
|
122
|
+
repo: 'test',
|
|
123
|
+
token: 'dummy'
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
await assert.rejects(
|
|
127
|
+
tool.execute({ action: 'comment' }),
|
|
128
|
+
/prNumber and body required/
|
|
129
|
+
)
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
test('merge-pr action should validate prNumber', async () => {
|
|
133
|
+
const tool = new GitTool({
|
|
134
|
+
owner: 'test',
|
|
135
|
+
repo: 'test',
|
|
136
|
+
token: 'dummy'
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
await assert.rejects(
|
|
140
|
+
tool.execute({ action: 'merge-pr' }),
|
|
141
|
+
/prNumber required/
|
|
142
|
+
)
|
|
143
|
+
})
|
|
144
|
+
})
|