jinzd-ai-cli 0.1.13 → 0.1.15
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 +13 -5
- package/dist/index.js +423 -67
- package/package.json +1 -1
package/CLAUDE.md
CHANGED
|
@@ -47,14 +47,17 @@ src/
|
|
|
47
47
|
│ └── index.ts # CommandRegistry + 15个命令(/help /about /provider /model /clear /session /system /context /status /search /undo /export /tools /config /exit)
|
|
48
48
|
└── tools/
|
|
49
49
|
├── types.ts # ToolDefinition / ToolCall / ToolResult / DangerLevel / getDangerLevel
|
|
50
|
-
├── registry.ts # ToolRegistry(注册全部内置工具,共
|
|
50
|
+
├── registry.ts # ToolRegistry(注册全部内置工具,共14个)
|
|
51
51
|
├── executor.ts # ToolExecutor(确认逻辑 + printToolCall/printToolResult)
|
|
52
52
|
└── builtin/
|
|
53
53
|
├── bash.ts # bash 工具(Windows: PowerShell + Buffer→UTF-8;Unix: $SHELL)
|
|
54
54
|
├── read-file.ts # read_file 工具
|
|
55
55
|
├── write-file.ts # write_file 工具
|
|
56
56
|
├── list-dir.ts # list_dir 工具
|
|
57
|
-
|
|
57
|
+
├── run-interactive.ts # run_interactive 工具(spawn 直连可执行文件,stdin_lines 依次输入)
|
|
58
|
+
├── ask-user.ts # ask_user 工具(agentic 循环中向用户提问,等待文本回答)
|
|
59
|
+
├── write-todos.ts # write_todos 工具(任务拆解与进度跟踪,终端实时渲染)
|
|
60
|
+
└── google-search.ts # google_search 工具(Google Custom Search API 搜索网页)
|
|
58
61
|
```
|
|
59
62
|
|
|
60
63
|
## 常用命令
|
|
@@ -122,6 +125,8 @@ AICLI_API_KEY_GEMINI Gemini API Key
|
|
|
122
125
|
AICLI_API_KEY_DEEPSEEK DeepSeek API Key
|
|
123
126
|
AICLI_API_KEY_ZHIPU 智谱 API Key
|
|
124
127
|
AICLI_API_KEY_KIMI Kimi API Key
|
|
128
|
+
AICLI_API_KEY_GOOGLESEARCH Google Custom Search API Key
|
|
129
|
+
AICLI_GOOGLE_CX Google Search Engine ID (cx)
|
|
125
130
|
AICLI_PROVIDER 默认 Provider ID
|
|
126
131
|
AICLI_NO_STREAM 设为 1 禁用流式输出
|
|
127
132
|
```
|
|
@@ -168,6 +173,9 @@ AICLI_NO_STREAM 设为 1 禁用流式输出
|
|
|
168
173
|
| `web_fetch` | safe | 抓取网页内容并转为 Markdown(含私有 IP 防 SSRF) |
|
|
169
174
|
| `save_last_response` | write(需确认) | 保存上一次 AI 回答到文件(tee 流式写盘) |
|
|
170
175
|
| `save_memory` | safe | 将重要信息追加到 `~/.aicli/memory.md`,跨会话持久化,启动时自动注入 system prompt |
|
|
176
|
+
| `ask_user` | safe | AI 在 agentic 循环中向用户提问,等待文本回答后继续执行 |
|
|
177
|
+
| `write_todos` | safe | AI 拆解复杂任务为子任务列表,终端实时渲染进度(pending/in_progress/completed) |
|
|
178
|
+
| `google_search` | safe | Google Custom Search API 搜索网页,需配置 API Key + Search Engine ID (cx) |
|
|
171
179
|
|
|
172
180
|
### 危险级别与确认机制
|
|
173
181
|
|
|
@@ -259,11 +267,11 @@ const stdinLines = Array.isArray(rawStdin) ? rawStdin.map(String)
|
|
|
259
267
|
- [x] **层级上下文文件系统**(2026-02-22):全局 `~/.aicli/AICLI.md` + 项目 `<git-root>/AICLI.md` + 子目录 `<cwd>/AICLI.md` 三层级自动发现、拼接注入 system prompt。`/context` 命令显示各层详情。
|
|
260
268
|
- [x] **持久记忆系统 `save_memory`**(2026-02-22):AI 可调用 `save_memory` 工具将重要信息追加到 `~/.aicli/memory.md`,跨会话自动加载注入 system prompt。支持时间戳、大小限制(10K 字符截取最新)。
|
|
261
269
|
- [x] **开发状态交接系统**(2026-02-22):`/provider` 和 `/model` 切换时自动调用当前 AI 生成结构化开发状态快照(7 段式:当前任务/已完成/进行中/关键决策/修改文件/下一步/备注),保存到 `~/.aicli/dev-state.md`,新模型启动时自动注入 system prompt 实现无缝交接。同值选择不触发任何变更。`/clear` 和 `/session new` 清除快照。
|
|
270
|
+
- [x] **`ask_user` 工具**(2026-02-23):AI 在 agentic 循环中可调用 `ask_user` 暂停执行、向用户显示问题、等待文本输入后继续。复用 `confirm()` 的 readline 模式(output 保存/恢复、resume/pause、once('line')),支持 Ctrl+C 取消。主循环 line handler 增加 `askUserContext.prompting` 守卫。
|
|
271
|
+
- [x] **`write_todos` 工具**(2026-02-23):AI 拆解复杂任务为子任务列表,终端实时渲染进度。参数采用 JSON 字符串(因 ToolParameterSchema 不支持嵌套对象数组),容错处理 AI 直接传数组。`execute()` 内直接 `console.log()` 渲染(绕过 executor 8 行截断),模块级变量保持会话内状态。
|
|
272
|
+
- [x] **Google 搜索集成**(2026-02-23):`google_search` 工具通过 Google Custom Search JSON API 搜索网页。需配置 API Key(`apiKeys['google-search']` 或 `AICLI_API_KEY_GOOGLESEARCH`)和 Search Engine ID(`googleSearchEngineId` 或 `AICLI_GOOGLE_CX`)。自动走全局 proxy,15s 超时,返回 Markdown 格式结果列表。配置向导新增 `Configure Google Search` 入口。
|
|
262
273
|
|
|
263
274
|
### 下一步待实现
|
|
264
|
-
- [ ] **`ask_user` 工具**:AI 在执行过程中向用户请求澄清或确认,增强代理交互能力
|
|
265
|
-
- [ ] **`write_todos` 工具**:AI 自动拆解复杂任务为子任务列表,跟踪进度
|
|
266
|
-
- [ ] **Google 搜索集成**:AI 可主动搜索网页验证事实(需接入搜索 API)
|
|
267
275
|
- [ ] **MCP 协议支持**:Model Context Protocol 扩展生态,接入外部工具
|
|
268
276
|
- [ ] **Agent Skills 系统**:可复用的专业技能包(`.aicli/skills/`)
|
|
269
277
|
|
package/dist/index.js
CHANGED
|
@@ -70,6 +70,10 @@ var ConfigSchema = z.object({
|
|
|
70
70
|
// 默认按顺序查找:AICLI.md → CLAUDE.md → .aicli/context.md
|
|
71
71
|
// 设为 false 可禁用此功能
|
|
72
72
|
contextFile: z.union([z.string(), z.literal(false)]).default("auto"),
|
|
73
|
+
// Google Custom Search API 的 Search Engine ID (cx 参数)
|
|
74
|
+
// API Key 通过 apiKeys['google-search'] 或 AICLI_API_KEY_GOOGLESEARCH 环境变量配置
|
|
75
|
+
// CX 也可通过 AICLI_GOOGLE_CX 环境变量覆盖
|
|
76
|
+
googleSearchEngineId: z.string().optional(),
|
|
73
77
|
// 插件加载开关(安全控制)
|
|
74
78
|
// 默认 false:不自动加载 ~/.aicli/plugins/ 中的插件文件。
|
|
75
79
|
// 插件以完整 Node.js 权限在主进程中执行(可读写文件、访问网络、执行命令),
|
|
@@ -84,7 +88,8 @@ var ENV_KEY_MAP = {
|
|
|
84
88
|
gemini: "AICLI_API_KEY_GEMINI",
|
|
85
89
|
deepseek: "AICLI_API_KEY_DEEPSEEK",
|
|
86
90
|
zhipu: "AICLI_API_KEY_ZHIPU",
|
|
87
|
-
kimi: "AICLI_API_KEY_KIMI"
|
|
91
|
+
kimi: "AICLI_API_KEY_KIMI",
|
|
92
|
+
"google-search": "AICLI_API_KEY_GOOGLESEARCH"
|
|
88
93
|
};
|
|
89
94
|
var EnvLoader = class {
|
|
90
95
|
/**
|
|
@@ -107,6 +112,10 @@ var EnvLoader = class {
|
|
|
107
112
|
static isStreamingDisabled() {
|
|
108
113
|
return process.env["AICLI_NO_STREAM"] === "1";
|
|
109
114
|
}
|
|
115
|
+
/** Google Custom Search Engine ID (cx) 环境变量 */
|
|
116
|
+
static getGoogleSearchEngineId() {
|
|
117
|
+
return process.env["AICLI_GOOGLE_CX"] || void 0;
|
|
118
|
+
}
|
|
110
119
|
};
|
|
111
120
|
|
|
112
121
|
// src/core/constants.ts
|
|
@@ -1345,7 +1354,7 @@ var SessionManager = class {
|
|
|
1345
1354
|
import * as readline from "readline";
|
|
1346
1355
|
import { existsSync as existsSync15, readFileSync as readFileSync10 } from "fs";
|
|
1347
1356
|
import { join as join10, resolve as resolve4, extname as extname3 } from "path";
|
|
1348
|
-
import
|
|
1357
|
+
import chalk9 from "chalk";
|
|
1349
1358
|
|
|
1350
1359
|
// src/repl/renderer.ts
|
|
1351
1360
|
import chalk from "chalk";
|
|
@@ -1425,7 +1434,7 @@ var Renderer = class {
|
|
|
1425
1434
|
console.log(chalk.dim(" DeepSeek \xB7 Kimi (Moonshot) \xB7 Claude (Anthropic)"));
|
|
1426
1435
|
console.log(chalk.dim(" Gemini (Google) \xB7 \u667A\u8C31\u6E05\u8A00 \xB7 \u81EA\u5B9A\u4E49 OpenAI \u517C\u5BB9"));
|
|
1427
1436
|
console.log(HR);
|
|
1428
|
-
const toolTotal =
|
|
1437
|
+
const toolTotal = 14 + pluginCount;
|
|
1429
1438
|
const toolLabel = pluginCount > 0 ? `\uFF0C\u542B ${pluginCount} \u4E2A\u63D2\u4EF6` : "\uFF0C\u63D2\u4EF6\u53EF\u6269\u5C55";
|
|
1430
1439
|
console.log(chalk.gray(` Agentic \u5DE5\u5177\uFF08${toolTotal}\u4E2A${toolLabel}\uFF09\uFF1A`));
|
|
1431
1440
|
console.log(tool("bash", "\u6267\u884C Shell \u547D\u4EE4\uFF08PowerShell/bash\uFF0CWindows \u5F3A\u5236 UTF-8\uFF09"));
|
|
@@ -1437,8 +1446,11 @@ var Renderer = class {
|
|
|
1437
1446
|
console.log(tool("glob_files", "\u6309 glob \u6A21\u5F0F\u5339\u914D\u6587\u4EF6\u8DEF\u5F84"));
|
|
1438
1447
|
console.log(tool("run_interactive", "\u8FD0\u884C\u9700\u8981 stdin \u4EA4\u4E92\u7684\u7A0B\u5E8F\uFF08spawn \u76F4\u8FDE\uFF09"));
|
|
1439
1448
|
console.log(tool("web_fetch", "\u6293\u53D6\u7F51\u9875\u5185\u5BB9\uFF08\u8F6C Markdown\uFF09"));
|
|
1449
|
+
console.log(tool("google_search", "Google \u641C\u7D22\uFF08\u9700\u914D\u7F6E API Key + CX\uFF09"));
|
|
1440
1450
|
console.log(tool("save_last_response", "\u4FDD\u5B58 AI \u56DE\u7B54\u5230\u6587\u4EF6\uFF08tee \u6D41\u5F0F\u5199\u76D8\uFF09"));
|
|
1441
1451
|
console.log(tool("save_memory", "\u5C06\u91CD\u8981\u4FE1\u606F\u6301\u4E45\u5316\u5230 ~/.aicli/memory.md\uFF0C\u8DE8\u4F1A\u8BDD\u81EA\u52A8\u6CE8\u5165"));
|
|
1452
|
+
console.log(tool("ask_user", "\u5411\u7528\u6237\u63D0\u95EE\u5E76\u7B49\u5F85\u56DE\u7B54\uFF08agentic \u5FAA\u73AF\u4E2D\u8BF7\u6C42\u6F84\u6E05\uFF09"));
|
|
1453
|
+
console.log(tool("write_todos", "\u62C6\u89E3\u4EFB\u52A1\u4E3A\u5B50\u4EFB\u52A1\u5217\u8868\uFF0C\u5B9E\u65F6\u663E\u793A\u8FDB\u5EA6"));
|
|
1442
1454
|
console.log(HR);
|
|
1443
1455
|
console.log(chalk.gray(" REPL \u547D\u4EE4\uFF0816\u4E2A\uFF09\uFF1A"));
|
|
1444
1456
|
console.log(chalk.dim(" /help /about /provider /model /clear /session"));
|
|
@@ -3509,6 +3521,276 @@ ${content}
|
|
|
3509
3521
|
}
|
|
3510
3522
|
};
|
|
3511
3523
|
|
|
3524
|
+
// src/tools/builtin/ask-user.ts
|
|
3525
|
+
import chalk4 from "chalk";
|
|
3526
|
+
var askUserContext = {
|
|
3527
|
+
prompting: false
|
|
3528
|
+
};
|
|
3529
|
+
var askUserTool = {
|
|
3530
|
+
definition: {
|
|
3531
|
+
name: "ask_user",
|
|
3532
|
+
description: "Ask the user a question and wait for their text response. Use this when you need clarification, confirmation, or any information from the user before proceeding with a task. The user will see the question in the terminal and can type a response. Returns the user's answer as text.",
|
|
3533
|
+
parameters: {
|
|
3534
|
+
question: {
|
|
3535
|
+
type: "string",
|
|
3536
|
+
description: "The question to ask the user. Be clear and specific.",
|
|
3537
|
+
required: true
|
|
3538
|
+
}
|
|
3539
|
+
},
|
|
3540
|
+
dangerous: false
|
|
3541
|
+
},
|
|
3542
|
+
async execute(args) {
|
|
3543
|
+
const question = String(args["question"] ?? "").trim();
|
|
3544
|
+
if (!question) throw new Error("question parameter is required");
|
|
3545
|
+
if (!askUserContext.rl) {
|
|
3546
|
+
throw new Error("ask_user is not available in this context (readline not initialized)");
|
|
3547
|
+
}
|
|
3548
|
+
const answer = await promptUser(askUserContext.rl, question);
|
|
3549
|
+
if (answer === null) {
|
|
3550
|
+
return "User did not respond (cancelled).";
|
|
3551
|
+
}
|
|
3552
|
+
return `User response: ${answer}`;
|
|
3553
|
+
}
|
|
3554
|
+
};
|
|
3555
|
+
function promptUser(rl, question) {
|
|
3556
|
+
const rlAny = rl;
|
|
3557
|
+
const savedOutput = rlAny.output;
|
|
3558
|
+
rlAny.output = process.stdout;
|
|
3559
|
+
rl.resume();
|
|
3560
|
+
askUserContext.prompting = true;
|
|
3561
|
+
console.log();
|
|
3562
|
+
console.log(chalk4.cyan("\u2753 ") + chalk4.bold(question));
|
|
3563
|
+
process.stdout.write(chalk4.cyan("> "));
|
|
3564
|
+
return new Promise((resolve5) => {
|
|
3565
|
+
let completed = false;
|
|
3566
|
+
const cleanup = (answer) => {
|
|
3567
|
+
if (completed) return;
|
|
3568
|
+
completed = true;
|
|
3569
|
+
rl.removeListener("line", onLine);
|
|
3570
|
+
askUserContext.cancelFn = void 0;
|
|
3571
|
+
rl.pause();
|
|
3572
|
+
rlAny.output = savedOutput;
|
|
3573
|
+
askUserContext.prompting = false;
|
|
3574
|
+
resolve5(answer);
|
|
3575
|
+
};
|
|
3576
|
+
const onLine = (line) => {
|
|
3577
|
+
cleanup(line);
|
|
3578
|
+
};
|
|
3579
|
+
askUserContext.cancelFn = () => {
|
|
3580
|
+
process.stdout.write(chalk4.gray("\n(cancelled)\n"));
|
|
3581
|
+
cleanup(null);
|
|
3582
|
+
};
|
|
3583
|
+
rl.once("line", onLine);
|
|
3584
|
+
});
|
|
3585
|
+
}
|
|
3586
|
+
|
|
3587
|
+
// src/tools/builtin/write-todos.ts
|
|
3588
|
+
import chalk5 from "chalk";
|
|
3589
|
+
var VALID_STATUSES = /* @__PURE__ */ new Set(["pending", "in_progress", "completed"]);
|
|
3590
|
+
var currentTodos = [];
|
|
3591
|
+
var writeTodosTool = {
|
|
3592
|
+
definition: {
|
|
3593
|
+
name: "write_todos",
|
|
3594
|
+
description: `Create or update a task list to track progress on complex tasks. Pass the COMPLETE list of todos each time (full replacement, not incremental). The list will be rendered in the terminal so the user can see progress. Use this proactively when working on multi-step tasks to show the user what you're doing.
|
|
3595
|
+
Parameter format: JSON array string, e.g. [{"title":"Read config","status":"completed"},{"title":"Parse args","status":"in_progress"},{"title":"Run tests","status":"pending"}]
|
|
3596
|
+
Valid statuses: pending, in_progress, completed.`,
|
|
3597
|
+
parameters: {
|
|
3598
|
+
todos: {
|
|
3599
|
+
type: "string",
|
|
3600
|
+
description: 'JSON array of todo objects. Each object must have "title" (string) and "status" ("pending"|"in_progress"|"completed"). Example: [{"title":"Step 1","status":"completed"},{"title":"Step 2","status":"in_progress"}]',
|
|
3601
|
+
required: true
|
|
3602
|
+
}
|
|
3603
|
+
},
|
|
3604
|
+
dangerous: false
|
|
3605
|
+
},
|
|
3606
|
+
async execute(args) {
|
|
3607
|
+
const raw = args["todos"];
|
|
3608
|
+
let parsed;
|
|
3609
|
+
if (typeof raw === "string") {
|
|
3610
|
+
const trimmed = raw.trim();
|
|
3611
|
+
if (!trimmed) throw new Error("todos parameter is required");
|
|
3612
|
+
try {
|
|
3613
|
+
parsed = JSON.parse(trimmed);
|
|
3614
|
+
} catch (err) {
|
|
3615
|
+
throw new Error(`Invalid JSON in todos parameter: ${err.message}`);
|
|
3616
|
+
}
|
|
3617
|
+
} else if (Array.isArray(raw)) {
|
|
3618
|
+
parsed = raw;
|
|
3619
|
+
} else {
|
|
3620
|
+
throw new Error("todos parameter must be a JSON array string");
|
|
3621
|
+
}
|
|
3622
|
+
if (!Array.isArray(parsed)) {
|
|
3623
|
+
throw new Error("todos must be a JSON array");
|
|
3624
|
+
}
|
|
3625
|
+
const todos = parsed.map((item, i) => {
|
|
3626
|
+
if (typeof item !== "object" || item === null) {
|
|
3627
|
+
throw new Error(`todos[${i}] must be an object`);
|
|
3628
|
+
}
|
|
3629
|
+
const obj = item;
|
|
3630
|
+
const title = String(obj["title"] ?? "").trim();
|
|
3631
|
+
const status = String(obj["status"] ?? "").trim();
|
|
3632
|
+
if (!title) throw new Error(`todos[${i}].title is required`);
|
|
3633
|
+
if (!VALID_STATUSES.has(status)) {
|
|
3634
|
+
throw new Error(`todos[${i}].status must be one of: pending, in_progress, completed (got "${status}")`);
|
|
3635
|
+
}
|
|
3636
|
+
return { title, status };
|
|
3637
|
+
});
|
|
3638
|
+
currentTodos = todos;
|
|
3639
|
+
renderTodoList(todos);
|
|
3640
|
+
const completed = todos.filter((t) => t.status === "completed").length;
|
|
3641
|
+
const inProgress = todos.filter((t) => t.status === "in_progress").length;
|
|
3642
|
+
const pending = todos.filter((t) => t.status === "pending").length;
|
|
3643
|
+
return `Todo list updated: ${todos.length} items (${completed} completed, ${inProgress} in progress, ${pending} pending)`;
|
|
3644
|
+
}
|
|
3645
|
+
};
|
|
3646
|
+
function renderTodoList(todos) {
|
|
3647
|
+
const completed = todos.filter((t) => t.status === "completed").length;
|
|
3648
|
+
const total = todos.length;
|
|
3649
|
+
console.log();
|
|
3650
|
+
console.log(
|
|
3651
|
+
chalk5.bold.cyan("\u{1F4CB} Todo List") + chalk5.dim(` (${completed}/${total} completed)`)
|
|
3652
|
+
);
|
|
3653
|
+
console.log(chalk5.dim(" " + "\u2500".repeat(40)));
|
|
3654
|
+
for (const todo of todos) {
|
|
3655
|
+
let icon;
|
|
3656
|
+
let text;
|
|
3657
|
+
switch (todo.status) {
|
|
3658
|
+
case "completed":
|
|
3659
|
+
icon = chalk5.green(" \u2713 ");
|
|
3660
|
+
text = chalk5.strikethrough.gray(todo.title);
|
|
3661
|
+
break;
|
|
3662
|
+
case "in_progress":
|
|
3663
|
+
icon = chalk5.yellow(" \u2192 ");
|
|
3664
|
+
text = chalk5.white(todo.title);
|
|
3665
|
+
break;
|
|
3666
|
+
case "pending":
|
|
3667
|
+
default:
|
|
3668
|
+
icon = chalk5.gray(" \u25CB ");
|
|
3669
|
+
text = chalk5.gray(todo.title);
|
|
3670
|
+
break;
|
|
3671
|
+
}
|
|
3672
|
+
console.log(icon + text);
|
|
3673
|
+
}
|
|
3674
|
+
console.log();
|
|
3675
|
+
}
|
|
3676
|
+
|
|
3677
|
+
// src/tools/builtin/google-search.ts
|
|
3678
|
+
var GOOGLE_SEARCH_API = "https://www.googleapis.com/customsearch/v1";
|
|
3679
|
+
var REQUEST_TIMEOUT_MS = 15e3;
|
|
3680
|
+
var MAX_RESULTS = 10;
|
|
3681
|
+
var DEFAULT_RESULTS = 5;
|
|
3682
|
+
var googleSearchContext = {};
|
|
3683
|
+
var googleSearchTool = {
|
|
3684
|
+
definition: {
|
|
3685
|
+
name: "google_search",
|
|
3686
|
+
description: "Search the web using Google Custom Search API. Returns titles, URLs, and descriptions of search results. Use this to look up current information, verify facts, find documentation, or research topics that require up-to-date web data.",
|
|
3687
|
+
parameters: {
|
|
3688
|
+
query: {
|
|
3689
|
+
type: "string",
|
|
3690
|
+
description: "The search query string.",
|
|
3691
|
+
required: true
|
|
3692
|
+
},
|
|
3693
|
+
num_results: {
|
|
3694
|
+
type: "number",
|
|
3695
|
+
description: "Number of results to return (1-10, default 5).",
|
|
3696
|
+
required: false
|
|
3697
|
+
}
|
|
3698
|
+
},
|
|
3699
|
+
dangerous: false
|
|
3700
|
+
},
|
|
3701
|
+
async execute(args) {
|
|
3702
|
+
const query = String(args["query"] ?? "").trim();
|
|
3703
|
+
if (!query) throw new Error("query parameter is required");
|
|
3704
|
+
const numResults = Math.min(
|
|
3705
|
+
Math.max(Math.floor(Number(args["num_results"] ?? DEFAULT_RESULTS)), 1),
|
|
3706
|
+
MAX_RESULTS
|
|
3707
|
+
);
|
|
3708
|
+
const { apiKey, cx } = resolveConfig();
|
|
3709
|
+
const url = new URL(GOOGLE_SEARCH_API);
|
|
3710
|
+
url.searchParams.set("key", apiKey);
|
|
3711
|
+
url.searchParams.set("cx", cx);
|
|
3712
|
+
url.searchParams.set("q", query);
|
|
3713
|
+
url.searchParams.set("num", String(numResults));
|
|
3714
|
+
const controller = new AbortController();
|
|
3715
|
+
const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
3716
|
+
try {
|
|
3717
|
+
const response = await fetch(url.toString(), {
|
|
3718
|
+
method: "GET",
|
|
3719
|
+
headers: {
|
|
3720
|
+
"Accept": "application/json"
|
|
3721
|
+
},
|
|
3722
|
+
signal: controller.signal
|
|
3723
|
+
});
|
|
3724
|
+
if (!response.ok) {
|
|
3725
|
+
const errorBody = await response.text().catch(() => "");
|
|
3726
|
+
if (response.status === 403) {
|
|
3727
|
+
throw new Error(
|
|
3728
|
+
`Google Search API 403 Forbidden \u2014 API Key \u65E0\u6548\u6216\u5DF2\u8D85\u51FA\u6BCF\u65E5\u514D\u8D39\u989D\u5EA6\uFF08100\u6B21/\u5929\uFF09\u3002
|
|
3729
|
+
\u8BF7\u68C0\u67E5 API Key \u548C Search Engine ID \u914D\u7F6E\u3002`
|
|
3730
|
+
);
|
|
3731
|
+
}
|
|
3732
|
+
if (response.status === 429) {
|
|
3733
|
+
throw new Error("Google Search API 429 \u2014 \u8BF7\u6C42\u8FC7\u4E8E\u9891\u7E41\uFF0C\u8BF7\u7A0D\u540E\u518D\u8BD5\u3002");
|
|
3734
|
+
}
|
|
3735
|
+
throw new Error(
|
|
3736
|
+
`Google Search API error: HTTP ${response.status} ${response.statusText}
|
|
3737
|
+
${errorBody.slice(0, 500)}`
|
|
3738
|
+
);
|
|
3739
|
+
}
|
|
3740
|
+
const data = await response.json();
|
|
3741
|
+
return formatResults(query, data, numResults);
|
|
3742
|
+
} catch (err) {
|
|
3743
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
3744
|
+
throw new Error(`Google Search \u8BF7\u6C42\u8D85\u65F6\uFF08${REQUEST_TIMEOUT_MS / 1e3}s\uFF09\uFF0C\u8BF7\u68C0\u67E5\u7F51\u7EDC\u8FDE\u63A5\u6216\u4EE3\u7406\u914D\u7F6E\u3002`);
|
|
3745
|
+
}
|
|
3746
|
+
throw err;
|
|
3747
|
+
} finally {
|
|
3748
|
+
clearTimeout(timeout);
|
|
3749
|
+
}
|
|
3750
|
+
}
|
|
3751
|
+
};
|
|
3752
|
+
function resolveConfig() {
|
|
3753
|
+
let apiKey;
|
|
3754
|
+
let cx;
|
|
3755
|
+
if (googleSearchContext.configManager) {
|
|
3756
|
+
apiKey = googleSearchContext.configManager.getApiKey("google-search");
|
|
3757
|
+
cx = EnvLoader.getGoogleSearchEngineId() ?? googleSearchContext.configManager.get("googleSearchEngineId");
|
|
3758
|
+
} else {
|
|
3759
|
+
apiKey = EnvLoader.getApiKey("google-search");
|
|
3760
|
+
cx = EnvLoader.getGoogleSearchEngineId();
|
|
3761
|
+
}
|
|
3762
|
+
if (!apiKey) {
|
|
3763
|
+
throw new Error(
|
|
3764
|
+
'Google Search API Key \u672A\u914D\u7F6E\u3002\n\u8BF7\u901A\u8FC7\u4EE5\u4E0B\u4EFB\u4E00\u65B9\u5F0F\u914D\u7F6E\uFF1A\n 1. \u8FD0\u884C /config \u2192 Configure Google Search\n 2. \u8BBE\u7F6E\u73AF\u5883\u53D8\u91CF AICLI_API_KEY_GOOGLESEARCH\n 3. \u5728 ~/.aicli/config.json \u4E2D\u6DFB\u52A0 apiKeys["google-search"]'
|
|
3765
|
+
);
|
|
3766
|
+
}
|
|
3767
|
+
if (!cx) {
|
|
3768
|
+
throw new Error(
|
|
3769
|
+
"Google Search Engine ID (cx) \u672A\u914D\u7F6E\u3002\n\u8BF7\u901A\u8FC7\u4EE5\u4E0B\u4EFB\u4E00\u65B9\u5F0F\u914D\u7F6E\uFF1A\n 1. \u8FD0\u884C /config \u2192 Configure Google Search\n 2. \u8BBE\u7F6E\u73AF\u5883\u53D8\u91CF AICLI_GOOGLE_CX\n 3. \u5728 ~/.aicli/config.json \u4E2D\u6DFB\u52A0 googleSearchEngineId\n\n\u83B7\u53D6\u65B9\u5F0F\uFF1Ahttps://programmablesearchengine.google.com/ \u2192 \u521B\u5EFA\u641C\u7D22\u5F15\u64CE \u2192 \u590D\u5236 Search Engine ID"
|
|
3770
|
+
);
|
|
3771
|
+
}
|
|
3772
|
+
return { apiKey, cx };
|
|
3773
|
+
}
|
|
3774
|
+
function formatResults(query, data, requested) {
|
|
3775
|
+
const items = data.items ?? [];
|
|
3776
|
+
if (items.length === 0) {
|
|
3777
|
+
const info2 = data.searchInformation;
|
|
3778
|
+
return `No results found for: "${query}"` + (info2 ? ` (searched ${info2.formattedTotalResults ?? "0"} pages in ${info2.formattedSearchTime ?? "?"}s)` : "");
|
|
3779
|
+
}
|
|
3780
|
+
const info = data.searchInformation;
|
|
3781
|
+
const header = `Search results for "${query}" (${items.length} of ~${info?.formattedTotalResults ?? "?"} results):
|
|
3782
|
+
`;
|
|
3783
|
+
const results = items.map((item, i) => {
|
|
3784
|
+
const title = item.title ?? "Untitled";
|
|
3785
|
+
const link = item.link ?? "";
|
|
3786
|
+
const snippet = item.snippet ?? "";
|
|
3787
|
+
return `${i + 1}. **${title}**
|
|
3788
|
+
URL: ${link}
|
|
3789
|
+
${snippet}`;
|
|
3790
|
+
});
|
|
3791
|
+
return header + "\n" + results.join("\n\n");
|
|
3792
|
+
}
|
|
3793
|
+
|
|
3512
3794
|
// src/tools/registry.ts
|
|
3513
3795
|
import { pathToFileURL } from "url";
|
|
3514
3796
|
import { existsSync as existsSync11, mkdirSync as mkdirSync8, readdirSync as readdirSync5 } from "fs";
|
|
@@ -3528,6 +3810,9 @@ var ToolRegistry = class {
|
|
|
3528
3810
|
this.register(webFetchTool);
|
|
3529
3811
|
this.register(saveLastResponseTool);
|
|
3530
3812
|
this.register(saveMemoryTool);
|
|
3813
|
+
this.register(askUserTool);
|
|
3814
|
+
this.register(writeTodosTool);
|
|
3815
|
+
this.register(googleSearchTool);
|
|
3531
3816
|
}
|
|
3532
3817
|
register(tool) {
|
|
3533
3818
|
this.tools.set(tool.definition.name, tool);
|
|
@@ -3618,7 +3903,7 @@ var ToolRegistry = class {
|
|
|
3618
3903
|
};
|
|
3619
3904
|
|
|
3620
3905
|
// src/tools/executor.ts
|
|
3621
|
-
import
|
|
3906
|
+
import chalk7 from "chalk";
|
|
3622
3907
|
import { existsSync as existsSync12, readFileSync as readFileSync8 } from "fs";
|
|
3623
3908
|
|
|
3624
3909
|
// src/tools/types.ts
|
|
@@ -3637,12 +3922,12 @@ function getDangerLevel(toolName, args) {
|
|
|
3637
3922
|
if (toolName === "write_file") return "write";
|
|
3638
3923
|
if (toolName === "edit_file") return "write";
|
|
3639
3924
|
if (toolName === "save_last_response") return "write";
|
|
3640
|
-
if (toolName === "read_file" || toolName === "list_dir" || toolName === "grep_files" || toolName === "glob_files" || toolName === "run_interactive" || toolName === "web_fetch" || toolName === "save_memory") return "safe";
|
|
3925
|
+
if (toolName === "read_file" || toolName === "list_dir" || toolName === "grep_files" || toolName === "glob_files" || toolName === "run_interactive" || toolName === "web_fetch" || toolName === "save_memory" || toolName === "ask_user" || toolName === "write_todos" || toolName === "google_search") return "safe";
|
|
3641
3926
|
return "write";
|
|
3642
3927
|
}
|
|
3643
3928
|
|
|
3644
3929
|
// src/tools/diff-utils.ts
|
|
3645
|
-
import
|
|
3930
|
+
import chalk6 from "chalk";
|
|
3646
3931
|
function renderDiff(oldText, newText, opts = {}) {
|
|
3647
3932
|
const contextLines = opts.contextLines ?? 3;
|
|
3648
3933
|
const maxLines = opts.maxLines ?? 120;
|
|
@@ -3651,21 +3936,21 @@ function renderDiff(oldText, newText, opts = {}) {
|
|
|
3651
3936
|
const newLines = newText.split("\n");
|
|
3652
3937
|
const hunks = computeHunks(oldLines, newLines, contextLines);
|
|
3653
3938
|
if (hunks.length === 0) {
|
|
3654
|
-
return
|
|
3939
|
+
return chalk6.dim(" (no changes)");
|
|
3655
3940
|
}
|
|
3656
3941
|
const output = [];
|
|
3657
3942
|
if (filePath) {
|
|
3658
|
-
output.push(
|
|
3659
|
-
output.push(
|
|
3943
|
+
output.push(chalk6.bold.white(`--- ${filePath} (before)`));
|
|
3944
|
+
output.push(chalk6.bold.white(`+++ ${filePath} (after)`));
|
|
3660
3945
|
}
|
|
3661
3946
|
let totalDisplayed = 0;
|
|
3662
3947
|
for (const hunk of hunks) {
|
|
3663
3948
|
if (totalDisplayed >= maxLines) {
|
|
3664
|
-
output.push(
|
|
3949
|
+
output.push(chalk6.dim(` ... (diff truncated, too many changes)`));
|
|
3665
3950
|
break;
|
|
3666
3951
|
}
|
|
3667
3952
|
output.push(
|
|
3668
|
-
|
|
3953
|
+
chalk6.cyan(
|
|
3669
3954
|
`@@ -${hunk.oldStart + 1},${hunk.oldCount} +${hunk.newStart + 1},${hunk.newCount} @@`
|
|
3670
3955
|
)
|
|
3671
3956
|
);
|
|
@@ -3673,11 +3958,11 @@ function renderDiff(oldText, newText, opts = {}) {
|
|
|
3673
3958
|
if (totalDisplayed >= maxLines) break;
|
|
3674
3959
|
totalDisplayed++;
|
|
3675
3960
|
if (line.type === "context") {
|
|
3676
|
-
output.push(
|
|
3961
|
+
output.push(chalk6.dim(` ${line.text}`));
|
|
3677
3962
|
} else if (line.type === "remove") {
|
|
3678
|
-
output.push(
|
|
3963
|
+
output.push(chalk6.red(`- ${line.text}`));
|
|
3679
3964
|
} else {
|
|
3680
|
-
output.push(
|
|
3965
|
+
output.push(chalk6.green(`+ ${line.text}`));
|
|
3681
3966
|
}
|
|
3682
3967
|
}
|
|
3683
3968
|
}
|
|
@@ -3902,9 +4187,9 @@ var ToolExecutor = class {
|
|
|
3902
4187
|
printToolCall(call) {
|
|
3903
4188
|
const dangerLevel = getDangerLevel(call.name, call.arguments);
|
|
3904
4189
|
console.log();
|
|
3905
|
-
const icon = dangerLevel === "write" ?
|
|
3906
|
-
const roundBadge = this.totalRounds > 0 ?
|
|
3907
|
-
console.log(icon +
|
|
4190
|
+
const icon = dangerLevel === "write" ? chalk7.yellow("\u270E Tool: ") : chalk7.bold.cyan("\u2699 Tool: ");
|
|
4191
|
+
const roundBadge = this.totalRounds > 0 ? chalk7.dim(` [${this.round}/${this.totalRounds}]`) : "";
|
|
4192
|
+
console.log(icon + chalk7.white(call.name) + roundBadge);
|
|
3908
4193
|
for (const [key, val] of Object.entries(call.arguments)) {
|
|
3909
4194
|
let valStr;
|
|
3910
4195
|
if (Array.isArray(val)) {
|
|
@@ -3915,7 +4200,7 @@ var ToolExecutor = class {
|
|
|
3915
4200
|
} else {
|
|
3916
4201
|
valStr = String(val);
|
|
3917
4202
|
}
|
|
3918
|
-
console.log(
|
|
4203
|
+
console.log(chalk7.gray(` ${key}: `) + chalk7.white(valStr));
|
|
3919
4204
|
}
|
|
3920
4205
|
}
|
|
3921
4206
|
/**
|
|
@@ -3937,19 +4222,19 @@ var ToolExecutor = class {
|
|
|
3937
4222
|
return;
|
|
3938
4223
|
}
|
|
3939
4224
|
if (oldContent === newContent) {
|
|
3940
|
-
console.log(
|
|
4225
|
+
console.log(chalk7.dim(" (file content unchanged)"));
|
|
3941
4226
|
return;
|
|
3942
4227
|
}
|
|
3943
4228
|
const diff = renderDiff(oldContent, newContent, { filePath, contextLines: 3 });
|
|
3944
|
-
console.log(
|
|
4229
|
+
console.log(chalk7.dim(" \u2500\u2500 diff preview \u2500\u2500"));
|
|
3945
4230
|
console.log(diff);
|
|
3946
4231
|
console.log();
|
|
3947
4232
|
} else {
|
|
3948
4233
|
const lines = newContent.split("\n");
|
|
3949
|
-
const preview = lines.slice(0, 20).map((l) =>
|
|
3950
|
-
const more = lines.length > 20 ?
|
|
4234
|
+
const preview = lines.slice(0, 20).map((l) => chalk7.green(`+ ${l}`)).join("\n");
|
|
4235
|
+
const more = lines.length > 20 ? chalk7.dim(`
|
|
3951
4236
|
... (+${lines.length - 20} more lines)`) : "";
|
|
3952
|
-
console.log(
|
|
4237
|
+
console.log(chalk7.dim(" \u2500\u2500 new file preview \u2500\u2500"));
|
|
3953
4238
|
console.log(preview + more);
|
|
3954
4239
|
console.log();
|
|
3955
4240
|
}
|
|
@@ -3964,17 +4249,17 @@ var ToolExecutor = class {
|
|
|
3964
4249
|
String(newStr ?? ""),
|
|
3965
4250
|
{ filePath, contextLines: 2 }
|
|
3966
4251
|
);
|
|
3967
|
-
console.log(
|
|
4252
|
+
console.log(chalk7.dim(" \u2500\u2500 diff preview \u2500\u2500"));
|
|
3968
4253
|
console.log(diff);
|
|
3969
4254
|
console.log();
|
|
3970
4255
|
} else if (call.arguments["insert_after_line"] !== void 0) {
|
|
3971
4256
|
const line = Number(call.arguments["insert_after_line"]);
|
|
3972
4257
|
const insertContent = String(call.arguments["insert_content"] ?? "");
|
|
3973
4258
|
const insertLines = insertContent.split("\n");
|
|
3974
|
-
const preview = insertLines.slice(0, 5).map((l) =>
|
|
3975
|
-
const more = insertLines.length > 5 ?
|
|
4259
|
+
const preview = insertLines.slice(0, 5).map((l) => chalk7.green(`+ ${l}`)).join("\n");
|
|
4260
|
+
const more = insertLines.length > 5 ? chalk7.dim(`
|
|
3976
4261
|
... (+${insertLines.length - 5} more lines)`) : "";
|
|
3977
|
-
console.log(
|
|
4262
|
+
console.log(chalk7.dim(` \u2500\u2500 insert after line ${line} \u2500\u2500`));
|
|
3978
4263
|
console.log(preview + more);
|
|
3979
4264
|
console.log();
|
|
3980
4265
|
} else if (call.arguments["delete_from_line"] !== void 0) {
|
|
@@ -3988,10 +4273,10 @@ var ToolExecutor = class {
|
|
|
3988
4273
|
}
|
|
3989
4274
|
const fileLines = fileContent.split("\n");
|
|
3990
4275
|
const deleted = fileLines.slice(from - 1, to);
|
|
3991
|
-
const preview = deleted.slice(0, 5).map((l) =>
|
|
3992
|
-
const more = deleted.length > 5 ?
|
|
4276
|
+
const preview = deleted.slice(0, 5).map((l) => chalk7.red(`- ${l}`)).join("\n");
|
|
4277
|
+
const more = deleted.length > 5 ? chalk7.dim(`
|
|
3993
4278
|
... (-${deleted.length - 5} more lines)`) : "";
|
|
3994
|
-
console.log(
|
|
4279
|
+
console.log(chalk7.dim(` \u2500\u2500 delete lines ${from}\u2013${to} \u2500\u2500`));
|
|
3995
4280
|
console.log(preview + more);
|
|
3996
4281
|
console.log();
|
|
3997
4282
|
}
|
|
@@ -3999,27 +4284,27 @@ var ToolExecutor = class {
|
|
|
3999
4284
|
}
|
|
4000
4285
|
printToolResult(name, content, isError, wasTruncated) {
|
|
4001
4286
|
if (isError) {
|
|
4002
|
-
console.log(
|
|
4287
|
+
console.log(chalk7.red(`\u26A0 ${name} error: `) + chalk7.gray(content.slice(0, 300)));
|
|
4003
4288
|
} else {
|
|
4004
4289
|
const lines = content.split("\n");
|
|
4005
4290
|
const maxLines = name === "run_interactive" ? 40 : 8;
|
|
4006
4291
|
const preview = lines.slice(0, maxLines).join("\n");
|
|
4007
|
-
const moreLines = lines.length > maxLines ?
|
|
4292
|
+
const moreLines = lines.length > maxLines ? chalk7.gray(`
|
|
4008
4293
|
... (${lines.length - maxLines} more lines)`) : "";
|
|
4009
|
-
const truncatedNote = wasTruncated ?
|
|
4294
|
+
const truncatedNote = wasTruncated ? chalk7.yellow(`
|
|
4010
4295
|
\u26A1 \u8F93\u51FA\u5DF2\u622A\u65AD\u81F3 ${MAX_TOOL_OUTPUT_CHARS} \u5B57\u7B26\u540E\u8FD4\u56DE\u7ED9 AI`) : "";
|
|
4011
|
-
console.log(
|
|
4296
|
+
console.log(chalk7.green("\u2713 Result: ") + chalk7.gray(preview) + moreLines + truncatedNote);
|
|
4012
4297
|
}
|
|
4013
4298
|
console.log();
|
|
4014
4299
|
}
|
|
4015
4300
|
confirm(call, level) {
|
|
4016
|
-
const color = level === "destructive" ?
|
|
4301
|
+
const color = level === "destructive" ? chalk7.red : chalk7.yellow;
|
|
4017
4302
|
const label = level === "destructive" ? "\u26A0 DESTRUCTIVE" : "\u270E Write";
|
|
4018
4303
|
console.log();
|
|
4019
|
-
console.log(color(`${label} operation: `) +
|
|
4304
|
+
console.log(color(`${label} operation: `) + chalk7.bold(call.name));
|
|
4020
4305
|
for (const [key, val] of Object.entries(call.arguments)) {
|
|
4021
4306
|
const valStr = typeof val === "string" && val.length > 200 ? val.slice(0, 200) + "..." : String(val);
|
|
4022
|
-
console.log(
|
|
4307
|
+
console.log(chalk7.gray(` ${key}: `) + valStr);
|
|
4023
4308
|
}
|
|
4024
4309
|
if (!this.rl) {
|
|
4025
4310
|
process.stdout.write(color("No readline: auto-rejected.\n"));
|
|
@@ -4043,7 +4328,7 @@ var ToolExecutor = class {
|
|
|
4043
4328
|
};
|
|
4044
4329
|
const onLine = (line) => cleanup(line.trim().toLowerCase());
|
|
4045
4330
|
this.cancelConfirmFn = () => {
|
|
4046
|
-
process.stdout.write(
|
|
4331
|
+
process.stdout.write(chalk7.gray("\n(cancelled)\n"));
|
|
4047
4332
|
cleanup("n");
|
|
4048
4333
|
};
|
|
4049
4334
|
rl.once("line", onLine);
|
|
@@ -4062,7 +4347,7 @@ var streamToFileContext = {
|
|
|
4062
4347
|
|
|
4063
4348
|
// src/repl/setup-wizard.ts
|
|
4064
4349
|
import { password, select, input } from "@inquirer/prompts";
|
|
4065
|
-
import
|
|
4350
|
+
import chalk8 from "chalk";
|
|
4066
4351
|
var PROVIDERS = [
|
|
4067
4352
|
{ value: "claude", name: "Claude (Anthropic)" },
|
|
4068
4353
|
{ value: "gemini", name: "Gemini (Google)" },
|
|
@@ -4079,7 +4364,7 @@ var SetupWizard = class {
|
|
|
4079
4364
|
this.config = config;
|
|
4080
4365
|
}
|
|
4081
4366
|
async runFirstRun() {
|
|
4082
|
-
console.log(
|
|
4367
|
+
console.log(chalk8.bold.cyan("\nWelcome to ai-cli!\n"));
|
|
4083
4368
|
console.log("Let's set up your first AI provider.\n");
|
|
4084
4369
|
try {
|
|
4085
4370
|
const providerId = await select({
|
|
@@ -4089,33 +4374,37 @@ var SetupWizard = class {
|
|
|
4089
4374
|
await this.setupProvider(providerId);
|
|
4090
4375
|
this.config.set("defaultProvider", providerId);
|
|
4091
4376
|
this.config.save();
|
|
4092
|
-
console.log(
|
|
4377
|
+
console.log(chalk8.green("\nSetup complete! Starting ai-cli...\n"));
|
|
4093
4378
|
return true;
|
|
4094
4379
|
} catch {
|
|
4095
4380
|
return false;
|
|
4096
4381
|
}
|
|
4097
4382
|
}
|
|
4098
4383
|
async runFull() {
|
|
4099
|
-
console.log(
|
|
4384
|
+
console.log(chalk8.bold.cyan("\nai-cli Configuration\n"));
|
|
4100
4385
|
let running = true;
|
|
4101
4386
|
while (running) {
|
|
4102
4387
|
const currentProxy = this.config.get("proxy") ?? "";
|
|
4103
|
-
const proxyStatus = currentProxy ?
|
|
4388
|
+
const proxyStatus = currentProxy ? chalk8.green(`[${currentProxy}]`) : chalk8.gray("[\u672A\u914D\u7F6E]");
|
|
4389
|
+
const googleKeyStatus = this.config.getApiKey("google-search") ? chalk8.green("[\u5DF2\u914D\u7F6E]") : chalk8.gray("[\u672A\u914D\u7F6E]");
|
|
4104
4390
|
const action = await select({
|
|
4105
4391
|
message: "What would you like to configure?",
|
|
4106
4392
|
choices: [
|
|
4107
4393
|
{ value: "apikey", name: "Manage API key for a provider" },
|
|
4108
4394
|
{ value: "default", name: "Change default provider" },
|
|
4109
4395
|
{ value: "proxy", name: `Configure proxy (HTTP/HTTPS) ${proxyStatus}` },
|
|
4396
|
+
{ value: "google", name: `Configure Google Search (API Key + CX) ${googleKeyStatus}` },
|
|
4110
4397
|
{ value: "done", name: "Done" }
|
|
4111
4398
|
]
|
|
4112
4399
|
});
|
|
4113
4400
|
if (action === "proxy") {
|
|
4114
4401
|
await this.setupProxy();
|
|
4402
|
+
} else if (action === "google") {
|
|
4403
|
+
await this.setupGoogleSearch();
|
|
4115
4404
|
} else if (action === "apikey") {
|
|
4116
4405
|
const choicesWithStatus = PROVIDERS.map((p) => {
|
|
4117
4406
|
const existingKey = this.config.getApiKey(p.value);
|
|
4118
|
-
const status = existingKey ?
|
|
4407
|
+
const status = existingKey ? chalk8.green(`[${maskKey(existingKey)}]`) : chalk8.gray("[\u672A\u914D\u7F6E]");
|
|
4119
4408
|
return { value: p.value, name: `${p.name} ${status}` };
|
|
4120
4409
|
});
|
|
4121
4410
|
const providerId = await select({
|
|
@@ -4130,7 +4419,7 @@ var SetupWizard = class {
|
|
|
4130
4419
|
});
|
|
4131
4420
|
this.config.set("defaultProvider", providerId);
|
|
4132
4421
|
this.config.save();
|
|
4133
|
-
console.log(
|
|
4422
|
+
console.log(chalk8.green(`Default provider set to: ${providerId}
|
|
4134
4423
|
`));
|
|
4135
4424
|
} else {
|
|
4136
4425
|
running = false;
|
|
@@ -4144,9 +4433,9 @@ var SetupWizard = class {
|
|
|
4144
4433
|
console.log(`
|
|
4145
4434
|
Managing ${displayName} API Key`);
|
|
4146
4435
|
if (existingKey) {
|
|
4147
|
-
console.log(
|
|
4436
|
+
console.log(chalk8.gray(` Current: ${maskKey(existingKey)}`));
|
|
4148
4437
|
} else {
|
|
4149
|
-
console.log(
|
|
4438
|
+
console.log(chalk8.gray(" Current: (\u672A\u914D\u7F6E)"));
|
|
4150
4439
|
}
|
|
4151
4440
|
const choices = existingKey ? [
|
|
4152
4441
|
{ value: "keep", name: "\u4FDD\u6301\u4E0D\u53D8 (keep current key)" },
|
|
@@ -4158,7 +4447,7 @@ Managing ${displayName} API Key`);
|
|
|
4158
4447
|
choices
|
|
4159
4448
|
});
|
|
4160
4449
|
if (action === "show" && existingKey) {
|
|
4161
|
-
console.log(
|
|
4450
|
+
console.log(chalk8.yellow(`
|
|
4162
4451
|
\u5B8C\u6574 Key: ${existingKey}
|
|
4163
4452
|
`));
|
|
4164
4453
|
const updateAfterShow = await select({
|
|
@@ -4177,15 +4466,15 @@ Managing ${displayName} API Key`);
|
|
|
4177
4466
|
validate: (val) => val.length > 0 || "API key cannot be empty"
|
|
4178
4467
|
});
|
|
4179
4468
|
this.config.setApiKey(providerId, newKey);
|
|
4180
|
-
console.log(
|
|
4469
|
+
console.log(chalk8.green(`API key saved for ${displayName}: ${maskKey(newKey)}
|
|
4181
4470
|
`));
|
|
4182
4471
|
}
|
|
4183
4472
|
async setupProxy() {
|
|
4184
4473
|
const current = this.config.get("proxy") ?? "";
|
|
4185
4474
|
console.log("\nHTTP/HTTPS Proxy Configuration");
|
|
4186
|
-
console.log(
|
|
4475
|
+
console.log(chalk8.gray(" Used for providers that require a proxy (e.g. Gemini in China)"));
|
|
4187
4476
|
if (current) {
|
|
4188
|
-
console.log(
|
|
4477
|
+
console.log(chalk8.gray(` Current: ${current}`));
|
|
4189
4478
|
}
|
|
4190
4479
|
const action = await select({
|
|
4191
4480
|
message: "Action:",
|
|
@@ -4202,7 +4491,7 @@ Managing ${displayName} API Key`);
|
|
|
4202
4491
|
if (action === "clear") {
|
|
4203
4492
|
this.config.set("proxy", void 0);
|
|
4204
4493
|
this.config.save();
|
|
4205
|
-
console.log(
|
|
4494
|
+
console.log(chalk8.green("Proxy cleared.\n"));
|
|
4206
4495
|
return;
|
|
4207
4496
|
}
|
|
4208
4497
|
const proxyUrl = await input({
|
|
@@ -4218,9 +4507,69 @@ Managing ${displayName} API Key`);
|
|
|
4218
4507
|
});
|
|
4219
4508
|
this.config.set("proxy", proxyUrl);
|
|
4220
4509
|
this.config.save();
|
|
4221
|
-
console.log(
|
|
4510
|
+
console.log(chalk8.green(`Proxy saved: ${proxyUrl}
|
|
4222
4511
|
`));
|
|
4223
4512
|
}
|
|
4513
|
+
async setupGoogleSearch() {
|
|
4514
|
+
console.log("\nGoogle Custom Search Configuration");
|
|
4515
|
+
console.log(chalk8.gray(" Enables AI to search the web via google_search tool"));
|
|
4516
|
+
console.log(chalk8.gray(" Requires: API Key + Search Engine ID (cx)"));
|
|
4517
|
+
console.log(chalk8.gray(" Get API Key: https://console.cloud.google.com/apis/credentials"));
|
|
4518
|
+
console.log(chalk8.gray(" Get CX: https://programmablesearchengine.google.com/"));
|
|
4519
|
+
const existingKey = this.config.getApiKey("google-search");
|
|
4520
|
+
if (existingKey) {
|
|
4521
|
+
console.log(chalk8.gray(`
|
|
4522
|
+
API Key: ${maskKey(existingKey)}`));
|
|
4523
|
+
} else {
|
|
4524
|
+
console.log(chalk8.gray("\n API Key: (\u672A\u914D\u7F6E)"));
|
|
4525
|
+
}
|
|
4526
|
+
const keyAction = await select({
|
|
4527
|
+
message: "API Key:",
|
|
4528
|
+
choices: existingKey ? [
|
|
4529
|
+
{ value: "keep", name: "\u4FDD\u6301\u4E0D\u53D8" },
|
|
4530
|
+
{ value: "change", name: "\u4FEE\u6539 API Key" }
|
|
4531
|
+
] : [
|
|
4532
|
+
{ value: "change", name: "\u8F93\u5165 API Key" },
|
|
4533
|
+
{ value: "skip", name: "\u8DF3\u8FC7" }
|
|
4534
|
+
]
|
|
4535
|
+
});
|
|
4536
|
+
if (keyAction === "change") {
|
|
4537
|
+
const newKey = await password({
|
|
4538
|
+
message: "Enter Google API Key:",
|
|
4539
|
+
validate: (val) => val.length > 0 || "API key cannot be empty"
|
|
4540
|
+
});
|
|
4541
|
+
this.config.setApiKey("google-search", newKey);
|
|
4542
|
+
console.log(chalk8.green(` API Key saved: ${maskKey(newKey)}`));
|
|
4543
|
+
}
|
|
4544
|
+
const existingCx = this.config.get("googleSearchEngineId") ?? "";
|
|
4545
|
+
if (existingCx) {
|
|
4546
|
+
console.log(chalk8.gray(`
|
|
4547
|
+
Search Engine ID: ${existingCx}`));
|
|
4548
|
+
} else {
|
|
4549
|
+
console.log(chalk8.gray("\n Search Engine ID: (\u672A\u914D\u7F6E)"));
|
|
4550
|
+
}
|
|
4551
|
+
const cxAction = await select({
|
|
4552
|
+
message: "Search Engine ID (cx):",
|
|
4553
|
+
choices: existingCx ? [
|
|
4554
|
+
{ value: "keep", name: "\u4FDD\u6301\u4E0D\u53D8" },
|
|
4555
|
+
{ value: "change", name: "\u4FEE\u6539 CX" }
|
|
4556
|
+
] : [
|
|
4557
|
+
{ value: "change", name: "\u8F93\u5165 CX" },
|
|
4558
|
+
{ value: "skip", name: "\u8DF3\u8FC7" }
|
|
4559
|
+
]
|
|
4560
|
+
});
|
|
4561
|
+
if (cxAction === "change") {
|
|
4562
|
+
const newCx = await input({
|
|
4563
|
+
message: "Enter Search Engine ID (cx):",
|
|
4564
|
+
default: existingCx || void 0,
|
|
4565
|
+
validate: (val) => val.length > 0 || "Search Engine ID cannot be empty"
|
|
4566
|
+
});
|
|
4567
|
+
this.config.set("googleSearchEngineId", newCx);
|
|
4568
|
+
this.config.save();
|
|
4569
|
+
console.log(chalk8.green(` Search Engine ID saved: ${newCx}`));
|
|
4570
|
+
}
|
|
4571
|
+
console.log();
|
|
4572
|
+
}
|
|
4224
4573
|
};
|
|
4225
4574
|
|
|
4226
4575
|
// src/repl/dev-state.ts
|
|
@@ -4688,7 +5037,7 @@ ${memory.content}`);
|
|
|
4688
5037
|
}
|
|
4689
5038
|
}
|
|
4690
5039
|
refreshPrompt() {
|
|
4691
|
-
const promptStr =
|
|
5040
|
+
const promptStr = chalk9.green(`[${this.currentProvider}]`) + chalk9.white(" > ");
|
|
4692
5041
|
this.rl.setPrompt(promptStr);
|
|
4693
5042
|
}
|
|
4694
5043
|
showPrompt() {
|
|
@@ -4723,13 +5072,13 @@ ${memory.content}`);
|
|
|
4723
5072
|
if (layers.length === 1) {
|
|
4724
5073
|
const l = layers[0];
|
|
4725
5074
|
process.stdout.write(
|
|
4726
|
-
|
|
5075
|
+
chalk9.dim(` \u{1F4C4} Context loaded: ${l.filePath} (${l.charCount} chars)
|
|
4727
5076
|
`)
|
|
4728
5077
|
);
|
|
4729
5078
|
} else {
|
|
4730
5079
|
const summary = layers.map((l) => `${l.level}: ${l.charCount}`).join(", ");
|
|
4731
5080
|
process.stdout.write(
|
|
4732
|
-
|
|
5081
|
+
chalk9.dim(` \u{1F4C4} Context loaded: ${layers.length} layers (${summary} chars)
|
|
4733
5082
|
`)
|
|
4734
5083
|
);
|
|
4735
5084
|
}
|
|
@@ -4737,25 +5086,25 @@ ${memory.content}`);
|
|
|
4737
5086
|
const memoryInfo = this.loadMemoryContent();
|
|
4738
5087
|
if (memoryInfo) {
|
|
4739
5088
|
process.stdout.write(
|
|
4740
|
-
|
|
5089
|
+
chalk9.dim(` \u{1F4DD} Memory loaded: ${memoryInfo.entryCount} entries (${memoryInfo.content.length} chars)
|
|
4741
5090
|
`)
|
|
4742
5091
|
);
|
|
4743
5092
|
}
|
|
4744
5093
|
const devStateContent = loadDevState();
|
|
4745
5094
|
if (devStateContent) {
|
|
4746
5095
|
process.stdout.write(
|
|
4747
|
-
|
|
5096
|
+
chalk9.dim(` \u{1F504} Dev state handoff: loaded (${devStateContent.length} chars)
|
|
4748
5097
|
`)
|
|
4749
5098
|
);
|
|
4750
5099
|
}
|
|
4751
5100
|
if (gitCtx) {
|
|
4752
|
-
const statusSummary = gitCtx.stagedFiles.length + gitCtx.changedFiles.length > 0 ?
|
|
5101
|
+
const statusSummary = gitCtx.stagedFiles.length + gitCtx.changedFiles.length > 0 ? chalk9.yellow(` (${gitCtx.stagedFiles.length} staged, ${gitCtx.changedFiles.length} modified)`) : chalk9.dim(" (clean)");
|
|
4753
5102
|
process.stdout.write(
|
|
4754
|
-
|
|
5103
|
+
chalk9.dim(` \u{1F500} Git branch: `) + chalk9.cyan(gitCtx.branch) + statusSummary + "\n"
|
|
4755
5104
|
);
|
|
4756
5105
|
}
|
|
4757
5106
|
if (pluginCount > 0) {
|
|
4758
|
-
process.stdout.write(
|
|
5107
|
+
process.stdout.write(chalk9.dim(` \u{1F50C} Plugins loaded: ${pluginCount} tool(s) from plugins/
|
|
4759
5108
|
`));
|
|
4760
5109
|
}
|
|
4761
5110
|
this.rl.on("SIGINT", () => {
|
|
@@ -4763,6 +5112,10 @@ ${memory.content}`);
|
|
|
4763
5112
|
this.toolExecutor.cancelConfirm();
|
|
4764
5113
|
return;
|
|
4765
5114
|
}
|
|
5115
|
+
if (askUserContext.prompting && askUserContext.cancelFn) {
|
|
5116
|
+
askUserContext.cancelFn();
|
|
5117
|
+
return;
|
|
5118
|
+
}
|
|
4766
5119
|
this.handleExit();
|
|
4767
5120
|
});
|
|
4768
5121
|
this.showPrompt();
|
|
@@ -4771,6 +5124,7 @@ ${memory.content}`);
|
|
|
4771
5124
|
this.rl.on("line", async (line) => {
|
|
4772
5125
|
if (this.toolExecutor.confirming) return;
|
|
4773
5126
|
if (this.selecting) return;
|
|
5127
|
+
if (askUserContext.prompting) return;
|
|
4774
5128
|
const input2 = line.trim();
|
|
4775
5129
|
if (!input2) {
|
|
4776
5130
|
this.showPrompt();
|
|
@@ -4816,13 +5170,13 @@ ${memory.content}`);
|
|
|
4816
5170
|
const { parts, hasImage, refs } = parseAtReferences(userInput, process.cwd());
|
|
4817
5171
|
for (const ref of refs) {
|
|
4818
5172
|
if (ref.type === "notfound") {
|
|
4819
|
-
process.stdout.write(
|
|
5173
|
+
process.stdout.write(chalk9.yellow(` \u26A0 File not found: ${ref.path}
|
|
4820
5174
|
`));
|
|
4821
5175
|
} else if (ref.type === "image") {
|
|
4822
|
-
process.stdout.write(
|
|
5176
|
+
process.stdout.write(chalk9.dim(` \u{1F4CE} Image: ${ref.path}
|
|
4823
5177
|
`));
|
|
4824
5178
|
} else {
|
|
4825
|
-
process.stdout.write(
|
|
5179
|
+
process.stdout.write(chalk9.dim(` \u{1F4C4} File: ${ref.path}
|
|
4826
5180
|
`));
|
|
4827
5181
|
}
|
|
4828
5182
|
}
|
|
@@ -4831,13 +5185,13 @@ ${memory.content}`);
|
|
|
4831
5185
|
const visionHint = this.getVisionModelHint();
|
|
4832
5186
|
if (visionHint) {
|
|
4833
5187
|
process.stdout.write(
|
|
4834
|
-
|
|
5188
|
+
chalk9.yellow(` \u2716 Vision not supported \u2013 ${visionHint}
|
|
4835
5189
|
`)
|
|
4836
5190
|
);
|
|
4837
5191
|
return;
|
|
4838
5192
|
}
|
|
4839
5193
|
process.stdout.write(
|
|
4840
|
-
|
|
5194
|
+
chalk9.dim(` \u{1F5BC} Vision request \u2013 sending image to ${this.currentProvider}
|
|
4841
5195
|
`)
|
|
4842
5196
|
);
|
|
4843
5197
|
}
|
|
@@ -5081,6 +5435,8 @@ ${memory.content}`);
|
|
|
5081
5435
|
return;
|
|
5082
5436
|
}
|
|
5083
5437
|
}
|
|
5438
|
+
askUserContext.rl = this.rl;
|
|
5439
|
+
googleSearchContext.configManager = this.config;
|
|
5084
5440
|
streamToFileContext.provider = provider;
|
|
5085
5441
|
streamToFileContext.model = this.currentModel;
|
|
5086
5442
|
streamToFileContext.systemPrompt = systemPrompt;
|
|
@@ -5191,7 +5547,7 @@ ${memory.content}`);
|
|
|
5191
5547
|
this.events.emit("session.end", { sessionId });
|
|
5192
5548
|
}
|
|
5193
5549
|
this.rl.close();
|
|
5194
|
-
console.log(
|
|
5550
|
+
console.log(chalk9.gray("\nGoodbye!"));
|
|
5195
5551
|
process.exit(0);
|
|
5196
5552
|
}
|
|
5197
5553
|
};
|