jinzd-ai-cli 0.1.3 → 0.1.5
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/CLAUDE.md +285 -0
- package/README.md +155 -0
- package/dist/index.js +316 -142
- package/package.json +3 -2
package/CLAUDE.md
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
# ai-cli 项目说明
|
|
2
|
+
|
|
3
|
+
## 项目简介
|
|
4
|
+
|
|
5
|
+
一个跨平台的 REPL 风格 AI 对话工具,支持多个主流 AI 提供商(Claude、Gemini、DeepSeek、智谱清言、Kimi),
|
|
6
|
+
带有 **AI 工具调用(Agentic)** 能力,支持执行 bash 命令、读写文件、运行交互式程序、流式生成大文档。
|
|
7
|
+
代理支持(`proxy` 配置字段 + 环境变量),Gemini 完整支持(2.5 Pro/Flash,array items schema 修复)。
|
|
8
|
+
设计上可扩展至 Electron/Tauri 桌面 GUI。
|
|
9
|
+
|
|
10
|
+
## 技术栈
|
|
11
|
+
|
|
12
|
+
- **语言**: TypeScript (ESM,`"type": "module"`,导入须用 `.js` 扩展名)
|
|
13
|
+
- **运行时**: Node.js >= 20
|
|
14
|
+
- **构建工具**: tsup → `dist/index.js`
|
|
15
|
+
- **包管理**: npm
|
|
16
|
+
|
|
17
|
+
## 项目结构
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
src/
|
|
21
|
+
├── index.ts # CLI 入口点 (Commander bin 脚本)
|
|
22
|
+
├── core/
|
|
23
|
+
│ ├── types.ts # 所有共享 TypeScript 接口(ChatRequest 含 timeout/_extraMessages)
|
|
24
|
+
│ ├── constants.ts # VERSION 等常量
|
|
25
|
+
│ ├── errors.ts # AiCliError → ProviderError / AuthError / RateLimitError 等
|
|
26
|
+
│ └── event-bus.ts # 类型安全的 EventEmitter(session.start/end, message.before/after)
|
|
27
|
+
├── providers/
|
|
28
|
+
│ ├── base.ts # 抽象 BaseProvider(chat/chatStream/validateApiKey/listModels/initialize)
|
|
29
|
+
│ ├── registry.ts # ProviderRegistry(initialize 时注入 apiKey + baseUrl + timeout)
|
|
30
|
+
│ ├── openai-compatible.ts # DeepSeek/Zhipu/Kimi 共用基类;实现 chatWithTools + buildToolResultMessages
|
|
31
|
+
│ ├── claude.ts # Anthropic SDK provider
|
|
32
|
+
│ ├── gemini.ts # Google Generative AI provider(role: assistant → model)
|
|
33
|
+
│ ├── deepseek.ts / zhipu.ts / kimi.ts # 继承 OpenAICompatibleProvider,只声明 defaultBaseUrl 和 info
|
|
34
|
+
├── config/
|
|
35
|
+
│ ├── schema.ts # Zod schema(含 timeouts / customBaseUrls / defaultModels)
|
|
36
|
+
│ ├── config-manager.ts # 读写 ~/.aicli/config.json,三层优先级:env > file > default
|
|
37
|
+
│ └── env-loader.ts # AICLI_API_KEY_* 等环境变量映射
|
|
38
|
+
├── session/
|
|
39
|
+
│ ├── session.ts # Session(addMessage / clear / toJSON / fromJSON)
|
|
40
|
+
│ └── session-manager.ts # CRUD for ~/.aicli/history/*.json
|
|
41
|
+
├── repl/
|
|
42
|
+
│ ├── repl.ts # 主 REPL 循环(MAX_TOOL_ROUNDS=20,handleChatWithTools agentic loop)
|
|
43
|
+
│ ├── renderer.ts # 终端输出(renderStream / renderResponse 均不加前置 \n)
|
|
44
|
+
│ ├── setup-wizard.ts # @inquirer/prompts 首次运行交互式设置
|
|
45
|
+
│ └── commands/
|
|
46
|
+
│ └── index.ts # CommandRegistry + 15个命令(/help /about /provider /model /clear /session /system /context /status /search /undo /export /tools /config /exit)
|
|
47
|
+
└── tools/
|
|
48
|
+
├── types.ts # ToolDefinition / ToolCall / ToolResult / DangerLevel / getDangerLevel
|
|
49
|
+
├── registry.ts # ToolRegistry(注册全部内置工具,共11个)
|
|
50
|
+
├── executor.ts # ToolExecutor(确认逻辑 + printToolCall/printToolResult)
|
|
51
|
+
└── builtin/
|
|
52
|
+
├── bash.ts # bash 工具(Windows: PowerShell + Buffer→UTF-8;Unix: $SHELL)
|
|
53
|
+
├── read-file.ts # read_file 工具
|
|
54
|
+
├── write-file.ts # write_file 工具
|
|
55
|
+
├── list-dir.ts # list_dir 工具
|
|
56
|
+
└── run-interactive.ts # run_interactive 工具(spawn 直连可执行文件,stdin_lines 依次输入)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## 常用命令
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
npm run dev # 开发模式(tsx 热重载)
|
|
63
|
+
npm run build # 构建到 dist/index.js(ESM)和 dist-cjs/index.cjs(CJS)
|
|
64
|
+
node dist/index.js # 运行
|
|
65
|
+
|
|
66
|
+
# 子命令
|
|
67
|
+
node dist/index.js providers # 列出所有 provider 及配置状态
|
|
68
|
+
node dist/index.js sessions # 列出最近会话
|
|
69
|
+
node dist/index.js config # 运行配置向导
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## 打包为独立可执行文件
|
|
73
|
+
|
|
74
|
+
使用 `@yao-pkg/pkg` 将项目打包为无需 Node.js 环境即可运行的单文件可执行程序(约 56MB)。
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
npm run pack:win # Windows x64 → release/ai-cli-win.exe
|
|
78
|
+
npm run pack:mac # macOS arm64 → release/ai-cli-mac
|
|
79
|
+
npm run pack:mac-x64 # macOS x64 → release/ai-cli-mac-x64
|
|
80
|
+
npm run pack:linux # Linux x64 → release/ai-cli-linux
|
|
81
|
+
npm run pack:all # 同时打包所有平台
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### 打包原理
|
|
85
|
+
|
|
86
|
+
- tsup 输出两套产物:
|
|
87
|
+
- `dist/index.js`(ESM,供 `node` 直接运行)
|
|
88
|
+
- `dist-cjs/index.cjs`(CJS + `noExternal: /.*/` 内联所有依赖,供 pkg 打包)
|
|
89
|
+
- pkg 使用 `--options no-deprecation` 内嵌到二进制,消除 punycode 弃用警告
|
|
90
|
+
- macOS/Linux 产物需在对应平台执行 `chmod +x` 后运行
|
|
91
|
+
|
|
92
|
+
### 关键兼容性问题(已解决)
|
|
93
|
+
|
|
94
|
+
**ora 在 pkg exe 中 Segfault**:`ora` spinner 在 `@yao-pkg/pkg` 打包的 Node 22 exe 里会触发段错误(exit code 139)。
|
|
95
|
+
**解决方案**:完全移除 `ora` 依赖,在 `src/repl/renderer.ts` 中用原生 `setInterval` + `process.stdout.write('\r\x1b[2K')` 实现轻量 spinner,非 TTY 环境自动降级为空操作。
|
|
96
|
+
|
|
97
|
+
## 配置存储
|
|
98
|
+
|
|
99
|
+
- **配置文件**: `~/.aicli/config.json`
|
|
100
|
+
- **对话历史**: `~/.aicli/history/*.json`
|
|
101
|
+
- **插件目录**: `~/.aicli/plugins/`(暂未实现)
|
|
102
|
+
|
|
103
|
+
当前已配置(用户机器):
|
|
104
|
+
```json
|
|
105
|
+
{
|
|
106
|
+
"defaultProvider": "deepseek",
|
|
107
|
+
"apiKeys": { "deepseek": "sk-d89cdb5490844071ad250996b04dc7b0" },
|
|
108
|
+
"customBaseUrls": { "deepseek": "https://api.deepseek.com" },
|
|
109
|
+
"timeouts": { "deepseek": 60000 }
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## 环境变量(优先级高于配置文件)
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
AICLI_API_KEY_CLAUDE Claude API Key
|
|
117
|
+
AICLI_API_KEY_GEMINI Gemini API Key
|
|
118
|
+
AICLI_API_KEY_DEEPSEEK DeepSeek API Key
|
|
119
|
+
AICLI_API_KEY_ZHIPU 智谱 API Key
|
|
120
|
+
AICLI_API_KEY_KIMI Kimi API Key
|
|
121
|
+
AICLI_PROVIDER 默认 Provider ID
|
|
122
|
+
AICLI_NO_STREAM 设为 1 禁用流式输出
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Tool Use(Agentic 工具调用)架构
|
|
126
|
+
|
|
127
|
+
### 工具一览
|
|
128
|
+
|
|
129
|
+
| 工具名 | 危险级别 | 说明 |
|
|
130
|
+
|---|---|---|
|
|
131
|
+
| `bash` | write/safe/destructive(按命令判断) | PowerShell(Win) / $SHELL(Unix) 执行命令,Windows 强制 UTF-8 |
|
|
132
|
+
| `read_file` | safe | 读取文件内容 |
|
|
133
|
+
| `write_file` | write(需确认) | 写入文件 |
|
|
134
|
+
| `edit_file` | write(需确认) | 精确字符串替换编辑文件 |
|
|
135
|
+
| `list_dir` | safe | 列出目录内容 |
|
|
136
|
+
| `grep_files` | safe | 正则搜索文件内容 |
|
|
137
|
+
| `glob_files` | safe | 按 glob 模式匹配文件路径 |
|
|
138
|
+
| `run_interactive` | safe | spawn 直连可执行文件 + stdin_lines 依次输入,用于交互式程序(猜数游戏等) |
|
|
139
|
+
| `web_fetch` | safe | 抓取网页内容并转为 Markdown |
|
|
140
|
+
| `save_last_response` | write(需确认) | 保存上一次 AI 回答到文件(tee 流式写盘) |
|
|
141
|
+
| `stream_to_file` | write(需确认) | 流式生成大文档(试卷/报告等)直接写入文件,规避 token 截断 |
|
|
142
|
+
|
|
143
|
+
### 危险级别与确认机制
|
|
144
|
+
|
|
145
|
+
`src/tools/types.ts` 的 `getDangerLevel()` 判断:
|
|
146
|
+
- `safe` → 自动执行,无需确认
|
|
147
|
+
- `write` → 显示 `✎ Write operation:` 并等待用户按 `y/N`
|
|
148
|
+
- `destructive` → 显示 `⚠ DESTRUCTIVE operation:` 并等待确认
|
|
149
|
+
|
|
150
|
+
### Agentic 循环(`repl.ts` → `handleChatWithTools`)
|
|
151
|
+
|
|
152
|
+
```
|
|
153
|
+
用户输入 → rl.pause() + rl.output=null(静默 readline)
|
|
154
|
+
→ chatWithTools(messages + toolDefs) → AI 返回 { toolCalls } 或 { content }
|
|
155
|
+
→ 若 toolCalls:executor.executeAll() → buildToolResultMessages() → 下一轮(最多 20 轮)
|
|
156
|
+
→ 若 content:renderResponse() → rl.output=savedOutput + rl.resume() + showPrompt()
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### stdin/readline 关键设计(重要!勿破坏)
|
|
160
|
+
|
|
161
|
+
1. **`rl.output = null`**:在 `line` handler 开始时执行,处理完毕后恢复。
|
|
162
|
+
防止 readline 在 AI 处理期间因 spinner/输出 触发行缓冲区重绘,导致用户输入被重复打印。
|
|
163
|
+
|
|
164
|
+
2. **`confirm()` 读取单键**:使用 `process.stdin.setRawMode(true)` + `data` 事件读取单个字符。
|
|
165
|
+
**禁止**:调用 `rl.question()`(会 pause readline,REPL 随后失去响应);
|
|
166
|
+
**禁止**:额外调用 `stdin.resume()`(会产生残留字节 `yy`);
|
|
167
|
+
**禁止**:在 executor 内操控 `rl.output`(由 repl.ts 统一管理)。
|
|
168
|
+
|
|
169
|
+
3. **`rl.prompt()` 而非 `stdout.write`**:readline 内部追踪 prompt 列宽,backspace 不会吃掉 prompt。
|
|
170
|
+
|
|
171
|
+
4. **`renderer.ts` 中 `renderStream/renderResponse` 不加前置 `\n`**:
|
|
172
|
+
用户按回车后 readline 已换行,spinner stop 后也已换行,再加 `\n` 会触发 readline 重绘。
|
|
173
|
+
|
|
174
|
+
5. **`processing` 标志防止 `rl.on('close')` 误触发退出**:
|
|
175
|
+
`line` handler 是 async 的,readline 不等待它完成。若 AI 处理期间 stdin 意外关闭(如 spinner 操作),
|
|
176
|
+
`close` 事件会提前 resolve Promise 导致 REPL 退出。用 `processing` 布尔标志,在 `close` handler 中判断,
|
|
177
|
+
仅在非处理中时才 resolve。
|
|
178
|
+
|
|
179
|
+
6. **全局异常捕获**(`src/index.ts`):`uncaughtException` + `unhandledRejection` 均打印完整堆栈到 stderr,
|
|
180
|
+
防止未知错误导致静默崩溃。ESM 中这两个 handler 必须写在文件顶部(import 语句之前)才能在模块加载期生效。
|
|
181
|
+
|
|
182
|
+
### Windows 编码处理
|
|
183
|
+
|
|
184
|
+
- **bash 工具**:命令前注入 `[Console]::OutputEncoding = UTF8`;`execSync` 用 `encoding:'buffer'`,手动 `.toString('utf-8')` 解码。
|
|
185
|
+
- **run_interactive 工具**:直接 `spawn(pythonExe, args)`,不经过任何 shell;环境变量 `PYTHONUTF8=1` + `PYTHONIOENCODING=utf-8`,stdout 用 `setEncoding('utf-8')`。
|
|
186
|
+
- **关键**:不要用 `cmd /c` 或 `powershell -Command` 包装 spawn——这会截断 stdin 管道,交互式程序收不到输入。
|
|
187
|
+
|
|
188
|
+
### run_interactive 工具参数容错设计
|
|
189
|
+
|
|
190
|
+
```typescript
|
|
191
|
+
// args 可能是数组(正确)或字符串(AI 传参错误时降级兼容)
|
|
192
|
+
const cmdArgs = Array.isArray(rawArgs) ? rawArgs.map(String) : [rawArgs.trim()];
|
|
193
|
+
// stdin_lines 可能是数组或逗号分隔字符串
|
|
194
|
+
const stdinLines = Array.isArray(rawStdin) ? rawStdin.map(String)
|
|
195
|
+
: rawStdin.split(',').map(s => s.trim());
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## 添加新 Provider
|
|
199
|
+
|
|
200
|
+
1. 在 `src/providers/` 创建新文件(如 `openrouter.ts`)
|
|
201
|
+
2. 继承 `OpenAICompatibleProvider`(OpenAI 兼容)或 `BaseProvider`
|
|
202
|
+
3. 实现 `info` 属性(id, displayName, defaultModel, models 数组)和 `defaultBaseUrl`
|
|
203
|
+
4. 在 `src/providers/registry.ts` 的 `BUILT_IN_PROVIDERS` 数组中添加该类
|
|
204
|
+
|
|
205
|
+
## 添加新 Tool
|
|
206
|
+
|
|
207
|
+
1. 在 `src/tools/builtin/` 创建新文件,实现 `Tool` 接口(`definition` + `execute(args)`)
|
|
208
|
+
2. 在 `src/tools/registry.ts` 的 `constructor` 中 `this.register(newTool)`
|
|
209
|
+
3. 在 `src/tools/types.ts` 的 `getDangerLevel()` 中为新工具设置危险级别
|
|
210
|
+
|
|
211
|
+
## GUI 扩展(Electron/Tauri)
|
|
212
|
+
|
|
213
|
+
- `src/core/`、`src/providers/`、`src/session/`、`src/config/`、`src/tools/` 无终端依赖,可直接被 Electron 主进程导入
|
|
214
|
+
- `src/repl/` 仅是 CLI 层,GUI 时完全替换此目录
|
|
215
|
+
- `EventBus` 作为 Electron IPC 桥接
|
|
216
|
+
- `package.json` 中的 `exports` 字段暴露核心 API 供第三方使用
|
|
217
|
+
|
|
218
|
+
## 代码风格
|
|
219
|
+
|
|
220
|
+
- TypeScript strict 模式
|
|
221
|
+
- 所有文件使用 `.js` 扩展名导入(ESM 要求)
|
|
222
|
+
- Provider 错误统一转换为 `ProviderError` 子类
|
|
223
|
+
- 不在核心层(`src/core/`)引入任何 CLI/终端相关依赖
|
|
224
|
+
|
|
225
|
+
## 已知待改进项(下次继续开发)
|
|
226
|
+
|
|
227
|
+
### 低优先级
|
|
228
|
+
- [ ] **插件系统**:`~/.aicli/plugins/` 目录已规划,加载逻辑未实现。
|
|
229
|
+
- [ ] **macOS/Linux 完整测试**:跨平台逻辑已处理(`$SHELL` fallback、UTF-8 env vars),但尚未在非 Windows 环境实际运行验证。
|
|
230
|
+
- [ ] **Token 用量显示**:`ui.showTokenCount` 配置字段已有,但终端展示逻辑未完整实现。
|
|
231
|
+
- [ ] **GitHub 仓库迁移**:当前在 gitee,npm 包受众需要 GitHub 托管。
|
|
232
|
+
|
|
233
|
+
## 本轮开发完成记录(2026-02-23)
|
|
234
|
+
|
|
235
|
+
### 新增功能
|
|
236
|
+
- **Gemini 完整支持**:`GeminiProvider` 接入代理(undici ProxyAgent)、baseUrl 透传、array 参数 `items` schema 修复(解决 400 Bad Request)、429/网络错误中文提示
|
|
237
|
+
- **代理配置**:`config.json` 新增 `proxy` 字段;`src/index.ts` 的 `setupProxy()` 读取配置+环境变量(优先级:env > config);设置向导新增 `Configure proxy` 入口(支持设置/修改/清除)
|
|
238
|
+
- **`stream_to_file` 工具上线**:注册到 `ToolRegistry`;`repl.ts` 在 `executeAll` 前注入 provider/model/messages/systemPrompt 上下文;`getDangerLevel` 添加 `write` 级别
|
|
239
|
+
- **`/config` REPL 命令**:REPL 内直接调用配置向导,无需退出;`repl.ts` 的 `runSetupWizard` 回调临时恢复 readline 状态供 inquirer 使用
|
|
240
|
+
- **README.md**:创建完整 npm 包文档(Provider 表、工具表、命令表、代理配置、项目上下文说明)
|
|
241
|
+
- **pkg 打包零警告**:`scripts/patch-sqlite.mjs` 在打包前替换 undici 内部 `require("sqlite")` 为 IIFE stub
|
|
242
|
+
- **版本升至 0.1.4**,发布至 npmjs
|
|
243
|
+
|
|
244
|
+
### 架构变更
|
|
245
|
+
- `src/config/schema.ts`:新增 `proxy?: string` 字段
|
|
246
|
+
- `src/repl/commands/index.ts`:`CommandContext` 新增 `runSetupWizard` 回调;新增 `/config` 命令;工具/命令计数更新(11工具、15命令)
|
|
247
|
+
- `src/repl/setup-wizard.ts`:`runFull()` 新增代理配置选项;`input` from `@inquirer/prompts` 用于代理 URL 输入
|
|
248
|
+
|
|
249
|
+
## 本轮开发完成记录(2026-02-22)
|
|
250
|
+
|
|
251
|
+
### 新增功能
|
|
252
|
+
- **全局超时统一为 300s**:`~/.aicli/config.json` 中 `timeouts.deepseek` 60000→300000、`timeouts.kimi` 600000→300000;`modelParams` 中所有模型的 `timeout` 统一为 300000(moonshot-v1-8k 新增 timeout 字段)
|
|
253
|
+
- **跨 session 历史搜索 `/search <keyword>`**:`SessionManager.searchMessages()` 遍历全部历史 JSON,不区分大小写全文匹配,每个 session 最多返回 3 条带上下文的匹配片段,全局最多 20 个 session;REPL 新增 `/search` 命令,结果按 updated 倒序展示,提示用 `/session load <id>` 加载
|
|
254
|
+
- **`run_interactive` 参数容错可观测化**:`args` 或 `stdin_lines` 传入错误类型(string 非 array)时,双路输出警告:① `process.stderr.write`(终端可见);② 工具返回值前缀追加警告文本(AI 可在工具结果中看到),便于持续观察 AI 实际调用行为
|
|
255
|
+
- **`/about` 命令升级**:新增工具列表(10个完整)、REPL 命令列表(14个)、主要特性区块;移除仓库地址(待迁移至 GitHub)
|
|
256
|
+
- **版本号升级至 0.1.2**,`constants.ts` 与 `package.json` 保持同步
|
|
257
|
+
- **发布至 npmjs**:`npm publish` → `jinzd-ai-cli@0.1.2`(https://www.npmjs.com/package/jinzd-ai-cli)
|
|
258
|
+
|
|
259
|
+
## 本轮开发完成记录(2026-02-21)
|
|
260
|
+
|
|
261
|
+
### 新增功能
|
|
262
|
+
- **模型上下文长度展示**:`/model` 选择器 hint 显示 `ctx:128K`;`/status` 新增 `Ctx Win` 行;启动欢迎界面 Model 行显示 `(ctx: 128K)`
|
|
263
|
+
- **Claude/Gemini 工具调用(Agentic)**:`ClaudeProvider` 和 `GeminiProvider` 均实现 `chatWithTools` + `buildToolResultMessages`,现在三大 provider 系列均支持完整 agentic 工具调用能力
|
|
264
|
+
- **工具调用最终回答流式输出**:`handleChatWithTools` 最终 content 轮次改为 `chatStream` 真正流式,`streaming=true` 时打字机效果;`streaming=false` 时保持原有非流式路径
|
|
265
|
+
- **Ctrl+C 取消工具确认**:`confirm()` 等待期间按 Ctrl+C 视为"按 N 取消",不再异常退出 REPL;通过 `ToolExecutor.cancelConfirm()` + SIGINT handler 协作实现
|
|
266
|
+
- **System prompt 注入当前日期时间**:每次会话启动自动注入 `当前日期时间:2026年02月21日 星期六 08:30:00`,所有模型均可感知;使用手动数字拼接(不依赖 locale API,兼容 pkg 精简 ICU 环境)
|
|
267
|
+
- **Kimi 长文本模型超时配置**:`~/.aicli/config.json` 中 `kimi-k2-0711-preview`、`kimi-k2-turbo-preview`、`moonshot-v1-128k` 统一设置 300000ms(5分钟)超时,适合出题等长文本生成场景
|
|
268
|
+
|
|
269
|
+
### 架构变更
|
|
270
|
+
- `repl.ts`:引入 `ToolCapableProvider` 接口(duck typing),`handleChatWithTools` 不再依赖 `OpenAICompatibleProvider` 具体类,Claude/Gemini 自动识别并走 agentic 路径
|
|
271
|
+
- `executor.ts`:新增 `cancelConfirmFn` 私有回调 + `cancelConfirm()` 公开方法
|
|
272
|
+
|
|
273
|
+
## 已解决的重大问题记录
|
|
274
|
+
|
|
275
|
+
| 问题 | 根本原因 | 解决方案 |
|
|
276
|
+
|------|---------|---------|
|
|
277
|
+
| exe 启动后发消息立即崩溃(Segfault) | `ora` 在 `@yao-pkg/pkg` Node 22 exe 中调用底层 TTY 接口触发段错误 | 完全移除 `ora`,用原生 `setInterval + stdout.write` 实现 spinner |
|
|
278
|
+
| punycode 弃用警告 | Node 22 对 `punycode` 内置模块发出 DEP0040 警告 | pkg 打包时加 `--options no-deprecation` |
|
|
279
|
+
| `yy` 双字符回显 | `rl.question()` 内部 echo + 手动 echo 叠加 | 改用 `setRawMode(true)` + `data` 事件读单字符,彻底不用 `rl.question()` |
|
|
280
|
+
| REPL 一轮后退出 | `rl.question()` 内部调用 `rl.pause()`,readline 失去响应 | 移除 `rl.question()`,仅用 raw stdin |
|
|
281
|
+
| 用户输入重复出现 | readline `terminal:true` + spinner 输出触发行缓冲区重绘 | AI 处理期间设 `rlAny.output = null` 禁止 readline 输出 |
|
|
282
|
+
| Windows 中文乱码 | PowerShell 默认 GBK 编码 | bash 工具注入 `[Console]::OutputEncoding=UTF8` + `encoding:'buffer'` 手动解码 |
|
|
283
|
+
| run_interactive stdin 截断 | 通过 shell 包装 spawn 时 stdin pipe 被截断 | 直接 `spawn(pythonExe, args)` 不经过任何 shell |
|
|
284
|
+
| Ctrl+C 在工具确认时异常退出 | SIGINT handler 直接调用 `handleExit()`,未检查 `confirm()` 状态 | SIGINT handler 先检查 `confirming` 标志,是则 `cancelConfirm()` 取消而非退出 |
|
|
285
|
+
| 日期注入在 pkg exe 中显示错误 | `toLocaleDateString/toLocaleTimeString` 依赖完整 ICU,pkg 精简版不支持 | 改为手动数字拼接:`${year}年${month}月${day}日 ${weekday} ${HH}:${mm}:${ss}` |
|
package/README.md
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# ai-cli
|
|
2
|
+
|
|
3
|
+
> 跨平台 REPL 风格 AI 对话工具,支持多 Provider 与 Agentic 工具调用
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/jinzd-ai-cli)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+
## 特性
|
|
9
|
+
|
|
10
|
+
- **多 Provider 支持**:Claude、Gemini、DeepSeek、智谱清言(GLM)、Kimi,可扩展自定义 OpenAI 兼容接口
|
|
11
|
+
- **Agentic 工具调用**:AI 自主执行 bash 命令、读写文件、搜索代码、抓取网页、流式写入大文件
|
|
12
|
+
- **流式输出**:打字机效果,实时展示 AI 响应
|
|
13
|
+
- **会话管理**:自动保存历史,支持跨会话全文搜索
|
|
14
|
+
- **项目上下文注入**:自动读取 `AICLI.md` / `CLAUDE.md` 并注入 system prompt
|
|
15
|
+
- **代理支持**:通过配置或环境变量让 Node.js 走代理(国内用户访问 Gemini/Claude 必备)
|
|
16
|
+
- **独立可执行文件**:打包为单文件 exe,无需安装 Node.js 即可运行
|
|
17
|
+
|
|
18
|
+
## 安装
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install -g jinzd-ai-cli
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
需要 Node.js >= 20。
|
|
25
|
+
|
|
26
|
+
## 快速开始
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
aicli
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
首次运行会进入交互式配置向导,选择 Provider 并输入 API Key。
|
|
33
|
+
|
|
34
|
+
也可运行 `aicli config` 随时修改配置。
|
|
35
|
+
|
|
36
|
+
## 支持的 Provider
|
|
37
|
+
|
|
38
|
+
| Provider | 说明 | 获取 API Key |
|
|
39
|
+
|---|---|---|
|
|
40
|
+
| **Claude** | Anthropic Claude 系列 | [console.anthropic.com](https://console.anthropic.com) |
|
|
41
|
+
| **Gemini** | Google Gemini 系列(含 2.5 Pro/Flash) | [aistudio.google.com](https://aistudio.google.com) |
|
|
42
|
+
| **DeepSeek** | DeepSeek-Chat / Reasoner | [platform.deepseek.com](https://platform.deepseek.com) |
|
|
43
|
+
| **智谱清言** | GLM-4 系列 | [open.bigmodel.cn](https://open.bigmodel.cn) |
|
|
44
|
+
| **Kimi** | Moonshot AI Kimi 系列 | [platform.moonshot.cn](https://platform.moonshot.cn) |
|
|
45
|
+
|
|
46
|
+
## REPL 命令
|
|
47
|
+
|
|
48
|
+
| 命令 | 说明 |
|
|
49
|
+
|---|---|
|
|
50
|
+
| `/provider` | 切换 AI Provider |
|
|
51
|
+
| `/model` | 切换模型 |
|
|
52
|
+
| `/clear` | 清空当前对话 |
|
|
53
|
+
| `/session` | 会话管理(new / list / load) |
|
|
54
|
+
| `/search <关键词>` | 跨会话全文搜索历史记录 |
|
|
55
|
+
| `/system <提示词>` | 设置 System Prompt |
|
|
56
|
+
| `/context` | 查看 / 重新加载项目上下文文件 |
|
|
57
|
+
| `/status` | 查看当前状态(Provider / Model / Token 用量) |
|
|
58
|
+
| `/tools` | 列出所有可用 AI 工具 |
|
|
59
|
+
| `/export` | 导出会话(Markdown / JSON) |
|
|
60
|
+
| `/undo` | 撤销上次文件操作 |
|
|
61
|
+
| `/about` | 显示版本和项目信息 |
|
|
62
|
+
| `/help` | 显示帮助 |
|
|
63
|
+
| `/exit` | 退出 |
|
|
64
|
+
|
|
65
|
+
## AI 工具(Agentic 能力)
|
|
66
|
+
|
|
67
|
+
AI 在对话中可自主调用以下工具:
|
|
68
|
+
|
|
69
|
+
| 工具 | 说明 |
|
|
70
|
+
|---|---|
|
|
71
|
+
| `bash` | 执行 shell 命令(Windows: PowerShell,Unix: $SHELL) |
|
|
72
|
+
| `read_file` | 读取文件内容 |
|
|
73
|
+
| `write_file` | 写入文件(需用户确认) |
|
|
74
|
+
| `edit_file` | 精确替换文件内容(需用户确认) |
|
|
75
|
+
| `list_dir` | 列出目录内容 |
|
|
76
|
+
| `grep_files` | 在文件中搜索文本 |
|
|
77
|
+
| `glob_files` | 按 glob 模式匹配文件 |
|
|
78
|
+
| `run_interactive` | 运行交互式程序并依次输入 |
|
|
79
|
+
| `web_fetch` | 抓取网页内容 |
|
|
80
|
+
| `save_last_response` | 保存上一次 AI 回复到文件 |
|
|
81
|
+
| `stream_to_file` | 流式生成大文档直接写入文件(规避 token 截断) |
|
|
82
|
+
|
|
83
|
+
危险操作(写入 / 删除)在执行前会提示用户确认。
|
|
84
|
+
|
|
85
|
+
## 配置
|
|
86
|
+
|
|
87
|
+
配置文件位于 `~/.aicli/config.json`,也可通过 `aicli config` 交互式修改。
|
|
88
|
+
|
|
89
|
+
### 代理配置(国内用户)
|
|
90
|
+
|
|
91
|
+
Gemini、Claude 等在国内需要代理。支持两种方式:
|
|
92
|
+
|
|
93
|
+
**方式一:配置文件(持久)**
|
|
94
|
+
```json
|
|
95
|
+
{
|
|
96
|
+
"proxy": "http://127.0.0.1:10809"
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
运行 `aicli config` → 选择 `Configure proxy` 即可配置。
|
|
101
|
+
|
|
102
|
+
**方式二:环境变量(临时)**
|
|
103
|
+
```bash
|
|
104
|
+
# Windows
|
|
105
|
+
set HTTPS_PROXY=http://127.0.0.1:10809
|
|
106
|
+
aicli
|
|
107
|
+
|
|
108
|
+
# macOS / Linux
|
|
109
|
+
HTTPS_PROXY=http://127.0.0.1:10809 aicli
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### 项目上下文(类似 CLAUDE.md)
|
|
113
|
+
|
|
114
|
+
在项目目录创建 `AICLI.md`,启动时自动注入到 system prompt:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
echo "这是一个 TypeScript 项目,使用 ESM 模块..." > AICLI.md
|
|
118
|
+
aicli
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
也支持 `CLAUDE.md` 或 `.aicli/context.md`。用 `/context reload` 重新加载。
|
|
122
|
+
|
|
123
|
+
## 子命令
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
aicli # 启动 REPL
|
|
127
|
+
aicli config # 配置向导
|
|
128
|
+
aicli providers # 列出所有 Provider 及配置状态
|
|
129
|
+
aicli sessions # 列出最近会话
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## 下载独立可执行文件
|
|
133
|
+
|
|
134
|
+
无需 Node.js 环境,直接运行:
|
|
135
|
+
|
|
136
|
+
- **Windows**: 从项目 `release/` 目录下载 `ai-cli-win.exe`
|
|
137
|
+
- **macOS**: 下载 `ai-cli-mac`,`chmod +x ai-cli-mac` 后运行
|
|
138
|
+
- **Linux**: 下载 `ai-cli-linux`,`chmod +x ai-cli-linux` 后运行
|
|
139
|
+
|
|
140
|
+
## 环境变量
|
|
141
|
+
|
|
142
|
+
| 变量 | 说明 |
|
|
143
|
+
|---|---|
|
|
144
|
+
| `AICLI_API_KEY_CLAUDE` | Claude API Key |
|
|
145
|
+
| `AICLI_API_KEY_GEMINI` | Gemini API Key |
|
|
146
|
+
| `AICLI_API_KEY_DEEPSEEK` | DeepSeek API Key |
|
|
147
|
+
| `AICLI_API_KEY_ZHIPU` | 智谱 API Key |
|
|
148
|
+
| `AICLI_API_KEY_KIMI` | Kimi API Key |
|
|
149
|
+
| `AICLI_PROVIDER` | 默认 Provider |
|
|
150
|
+
| `AICLI_NO_STREAM` | 设为 `1` 禁用流式输出 |
|
|
151
|
+
| `HTTPS_PROXY` / `HTTP_PROXY` | 代理地址 |
|
|
152
|
+
|
|
153
|
+
## License
|
|
154
|
+
|
|
155
|
+
MIT © 晋正东
|
package/dist/index.js
CHANGED
|
@@ -104,7 +104,7 @@ var EnvLoader = class {
|
|
|
104
104
|
};
|
|
105
105
|
|
|
106
106
|
// src/core/constants.ts
|
|
107
|
-
var VERSION = "0.1.
|
|
107
|
+
var VERSION = "0.1.5";
|
|
108
108
|
var CONFIG_DIR_NAME = ".aicli";
|
|
109
109
|
var CONFIG_FILE_NAME = "config.json";
|
|
110
110
|
var HISTORY_DIR_NAME = "history";
|
|
@@ -1295,7 +1295,7 @@ var SessionManager = class {
|
|
|
1295
1295
|
import * as readline from "readline";
|
|
1296
1296
|
import { existsSync as existsSync12, readFileSync as readFileSync8 } from "fs";
|
|
1297
1297
|
import { join as join7, resolve as resolve3, extname as extname2 } from "path";
|
|
1298
|
-
import
|
|
1298
|
+
import chalk7 from "chalk";
|
|
1299
1299
|
|
|
1300
1300
|
// src/repl/renderer.ts
|
|
1301
1301
|
import chalk from "chalk";
|
|
@@ -1375,7 +1375,7 @@ var Renderer = class {
|
|
|
1375
1375
|
console.log(chalk.dim(" DeepSeek \xB7 Kimi (Moonshot) \xB7 Claude (Anthropic)"));
|
|
1376
1376
|
console.log(chalk.dim(" Gemini (Google) \xB7 \u667A\u8C31\u6E05\u8A00 \xB7 \u81EA\u5B9A\u4E49 OpenAI \u517C\u5BB9"));
|
|
1377
1377
|
console.log(HR);
|
|
1378
|
-
console.log(chalk.gray(" Agentic \u5DE5\u5177\
|
|
1378
|
+
console.log(chalk.gray(" Agentic \u5DE5\u5177\uFF0811\u4E2A\uFF09\uFF1A"));
|
|
1379
1379
|
console.log(tool("bash", "\u6267\u884C Shell \u547D\u4EE4\uFF08PowerShell/bash\uFF0CWindows \u5F3A\u5236 UTF-8\uFF09"));
|
|
1380
1380
|
console.log(tool("read_file", "\u8BFB\u53D6\u6587\u4EF6\u5185\u5BB9"));
|
|
1381
1381
|
console.log(tool("write_file", "\u5199\u5165\u6587\u4EF6\uFF08\u5371\u9669\u7EA7 write\uFF0C\u9700\u786E\u8BA4\uFF09"));
|
|
@@ -1386,11 +1386,12 @@ var Renderer = class {
|
|
|
1386
1386
|
console.log(tool("run_interactive", "\u8FD0\u884C\u9700\u8981 stdin \u4EA4\u4E92\u7684\u7A0B\u5E8F\uFF08spawn \u76F4\u8FDE\uFF09"));
|
|
1387
1387
|
console.log(tool("web_fetch", "\u6293\u53D6\u7F51\u9875\u5185\u5BB9\uFF08\u8F6C Markdown\uFF09"));
|
|
1388
1388
|
console.log(tool("save_last_response", "\u4FDD\u5B58 AI \u56DE\u7B54\u5230\u6587\u4EF6\uFF08tee \u6D41\u5F0F\u5199\u76D8\uFF09"));
|
|
1389
|
+
console.log(tool("stream_to_file", "\u6D41\u5F0F\u751F\u6210\u5927\u6587\u6863\u76F4\u63A5\u5199\u5165\u6587\u4EF6\uFF08\u89C4\u907F token \u622A\u65AD\uFF09"));
|
|
1389
1390
|
console.log(HR);
|
|
1390
|
-
console.log(chalk.gray(" REPL \u547D\u4EE4\
|
|
1391
|
+
console.log(chalk.gray(" REPL \u547D\u4EE4\uFF0815\u4E2A\uFF09\uFF1A"));
|
|
1391
1392
|
console.log(chalk.dim(" /help /about /provider /model /clear /session"));
|
|
1392
1393
|
console.log(chalk.dim(" /system /context /status /search /undo /export"));
|
|
1393
|
-
console.log(chalk.dim(" /tools /exit"));
|
|
1394
|
+
console.log(chalk.dim(" /tools /config /exit"));
|
|
1394
1395
|
console.log(HR);
|
|
1395
1396
|
console.log(chalk.gray(" \u4E3B\u8981\u7279\u6027\uFF1A"));
|
|
1396
1397
|
console.log(feat("Agentic \u5FAA\u73AF\uFF08\u6700\u591A 20 \u8F6E\u5DE5\u5177\u8C03\u7528\uFF0C\u6700\u7EC8\u56DE\u7B54\u6D41\u5F0F\u8F93\u51FA\uFF09"));
|
|
@@ -1658,6 +1659,7 @@ function createDefaultCommands() {
|
|
|
1658
1659
|
" /undo - Undo the last file write/edit operation",
|
|
1659
1660
|
" /export [md|json] [file] - Export session to file (default: auto-named .md)",
|
|
1660
1661
|
" /tools - List all AI tools available",
|
|
1662
|
+
" /config - Open configuration wizard (API keys, proxy, etc.)",
|
|
1661
1663
|
" /exit - Exit"
|
|
1662
1664
|
] : [];
|
|
1663
1665
|
console.log("\nAvailable commands:");
|
|
@@ -2036,6 +2038,14 @@ ${text}
|
|
|
2036
2038
|
}
|
|
2037
2039
|
}
|
|
2038
2040
|
},
|
|
2041
|
+
{
|
|
2042
|
+
name: "config",
|
|
2043
|
+
description: "Open configuration wizard (API keys, proxy, default provider)",
|
|
2044
|
+
usage: "/config",
|
|
2045
|
+
async execute(_args, ctx) {
|
|
2046
|
+
await ctx.runSetupWizard();
|
|
2047
|
+
}
|
|
2048
|
+
},
|
|
2039
2049
|
{
|
|
2040
2050
|
name: "exit",
|
|
2041
2051
|
description: "Exit the REPL",
|
|
@@ -3222,6 +3232,103 @@ var saveLastResponseTool = {
|
|
|
3222
3232
|
}
|
|
3223
3233
|
};
|
|
3224
3234
|
|
|
3235
|
+
// src/tools/builtin/stream-to-file.ts
|
|
3236
|
+
import { createWriteStream as createWriteStream2, mkdirSync as mkdirSync7 } from "fs";
|
|
3237
|
+
import { dirname as dirname5 } from "path";
|
|
3238
|
+
var streamToFileContext = {
|
|
3239
|
+
provider: null,
|
|
3240
|
+
model: "",
|
|
3241
|
+
systemPrompt: void 0,
|
|
3242
|
+
messages: [],
|
|
3243
|
+
extraMessages: []
|
|
3244
|
+
};
|
|
3245
|
+
var streamToFileTool = {
|
|
3246
|
+
definition: {
|
|
3247
|
+
name: "stream_to_file",
|
|
3248
|
+
description: `\u5C06\u5927\u578B\u5185\u5BB9\uFF08\u8D85\u8FC7 5KB \u6216 300 \u884C\u7684\u8BD5\u5377\u3001\u62A5\u544A\u7B49\uFF09\u76F4\u63A5\u4EE5\u6D41\u5F0F\u65B9\u5F0F\u751F\u6210\u5E76\u4FDD\u5B58\u5230\u6587\u4EF6\u3002
|
|
3249
|
+
\u9002\u7528\u573A\u666F\uFF1A\u5F53\u4F60\u9700\u8981\u751F\u6210\u5B8C\u6574\u7684\u6A21\u8003\u8BD5\u9898\uFF08600-700\u884C\uFF09\u3001\u957F\u7BC7\u62A5\u544A\u7B49\u5927\u6587\u6863\u65F6\uFF0C\u5FC5\u987B\u4F7F\u7528\u6B64\u5DE5\u5177\uFF0C
|
|
3250
|
+
\u4E0D\u8981\u5148\u5728\u5BF9\u8BDD\u4E2D\u8F93\u51FA\u518D\u4FDD\u5B58\uFF08\u90A3\u6837\u4F1A\u88AB maxTokens \u622A\u65AD\uFF09\u3002
|
|
3251
|
+
\u4F7F\u7528\u65B9\u6CD5\uFF1A\u4F20\u5165\u76EE\u6807\u6587\u4EF6\u8DEF\u5F84\u548C\u751F\u6210\u6307\u4EE4\uFF0C\u5DE5\u5177\u4F1A\u81EA\u52A8\u5411 AI \u53D1\u8D77\u6D41\u5F0F\u8BF7\u6C42\u5E76\u5C06\u5185\u5BB9\u5B9E\u65F6\u5199\u5165\u6587\u4EF6\u3002
|
|
3252
|
+
\u6CE8\u610F\uFF1Aprompt \u53C2\u6570\u5E94\u5305\u542B\u5B8C\u6574\u7684\u751F\u6210\u6307\u4EE4\uFF0C\u5982"\u8BF7\u751F\u6210\u4E00\u4EFD\u91D1\u6807\u98CE\u683C\u8FDB\u9636\u96BE\u5EA6\u7684\u5B8C\u6574\u6A21\u8003\u8BD5\u9898\uFF0C\u5305\u542B\u6240\u6709\u9898\u76EE\u548C\u53C2\u8003\u7B54\u6848"\u3002`,
|
|
3253
|
+
parameters: {
|
|
3254
|
+
path: {
|
|
3255
|
+
type: "string",
|
|
3256
|
+
description: "\u4FDD\u5B58\u6587\u4EF6\u7684\u8DEF\u5F84\uFF08\u542B\u6587\u4EF6\u540D\uFF09\uFF0C\u5982 exam_papers/20260221-01-\u6A21\u8003-\u8FDB\u9636.md",
|
|
3257
|
+
required: true
|
|
3258
|
+
},
|
|
3259
|
+
prompt: {
|
|
3260
|
+
type: "string",
|
|
3261
|
+
description: "\u751F\u6210\u5185\u5BB9\u7684\u5B8C\u6574\u6307\u4EE4\uFF08\u4F1A\u643A\u5E26\u5F53\u524D\u5BF9\u8BDD\u5386\u53F2\u4F5C\u4E3A\u4E0A\u4E0B\u6587\uFF09",
|
|
3262
|
+
required: true
|
|
3263
|
+
}
|
|
3264
|
+
},
|
|
3265
|
+
dangerous: false
|
|
3266
|
+
},
|
|
3267
|
+
async execute(args) {
|
|
3268
|
+
const filePath = String(args["path"] ?? "");
|
|
3269
|
+
const prompt = String(args["prompt"] ?? "");
|
|
3270
|
+
if (!filePath) throw new Error("path is required");
|
|
3271
|
+
if (!prompt) throw new Error("prompt is required");
|
|
3272
|
+
const ctx = streamToFileContext;
|
|
3273
|
+
if (!ctx.provider) {
|
|
3274
|
+
throw new Error("stream_to_file: provider not initialized");
|
|
3275
|
+
}
|
|
3276
|
+
const requestMessages = [
|
|
3277
|
+
...ctx.messages,
|
|
3278
|
+
{ role: "user", content: prompt, timestamp: /* @__PURE__ */ new Date() }
|
|
3279
|
+
];
|
|
3280
|
+
undoStack.push(filePath, `stream_to_file: ${filePath}`);
|
|
3281
|
+
mkdirSync7(dirname5(filePath), { recursive: true });
|
|
3282
|
+
const writeStream = createWriteStream2(filePath, { encoding: "utf-8" });
|
|
3283
|
+
let totalBytes = 0;
|
|
3284
|
+
let totalChars = 0;
|
|
3285
|
+
const startTime = Date.now();
|
|
3286
|
+
let lastProgressAt = 0;
|
|
3287
|
+
const showProgress = (done) => {
|
|
3288
|
+
const now = Date.now();
|
|
3289
|
+
if (!done && now - lastProgressAt < 200) return;
|
|
3290
|
+
lastProgressAt = now;
|
|
3291
|
+
const elapsed = ((now - startTime) / 1e3).toFixed(1);
|
|
3292
|
+
const kb = (totalBytes / 1024).toFixed(1);
|
|
3293
|
+
process.stdout.write(`\r \u270D Writing ${filePath} ... ${kb} KB (${elapsed}s)`);
|
|
3294
|
+
};
|
|
3295
|
+
try {
|
|
3296
|
+
const stream = ctx.provider.chatStream({
|
|
3297
|
+
messages: requestMessages,
|
|
3298
|
+
model: ctx.model,
|
|
3299
|
+
systemPrompt: ctx.systemPrompt,
|
|
3300
|
+
stream: true,
|
|
3301
|
+
temperature: ctx.temperature,
|
|
3302
|
+
maxTokens: 32768,
|
|
3303
|
+
// 大文档专用,放宽到 32K tokens(约 24000 汉字)
|
|
3304
|
+
timeout: ctx.timeout ?? 3e5,
|
|
3305
|
+
...ctx.extraMessages.length > 0 ? { _extraMessages: ctx.extraMessages } : {}
|
|
3306
|
+
});
|
|
3307
|
+
for await (const chunk of stream) {
|
|
3308
|
+
if (chunk.delta) {
|
|
3309
|
+
const bytes = Buffer.byteLength(chunk.delta, "utf-8");
|
|
3310
|
+
totalBytes += bytes;
|
|
3311
|
+
totalChars += chunk.delta.length;
|
|
3312
|
+
writeStream.write(chunk.delta);
|
|
3313
|
+
showProgress(false);
|
|
3314
|
+
}
|
|
3315
|
+
}
|
|
3316
|
+
await new Promise((resolve4, reject) => {
|
|
3317
|
+
writeStream.end((err) => {
|
|
3318
|
+
if (err) reject(err);
|
|
3319
|
+
else resolve4();
|
|
3320
|
+
});
|
|
3321
|
+
});
|
|
3322
|
+
process.stdout.write("\r\x1B[2K");
|
|
3323
|
+
const lines = totalChars > 0 ? Math.round(totalChars / 40) : 0;
|
|
3324
|
+
return `File saved: ${filePath} (~${lines} lines, ${(totalBytes / 1024).toFixed(1)} KB)`;
|
|
3325
|
+
} catch (err) {
|
|
3326
|
+
writeStream.destroy();
|
|
3327
|
+
throw err;
|
|
3328
|
+
}
|
|
3329
|
+
}
|
|
3330
|
+
};
|
|
3331
|
+
|
|
3225
3332
|
// src/tools/registry.ts
|
|
3226
3333
|
var ToolRegistry = class {
|
|
3227
3334
|
tools = /* @__PURE__ */ new Map();
|
|
@@ -3236,6 +3343,7 @@ var ToolRegistry = class {
|
|
|
3236
3343
|
this.register(runInteractiveTool);
|
|
3237
3344
|
this.register(webFetchTool);
|
|
3238
3345
|
this.register(saveLastResponseTool);
|
|
3346
|
+
this.register(streamToFileTool);
|
|
3239
3347
|
}
|
|
3240
3348
|
register(tool) {
|
|
3241
3349
|
this.tools.set(tool.definition.name, tool);
|
|
@@ -3269,6 +3377,7 @@ function getDangerLevel(toolName, args) {
|
|
|
3269
3377
|
if (toolName === "write_file") return "write";
|
|
3270
3378
|
if (toolName === "edit_file") return "write";
|
|
3271
3379
|
if (toolName === "save_last_response") return "write";
|
|
3380
|
+
if (toolName === "stream_to_file") return "write";
|
|
3272
3381
|
if (toolName === "read_file" || toolName === "list_dir" || toolName === "grep_files" || toolName === "glob_files" || toolName === "run_interactive" || toolName === "web_fetch") return "safe";
|
|
3273
3382
|
return "write";
|
|
3274
3383
|
}
|
|
@@ -3671,6 +3780,169 @@ var ToolExecutor = class {
|
|
|
3671
3780
|
}
|
|
3672
3781
|
};
|
|
3673
3782
|
|
|
3783
|
+
// src/repl/setup-wizard.ts
|
|
3784
|
+
import { password, select, input } from "@inquirer/prompts";
|
|
3785
|
+
import chalk6 from "chalk";
|
|
3786
|
+
var PROVIDERS = [
|
|
3787
|
+
{ value: "claude", name: "Claude (Anthropic)" },
|
|
3788
|
+
{ value: "gemini", name: "Gemini (Google)" },
|
|
3789
|
+
{ value: "deepseek", name: "DeepSeek" },
|
|
3790
|
+
{ value: "zhipu", name: "\u667A\u8C31\u6E05\u8A00 (GLM)" },
|
|
3791
|
+
{ value: "kimi", name: "Kimi (Moonshot AI)" }
|
|
3792
|
+
];
|
|
3793
|
+
function maskKey(key) {
|
|
3794
|
+
if (key.length <= 10) return "****";
|
|
3795
|
+
return key.slice(0, 6) + "****" + key.slice(-4);
|
|
3796
|
+
}
|
|
3797
|
+
var SetupWizard = class {
|
|
3798
|
+
constructor(config) {
|
|
3799
|
+
this.config = config;
|
|
3800
|
+
}
|
|
3801
|
+
async runFirstRun() {
|
|
3802
|
+
console.log(chalk6.bold.cyan("\nWelcome to ai-cli!\n"));
|
|
3803
|
+
console.log("Let's set up your first AI provider.\n");
|
|
3804
|
+
try {
|
|
3805
|
+
const providerId = await select({
|
|
3806
|
+
message: "Which AI provider do you want to set up first?",
|
|
3807
|
+
choices: PROVIDERS
|
|
3808
|
+
});
|
|
3809
|
+
await this.setupProvider(providerId);
|
|
3810
|
+
this.config.set("defaultProvider", providerId);
|
|
3811
|
+
this.config.save();
|
|
3812
|
+
console.log(chalk6.green("\nSetup complete! Starting ai-cli...\n"));
|
|
3813
|
+
return true;
|
|
3814
|
+
} catch {
|
|
3815
|
+
return false;
|
|
3816
|
+
}
|
|
3817
|
+
}
|
|
3818
|
+
async runFull() {
|
|
3819
|
+
console.log(chalk6.bold.cyan("\nai-cli Configuration\n"));
|
|
3820
|
+
let running = true;
|
|
3821
|
+
while (running) {
|
|
3822
|
+
const currentProxy = this.config.get("proxy") ?? "";
|
|
3823
|
+
const proxyStatus = currentProxy ? chalk6.green(`[${currentProxy}]`) : chalk6.gray("[\u672A\u914D\u7F6E]");
|
|
3824
|
+
const action = await select({
|
|
3825
|
+
message: "What would you like to configure?",
|
|
3826
|
+
choices: [
|
|
3827
|
+
{ value: "apikey", name: "Manage API key for a provider" },
|
|
3828
|
+
{ value: "default", name: "Change default provider" },
|
|
3829
|
+
{ value: "proxy", name: `Configure proxy (HTTP/HTTPS) ${proxyStatus}` },
|
|
3830
|
+
{ value: "done", name: "Done" }
|
|
3831
|
+
]
|
|
3832
|
+
});
|
|
3833
|
+
if (action === "proxy") {
|
|
3834
|
+
await this.setupProxy();
|
|
3835
|
+
} else if (action === "apikey") {
|
|
3836
|
+
const choicesWithStatus = PROVIDERS.map((p) => {
|
|
3837
|
+
const existingKey = this.config.getApiKey(p.value);
|
|
3838
|
+
const status = existingKey ? chalk6.green(`[${maskKey(existingKey)}]`) : chalk6.gray("[\u672A\u914D\u7F6E]");
|
|
3839
|
+
return { value: p.value, name: `${p.name} ${status}` };
|
|
3840
|
+
});
|
|
3841
|
+
const providerId = await select({
|
|
3842
|
+
message: "Select provider:",
|
|
3843
|
+
choices: choicesWithStatus
|
|
3844
|
+
});
|
|
3845
|
+
await this.setupProvider(providerId);
|
|
3846
|
+
} else if (action === "default") {
|
|
3847
|
+
const providerId = await select({
|
|
3848
|
+
message: "Select default provider:",
|
|
3849
|
+
choices: PROVIDERS
|
|
3850
|
+
});
|
|
3851
|
+
this.config.set("defaultProvider", providerId);
|
|
3852
|
+
this.config.save();
|
|
3853
|
+
console.log(chalk6.green(`Default provider set to: ${providerId}
|
|
3854
|
+
`));
|
|
3855
|
+
} else {
|
|
3856
|
+
running = false;
|
|
3857
|
+
}
|
|
3858
|
+
}
|
|
3859
|
+
}
|
|
3860
|
+
async setupProvider(providerId) {
|
|
3861
|
+
const provider = PROVIDERS.find((p) => p.value === providerId);
|
|
3862
|
+
const displayName = provider?.name ?? providerId;
|
|
3863
|
+
const existingKey = this.config.getApiKey(providerId);
|
|
3864
|
+
console.log(`
|
|
3865
|
+
Managing ${displayName} API Key`);
|
|
3866
|
+
if (existingKey) {
|
|
3867
|
+
console.log(chalk6.gray(` Current: ${maskKey(existingKey)}`));
|
|
3868
|
+
} else {
|
|
3869
|
+
console.log(chalk6.gray(" Current: (\u672A\u914D\u7F6E)"));
|
|
3870
|
+
}
|
|
3871
|
+
const choices = existingKey ? [
|
|
3872
|
+
{ value: "keep", name: "\u4FDD\u6301\u4E0D\u53D8 (keep current key)" },
|
|
3873
|
+
{ value: "show", name: "\u663E\u793A\u539F\u59CB Key (show full key)" },
|
|
3874
|
+
{ value: "change", name: "\u4FEE\u6539 Key (update key)" }
|
|
3875
|
+
] : [{ value: "change", name: "\u8F93\u5165 Key (enter key)" }, { value: "skip", name: "\u8DF3\u8FC7 (skip)" }];
|
|
3876
|
+
const action = await select({
|
|
3877
|
+
message: "Action:",
|
|
3878
|
+
choices
|
|
3879
|
+
});
|
|
3880
|
+
if (action === "show" && existingKey) {
|
|
3881
|
+
console.log(chalk6.yellow(`
|
|
3882
|
+
\u5B8C\u6574 Key: ${existingKey}
|
|
3883
|
+
`));
|
|
3884
|
+
const updateAfterShow = await select({
|
|
3885
|
+
message: "Would you like to update this key?",
|
|
3886
|
+
choices: [
|
|
3887
|
+
{ value: "keep", name: "\u4FDD\u6301\u4E0D\u53D8 (keep)" },
|
|
3888
|
+
{ value: "change", name: "\u4FEE\u6539 (update)" }
|
|
3889
|
+
]
|
|
3890
|
+
});
|
|
3891
|
+
if (updateAfterShow !== "change") return;
|
|
3892
|
+
} else if (action === "keep" || action === "skip") {
|
|
3893
|
+
return;
|
|
3894
|
+
}
|
|
3895
|
+
const newKey = await password({
|
|
3896
|
+
message: `Enter ${displayName} API key:`,
|
|
3897
|
+
validate: (val) => val.length > 0 || "API key cannot be empty"
|
|
3898
|
+
});
|
|
3899
|
+
this.config.setApiKey(providerId, newKey);
|
|
3900
|
+
console.log(chalk6.green(`API key saved for ${displayName}: ${maskKey(newKey)}
|
|
3901
|
+
`));
|
|
3902
|
+
}
|
|
3903
|
+
async setupProxy() {
|
|
3904
|
+
const current = this.config.get("proxy") ?? "";
|
|
3905
|
+
console.log("\nHTTP/HTTPS Proxy Configuration");
|
|
3906
|
+
console.log(chalk6.gray(" Used for providers that require a proxy (e.g. Gemini in China)"));
|
|
3907
|
+
if (current) {
|
|
3908
|
+
console.log(chalk6.gray(` Current: ${current}`));
|
|
3909
|
+
}
|
|
3910
|
+
const action = await select({
|
|
3911
|
+
message: "Action:",
|
|
3912
|
+
choices: current ? [
|
|
3913
|
+
{ value: "keep", name: "\u4FDD\u6301\u4E0D\u53D8 (keep current)" },
|
|
3914
|
+
{ value: "change", name: "\u4FEE\u6539 (update)" },
|
|
3915
|
+
{ value: "clear", name: "\u6E05\u9664\u4EE3\u7406 (remove proxy)" }
|
|
3916
|
+
] : [
|
|
3917
|
+
{ value: "change", name: "\u8BBE\u7F6E\u4EE3\u7406 (set proxy)" },
|
|
3918
|
+
{ value: "skip", name: "\u8DF3\u8FC7 (skip)" }
|
|
3919
|
+
]
|
|
3920
|
+
});
|
|
3921
|
+
if (action === "keep" || action === "skip") return;
|
|
3922
|
+
if (action === "clear") {
|
|
3923
|
+
this.config.set("proxy", void 0);
|
|
3924
|
+
this.config.save();
|
|
3925
|
+
console.log(chalk6.green("Proxy cleared.\n"));
|
|
3926
|
+
return;
|
|
3927
|
+
}
|
|
3928
|
+
const proxyUrl = await input({
|
|
3929
|
+
message: "Enter proxy URL (e.g. http://127.0.0.1:10809):",
|
|
3930
|
+
default: current || void 0,
|
|
3931
|
+
validate: (val) => {
|
|
3932
|
+
if (!val) return "Proxy URL cannot be empty";
|
|
3933
|
+
if (!val.startsWith("http://") && !val.startsWith("https://")) {
|
|
3934
|
+
return "Must start with http:// or https://";
|
|
3935
|
+
}
|
|
3936
|
+
return true;
|
|
3937
|
+
}
|
|
3938
|
+
});
|
|
3939
|
+
this.config.set("proxy", proxyUrl);
|
|
3940
|
+
this.config.save();
|
|
3941
|
+
console.log(chalk6.green(`Proxy saved: ${proxyUrl}
|
|
3942
|
+
`));
|
|
3943
|
+
}
|
|
3944
|
+
};
|
|
3945
|
+
|
|
3674
3946
|
// src/tools/git-context.ts
|
|
3675
3947
|
import { execSync as execSync2 } from "child_process";
|
|
3676
3948
|
import { existsSync as existsSync11 } from "fs";
|
|
@@ -3766,13 +4038,13 @@ var IMAGE_MIME = {
|
|
|
3766
4038
|
".gif": "image/gif",
|
|
3767
4039
|
".webp": "image/webp"
|
|
3768
4040
|
};
|
|
3769
|
-
function parseAtReferences(
|
|
4041
|
+
function parseAtReferences(input2, cwd) {
|
|
3770
4042
|
const atPattern = /@(?:"([^"]+)"|'([^']+)'|(\S+))/g;
|
|
3771
4043
|
const refs = [];
|
|
3772
4044
|
const imageParts = [];
|
|
3773
|
-
let textBody =
|
|
4045
|
+
let textBody = input2;
|
|
3774
4046
|
let match;
|
|
3775
|
-
while ((match = atPattern.exec(
|
|
4047
|
+
while ((match = atPattern.exec(input2)) !== null) {
|
|
3776
4048
|
const rawPath = match[1] ?? match[2] ?? match[3] ?? "";
|
|
3777
4049
|
const absPath = resolve3(cwd, rawPath);
|
|
3778
4050
|
const ext = extname2(rawPath).toLowerCase();
|
|
@@ -3918,7 +4190,7 @@ ${this.activeSystemPrompt}`;
|
|
|
3918
4190
|
return dateTimeInfo;
|
|
3919
4191
|
}
|
|
3920
4192
|
refreshPrompt() {
|
|
3921
|
-
const promptStr =
|
|
4193
|
+
const promptStr = chalk7.green(`[${this.currentProvider}]`) + chalk7.white(" > ");
|
|
3922
4194
|
this.rl.setPrompt(promptStr);
|
|
3923
4195
|
}
|
|
3924
4196
|
showPrompt() {
|
|
@@ -3949,14 +4221,14 @@ ${this.activeSystemPrompt}`;
|
|
|
3949
4221
|
this.renderer.printWelcome(this.currentProvider, this.currentModel, welcomeModelInfo?.contextWindow);
|
|
3950
4222
|
if (ctx) {
|
|
3951
4223
|
process.stdout.write(
|
|
3952
|
-
|
|
4224
|
+
chalk7.dim(` \u{1F4C4} Project context loaded: ${ctx.filePath} (${ctx.content.length} chars)
|
|
3953
4225
|
`)
|
|
3954
4226
|
);
|
|
3955
4227
|
}
|
|
3956
4228
|
if (gitCtx) {
|
|
3957
|
-
const statusSummary = gitCtx.stagedFiles.length + gitCtx.changedFiles.length > 0 ?
|
|
4229
|
+
const statusSummary = gitCtx.stagedFiles.length + gitCtx.changedFiles.length > 0 ? chalk7.yellow(` (${gitCtx.stagedFiles.length} staged, ${gitCtx.changedFiles.length} modified)`) : chalk7.dim(" (clean)");
|
|
3958
4230
|
process.stdout.write(
|
|
3959
|
-
|
|
4231
|
+
chalk7.dim(` \u{1F500} Git branch: `) + chalk7.cyan(gitCtx.branch) + statusSummary + "\n"
|
|
3960
4232
|
);
|
|
3961
4233
|
}
|
|
3962
4234
|
this.rl.on("SIGINT", () => {
|
|
@@ -3972,8 +4244,8 @@ ${this.activeSystemPrompt}`;
|
|
|
3972
4244
|
this.rl.on("line", async (line) => {
|
|
3973
4245
|
if (this.toolExecutor.confirming) return;
|
|
3974
4246
|
if (this.selecting) return;
|
|
3975
|
-
const
|
|
3976
|
-
if (!
|
|
4247
|
+
const input2 = line.trim();
|
|
4248
|
+
if (!input2) {
|
|
3977
4249
|
this.showPrompt();
|
|
3978
4250
|
return;
|
|
3979
4251
|
}
|
|
@@ -3983,10 +4255,10 @@ ${this.activeSystemPrompt}`;
|
|
|
3983
4255
|
const savedOutput = rlAny.output;
|
|
3984
4256
|
rlAny.output = null;
|
|
3985
4257
|
try {
|
|
3986
|
-
if (
|
|
3987
|
-
await this.handleCommand(
|
|
4258
|
+
if (input2.startsWith("/")) {
|
|
4259
|
+
await this.handleCommand(input2);
|
|
3988
4260
|
} else {
|
|
3989
|
-
await this.handleChat(
|
|
4261
|
+
await this.handleChat(input2);
|
|
3990
4262
|
}
|
|
3991
4263
|
} catch (err) {
|
|
3992
4264
|
this.renderer.renderError(err);
|
|
@@ -4017,13 +4289,13 @@ ${this.activeSystemPrompt}`;
|
|
|
4017
4289
|
const { parts, hasImage, refs } = parseAtReferences(userInput, process.cwd());
|
|
4018
4290
|
for (const ref of refs) {
|
|
4019
4291
|
if (ref.type === "notfound") {
|
|
4020
|
-
process.stdout.write(
|
|
4292
|
+
process.stdout.write(chalk7.yellow(` \u26A0 File not found: ${ref.path}
|
|
4021
4293
|
`));
|
|
4022
4294
|
} else if (ref.type === "image") {
|
|
4023
|
-
process.stdout.write(
|
|
4295
|
+
process.stdout.write(chalk7.dim(` \u{1F4CE} Image: ${ref.path}
|
|
4024
4296
|
`));
|
|
4025
4297
|
} else {
|
|
4026
|
-
process.stdout.write(
|
|
4298
|
+
process.stdout.write(chalk7.dim(` \u{1F4C4} File: ${ref.path}
|
|
4027
4299
|
`));
|
|
4028
4300
|
}
|
|
4029
4301
|
}
|
|
@@ -4032,13 +4304,13 @@ ${this.activeSystemPrompt}`;
|
|
|
4032
4304
|
const visionHint = this.getVisionModelHint();
|
|
4033
4305
|
if (visionHint) {
|
|
4034
4306
|
process.stdout.write(
|
|
4035
|
-
|
|
4307
|
+
chalk7.yellow(` \u2716 Vision not supported \u2013 ${visionHint}
|
|
4036
4308
|
`)
|
|
4037
4309
|
);
|
|
4038
4310
|
return;
|
|
4039
4311
|
}
|
|
4040
4312
|
process.stdout.write(
|
|
4041
|
-
|
|
4313
|
+
chalk7.dim(` \u{1F5BC} Vision request \u2013 sending image to ${this.currentProvider}
|
|
4042
4314
|
`)
|
|
4043
4315
|
);
|
|
4044
4316
|
}
|
|
@@ -4263,6 +4535,13 @@ ${this.activeSystemPrompt}`;
|
|
|
4263
4535
|
return;
|
|
4264
4536
|
}
|
|
4265
4537
|
}
|
|
4538
|
+
streamToFileContext.provider = provider;
|
|
4539
|
+
streamToFileContext.model = this.currentModel;
|
|
4540
|
+
streamToFileContext.systemPrompt = systemPrompt;
|
|
4541
|
+
streamToFileContext.messages = apiMessages;
|
|
4542
|
+
streamToFileContext.extraMessages = extraMessages;
|
|
4543
|
+
streamToFileContext.temperature = modelParams.temperature;
|
|
4544
|
+
streamToFileContext.timeout = modelParams.timeout;
|
|
4266
4545
|
const toolResults = await this.toolExecutor.executeAll(result.toolCalls);
|
|
4267
4546
|
const newMsgs = provider.buildToolResultMessages(result.toolCalls, toolResults);
|
|
4268
4547
|
extraMessages.push(...newMsgs);
|
|
@@ -4279,8 +4558,8 @@ ${this.activeSystemPrompt}`;
|
|
|
4279
4558
|
spinner.stop();
|
|
4280
4559
|
}
|
|
4281
4560
|
}
|
|
4282
|
-
async handleCommand(
|
|
4283
|
-
const parts =
|
|
4561
|
+
async handleCommand(input2) {
|
|
4562
|
+
const parts = input2.slice(1).split(" ");
|
|
4284
4563
|
const cmdName = parts[0].toLowerCase();
|
|
4285
4564
|
const args = parts.slice(1);
|
|
4286
4565
|
const cmd = this.commands.get(cmdName);
|
|
@@ -4338,6 +4617,18 @@ ${this.activeSystemPrompt}`;
|
|
|
4338
4617
|
},
|
|
4339
4618
|
getSessionTokenUsage: () => ({ ...this.sessionTokenUsage }),
|
|
4340
4619
|
getGitBranch: () => this.gitBranch,
|
|
4620
|
+
runSetupWizard: async () => {
|
|
4621
|
+
const rlAny = this.rl;
|
|
4622
|
+
rlAny.output = process.stdout;
|
|
4623
|
+
process.stdin.resume();
|
|
4624
|
+
try {
|
|
4625
|
+
const wizard = new SetupWizard(this.config);
|
|
4626
|
+
await wizard.runFull();
|
|
4627
|
+
} finally {
|
|
4628
|
+
rlAny.output = null;
|
|
4629
|
+
process.stdin.pause();
|
|
4630
|
+
}
|
|
4631
|
+
},
|
|
4341
4632
|
exit: () => this.handleExit()
|
|
4342
4633
|
};
|
|
4343
4634
|
await cmd.execute(args, ctx);
|
|
@@ -4349,128 +4640,11 @@ ${this.activeSystemPrompt}`;
|
|
|
4349
4640
|
this.events.emit("session.end", { sessionId });
|
|
4350
4641
|
}
|
|
4351
4642
|
this.rl.close();
|
|
4352
|
-
console.log(
|
|
4643
|
+
console.log(chalk7.gray("\nGoodbye!"));
|
|
4353
4644
|
process.exit(0);
|
|
4354
4645
|
}
|
|
4355
4646
|
};
|
|
4356
4647
|
|
|
4357
|
-
// src/repl/setup-wizard.ts
|
|
4358
|
-
import { password, select } from "@inquirer/prompts";
|
|
4359
|
-
import chalk7 from "chalk";
|
|
4360
|
-
var PROVIDERS = [
|
|
4361
|
-
{ value: "claude", name: "Claude (Anthropic)" },
|
|
4362
|
-
{ value: "gemini", name: "Gemini (Google)" },
|
|
4363
|
-
{ value: "deepseek", name: "DeepSeek" },
|
|
4364
|
-
{ value: "zhipu", name: "\u667A\u8C31\u6E05\u8A00 (GLM)" },
|
|
4365
|
-
{ value: "kimi", name: "Kimi (Moonshot AI)" }
|
|
4366
|
-
];
|
|
4367
|
-
function maskKey(key) {
|
|
4368
|
-
if (key.length <= 10) return "****";
|
|
4369
|
-
return key.slice(0, 6) + "****" + key.slice(-4);
|
|
4370
|
-
}
|
|
4371
|
-
var SetupWizard = class {
|
|
4372
|
-
constructor(config) {
|
|
4373
|
-
this.config = config;
|
|
4374
|
-
}
|
|
4375
|
-
async runFirstRun() {
|
|
4376
|
-
console.log(chalk7.bold.cyan("\nWelcome to ai-cli!\n"));
|
|
4377
|
-
console.log("Let's set up your first AI provider.\n");
|
|
4378
|
-
try {
|
|
4379
|
-
const providerId = await select({
|
|
4380
|
-
message: "Which AI provider do you want to set up first?",
|
|
4381
|
-
choices: PROVIDERS
|
|
4382
|
-
});
|
|
4383
|
-
await this.setupProvider(providerId);
|
|
4384
|
-
this.config.set("defaultProvider", providerId);
|
|
4385
|
-
this.config.save();
|
|
4386
|
-
console.log(chalk7.green("\nSetup complete! Starting ai-cli...\n"));
|
|
4387
|
-
return true;
|
|
4388
|
-
} catch {
|
|
4389
|
-
return false;
|
|
4390
|
-
}
|
|
4391
|
-
}
|
|
4392
|
-
async runFull() {
|
|
4393
|
-
console.log(chalk7.bold.cyan("\nai-cli Configuration\n"));
|
|
4394
|
-
let running = true;
|
|
4395
|
-
while (running) {
|
|
4396
|
-
const action = await select({
|
|
4397
|
-
message: "What would you like to configure?",
|
|
4398
|
-
choices: [
|
|
4399
|
-
{ value: "apikey", name: "Manage API key for a provider" },
|
|
4400
|
-
{ value: "default", name: "Change default provider" },
|
|
4401
|
-
{ value: "done", name: "Done" }
|
|
4402
|
-
]
|
|
4403
|
-
});
|
|
4404
|
-
if (action === "apikey") {
|
|
4405
|
-
const choicesWithStatus = PROVIDERS.map((p) => {
|
|
4406
|
-
const existingKey = this.config.getApiKey(p.value);
|
|
4407
|
-
const status = existingKey ? chalk7.green(`[${maskKey(existingKey)}]`) : chalk7.gray("[\u672A\u914D\u7F6E]");
|
|
4408
|
-
return { value: p.value, name: `${p.name} ${status}` };
|
|
4409
|
-
});
|
|
4410
|
-
const providerId = await select({
|
|
4411
|
-
message: "Select provider:",
|
|
4412
|
-
choices: choicesWithStatus
|
|
4413
|
-
});
|
|
4414
|
-
await this.setupProvider(providerId);
|
|
4415
|
-
} else if (action === "default") {
|
|
4416
|
-
const providerId = await select({
|
|
4417
|
-
message: "Select default provider:",
|
|
4418
|
-
choices: PROVIDERS
|
|
4419
|
-
});
|
|
4420
|
-
this.config.set("defaultProvider", providerId);
|
|
4421
|
-
this.config.save();
|
|
4422
|
-
console.log(chalk7.green(`Default provider set to: ${providerId}
|
|
4423
|
-
`));
|
|
4424
|
-
} else {
|
|
4425
|
-
running = false;
|
|
4426
|
-
}
|
|
4427
|
-
}
|
|
4428
|
-
}
|
|
4429
|
-
async setupProvider(providerId) {
|
|
4430
|
-
const provider = PROVIDERS.find((p) => p.value === providerId);
|
|
4431
|
-
const displayName = provider?.name ?? providerId;
|
|
4432
|
-
const existingKey = this.config.getApiKey(providerId);
|
|
4433
|
-
console.log(`
|
|
4434
|
-
Managing ${displayName} API Key`);
|
|
4435
|
-
if (existingKey) {
|
|
4436
|
-
console.log(chalk7.gray(` Current: ${maskKey(existingKey)}`));
|
|
4437
|
-
} else {
|
|
4438
|
-
console.log(chalk7.gray(" Current: (\u672A\u914D\u7F6E)"));
|
|
4439
|
-
}
|
|
4440
|
-
const choices = existingKey ? [
|
|
4441
|
-
{ value: "keep", name: "\u4FDD\u6301\u4E0D\u53D8 (keep current key)" },
|
|
4442
|
-
{ value: "show", name: "\u663E\u793A\u539F\u59CB Key (show full key)" },
|
|
4443
|
-
{ value: "change", name: "\u4FEE\u6539 Key (update key)" }
|
|
4444
|
-
] : [{ value: "change", name: "\u8F93\u5165 Key (enter key)" }, { value: "skip", name: "\u8DF3\u8FC7 (skip)" }];
|
|
4445
|
-
const action = await select({
|
|
4446
|
-
message: "Action:",
|
|
4447
|
-
choices
|
|
4448
|
-
});
|
|
4449
|
-
if (action === "show" && existingKey) {
|
|
4450
|
-
console.log(chalk7.yellow(`
|
|
4451
|
-
\u5B8C\u6574 Key: ${existingKey}
|
|
4452
|
-
`));
|
|
4453
|
-
const updateAfterShow = await select({
|
|
4454
|
-
message: "Would you like to update this key?",
|
|
4455
|
-
choices: [
|
|
4456
|
-
{ value: "keep", name: "\u4FDD\u6301\u4E0D\u53D8 (keep)" },
|
|
4457
|
-
{ value: "change", name: "\u4FEE\u6539 (update)" }
|
|
4458
|
-
]
|
|
4459
|
-
});
|
|
4460
|
-
if (updateAfterShow !== "change") return;
|
|
4461
|
-
} else if (action === "keep" || action === "skip") {
|
|
4462
|
-
return;
|
|
4463
|
-
}
|
|
4464
|
-
const newKey = await password({
|
|
4465
|
-
message: `Enter ${displayName} API key:`,
|
|
4466
|
-
validate: (val) => val.length > 0 || "API key cannot be empty"
|
|
4467
|
-
});
|
|
4468
|
-
this.config.setApiKey(providerId, newKey);
|
|
4469
|
-
console.log(chalk7.green(`API key saved for ${displayName}: ${maskKey(newKey)}
|
|
4470
|
-
`));
|
|
4471
|
-
}
|
|
4472
|
-
};
|
|
4473
|
-
|
|
4474
4648
|
// src/core/event-bus.ts
|
|
4475
4649
|
import { EventEmitter } from "events";
|
|
4476
4650
|
var EventBus = class extends EventEmitter {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jinzd-ai-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "Cross-platform REPL-style AI CLI with multi-provider support",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -46,7 +46,8 @@
|
|
|
46
46
|
},
|
|
47
47
|
"files": [
|
|
48
48
|
"dist",
|
|
49
|
-
"README.md"
|
|
49
|
+
"README.md",
|
|
50
|
+
"CLAUDE.md"
|
|
50
51
|
],
|
|
51
52
|
"keywords": [
|
|
52
53
|
"ai",
|