@raolin2025/claude-code-node 2.0.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/channel/notify-daemon.js +16 -13
- package/src/core/cli.js +45 -5
- package/src/core/query-engine.js +57 -54
- package/src/core/session.js +3 -1
- 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/mcp/client.js +13 -3
- package/src/security/path-guard.js +11 -1
- package/src/tools/git-tool.js +308 -0
- package/src/tools/index.js +2 -0
- package/src/tools/web-fetch.js +3 -1
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
|
+
})
|
|
@@ -491,28 +491,31 @@ async function main() {
|
|
|
491
491
|
// 确保 socket 目录存在
|
|
492
492
|
mkdirSync(SOCK_DIR, { recursive: true });
|
|
493
493
|
|
|
494
|
-
//
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
494
|
+
// M9 fix: PID file lock — atomic create with retry, no unlink+write race
|
|
495
|
+
let pidAcquired = false;
|
|
496
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
497
|
+
try {
|
|
498
|
+
const fd = openSync(config.pidFile, "wx");
|
|
499
|
+
writeFileSync(fd, String(process.pid));
|
|
500
|
+
closeSync(fd);
|
|
501
|
+
pidAcquired = true;
|
|
502
|
+
break;
|
|
503
|
+
} catch (err) {
|
|
504
|
+
if (err.code !== "EEXIST") throw err;
|
|
501
505
|
const oldPid = parseInt(readFileSync(config.pidFile, "utf8").trim(), 10);
|
|
502
506
|
try {
|
|
503
507
|
process.kill(oldPid, 0);
|
|
504
508
|
console.error("cc-notify already running (PID " + oldPid + "). Use --stop first.");
|
|
505
509
|
process.exit(1);
|
|
506
510
|
} catch {
|
|
507
|
-
try {
|
|
508
|
-
|
|
509
|
-
} catch {}
|
|
510
|
-
writeFileSync(config.pidFile, String(process.pid));
|
|
511
|
+
try { unlinkSync(config.pidFile); } catch {}
|
|
512
|
+
if (attempt < 2) await sleep(100);
|
|
511
513
|
}
|
|
512
|
-
} else {
|
|
513
|
-
throw err;
|
|
514
514
|
}
|
|
515
515
|
}
|
|
516
|
+
if (!pidAcquired) {
|
|
517
|
+
writeFileSync(config.pidFile, String(process.pid));
|
|
518
|
+
}
|
|
516
519
|
|
|
517
520
|
const cleanup = () => {
|
|
518
521
|
log("Shutting down...");
|
package/src/core/cli.js
CHANGED
|
@@ -138,6 +138,7 @@ Commands:
|
|
|
138
138
|
/channel CMD — Manage notification channels (list|send|test)
|
|
139
139
|
/cost — Show API cost report
|
|
140
140
|
/compact — Manually compact conversation context
|
|
141
|
+
/allow [tool] — Allow a tool for the current session (default: all)
|
|
141
142
|
/exit — Exit (also Ctrl+C)
|
|
142
143
|
/quit — Same as /exit
|
|
143
144
|
`
|
|
@@ -241,6 +242,8 @@ export async function main() {
|
|
|
241
242
|
session = await sessionManager.create()
|
|
242
243
|
}
|
|
243
244
|
|
|
245
|
+
// M1 fix: tokenBudget 必须在 engineConfig 之前定义,否则 TDZ ReferenceError
|
|
246
|
+
const tokenBudget = new TokenBudget({ maxTokens: config.get('maxBudgetTokens') || 200_000 })
|
|
244
247
|
const costTracker = new CostTracker({ model })
|
|
245
248
|
|
|
246
249
|
const engineConfig = new QueryEngineConfig({
|
|
@@ -252,11 +255,17 @@ export async function main() {
|
|
|
252
255
|
})
|
|
253
256
|
const engine = new QueryEngine(engineConfig)
|
|
254
257
|
|
|
255
|
-
// M5: 恢复会话历史和状态
|
|
258
|
+
// M5: 恢复会话历史和状态 — 完整恢复所有角色(含 tool_calls、tool 结果)
|
|
256
259
|
if (session?.messages?.length) {
|
|
257
260
|
for (const msg of session.messages) {
|
|
258
|
-
|
|
259
|
-
|
|
261
|
+
const entry = { role: msg.role, content: msg.content }
|
|
262
|
+
if (msg.role === 'assistant' && msg.toolCalls?.length > 0) {
|
|
263
|
+
entry.toolCalls = msg.toolCalls
|
|
264
|
+
}
|
|
265
|
+
if (msg.role === 'tool' && msg.tool_call_id) {
|
|
266
|
+
entry.tool_call_id = msg.tool_call_id
|
|
267
|
+
}
|
|
268
|
+
engine.state.messages.push(entry)
|
|
260
269
|
}
|
|
261
270
|
// 恢复 turn count
|
|
262
271
|
if (session.state?.turnCount) engine.state.turnCount = session.state.turnCount
|
|
@@ -268,7 +277,6 @@ export async function main() {
|
|
|
268
277
|
}
|
|
269
278
|
}
|
|
270
279
|
|
|
271
|
-
const tokenBudget = new TokenBudget({ maxTokens: config.get('maxBudgetTokens') || 200_000 })
|
|
272
280
|
|
|
273
281
|
const channelManager = new ChannelManager({
|
|
274
282
|
channels: config.get('channels') || {},
|
|
@@ -277,8 +285,16 @@ export async function main() {
|
|
|
277
285
|
|
|
278
286
|
// 一次性输入模式
|
|
279
287
|
if (cliArgs.oneShot) {
|
|
288
|
+
// 一次性模式下用户已明确表达了执行意图,自动批准所有工具调用
|
|
289
|
+
if (engine.permissionChecker.mode === 'ask') {
|
|
290
|
+
engine.config.onConfirmTool = async () => true
|
|
291
|
+
}
|
|
280
292
|
const result = await engine.processMessage(cliArgs.oneShot)
|
|
281
293
|
console.log(result.response)
|
|
294
|
+
// 保存会话
|
|
295
|
+
session = await sessionManager.create(`one-shot: ${cliArgs.oneShot.slice(0, 50)}`)
|
|
296
|
+
await sessionManager.appendMessage({ role: 'user', content: cliArgs.oneShot })
|
|
297
|
+
await sessionManager.appendMessage({ role: 'assistant', content: result.response })
|
|
282
298
|
if (channelManager.list().length > 0) {
|
|
283
299
|
await channelManager.sendTemplate('task-done', {
|
|
284
300
|
task: cliArgs.oneShot.slice(0, 80),
|
|
@@ -293,6 +309,19 @@ export async function main() {
|
|
|
293
309
|
|
|
294
310
|
const rl = createInterface({ input: process.stdin, output: process.stdout, prompt: '> ' })
|
|
295
311
|
|
|
312
|
+
// 将 readline 注入引擎配置,用于 ask 模式确认和 AskUserQuestion 工具
|
|
313
|
+
if (permissionMode === 'ask') {
|
|
314
|
+
engine.config.onConfirmTool = async (toolName, input) => {
|
|
315
|
+
return new Promise((resolve) => {
|
|
316
|
+
const snippet = JSON.stringify(input).slice(0, 120) || '(no params)'
|
|
317
|
+
rl.question(`\n⚠️ Allow tool "${toolName}"?\n Input: ${snippet}\n (y/N) `, (answer) => {
|
|
318
|
+
resolve(answer.toLowerCase().startsWith('y'))
|
|
319
|
+
})
|
|
320
|
+
})
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
engine.config.readline = rl
|
|
324
|
+
|
|
296
325
|
console.log(BANNER)
|
|
297
326
|
console.log(`Model: ${model} | Permission: ${permissionMode} | Tools: ${registry.getNames().join(', ')}`)
|
|
298
327
|
console.log(`Socket: ${SOCK_PATH} (cc-notify can connect)`)
|
|
@@ -304,7 +333,7 @@ export async function main() {
|
|
|
304
333
|
console.log()
|
|
305
334
|
rl.prompt()
|
|
306
335
|
|
|
307
|
-
//
|
|
336
|
+
// REPL 消息处理包装(留作扩展点)
|
|
308
337
|
async function processInput(input) {
|
|
309
338
|
return engine.processMessage(input)
|
|
310
339
|
}
|
|
@@ -380,6 +409,12 @@ export async function main() {
|
|
|
380
409
|
}
|
|
381
410
|
break
|
|
382
411
|
}
|
|
412
|
+
case 'allow': {
|
|
413
|
+
const allowTool = rest.join(' ') || '*'
|
|
414
|
+
engine.permissionChecker.allowForSession(allowTool, '*')
|
|
415
|
+
console.log(`✅ Tool "${allowTool}" allowed for this session`)
|
|
416
|
+
break
|
|
417
|
+
}
|
|
383
418
|
case 'cost':
|
|
384
419
|
console.log(engine.costTracker.formatReport())
|
|
385
420
|
break
|
|
@@ -415,6 +450,11 @@ export async function main() {
|
|
|
415
450
|
console.log()
|
|
416
451
|
await sessionManager.appendMessage({ role: 'user', content: input })
|
|
417
452
|
await sessionManager.appendMessage({ role: 'assistant', content: result.response })
|
|
453
|
+
// 保存引擎状态到会话
|
|
454
|
+
session.state = session.state || {}
|
|
455
|
+
session.state.turnCount = engine.state.turnCount
|
|
456
|
+
session.state.costHistory = engine.costTracker.history.slice(-50)
|
|
457
|
+
await sessionManager.save(session)
|
|
418
458
|
if (verbose) console.log(`[Turns: ${result.turns} | Tools: ${result.toolResults.length}]`)
|
|
419
459
|
// 显示费用(即使非 verbose 也显示)
|
|
420
460
|
if (engine.costTracker && engine.costTracker.totalApiCalls > 0) {
|