jinzd-ai-cli 0.1.26 → 0.1.28

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 CHANGED
@@ -337,7 +337,7 @@ const stdinLines = Array.isArray(rawStdin) ? rawStdin.map(String)
337
337
  ## 已知待改进项
338
338
 
339
339
  ### 低优先级
340
- - [ ] **macOS/Linux 完整测试**:跨平台逻辑已处理(`$SHELL` fallback、UTF-8 env vars),但尚未在非 Windows 环境实际运行验证。
340
+ - [x] **macOS/Linux 完整测试**(2026-03-01):已在 Linux 环境完成测试,基本无问题。跨平台逻辑(`$SHELL` fallback、UTF-8 env vars)验证通过。
341
341
  - [x] **Token 用量显示**:已通过 `/cost` 命令实现(v0.1.23),显示 session 累计 input/output/total tokens。
342
342
  - [ ] **GitHub 仓库迁移**:当前在 gitee,npm 包受众需要 GitHub 托管。
343
343
  - [x] **web_fetch DNS 解析时 SSRF 防护**(v0.1.25):新增 `resolveAndCheck()` 函数,用 `dns.promises.lookup()` 预解析域名,检查结果 IP 是否为私有地址。初始 URL 和 redirect 目标均校验。
@@ -802,3 +802,94 @@ const stdinLines = Array.isArray(rawStdin) ? rawStdin.map(String)
802
802
  | run_interactive stdin 截断 | 通过 shell 包装 spawn 时 stdin pipe 被截断 | 直接 `spawn(pythonExe, args)` 不经过任何 shell |
803
803
  | Ctrl+C 在工具确认时异常退出 | SIGINT handler 直接调用 `handleExit()`,未检查 `confirm()` 状态 | SIGINT handler 先检查 `confirming` 标志,是则 `cancelConfirm()` 取消而非退出 |
804
804
  | 日期注入在 pkg exe 中显示错误 | `toLocaleDateString/toLocaleTimeString` 依赖完整 ICU,pkg 精简版不支持 | 改为手动数字拼接:`${year}年${month}月${day}日 ${weekday} ${HH}:${mm}:${ss}` |
805
+
806
+ ---
807
+
808
+ ## 代码审查报告(2026-03-01)
809
+
810
+ > 全面扫描 src/ 目录 35+ 个 TypeScript 源文件,共发现 **4 高危 + 8 中危 + 7 低危** 问题。
811
+
812
+ ### 🔴 高危问题(立即修复)
813
+
814
+ **H1 — MCP pendingRequests 内存泄漏**
815
+ - **文件**:`src/mcp/client.ts`
816
+ - **描述**:进程崩溃时 `pendingRequests.delete(id)` 可能不触发,timer 未清理;长期运行时内存持续增长
817
+ - **修复**:在 `sendRequest` 中用 `finally` 确保 `pendingRequests.delete(id)` + `clearTimeout(timer)` 总被执行
818
+
819
+ **H2 — readline 竞态条件:`confirming` 标志失效**
820
+ - **文件**:`src/tools/executor.ts`(行 58, 155, 307, 320-356)
821
+ - **描述**:`batchConfirm()` 提前返回 `'none'` 时 `confirming = true` 与实际状态不匹配;`once('line')` 在 edge case 下可能触发两次,误消费用户输入;`ask_user` 工具的 `askUserContext.prompting` 存在同样问题
822
+ - **修复**:使用 `completed` 布尔标志防止二次触发,或改用 Promise-based 状态机
823
+
824
+ **H3 — 工具执行错误语义混淆(isError 混用)**
825
+ - **文件**:`src/tools/executor.ts`(行 128, 160, 170)
826
+ - **描述**:权限 deny 和用户取消均返回 `{ isError: false }`,AI 无法区分"用户拒绝"与"工具正常返回",可能产生错误推理
827
+ - **修复**:统一为 `isError: true`,内容前缀加 `[User cancelled]` 或 `[Permission denied]`
828
+
829
+ **H4 — MCP 断开后无恢复机制**
830
+ - **文件**:`src/mcp/client.ts` + `src/mcp/manager.ts`
831
+ - **描述**:服务器子进程意外退出后,客户端对象仍留在 Map 中;工具列表可见但调用时 silent failure;用户需重启 CLI 才能恢复
832
+ - **修复**:`callTool()` 中检测 `isConnected`,返回友好错误提示;可选:实现指数退避自动重连
833
+
834
+ ### 🟠 中危问题(近期修复)
835
+
836
+ | # | 文件 | 问题描述 | 修复方向 |
837
+ |---|------|---------|---------|
838
+ | M5 | `src/tools/builtin/bash.ts:76-86` | `cwd` 指向不存在目录时不报错,AI 误判 cd 成功 | `existsSync` 校验后再设置 `effectiveCwd` |
839
+ | M6 | `src/tools/builtin/web-fetch.ts` | SSRF:重定向链中间 URL 不检查(仅检查首尾) | 对每个中间重定向 URL 均调用 `resolveAndCheck()` |
840
+ | M7 | `src/tools/builtin/run-interactive.ts:131-144` | stdin 写入无背压处理,大量输入可能溢出 | 监听 `drain` 事件,写满后暂停直到缓冲区释放 |
841
+ | M8 | `src/session/session-manager.ts` | 会话 JSON 损坏时错误被吞噬,无日志,用户无法诊断 | `catch` 中输出 `stderr` 警告含文件名和错误信息 |
842
+ | M9 | `src/mcp/client.ts` | `close()` 时事件监听器未移除,长期运行积累僵尸监听器 | `killProcess()` 中调用 `removeAllListeners()` |
843
+ | M10 | `src/tools/permissions.ts` | `pathPattern` 直接 `new RegExp()`,恶意配置可触发 ReDoS | 加 try/catch 包裹正则编译,或限制正则复杂度 |
844
+ | M11 | `src/repl/repl.ts`(上下文加载) | `contextFile` 设为相对路径可能遍历项目外目录 | `resolve()` 后校验路径以项目目录为前缀 |
845
+ | M12 | `src/mcp/manager.ts` | MCP 工具 schema 扁平化时丢失嵌套结构,AI 传参可能失败 | `convertDefinition()` 支持递归转换嵌套 object/array |
846
+
847
+ ### 🟡 低危问题(后续改进)
848
+
849
+ | # | 文件 | 问题描述 |
850
+ |---|------|---------|
851
+ | L1 | `src/tools/builtin/run-tests.ts:35` | `JSON.parse(package.json)` 无细粒度错误处理 |
852
+ | L2 | `src/tools/builtin/web-fetch.ts` | 恶意 HTML 大量 script 标签时正则性能下降 |
853
+ | L3 | `src/session/session.ts` | `new Date(d.created)` 非法字符串返回 Invalid Date,比较失败 |
854
+ | L4 | `src/tools/builtin/ask-user.ts` / `google-search.ts` | 模块级全局上下文,多会话架构下会串扰 |
855
+ | L5 | `src/config/schema.ts` | `modelParams.timeout` 与 provider 级 `timeout` 优先级不清晰 |
856
+ | L6 | `src/tools/executor.ts` | `call.arguments['path']` 未验证是否绝对路径,null 传入可能异常 |
857
+ | L7 | `src/repl/repl.ts` | 主循环 `line` handler 是 async 但无整体 try/catch,未捕获异常可能停响应 |
858
+
859
+ ---
860
+
861
+ ## 与 Claude Code 功能对比差距(2026-03-01)
862
+
863
+ ### ✅ ai-cli 已超越或持平 Claude Code 的功能
864
+
865
+ - 多 Provider 支持(Claude Code 仅支持 Claude 系列)
866
+ - 三层级上下文文件(Claude Code 为双层)
867
+ - Dev State 开发状态交接
868
+ - Agent Skills 系统
869
+ - 企业级权限规则 + Hooks 系统
870
+
871
+ ### ❌ 缺失功能路线图(新增)
872
+
873
+ #### P0 — 核心竞争力缺口
874
+ - [ ] **中断生成**:`Escape` 键立即停止 AI 流式输出(当前无法中断,必须等待完成)
875
+ - [ ] **多模态输入(图片)**:支持粘贴/引用图片路径作为输入(当前纯文本)
876
+ - [ ] **`/add-dir` 命令**:运行时动态添加目录到上下文
877
+ - [ ] **并行工具调用**:AI 一次返回多个工具同时执行(当前为分组串行)
878
+
879
+ #### P1 — 重要差距
880
+ - [ ] **`/memory` 命令**:查看/编辑/清理 memory.md 内容(当前只能由工具写入)
881
+ - [ ] **`/doctor` 健康检查**:诊断 API Key、网络、配置问题
882
+ - [ ] **`/bug` 反馈命令**:一键提交 bug 报告
883
+ - [ ] **Vim 编辑模式**:命令行输入支持 vim 键绑定
884
+ - [ ] **桌面通知**:长任务完成时发送系统通知
885
+ - [ ] **`--allowedTools` / `--blockedTools` 启动参数**:启动时动态限制工具集
886
+ - [ ] **流式 JSON 输出**:`--output-format streaming-json` 逐行输出(当前仅支持一次性 `--json`)
887
+
888
+ #### P2 — 体验增强
889
+ - [ ] **项目级 `.mcp.json`**:项目根目录独立 MCP 配置,优先全局 config
890
+ - [ ] **IDE 集成**:VS Code / JetBrains 扩展(架构已准备就绪)
891
+ - [ ] **OAuth/浏览器登录**:无需手动填 API Key
892
+ - [ ] **`--resume <id>` 启动参数**:命令行直接恢复指定会话
893
+ - [ ] **Extended Thinking**:Claude 3.7 深度推理模式集成
894
+ - [ ] **Word wrap 配置**:终端输出折行宽度可配置
895
+ - [ ] **主题/颜色自定义**:dark/light/custom 主题
@@ -94,7 +94,11 @@ function detectProject(cwd) {
94
94
  if (testScript && testScript !== 'echo "Error: no test specified" && exit 1') {
95
95
  return { type: "node", framework: "npm", command: "npm test" };
96
96
  }
97
- } catch {
97
+ } catch (err) {
98
+ process.stderr.write(
99
+ `[Warning] Failed to parse package.json: ${err instanceof Error ? err.message : String(err)}
100
+ `
101
+ );
98
102
  }
99
103
  }
100
104
  if (existsSync(join(cwd, "pyproject.toml")) || existsSync(join(cwd, "setup.py")) || existsSync(join(cwd, "pytest.ini"))) {
package/dist/index.js CHANGED
@@ -27,7 +27,7 @@ import {
27
27
  SUBAGENT_MAX_ROUNDS_LIMIT,
28
28
  VERSION,
29
29
  runTestsTool
30
- } from "./chunk-BE5SYJ2G.js";
30
+ } from "./chunk-3EJWGNHV.js";
31
31
 
32
32
  // src/index.ts
33
33
  import { program } from "commander";
@@ -75,6 +75,11 @@ var ConfigSchema = z.object({
75
75
  apiKeys: z.record(z.string()).default({}),
76
76
  customBaseUrls: z.record(z.string()).default({}),
77
77
  // Per-provider timeout in ms (e.g. { deepseek: 60000 })
78
+ // ⚠️ Timeout 优先级说明(L5):
79
+ // 1. modelParams[modelId].timeout — 最高优先级,精确到具体模型(如 deepseek-reasoner 需要更长超时)
80
+ // 2. timeouts[providerId] — Provider 级默认,覆盖内置默认值
81
+ // 3. 内置 Provider 的硬编码默认值 — 最低兜底(如 OpenAI 兼容系列默认 60000ms)
82
+ // 实现位置:src/providers/registry.ts initialize(),将 timeouts[id] 注入 provider
78
83
  timeouts: z.record(z.number()).default({}),
79
84
  // HTTP/HTTPS 代理地址(Node.js 不自动使用系统代理,需显式配置)
80
85
  // 例:http://127.0.0.1:10809
@@ -1477,6 +1482,10 @@ var Session = class _Session {
1477
1482
  };
1478
1483
 
1479
1484
  // src/session/session-manager.ts
1485
+ function safeDate(value) {
1486
+ const d = new Date(value);
1487
+ return isNaN(d.getTime()) ? /* @__PURE__ */ new Date(0) : d;
1488
+ }
1480
1489
  var SessionManager = class {
1481
1490
  constructor(config) {
1482
1491
  this.config = config;
@@ -1522,11 +1531,15 @@ var SessionManager = class {
1522
1531
  provider: data.provider,
1523
1532
  model: data.model,
1524
1533
  messageCount: data.messages?.length ?? 0,
1525
- created: new Date(data.created),
1526
- updated: new Date(data.updated),
1534
+ created: safeDate(data.created),
1535
+ updated: safeDate(data.updated),
1527
1536
  title: data.title
1528
1537
  });
1529
- } catch {
1538
+ } catch (err) {
1539
+ process.stderr.write(
1540
+ `[Warning] Skipping corrupted session file "${file}": ${err instanceof Error ? err.message : String(err)}
1541
+ `
1542
+ );
1530
1543
  }
1531
1544
  }
1532
1545
  return metas.sort((a, b) => b.updated.getTime() - a.updated.getTime());
@@ -1571,14 +1584,18 @@ var SessionManager = class {
1571
1584
  provider: data.provider,
1572
1585
  model: data.model,
1573
1586
  messageCount: messages.length,
1574
- created: new Date(data.created),
1575
- updated: new Date(data.updated),
1587
+ created: safeDate(data.created),
1588
+ updated: safeDate(data.updated),
1576
1589
  title: data.title
1577
1590
  },
1578
1591
  matches
1579
1592
  });
1580
1593
  }
1581
- } catch {
1594
+ } catch (err) {
1595
+ process.stderr.write(
1596
+ `[Warning] Skipping corrupted session file "${filePath}": ${err instanceof Error ? err.message : String(err)}
1597
+ `
1598
+ );
1582
1599
  }
1583
1600
  }
1584
1601
  return results.sort((a, b) => b.sessionMeta.updated.getTime() - a.sessionMeta.updated.getTime());
@@ -3182,7 +3199,7 @@ ${text}
3182
3199
  description: "Run project tests and show structured report",
3183
3200
  usage: "/test [command|filter]",
3184
3201
  async execute(args, _ctx) {
3185
- const { executeTests } = await import("./run-tests-QXZ3ZQ4S.js");
3202
+ const { executeTests } = await import("./run-tests-MKMB3TXE.js");
3186
3203
  const argStr = args.join(" ").trim();
3187
3204
  let testArgs = {};
3188
3205
  if (argStr) {
@@ -3455,12 +3472,13 @@ var bashTool = {
3455
3472
  let effectiveCwd = persistentCwd;
3456
3473
  if (cwdArg) {
3457
3474
  const resolved = resolve2(persistentCwd, cwdArg);
3458
- if (existsSync6(resolved)) {
3459
- effectiveCwd = resolved;
3460
- persistentCwd = resolved;
3461
- } else {
3462
- effectiveCwd = resolved;
3475
+ if (!existsSync6(resolved)) {
3476
+ throw new Error(
3477
+ `cwd directory does not exist: "${resolved}". Create it first (e.g. mkdir -p "${resolved}") before specifying it as cwd.`
3478
+ );
3463
3479
  }
3480
+ effectiveCwd = resolved;
3481
+ persistentCwd = resolved;
3464
3482
  }
3465
3483
  let actualCommand;
3466
3484
  if (IS_WINDOWS) {
@@ -4300,10 +4318,13 @@ var runInteractiveTool = {
4300
4318
  let lineIdx = 0;
4301
4319
  const writeNextLine = () => {
4302
4320
  if (lineIdx < stdinLines.length && !child.stdin.destroyed) {
4303
- setTimeout(() => {
4304
- child.stdin.write(stdinLines[lineIdx++] + (IS_WINDOWS2 ? "\r\n" : "\n"));
4305
- writeNextLine();
4306
- }, 150);
4321
+ const line = stdinLines[lineIdx++] + (IS_WINDOWS2 ? "\r\n" : "\n");
4322
+ const canContinue = child.stdin.write(line);
4323
+ if (canContinue) {
4324
+ setTimeout(writeNextLine, 150);
4325
+ } else {
4326
+ child.stdin.once("drain", () => setTimeout(writeNextLine, 150));
4327
+ }
4307
4328
  } else if (!child.stdin.destroyed) {
4308
4329
  child.stdin.end();
4309
4330
  }
@@ -4346,6 +4367,10 @@ ${stderr}`);
4346
4367
  // src/tools/builtin/web-fetch.ts
4347
4368
  import { promises as dnsPromises } from "dns";
4348
4369
  function htmlToText(html) {
4370
+ const HTML_REGEX_LIMIT = 2e5;
4371
+ if (html.length > HTML_REGEX_LIMIT) {
4372
+ html = html.slice(0, HTML_REGEX_LIMIT);
4373
+ }
4349
4374
  let text = html.replace(/<script[\s\S]*?<\/script>/gi, "").replace(/<style[\s\S]*?<\/style>/gi, "").replace(/<noscript[\s\S]*?<\/noscript>/gi, "").replace(/<svg[\s\S]*?<\/svg>/gi, "");
4350
4375
  text = text.replace(/<h([1-6])[^>]*>([\s\S]*?)<\/h\1>/gi, (_m, lvl, content) => {
4351
4376
  const prefix = "#".repeat(Number(lvl));
@@ -4450,28 +4475,46 @@ var webFetchTool = {
4450
4475
  let rawHtml;
4451
4476
  let finalUrl;
4452
4477
  let contentType;
4478
+ const MAX_REDIRECTS = 10;
4479
+ const FETCH_HEADERS = {
4480
+ "User-Agent": "Mozilla/5.0 (compatible; ai-cli/1.0; +https://github.com/ai-cli)",
4481
+ Accept: "text/html,application/xhtml+xml,text/plain,*/*",
4482
+ "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8"
4483
+ };
4453
4484
  try {
4454
- const resp = await fetch(url, {
4455
- signal: controller.signal,
4456
- headers: {
4457
- "User-Agent": "Mozilla/5.0 (compatible; ai-cli/1.0; +https://github.com/ai-cli)",
4458
- Accept: "text/html,application/xhtml+xml,text/plain,*/*",
4459
- "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8"
4460
- },
4461
- redirect: "follow"
4462
- });
4463
- clearTimeout(timeoutId);
4464
- finalUrl = resp.url;
4465
- contentType = resp.headers.get("content-type") ?? "";
4466
- try {
4467
- const finalParsed = new URL(finalUrl);
4468
- if (isPrivateHost(finalParsed.hostname)) {
4469
- throw new Error(`Blocked: redirect landed on private address "${finalUrl}".`);
4485
+ let currentUrl = url;
4486
+ let resp = null;
4487
+ for (let hop = 0; hop <= MAX_REDIRECTS; hop++) {
4488
+ const parsedHop = new URL(currentUrl);
4489
+ if (isPrivateHost(parsedHop.hostname)) {
4490
+ throw new Error(`Blocked: redirect to private/internal address "${currentUrl}".`);
4491
+ }
4492
+ await resolveAndCheck(parsedHop.hostname);
4493
+ const r = await fetch(currentUrl, {
4494
+ signal: controller.signal,
4495
+ headers: FETCH_HEADERS,
4496
+ redirect: "manual"
4497
+ // 手动控制重定向
4498
+ });
4499
+ if (r.status >= 300 && r.status < 400) {
4500
+ if (hop >= MAX_REDIRECTS) {
4501
+ throw new Error(`Too many redirects (>${MAX_REDIRECTS}): ${url}`);
4502
+ }
4503
+ const location = r.headers.get("Location");
4504
+ if (!location) {
4505
+ resp = r;
4506
+ break;
4507
+ }
4508
+ currentUrl = new URL(location, currentUrl).href;
4509
+ continue;
4470
4510
  }
4471
- await resolveAndCheck(finalParsed.hostname);
4472
- } catch (e) {
4473
- if (e.message.startsWith("Blocked:")) throw e;
4511
+ resp = r;
4512
+ break;
4474
4513
  }
4514
+ clearTimeout(timeoutId);
4515
+ if (!resp) throw new Error(`Too many redirects (>${MAX_REDIRECTS}): ${url}`);
4516
+ finalUrl = currentUrl;
4517
+ contentType = resp.headers.get("content-type") ?? "";
4475
4518
  if (!resp.ok) {
4476
4519
  throw new Error(`HTTP ${resp.status} ${resp.statusText}`);
4477
4520
  }
@@ -5592,7 +5635,7 @@ var ToolExecutor = class {
5592
5635
  if (this.permissionRules.length > 0) {
5593
5636
  const action = checkPermission(call.name, call.arguments, dangerLevel, this.permissionRules, this.defaultPermission);
5594
5637
  if (action === "deny") {
5595
- return { callId: call.id, content: `Permission denied by rule for tool: ${call.name}`, isError: false };
5638
+ return { callId: call.id, content: `Permission denied by rule for tool: ${call.name}`, isError: true };
5596
5639
  }
5597
5640
  if (action === "auto-approve") {
5598
5641
  this.printToolCall(call);
@@ -5619,7 +5662,7 @@ var ToolExecutor = class {
5619
5662
  return {
5620
5663
  callId: call.id,
5621
5664
  content: "User cancelled the operation.",
5622
- isError: false
5665
+ isError: true
5623
5666
  };
5624
5667
  }
5625
5668
  } else if (dangerLevel === "destructive") {
@@ -5628,7 +5671,7 @@ var ToolExecutor = class {
5628
5671
  return {
5629
5672
  callId: call.id,
5630
5673
  content: "User cancelled the operation.",
5631
- isError: false
5674
+ isError: true
5632
5675
  };
5633
5676
  }
5634
5677
  this.printToolCall(call);
@@ -5721,7 +5764,7 @@ var ToolExecutor = class {
5721
5764
  }
5722
5765
  } else {
5723
5766
  console.log(chalk8.gray(` [${i + 1}] `) + chalk8.dim("rejected"));
5724
- results.push({ callId: call.id, content: "User rejected the operation.", isError: false });
5767
+ results.push({ callId: call.id, content: "User rejected the operation.", isError: true });
5725
5768
  }
5726
5769
  }
5727
5770
  return results;
@@ -5752,7 +5795,10 @@ var ToolExecutor = class {
5752
5795
  this.confirming = false;
5753
5796
  resolve5(result);
5754
5797
  };
5798
+ let completed = false;
5755
5799
  const onLine = (line) => {
5800
+ if (completed) return;
5801
+ completed = true;
5756
5802
  const input2 = line.trim().toLowerCase();
5757
5803
  if (input2 === "a" || input2 === "all" || input2 === "y") {
5758
5804
  cleanup("all");
@@ -5768,6 +5814,8 @@ var ToolExecutor = class {
5768
5814
  }
5769
5815
  };
5770
5816
  this.cancelConfirmFn = () => {
5817
+ if (completed) return;
5818
+ completed = true;
5771
5819
  process.stdout.write(chalk8.gray("\n(cancelled)\n"));
5772
5820
  cleanup("none");
5773
5821
  };
@@ -5916,8 +5964,15 @@ var ToolExecutor = class {
5916
5964
  this.confirming = false;
5917
5965
  resolve5(answer === "y");
5918
5966
  };
5919
- const onLine = (line) => cleanup(line.trim().toLowerCase());
5967
+ let completed = false;
5968
+ const onLine = (line) => {
5969
+ if (completed) return;
5970
+ completed = true;
5971
+ cleanup(line.trim().toLowerCase());
5972
+ };
5920
5973
  this.cancelConfirmFn = () => {
5974
+ if (completed) return;
5975
+ completed = true;
5921
5976
  process.stdout.write(chalk8.gray("\n(cancelled)\n"));
5922
5977
  cleanup("n");
5923
5978
  };
@@ -6525,7 +6580,9 @@ var McpClient = class {
6525
6580
  // ══════════════════════════════════════════════════════════════════
6526
6581
  ensureConnected() {
6527
6582
  if (!this.connected) {
6528
- throw new Error(`MCP server [${this.serverId}] is not connected`);
6583
+ throw new Error(
6584
+ `MCP server [${this.serverId}] is not connected` + (this.errorMessage ? `: ${this.errorMessage}` : "")
6585
+ );
6529
6586
  }
6530
6587
  }
6531
6588
  /** Promise 超时包装 */
@@ -6551,9 +6608,13 @@ var McpClient = class {
6551
6608
  }
6552
6609
  this.pendingRequests.clear();
6553
6610
  }
6554
- /** 杀掉子进程 */
6611
+ /** 杀掉子进程,并移除所有事件监听器防止僵尸引用 */
6555
6612
  killProcess() {
6556
6613
  if (this.process) {
6614
+ this.process.removeAllListeners("error");
6615
+ this.process.removeAllListeners("exit");
6616
+ this.process.stdout?.removeAllListeners("data");
6617
+ this.process.stderr?.removeAllListeners("data");
6557
6618
  try {
6558
6619
  this.process.stdin?.end();
6559
6620
  this.process.kill();
@@ -6661,6 +6722,11 @@ var McpManager = class {
6661
6722
  return {
6662
6723
  definition,
6663
6724
  execute: async (args) => {
6725
+ if (!client.isConnected) {
6726
+ throw new Error(
6727
+ `MCP server [${serverId}] is disconnected` + (client.errorMessage ? `: ${client.errorMessage}` : "") + `. Use /mcp to check status, or restart ai-cli to reconnect.`
6728
+ );
6729
+ }
6664
6730
  try {
6665
6731
  const result = await client.callTool(mcpTool.name, args);
6666
6732
  if (result.isError) {
@@ -7011,7 +7077,14 @@ var Repl = class {
7011
7077
  if (setting === false) return { layers: [], mergedContent: "" };
7012
7078
  const cwd = process.cwd();
7013
7079
  if (setting !== "auto") {
7014
- const fullPath = join13(cwd, setting);
7080
+ const fullPath = resolve4(cwd, String(setting));
7081
+ if (!fullPath.startsWith(resolve4(cwd))) {
7082
+ process.stderr.write(
7083
+ `[Warning] contextFile path "${setting}" is outside current directory, ignoring.
7084
+ `
7085
+ );
7086
+ return { layers: [], mergedContent: "" };
7087
+ }
7015
7088
  if (existsSync18(fullPath)) {
7016
7089
  const content = readFileSync13(fullPath, "utf-8").trim();
7017
7090
  if (content) {
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  executeTests,
4
4
  runTestsTool
5
- } from "./chunk-BE5SYJ2G.js";
5
+ } from "./chunk-3EJWGNHV.js";
6
6
  export {
7
7
  executeTests,
8
8
  runTestsTool
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jinzd-ai-cli",
3
- "version": "0.1.26",
3
+ "version": "0.1.28",
4
4
  "description": "Cross-platform REPL-style AI CLI with multi-provider support",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",