@raolin2025/claude-code-node 2.1.0 → 2.2.1

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 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
  [![npm version](https://img.shields.io/npm/v/@raolin2025/claude-code-node.svg)](https://www.npmjs.com/package/@raolin2025/claude-code-node) [![GitHub](https://img.shields.io/badge/GitHub-bg1avd%2Fclaude--code--node-blue)](https://github.com/bg1avd/claude-code-node) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@raolin2025/claude-code-node",
3
- "version": "2.1.0",
3
+ "version": "2.2.1",
4
4
  "description": "Node.js AI Code Agent CLI - Zero dependencies, pure JavaScript, security hardened, multi-channel notifications",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -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
+ })
@@ -114,12 +114,12 @@ export class QueryEngine {
114
114
  // 没有工具调用 → 最终回复
115
115
  if (!response.toolCalls || response.toolCalls.length === 0) {
116
116
  finalResponse = response.content
117
- this.state.messages.push(new AssistantMessage(response.content))
117
+ this.state.messages.push(new AssistantMessage(response.content, [], response.reasoningContent))
118
118
  break
119
119
  }
120
120
 
121
121
  // 有工具调用 → 记录 assistant 消息(含 tool_calls)
122
- this.state.messages.push(new AssistantMessage(response.content, response.toolCalls))
122
+ this.state.messages.push(new AssistantMessage(response.content, response.toolCalls, response.reasoningContent))
123
123
 
124
124
  // 执行工具
125
125
  const toolResults = await this._executeToolCalls(response.toolCalls)
@@ -168,21 +168,27 @@ export class QueryEngine {
168
168
  } else if (msg.role === 'user') {
169
169
  request.push({ role: 'user', content: this._formatContent(msg.content) })
170
170
  } else if (msg.role === 'assistant') {
171
- // tool_calls 的 assistant 消息:OpenAI 格式
171
+ // 构建 assistant 消息基础
172
+ const asstMsg = {
173
+ role: 'assistant',
174
+ content: msg.content || null,
175
+ }
176
+
177
+ // DeepSeek thinking mode: 必须传回 reasoning_content (tool call 场景)
178
+ if (msg.reasoningContent) {
179
+ asstMsg.reasoning_content = msg.reasoningContent
180
+ }
181
+
182
+ // tool_calls
172
183
  if (msg.toolCalls && msg.toolCalls.length > 0) {
173
- request.push({
174
- role: 'assistant',
175
- content: msg.content || null,
176
- tool_calls: msg.toolCalls.map(tc => ({
177
- id: tc.id,
178
- type: 'function',
179
- function: { name: tc.name, arguments: typeof tc.input === 'string' ? tc.input : JSON.stringify(tc.input) },
180
- })),
181
- })
182
- } else {
183
- // 纯文本 assistant 消息
184
- request.push({ role: 'assistant', content: this._formatContent(msg.content) })
184
+ asstMsg.tool_calls = msg.toolCalls.map(tc => ({
185
+ id: tc.id,
186
+ type: 'function',
187
+ function: { name: tc.name, arguments: typeof tc.input === 'string' ? tc.input : JSON.stringify(tc.input) },
188
+ }))
185
189
  }
190
+
191
+ request.push(asstMsg)
186
192
  } else if (msg.role === 'tool') {
187
193
  // tool 结果消息 — 直接透传
188
194
  request.push({
@@ -16,6 +16,7 @@ export async function* parseStream(response) {
16
16
 
17
17
  const result = {
18
18
  content: '',
19
+ reasoningContent: '',
19
20
  toolCalls: [],
20
21
  usage: { input_tokens: 0, output_tokens: 0 },
21
22
  }
@@ -40,6 +41,11 @@ export async function* parseStream(response) {
40
41
 
41
42
  if (!delta) continue
42
43
 
44
+ // reasoning_content (DeepSeek thinking mode)
45
+ if (delta.reasoning_content) {
46
+ result.reasoningContent += delta.reasoning_content
47
+ }
48
+
43
49
  // 文本
44
50
  if (delta.content) {
45
51
  result.content += delta.content
@@ -95,13 +101,17 @@ export async function* parseStream(response) {
95
101
  * 解析非流式响应
96
102
  */
97
103
  export function parseNonStreamResponse(data) {
98
- const result = { content: '', toolCalls: [], usage: {} }
104
+ const result = { content: '', reasoningContent: '', toolCalls: [], usage: {} }
99
105
  const choice = data.choices?.[0]
100
106
 
101
107
  if (choice?.message?.content) {
102
108
  result.content = choice.message.content
103
109
  }
104
110
 
111
+ if (choice?.message?.reasoning_content) {
112
+ result.reasoningContent = choice.message.reasoning_content
113
+ }
114
+
105
115
  for (const tc of (choice?.message?.tool_calls || [])) {
106
116
  let input = {}
107
117
  try { input = JSON.parse(tc.function.arguments) } catch {}