@raolin2025/claude-code-node 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/README.md ADDED
@@ -0,0 +1,294 @@
1
+ # AI Code Agent — Node.js Edition
2
+
3
+ > 基于 OpenAI 兼容协议的轻量级 AI 编程助手
4
+ > 零外部依赖 · 纯 JavaScript · DeepSeek 默认 · 安全加固
5
+
6
+ ---
7
+
8
+ ## 🚀 快速开始
9
+
10
+ ### 前置要求
11
+ - Node.js ≥ 18.0.0
12
+ - 至少一个 API Key:`DEEPSEEK_API_KEY`(默认)或 `LLM_API_KEY`(通用)
13
+
14
+ ### 启动
15
+
16
+ ```bash
17
+ # 进入项目目录
18
+ cd ~/.openclaw/workspace/claude-code-node
19
+
20
+ # 设置 API Key(DeepSeek 为默认)
21
+ export DEEPSEEK_API_KEY=***
22
+ # 或通用方式
23
+ export LLM_API_KEY=***
24
+
25
+ # 启动 REPL(默认使用 DeepSeek)
26
+ node src/index.js
27
+
28
+ # 一次性执行
29
+ node src/index.js "列出当前目录的文件"
30
+
31
+ # 指定模型
32
+ node src/index.js --model deepseek-reasoner
33
+
34
+ # 切换其他提供商
35
+ node src/index.js --model qwen-plus --api-base https://dashscope.aliyuncs.com/compatible-mode/v1
36
+
37
+ # 恢复上一次会话
38
+ node src/index.js --resume session-1747000000000-abc123
39
+ ```
40
+
41
+ ---
42
+
43
+ ## 📖 命令行参数
44
+
45
+ | 参数 | 短写 | 说明 | 默认值 |
46
+ |------|------|------|--------|
47
+ | `--model` | `-m` | LLM 模型名 | `deepseek-chat` |
48
+ | `--system-prompt` | `-s` | 系统提示词 | `""` |
49
+ | `--permission-mode` | `-p` | 权限模式 | `ask` |
50
+ | `--max-turns` | `-t` | 最大工具循环轮数 | `100` |
51
+ | `--api-base` | | API 基础 URL | `https://api.deepseek.com/v1` |
52
+ | `--resume` | `-r` | 恢复会话 ID | |
53
+ | `--verbose` | `-v` | 详细输出 | `false` |
54
+ | `--no-stream` | | 禁用流式响应 | `false` |
55
+ | `--help` | `-h` | 显示帮助 | |
56
+
57
+ ### 权限模式
58
+
59
+ | 模式 | 说明 |
60
+ |------|------|
61
+ | `ask` | 每次工具调用需确认(安全,推荐) |
62
+ | `always-allow` | 自动允许所有工具调用(仍受安全策略约束) |
63
+ | `deny` | 拒绝所有工具调用 |
64
+
65
+ ---
66
+
67
+ ## 💬 REPL 内置命令
68
+
69
+ 进入 REPL 后,输入 `/` 开头的命令:
70
+
71
+ | 命令 | 说明 |
72
+ |------|------|
73
+ | `/help` | 显示帮助 |
74
+ | `/model NAME` | 切换模型 |
75
+ | `/tools` | 列出可用工具 |
76
+ | `/session` | 查看当前会话信息 |
77
+ | `/sessions` | 列出所有会话 |
78
+ | `/clear` | 清空当前对话 |
79
+ | `/config KEY` | 查看配置(支持点号路径如 `tools.bash.timeout`) |
80
+ | `/budget` | 查看 Token 预算使用情况 |
81
+ | `/exit` `/quit` | 退出(Ctrl+C 也可以) |
82
+
83
+ ---
84
+
85
+ ## 🛠️ 内置工具(9 个)
86
+
87
+ | 工具 | 说明 | 权限级别 | 安全检查 |
88
+ |------|------|---------|---------|
89
+ | **Bash** | 执行 shell 命令 | `ask` | ✅ 命令安全扫描 |
90
+ | **Read** | 读取文件 | `always-allow` | ✅ 路径安全检查 |
91
+ | **Edit** | 精确文本替换编辑 | `ask` | ✅ 写入路径安全 |
92
+ | **Write** | 创建/覆盖文件 | `ask` | ✅ 写入路径安全 |
93
+ | **Glob** | 文件模式搜索 | `always-allow` | — |
94
+ | **Grep** | 内容搜索(rg/grep) | `always-allow` | — |
95
+ | **WebFetch** | 抓取网页内容 | `ask` | ✅ SSRF 防护 |
96
+ | **WebSearch** | 网页搜索 | `ask` | 需要 API Key |
97
+ | **AskUserQuestion** | 向用户提问 | `always-allow` | — |
98
+
99
+ ---
100
+
101
+ ## 🔒 安全架构
102
+
103
+ 本项目包含 **4 层安全防护**,总计 **964 行安全代码**:
104
+
105
+ ### 1️⃣ SSRF 防护(178 行)
106
+ 阻止 LLM 通过 WebFetch 访问内网和云元数据:
107
+
108
+ ```
109
+ 🚫 10.0.0.0/8 — 私有网络
110
+ 🚫 172.16.0.0/12 — 私有网络
111
+ 🚫 192.168.0.0/16 — 私有网络
112
+ 🚫 169.254.0.0/16 — AWS/GCP 元数据
113
+ 🚫 100.64.0.0/10 — 阿里云元数据 (100.100.100.200)
114
+ 🚫 fc00::/7 — IPv6 唯一本地
115
+ 🚫 fe80::/10 — IPv6 链路本地
116
+ ✅ 127.0.0.0/8 — 回环(允许,本地开发)
117
+ ✅ ::1 — IPv6 回环
118
+ ```
119
+
120
+ ### 2️⃣ Bash 命令安全(279 行)
121
+ 阻止 LLM 执行危险 shell 命令:
122
+
123
+ | 类别 | 示例 | 严重性 |
124
+ |------|------|--------|
125
+ | 破坏性操作 | `rm -rf /`, `dd of=/dev/sda`, `mkfs` | 🚫 CRITICAL |
126
+ | 敏感文件访问 | `cat /etc/shadow`, `~/.ssh/id_rsa` | 🚫 CRITICAL/HIGH |
127
+ | 远程执行 | `curl \| bash`, `wget \| sh` | 🚫 CRITICAL |
128
+ | 提权 | `sudo su`, `pkexec` | ⚠️ HIGH |
129
+ | 容器逃逸 | `nsenter --target 1`, 特权 docker | 🚫 CRITICAL |
130
+ | 内网数据外泄 | `curl http://192.168.x.x` | 🚫 CRITICAL |
131
+ | 系统重定向 | `> /etc/hosts` | 🚫 CRITICAL |
132
+
133
+ ### 3️⃣ 路径安全防护(190 行)
134
+ 防止 LLM 访问/修改敏感文件:
135
+
136
+ - **路径遍历检测** — `../../../etc/passwd` → 阻止
137
+ - **SSH 密钥保护** — 禁止读取 `~/.ssh/id_*` 私钥
138
+ - **系统目录写入保护** — 禁止写 `/etc/`, `/boot/`, `/usr/bin/`
139
+ - **敏感路径列表** — `/etc/shadow`, `/etc/sudoers` 等
140
+
141
+ ### 4️⃣ 增强权限系统(310 行)
142
+
143
+ ```mermaid
144
+ graph TD
145
+ A[工具调用请求] --> B{安全检查}
146
+ B -->|🚫 不安全| C[直接拒绝]
147
+ B -->|✅ 安全| D{规则匹配}
148
+ D -->|DENY 规则| C
149
+ D -->|ALLOW 规则| E[执行工具]
150
+ D -->|无匹配| F{权限模式}
151
+ F -->|ask| G[请求用户确认]
152
+ F -->|always-allow| E
153
+ F -->|deny| C
154
+ C --> H[审计日志]
155
+ E --> H
156
+ G --> H
157
+ ```
158
+
159
+ 特性:
160
+ - **规则持久化** — 保存到 `.claude-code/permissions.json`
161
+ - **审计日志** — 记录到 `.claude-code/audit.log`
162
+ - **安全一票否决** — 即使规则允许,安全检查不通过仍拒绝
163
+
164
+ ---
165
+
166
+ ## 🌐 API — OpenAI 兼容协议(全行业通用)
167
+
168
+ ### DeepSeek(默认)
169
+ ```bash
170
+ export DEEPSEEK_API_KEY=***
171
+ node src/index.js
172
+ ```
173
+
174
+ ### 其他 OpenAI 兼容提供商
175
+ ```bash
176
+ # 通义千问
177
+ export LLM_API_KEY=***
178
+ node src/index.js --model qwen-plus --api-base https://dashscope.aliyuncs.com/compatible-mode/v1
179
+
180
+ # 智谱 GLM
181
+ node src/index.js --model glm-4-flash --api-base https://open.bigmodel.cn/api/paas/v4
182
+
183
+ # Moonshot Kimi
184
+ node src/index.js --model kimi-k2-0711 --api-base https://api.moonshot.cn/v1
185
+
186
+ # OpenAI
187
+ node src/index.js --model gpt-4o --api-base https://api.openai.com/v1
188
+
189
+ # Ollama 本地
190
+ node src/index.js --model qwen2.5 --api-base http://localhost:11434/v1
191
+ ```
192
+
193
+ ---
194
+
195
+ ## 📂 配置文件
196
+
197
+ ### 项目级
198
+ `.claude-code/config.json` — 存放在项目根目录
199
+
200
+ ### 用户级
201
+ `~/.claude-code/config.json` — 全局默认配置
202
+
203
+ ### 配置项
204
+
205
+ ```json
206
+ {
207
+ "model": "deepseek-chat",
208
+ "maxTurns": 100,
209
+ "maxBudgetTokens": 1000000,
210
+ "permissionMode": "ask",
211
+ "tools": {
212
+ "bash": { "timeout": 120 },
213
+ "fileRead": { "maxLines": 2000 },
214
+ "webFetch": { "timeout": 30 }
215
+ },
216
+ "mcp": {
217
+ "servers": {}
218
+ }
219
+ }
220
+ ```
221
+
222
+ ---
223
+
224
+ ## 🔌 MCP 服务器
225
+
226
+ 支持通过 Model Context Protocol 连接外部工具服务器:
227
+
228
+ ```javascript
229
+ import { MCPRegistry } from './src/mcp/index.js'
230
+
231
+ const registry = new MCPRegistry()
232
+ registry.register('my-server', {
233
+ command: 'npx',
234
+ args: ['my-mcp-server'],
235
+ env: { API_KEY: 'xxx' }
236
+ })
237
+
238
+ await registry.connectAll()
239
+ const tools = registry.getAllTools()
240
+ ```
241
+
242
+ ---
243
+
244
+ ## 📊 项目结构
245
+
246
+ ```
247
+ claude-code-node/ 33 文件 · 4307 行
248
+ ├── src/
249
+ │ ├── core/ 1279 行 — 引擎、CLI、会话、配置
250
+ │ ├── tools/ 944 行 — 9 个内置工具
251
+ │ ├── security/ 964 行 — 4 层安全防护
252
+ │ ├── utils/ 544 行 — 差异、文件、进程、格式
253
+ │ ├── mcp/ 385 行 — MCP 客户端+注册表
254
+ │ ├── types/ 125 行 — 类型定义
255
+ │ ├── permission/ 37 行 — 基础权限(兼容)
256
+ │ └── index.js 8 行 — 入口
257
+ └── package.json
258
+ ```
259
+
260
+ ---
261
+
262
+ ## ⚠️ 安全注意事项
263
+
264
+ 1. **始终使用 `ask` 权限模式** — 除非你完全信任 LLM 输出
265
+ 2. **不要暴露 API Key** — 使用环境变量,不要硬编码
266
+ 3. **WebSearch 需要单独配置** — 设置 `BRAVE_SEARCH_API_KEY` 或 `GOOGLE_SEARCH_API_KEY`
267
+ 4. **审计日志定期审查** — 检查 `.claude-code/audit.log` 中的 DENY 记录
268
+ 5. **安全规则可持久化** — 使用 `/allow` 命令添加会话级规则
269
+
270
+ ---
271
+
272
+ ## 🔧 与原 Claude Code 的对比
273
+
274
+ | 特性 | 原版 (TypeScript/Bun) | 本版 (Node.js) |
275
+ |------|----------------------|----------------|
276
+ | 运行时 | Bun | Node.js ≥ 18 |
277
+ | 语言 | TypeScript | JavaScript (ESM) |
278
+ | 依赖 | ~200 npm 包 | **0 外部依赖** |
279
+ | 代码量 | 512,000+ 行 | 4,307 行 |
280
+ | 工具数 | ~40 | 9(核心) |
281
+ | API 协议 | Anthropic | **OpenAI 兼容(全行业通用)** |
282
+ | SSRF 防护 | ✅ | ✅ |
283
+ | 命令安全 | ✅ (2592行) | ✅ (279行) |
284
+ | 路径安全 | ✅ | ✅ |
285
+ | 审计日志 | ✅ | ✅ |
286
+ | MCP 支持 | ✅ 完整 | ✅ 简化版 |
287
+ | 流式响应 | ✅ | ✅ |
288
+ | 会话管理 | ✅ | ✅ |
289
+ | UI | Ink (React CLI) | 纯 readline |
290
+
291
+ ---
292
+
293
+ *基于 Claude Code 架构的 OpenAI 兼容重构*
294
+ *零外部依赖 · DeepSeek 默认 · 安全加固 · MIT License*
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@raolin2025/claude-code-node",
3
+ "version": "1.0.0",
4
+ "description": "Node.js 重构版 Claude Code CLI Agent 框架",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "bin": {
8
+ "cc-node": "src/index.js"
9
+ },
10
+ "scripts": {
11
+ "start": "node src/index.js",
12
+ "dev": "node --watch src/index.js",
13
+ "test": "node --test src/__tests__/*.js",
14
+ "repl": "node src/index.js"
15
+ },
16
+ "keywords": ["claude", "code", "agent", "cli", "llm"],
17
+ "license": "MIT",
18
+ "engines": {
19
+ "node": ">=18.0.0"
20
+ },
21
+ "files": [
22
+ "src/"
23
+ ]
24
+ }
@@ -0,0 +1,314 @@
1
+ /**
2
+ * CLI 入口 — 命令行解析和 REPL 循环
3
+ * 对应原版: src/cli/ + src/entrypoints/
4
+ */
5
+ import { createInterface } from 'readline'
6
+ import { QueryEngine, QueryEngineConfig } from './query-engine.js'
7
+ import { createDefaultRegistry } from '../tools/index.js'
8
+ import { SessionManager } from './session.js'
9
+ import { Config } from './config.js'
10
+ import { TokenBudget } from './token-budget.js'
11
+ import { PermissionChecker } from '../permission/permission.js'
12
+
13
+ const BANNER = `
14
+ ╔═══════════════════════════════════════════════╗
15
+ ║ AI Code Agent — Node.js Edition ║
16
+ ║ OpenAI-Compatible · DeepSeek Default ║
17
+ ║ Type '/help' for commands ║
18
+ ║ Type '/exit' or Ctrl+C to quit ║
19
+ ╚═══════════════════════════════════════════════╝
20
+ `.trim()
21
+
22
+ const HELP_TEXT = `
23
+ Commands:
24
+ /help — Show this help
25
+ /model NAME — Switch model
26
+ /tools — List available tools
27
+ /session — Show session info
28
+ /sessions — List all sessions
29
+ /clear — Clear conversation
30
+ /config KEY — Show config value
31
+ /budget — Show token budget
32
+ /exit — Exit (also Ctrl+C)
33
+ /quit — Same as /exit
34
+ `
35
+
36
+ /**
37
+ * 解析命令行参数
38
+ */
39
+ function parseArgs(argv) {
40
+ const args = {
41
+ model: 'deepseek-chat',
42
+ systemPrompt: '',
43
+ permissionMode: 'ask',
44
+ maxTurns: 100,
45
+ verbose: false,
46
+ apiBase: 'https://api.deepseek.com/v1',
47
+ resume: null,
48
+ noStream: false,
49
+ }
50
+
51
+ let i = 2 // skip node and script name
52
+ while (i < argv.length) {
53
+ const arg = argv[i]
54
+ switch (arg) {
55
+ case '--model':
56
+ case '-m':
57
+ args.model = argv[++i]
58
+ break
59
+ case '--system-prompt':
60
+ case '-s':
61
+ args.systemPrompt = argv[++i]
62
+ break
63
+ case '--permission-mode':
64
+ case '-p':
65
+ args.permissionMode = argv[++i]
66
+ break
67
+ case '--max-turns':
68
+ case '-t':
69
+ args.maxTurns = parseInt(argv[++i], 10)
70
+ break
71
+ case '--api-key':
72
+ args.apiKey = argv[++i]
73
+ break
74
+ case '--api-base':
75
+ args.apiBase = argv[++i]
76
+ break
77
+ case '--resume':
78
+ case '-r':
79
+ args.resume = argv[++i]
80
+ break
81
+ case '--verbose':
82
+ case '-v':
83
+ args.verbose = true
84
+ break
85
+ case '--no-stream':
86
+ args.noStream = true
87
+ break
88
+ case '--help':
89
+ case '-h':
90
+ console.log(`Usage: cc-node [options]
91
+
92
+ Options:
93
+ -m, --model NAME Model to use (required, e.g. deepseek-chat, qwen-plus, glm-4-flash)
94
+ -s, --system-prompt TEXT System prompt
95
+ -p, --permission-mode Permission mode: ask|always-allow|deny (default: ask)
96
+ -t, --max-turns N Max tool loop turns (default: 100)
97
+ --api-base URL API base URL (default: https://api.deepseek.com/v1)
98
+ --api-key KEY API key (or set LLM_API_KEY env)
99
+ -r, --resume ID Resume a session
100
+ -v, --verbose Verbose mode
101
+ --no-stream Disable streaming
102
+ -h, --help Show this help
103
+
104
+ Environment variables:
105
+ LLM_API_KEY Universal API key (recommended)
106
+ DEEPSEEK_API_KEY DeepSeek API key (default)
107
+ OPENAI_API_KEY OpenAI API key
108
+ QWEN_API_KEY Qwen (DashScope) API key
109
+ GLM_API_KEY Zhipu GLM API key
110
+ KIMI_API_KEY Moonshot Kimi API key
111
+ LLM_API_BASE API base URL (default: https://api.deepseek.com/v1)`)
112
+ process.exit(0)
113
+ default:
114
+ if (!arg.startsWith('-')) {
115
+ // 非选项参数视为一次性输入
116
+ args.oneShot = argv.slice(i).join(' ')
117
+ i = argv.length
118
+ }
119
+ break
120
+ }
121
+ i++
122
+ }
123
+
124
+ return args
125
+ }
126
+
127
+ /**
128
+ * 主入口
129
+ */
130
+ export async function main() {
131
+ const cliArgs = parseArgs(process.argv)
132
+
133
+ // 加载配置
134
+ const config = new Config()
135
+ await config.load(process.cwd())
136
+
137
+ // 合并 CLI 参数 > 项目配置 > 用户配置 > 默认值
138
+ const model = cliArgs.model || config.get('model')
139
+ const systemPrompt = cliArgs.systemPrompt || ''
140
+ const permissionMode = cliArgs.permissionMode || config.get('permissionMode')
141
+ const maxTurns = cliArgs.maxTurns || config.get('maxTurns')
142
+ const apiBase = cliArgs.apiBase || config.get('apiBase') || process.env.LLM_API_BASE || ''
143
+ const apiKey = cliArgs.apiKey || config.get('apiKey') || ''
144
+ const verbose = cliArgs.verbose || config.get('verbose')
145
+
146
+ // 创建工具注册表
147
+ const registry = createDefaultRegistry()
148
+
149
+ // 创建会话管理器
150
+ const sessionManager = new SessionManager({
151
+ sessionsDir: config.get('sessionsDir'),
152
+ })
153
+
154
+ // 恢复或创建会话
155
+ let session
156
+ if (cliArgs.resume) {
157
+ session = await sessionManager.load(cliArgs.resume)
158
+ if (!session) {
159
+ console.error(`Session not found: ${cliArgs.resume}`)
160
+ process.exit(1)
161
+ }
162
+ } else {
163
+ session = await sessionManager.create()
164
+ }
165
+
166
+ // 创建查询引擎
167
+ const engineConfig = new QueryEngineConfig({
168
+ model,
169
+ systemPrompt,
170
+ permissionMode,
171
+ maxTurns,
172
+ apiBase,
173
+ apiKey,
174
+ verbose,
175
+ tools: registry.getAll(),
176
+ })
177
+ const engine = new QueryEngine(engineConfig)
178
+
179
+ // 恢复会话历史
180
+ if (session?.messages?.length) {
181
+ for (const msg of session.messages) {
182
+ if (msg.role === 'user') {
183
+ engine.state.messages.push({ role: 'user', content: msg.content })
184
+ } else if (msg.role === 'assistant') {
185
+ engine.state.messages.push({ role: 'assistant', content: msg.content })
186
+ }
187
+ }
188
+ }
189
+
190
+ const tokenBudget = new TokenBudget({
191
+ maxTokens: config.get('maxBudgetTokens') || 200_000,
192
+ })
193
+
194
+ // 一次性输入模式
195
+ if (cliArgs.oneShot) {
196
+ const result = await engine.processMessage(cliArgs.oneShot)
197
+ console.log(result.response)
198
+ process.exit(0)
199
+ }
200
+
201
+ // REPL 模式
202
+ const rl = createInterface({
203
+ input: process.stdin,
204
+ output: process.stdout,
205
+ prompt: '> ',
206
+ })
207
+
208
+ console.log(BANNER)
209
+ console.log(`Model: ${model} | Permission: ${permissionMode} | Tools: ${registry.getNames().join(', ')}`)
210
+ console.log()
211
+
212
+ rl.prompt()
213
+
214
+ rl.on('line', async (line) => {
215
+ const input = line.trim()
216
+ if (!input) {
217
+ rl.prompt()
218
+ return
219
+ }
220
+
221
+ // 命令处理
222
+ if (input.startsWith('/')) {
223
+ const [cmd, ...rest] = input.slice(1).split(' ')
224
+ switch (cmd) {
225
+ case 'help':
226
+ console.log(HELP_TEXT)
227
+ break
228
+ case 'model':
229
+ if (rest[0]) {
230
+ engine.config.model = rest.join(' ')
231
+ console.log(`Model switched to: ${engine.config.model}`)
232
+ } else {
233
+ console.log(`Current model: ${engine.config.model}`)
234
+ }
235
+ break
236
+ case 'tools':
237
+ console.log('Available tools:')
238
+ for (const name of registry.getNames()) {
239
+ const tool = registry.get(name)
240
+ console.log(` ${name} — ${tool.description.split('\n')[0]}`)
241
+ }
242
+ break
243
+ case 'session':
244
+ console.log(`Session: ${session.id}`)
245
+ console.log(`Title: ${session.title}`)
246
+ console.log(`Messages: ${session.messages?.length || 0}`)
247
+ console.log(`Turns: ${engine.state.turnCount}`)
248
+ break
249
+ case 'sessions': {
250
+ const sessions = await sessionManager.list()
251
+ if (sessions.length === 0) {
252
+ console.log('No sessions found')
253
+ } else {
254
+ for (const s of sessions) {
255
+ console.log(` ${s.id} — ${s.title} (${s.messageCount} msgs, ${s.updated})`)
256
+ }
257
+ }
258
+ break
259
+ }
260
+ case 'clear':
261
+ engine.reset()
262
+ session = await sessionManager.create()
263
+ console.log('Conversation cleared')
264
+ break
265
+ case 'config':
266
+ if (rest[0]) {
267
+ const val = config.get(rest.join(' '))
268
+ console.log(`${rest.join(' ')} = ${JSON.stringify(val, null, 2)}`)
269
+ } else {
270
+ console.log(JSON.stringify(config.toJSON(), null, 2))
271
+ }
272
+ break
273
+ case 'budget':
274
+ console.log(tokenBudget.format())
275
+ break
276
+ case 'exit':
277
+ case 'quit':
278
+ console.log('Goodbye!')
279
+ process.exit(0)
280
+ default:
281
+ console.log(`Unknown command: /${cmd}. Type /help for available commands.`)
282
+ }
283
+ rl.prompt()
284
+ return
285
+ }
286
+
287
+ // 发送到引擎
288
+ try {
289
+ const result = await engine.processMessage(input)
290
+
291
+ // 输出助手回复
292
+ console.log()
293
+ console.log(result.response)
294
+ console.log()
295
+
296
+ // 保存到会话
297
+ await sessionManager.appendMessage({ role: 'user', content: input })
298
+ await sessionManager.appendMessage({ role: 'assistant', content: result.response })
299
+
300
+ if (verbose) {
301
+ console.log(`[Turns: ${result.turns} | Tools: ${result.toolResults.length}]`)
302
+ }
303
+ } catch (err) {
304
+ console.error(`\nError: ${err.message}\n`)
305
+ }
306
+
307
+ rl.prompt()
308
+ })
309
+
310
+ rl.on('close', () => {
311
+ console.log('\nGoodbye!')
312
+ process.exit(0)
313
+ })
314
+ }