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.
Files changed (3) hide show
  1. package/CLAUDE.md +13 -5
  2. package/dist/index.js +423 -67
  3. 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(注册全部内置工具,共11个)
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
- └── run-interactive.ts # run_interactive 工具(spawn 直连可执行文件,stdin_lines 依次输入)
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 chalk7 from "chalk";
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 = 11 + pluginCount;
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 chalk5 from "chalk";
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 chalk4 from "chalk";
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 chalk4.dim(" (no changes)");
3939
+ return chalk6.dim(" (no changes)");
3655
3940
  }
3656
3941
  const output = [];
3657
3942
  if (filePath) {
3658
- output.push(chalk4.bold.white(`--- ${filePath} (before)`));
3659
- output.push(chalk4.bold.white(`+++ ${filePath} (after)`));
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(chalk4.dim(` ... (diff truncated, too many changes)`));
3949
+ output.push(chalk6.dim(` ... (diff truncated, too many changes)`));
3665
3950
  break;
3666
3951
  }
3667
3952
  output.push(
3668
- chalk4.cyan(
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(chalk4.dim(` ${line.text}`));
3961
+ output.push(chalk6.dim(` ${line.text}`));
3677
3962
  } else if (line.type === "remove") {
3678
- output.push(chalk4.red(`- ${line.text}`));
3963
+ output.push(chalk6.red(`- ${line.text}`));
3679
3964
  } else {
3680
- output.push(chalk4.green(`+ ${line.text}`));
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" ? chalk5.yellow("\u270E Tool: ") : chalk5.bold.cyan("\u2699 Tool: ");
3906
- const roundBadge = this.totalRounds > 0 ? chalk5.dim(` [${this.round}/${this.totalRounds}]`) : "";
3907
- console.log(icon + chalk5.white(call.name) + roundBadge);
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(chalk5.gray(` ${key}: `) + chalk5.white(valStr));
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(chalk5.dim(" (file content unchanged)"));
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(chalk5.dim(" \u2500\u2500 diff preview \u2500\u2500"));
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) => chalk5.green(`+ ${l}`)).join("\n");
3950
- const more = lines.length > 20 ? chalk5.dim(`
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(chalk5.dim(" \u2500\u2500 new file preview \u2500\u2500"));
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(chalk5.dim(" \u2500\u2500 diff preview \u2500\u2500"));
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) => chalk5.green(`+ ${l}`)).join("\n");
3975
- const more = insertLines.length > 5 ? chalk5.dim(`
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(chalk5.dim(` \u2500\u2500 insert after line ${line} \u2500\u2500`));
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) => chalk5.red(`- ${l}`)).join("\n");
3992
- const more = deleted.length > 5 ? chalk5.dim(`
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(chalk5.dim(` \u2500\u2500 delete lines ${from}\u2013${to} \u2500\u2500`));
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(chalk5.red(`\u26A0 ${name} error: `) + chalk5.gray(content.slice(0, 300)));
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 ? chalk5.gray(`
4292
+ const moreLines = lines.length > maxLines ? chalk7.gray(`
4008
4293
  ... (${lines.length - maxLines} more lines)`) : "";
4009
- const truncatedNote = wasTruncated ? chalk5.yellow(`
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(chalk5.green("\u2713 Result: ") + chalk5.gray(preview) + moreLines + truncatedNote);
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" ? chalk5.red : chalk5.yellow;
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: `) + chalk5.bold(call.name));
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(chalk5.gray(` ${key}: `) + valStr);
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(chalk5.gray("\n(cancelled)\n"));
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 chalk6 from "chalk";
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(chalk6.bold.cyan("\nWelcome to ai-cli!\n"));
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(chalk6.green("\nSetup complete! Starting ai-cli...\n"));
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(chalk6.bold.cyan("\nai-cli Configuration\n"));
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 ? chalk6.green(`[${currentProxy}]`) : chalk6.gray("[\u672A\u914D\u7F6E]");
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 ? chalk6.green(`[${maskKey(existingKey)}]`) : chalk6.gray("[\u672A\u914D\u7F6E]");
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(chalk6.green(`Default provider set to: ${providerId}
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(chalk6.gray(` Current: ${maskKey(existingKey)}`));
4436
+ console.log(chalk8.gray(` Current: ${maskKey(existingKey)}`));
4148
4437
  } else {
4149
- console.log(chalk6.gray(" Current: (\u672A\u914D\u7F6E)"));
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(chalk6.yellow(`
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(chalk6.green(`API key saved for ${displayName}: ${maskKey(newKey)}
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(chalk6.gray(" Used for providers that require a proxy (e.g. Gemini in China)"));
4475
+ console.log(chalk8.gray(" Used for providers that require a proxy (e.g. Gemini in China)"));
4187
4476
  if (current) {
4188
- console.log(chalk6.gray(` Current: ${current}`));
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(chalk6.green("Proxy cleared.\n"));
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(chalk6.green(`Proxy saved: ${proxyUrl}
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 = chalk7.green(`[${this.currentProvider}]`) + chalk7.white(" > ");
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
- chalk7.dim(` \u{1F4C4} Context loaded: ${l.filePath} (${l.charCount} chars)
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
- chalk7.dim(` \u{1F4C4} Context loaded: ${layers.length} layers (${summary} chars)
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
- chalk7.dim(` \u{1F4DD} Memory loaded: ${memoryInfo.entryCount} entries (${memoryInfo.content.length} chars)
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
- chalk7.dim(` \u{1F504} Dev state handoff: loaded (${devStateContent.length} chars)
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 ? chalk7.yellow(` (${gitCtx.stagedFiles.length} staged, ${gitCtx.changedFiles.length} modified)`) : chalk7.dim(" (clean)");
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
- chalk7.dim(` \u{1F500} Git branch: `) + chalk7.cyan(gitCtx.branch) + statusSummary + "\n"
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(chalk7.dim(` \u{1F50C} Plugins loaded: ${pluginCount} tool(s) from plugins/
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(chalk7.yellow(` \u26A0 File not found: ${ref.path}
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(chalk7.dim(` \u{1F4CE} Image: ${ref.path}
5176
+ process.stdout.write(chalk9.dim(` \u{1F4CE} Image: ${ref.path}
4823
5177
  `));
4824
5178
  } else {
4825
- process.stdout.write(chalk7.dim(` \u{1F4C4} File: ${ref.path}
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
- chalk7.yellow(` \u2716 Vision not supported \u2013 ${visionHint}
5188
+ chalk9.yellow(` \u2716 Vision not supported \u2013 ${visionHint}
4835
5189
  `)
4836
5190
  );
4837
5191
  return;
4838
5192
  }
4839
5193
  process.stdout.write(
4840
- chalk7.dim(` \u{1F5BC} Vision request \u2013 sending image to ${this.currentProvider}
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(chalk7.gray("\nGoodbye!"));
5550
+ console.log(chalk9.gray("\nGoodbye!"));
5195
5551
  process.exit(0);
5196
5552
  }
5197
5553
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jinzd-ai-cli",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
4
4
  "description": "Cross-platform REPL-style AI CLI with multi-provider support",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",