jinzd-ai-cli 0.1.47 → 0.1.49

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
@@ -38,8 +38,8 @@ src/
38
38
  │ ├── config-manager.ts # 读写 ~/.aicli/config.json,三层优先级:env > file > default
39
39
  │ └── env-loader.ts # AICLI_API_KEY_* 等环境变量映射
40
40
  ├── session/
41
- │ ├── session.ts # Session(addMessage / clear / compact / toJSON / fromJSON)
42
- │ └── session-manager.ts # CRUD for ~/.aicli/history/*.json
41
+ │ ├── session.ts # Session(addMessage / clear / compact / fork / toJSON / fromJSON)
42
+ │ └── session-manager.ts # CRUD + forkSession for ~/.aicli/history/*.json
43
43
  ├── repl/
44
44
  │ ├── repl.ts # 主 REPL 循环(MAX_TOOL_ROUNDS=20,handleChatWithTools agentic loop)
45
45
  │ ├── renderer.ts # 终端输出(renderStream / renderResponse 均不加前置 \n)
@@ -47,7 +47,7 @@ src/
47
47
  │ ├── dev-state.ts # 开发状态交接(provider/model 切换时快照生成、save/load/clear)
48
48
  │ ├── setup-wizard.ts # @inquirer/prompts 首次运行交互式设置
49
49
  │ └── commands/
50
- │ └── index.ts # CommandRegistry + 33个命令(/help /about /provider /model /clear /compact /plan /session /system /context /status /search /undo /export /copy /cost /init /skill /tools /plugins /mcp /config /checkpoint /review /commands /test /scaffold /add-dir /memory /doctor /bug /think /exit)
50
+ │ └── index.ts # CommandRegistry + 35个命令(/help /about /provider /model /clear /compact /plan /session /system /context /status /search /undo /export /copy /cost /init /skill /tools /plugins /mcp /config /checkpoint /review /commands /test /scaffold /add-dir /memory /doctor /bug /think /diff /fork /exit)
51
51
  │ ├── custom-commands.ts # CustomCommandManager(~/.aicli/commands/*.md 用户自定义命令)
52
52
  ├── skills/
53
53
  │ ├── types.ts # Skill/SkillMeta 接口、parseSkillFile(YAML frontmatter 解析)
@@ -350,6 +350,81 @@ const stdinLines = Array.isArray(rawStdin) ? rawStdin.map(String)
350
350
  - [x] **web_fetch DNS 解析时 SSRF 防护**(v0.1.25):新增 `resolveAndCheck()` 函数,用 `dns.promises.lookup()` 预解析域名,检查结果 IP 是否为私有地址。初始 URL 和 redirect 目标均校验。
351
351
  - [ ] **`persistentCwd` 全局状态**:bash 工具的当前工作目录是模块级全局变量,多 session 并发时可能串扰。现阶段单 session REPL 无影响,GUI 多会话扩展时需重构为 per-session 状态。
352
352
 
353
+ ## 本轮开发完成记录(2026-03-08,v0.1.47 → v0.1.48)
354
+
355
+ ### Tier 2 体验增强:/undo 增强 + /fork 对话分支
356
+
357
+ **Feature 1:/undo 增强 — bash 工具文件追踪 + 命令增强**
358
+
359
+ | 文件 | 变更 |
360
+ |------|------|
361
+ | `src/tools/undo-stack.ts` | `UndoEntry` 新增 `isDirectory?: boolean` + `pushNewFile()` / `pushNewDir()` 方法 + `undo()` 目录分支 |
362
+ | `src/tools/builtin/bash.ts` | 三辅助函数 + `execute()` 集成 |
363
+ | `src/repl/commands/index.ts` | `/undo` 重写为 `/undo [list\|<n>]` |
364
+
365
+ - **UndoStack 扩展**:新增 `pushNewFile(filePath, desc)` 和 `pushNewDir(dirPath, desc)` 方法,previousContent=null 表示新建(undo 时删除)。`undo()` 新增 `isDirectory` 分支,用 `rmdirSync` 尝试删除空目录,非空给出提示。
366
+ - **Bash 工具文件追踪**(浅层 CWD 快照 + 命令模式解析):
367
+ - `snapshotDir(dir)` — `readdirSync` 获取目录下所有条目的绝对路径 Set
368
+ - `parseCreationTargets(command, cwd)` — 正则解析 touch/mkdir/echo>/cp/New-Item 等常见创建命令的目标路径
369
+ - `pushBashUndoEntries(beforeSnapshot, parsedTargetsBefore, cwd)` — 对比前后快照 + 解析目标,为新建文件/目录推入 undo 条目
370
+ - 集成到 `execute()`:执行前快照 + 构建目标存在状态 Map;执行后(成功/失败均)调用 pushBashUndoEntries
371
+ - **/undo 命令增强**:
372
+ - `/undo` — 撤销最近 1 次(向后兼容)
373
+ - `/undo list` — 显示完整 undo 栈(编号、类型标签 `[new]`/`[mod]`/`[dir]`、描述、时间)
374
+ - `/undo <n>` — 连续撤销最近 N 次操作,逐条显示进度
375
+
376
+ **Feature 2:/fork 对话分支**
377
+
378
+ | 文件 | 变更 |
379
+ |------|------|
380
+ | `src/session/session.ts` | 静态 `fork(original, newId, messageCount, newTitle?)` 方法 |
381
+ | `src/session/session-manager.ts` | `forkSession(messageCount, title?)` 方法 |
382
+ | `src/repl/commands/index.ts` | `/fork` 命令 + `CommandContext.forkSession` |
383
+ | `src/repl/repl.ts` | ctx.forkSession 注入 + tab 补全(/undo list, /fork checkpoint 名称) |
384
+
385
+ - **Session.fork()**:复制 messages[0..messageCount],保留范围内 checkpoints(深拷贝),新 UUID,title 默认 "Fork of <原标题>"
386
+ - **SessionManager.forkSession()**:先保存原始 session → Session.fork 创建分叉 → 设为当前 → 保存并返回
387
+ - **/fork 命令**:
388
+ - `/fork` — 从当前位置分叉(复制全部消息)
389
+ - `/fork <checkpoint-name>` — 从指定 checkpoint 分叉(仅复制到该 checkpoint 的消息)
390
+ - 显示原始/新 session ID、title、消息数、保留 checkpoint 数
391
+ - 提示用户 `/session load <id>` 切回原始
392
+ - **Tab 补全**:`/undo` 提供 `list`;`/fork` 提供当前 session 的 checkpoint 名称列表
393
+
394
+ ### 版本与收尾
395
+ - `src/core/constants.ts`:VERSION `0.1.47` → `0.1.48`
396
+ - `package.json`:version 同步
397
+ - `src/repl/renderer.ts`:命令计数 34 → 35,命令列表新增 `/fork`,新增 1 条特性(/fork 对话分支),更新 /undo 描述
398
+ - 构建验证:`npm run build` 零错误(ESM + CJS 双产物)
399
+ - 发布:`npm publish` → `jinzd-ai-cli@0.1.48`
400
+
401
+ ### 本轮变更文件汇总
402
+
403
+ | 文件 | 变更类型 | 说明 |
404
+ |------|---------|------|
405
+ | `src/core/constants.ts` | 修改 | VERSION 0.1.47 → 0.1.48 |
406
+ | `src/tools/undo-stack.ts` | 修改 | isDirectory 字段 + pushNewFile/pushNewDir + undo 目录支持 |
407
+ | `src/tools/builtin/bash.ts` | 修改 | snapshotDir + parseCreationTargets + pushBashUndoEntries + execute 集成 |
408
+ | `src/session/session.ts` | 修改 | 静态 fork() 方法 |
409
+ | `src/session/session-manager.ts` | 修改 | forkSession() 方法 |
410
+ | `src/repl/commands/index.ts` | 修改 | /undo 增强 + /fork 命令 + CommandContext.forkSession + /help 更新 |
411
+ | `src/repl/repl.ts` | 修改 | ctx.forkSession 注入 + tab 补全(/undo, /fork) |
412
+ | `src/repl/renderer.ts` | 修改 | /about 35 命令 + 特性更新 |
413
+ | `package.json` | 修改 | version 0.1.47 → 0.1.48 |
414
+
415
+ ### 下一步建议
416
+
417
+ #### Tier 2 — 体验增强(剩余)
418
+ 1. **L1 低危**:`run-tests.ts` 的 `JSON.parse(package.json)` 细粒度错误处理
419
+ 2. **IDE 集成**:VS Code 扩展(架构已准备就绪,core/providers/tools 无终端依赖)
420
+ 3. **OAuth/浏览器登录**:无需手动填 API Key,打开浏览器完成 OAuth 流程自动保存 token
421
+
422
+ #### Tier 3 — 长远方向
423
+ 4. **Web UI**:基于 EventBus + WebSocket 的浏览器界面,复用 core/providers/tools 层
424
+ 5. **GitHub 仓库迁移**:从 gitee 迁移到 GitHub,npm 包受众更广
425
+
426
+ ---
427
+
353
428
  ## 本轮开发完成记录(2026-03-07,v0.1.40 → v0.1.41)
354
429
 
355
430
  ### Bug 修复:Kimi 虚假完成声明(方案 C)
@@ -536,8 +611,8 @@ const stdinLines = Array.isArray(rawStdin) ? rawStdin.map(String)
536
611
  4. **L1 低危**:`run-tests.ts` 的 `JSON.parse(package.json)` 细粒度错误处理
537
612
  5. **IDE 集成**:VS Code 扩展(架构已准备就绪,core/providers/tools 无终端依赖)
538
613
  6. **OAuth/浏览器登录**:无需手动填 API Key,打开浏览器完成 OAuth 流程自动保存 token
539
- 7. **`/undo` 增强**:支持 bash 工具创建的文件/目录的撤销(当前仅支持 write_file / edit_file)
540
- 8. **对话分支(fork)**:`/fork` 从当前对话某个 checkpoint 分叉为新 session,探索不同方案
614
+ 7. ~~**`/undo` 增强**~~:✅ 已在 v0.1.48 实现(bash 文件追踪 + /undo list + /undo <n>)
615
+ 8. ~~**对话分支(fork)**~~:✅ 已在 v0.1.48 实现(/fork [checkpoint-name])
541
616
 
542
617
  #### Tier 3 — 长远方向
543
618
  9. **Web UI**:基于 EventBus + WebSocket 的浏览器界面,复用 core/providers/tools 层
@@ -8,7 +8,7 @@ import { platform } from "os";
8
8
  import chalk from "chalk";
9
9
 
10
10
  // src/core/constants.ts
11
- var VERSION = "0.1.47";
11
+ var VERSION = "0.1.49";
12
12
  var APP_NAME = "ai-cli";
13
13
  var CONFIG_DIR_NAME = ".aicli";
14
14
  var CONFIG_FILE_NAME = "config.json";
package/dist/index.js CHANGED
@@ -29,7 +29,7 @@ import {
29
29
  SUBAGENT_MAX_ROUNDS_LIMIT,
30
30
  VERSION,
31
31
  runTestsTool
32
- } from "./chunk-ZUZDWQVG.js";
32
+ } from "./chunk-P4XKHPXU.js";
33
33
 
34
34
  // src/index.ts
35
35
  import { program } from "commander";
@@ -1846,6 +1846,26 @@ var Session = class _Session {
1846
1846
  }))
1847
1847
  };
1848
1848
  }
1849
+ /**
1850
+ * 从现有 session 分叉创建新 session。
1851
+ *
1852
+ * 复制 messages[0..messageCount],保留范围内的 checkpoints。
1853
+ * 新 session 拥有独立 UUID,title 默认 "Fork of <原标题>"。
1854
+ *
1855
+ * @param original 原始 session
1856
+ * @param newId 新 session 的 UUID
1857
+ * @param messageCount 复制的消息数量(0 = 空 session,> messages.length 则取全部)
1858
+ * @param newTitle 可选的新标题
1859
+ */
1860
+ static fork(original, newId, messageCount, newTitle) {
1861
+ const forked = new _Session(newId, original.provider, original.model);
1862
+ forked.title = newTitle ?? (original.title ? `Fork of ${original.title}` : void 0);
1863
+ const clampedCount = Math.min(Math.max(messageCount, 0), original.messages.length);
1864
+ forked.messages = original.messages.slice(0, clampedCount).map((m) => ({ ...m }));
1865
+ forked.checkpoints = original.checkpoints.filter((c) => c.messageIndex <= clampedCount).map((c) => ({ ...c, timestamp: new Date(c.timestamp.getTime()) }));
1866
+ forked.updated = /* @__PURE__ */ new Date();
1867
+ return forked;
1868
+ }
1849
1869
  /**
1850
1870
  * 从磁盘 JSON 数据恢复 Session 实例。
1851
1871
  * 添加运行时校验:损坏或不兼容的历史文件会抛出明确错误,而非 TypeError 崩溃。
@@ -1961,6 +1981,26 @@ var SessionManager = class {
1961
1981
  }
1962
1982
  return metas.sort((a, b) => b.updated.getTime() - a.updated.getTime());
1963
1983
  }
1984
+ /**
1985
+ * 从当前 session 分叉创建新 session。
1986
+ *
1987
+ * 先保存原始 session(保留完整历史),然后创建分叉(截取到 messageCount),
1988
+ * 将分叉设为当前 session 并保存。
1989
+ *
1990
+ * @param messageCount 复制的消息数量
1991
+ * @param title 可选的新标题
1992
+ * @returns 新的分叉 session
1993
+ */
1994
+ async forkSession(messageCount, title) {
1995
+ if (!this._current) {
1996
+ throw new Error("No active session to fork");
1997
+ }
1998
+ await this.save();
1999
+ const forked = Session.fork(this._current, uuidv4(), messageCount, title);
2000
+ this._current = forked;
2001
+ await this.save();
2002
+ return forked;
2003
+ }
1964
2004
  /**
1965
2005
  * 跨 session 全文搜索。
1966
2006
  * 遍历所有历史 JSON 文件,逐条匹配消息内容(不区分大小写),
@@ -2021,8 +2061,8 @@ var SessionManager = class {
2021
2061
 
2022
2062
  // src/repl/repl.ts
2023
2063
  import * as readline from "readline";
2024
- import { existsSync as existsSync19, readFileSync as readFileSync13, readdirSync as readdirSync9, statSync as statSync7 } from "fs";
2025
- import { join as join14, resolve as resolve4, extname as extname4, dirname as dirname5, basename as basename5 } from "path";
2064
+ import { existsSync as existsSync19, readFileSync as readFileSync13, readdirSync as readdirSync11, statSync as statSync8 } from "fs";
2065
+ import { join as join14, resolve as resolve5, extname as extname4, dirname as dirname6, basename as basename6 } from "path";
2026
2066
  import chalk10 from "chalk";
2027
2067
 
2028
2068
  // src/repl/renderer.ts
@@ -2231,12 +2271,12 @@ var Renderer = class {
2231
2271
  console.log(tool("spawn_agent", "\u59D4\u6D3E\u72EC\u7ACB\u5B50\u4EE3\u7406\u6267\u884C\u7279\u5B9A\u4EFB\u52A1\uFF08\u9694\u79BB\u5BF9\u8BDD + \u81EA\u52A8\u5DE5\u5177\u8C03\u7528\u5FAA\u73AF\uFF09"));
2232
2272
  console.log(tool("run_tests", "\u8FD0\u884C\u9879\u76EE\u6D4B\u8BD5\u5E76\u8FD4\u56DE\u7ED3\u6784\u5316\u62A5\u544A\uFF08\u81EA\u52A8\u68C0\u6D4B Maven/npm/pytest \u7B49\uFF09"));
2233
2273
  console.log(HR);
2234
- console.log(theme.dim(" REPL \u547D\u4EE4\uFF0834\u4E2A\uFF09\uFF1A"));
2274
+ console.log(theme.dim(" REPL \u547D\u4EE4\uFF0835\u4E2A\uFF09\uFF1A"));
2235
2275
  console.log(theme.dim(" /help /about /provider /model /clear /compact /plan /session"));
2236
2276
  console.log(theme.dim(" /system /context /status /search /undo /export /copy /cost"));
2237
2277
  console.log(theme.dim(" /init /skill /tools /plugins /mcp /config /checkpoint /review"));
2238
2278
  console.log(theme.dim(" /commands /test /scaffold /add-dir /memory /doctor /bug /think"));
2239
- console.log(theme.dim(" /diff /exit"));
2279
+ console.log(theme.dim(" /diff /fork /exit"));
2240
2280
  console.log(HR);
2241
2281
  console.log(theme.dim(" \u4E3B\u8981\u7279\u6027\uFF1A"));
2242
2282
  console.log(feat("Agentic \u5FAA\u73AF\uFF08\u6700\u591A 25 \u8F6E\u5DE5\u5177\u8C03\u7528\uFF0C\u6700\u7EC8\u56DE\u7B54\u6D41\u5F0F\u8F93\u51FA\uFF09"));
@@ -2244,7 +2284,7 @@ var Renderer = class {
2244
2284
  console.log(feat("Git \u4E0A\u4E0B\u6587\u611F\u77E5\uFF1A\u542F\u52A8\u81EA\u52A8\u6CE8\u5165\u5206\u652F\u540D\u4E0E\u6587\u4EF6\u53D8\u66F4\u72B6\u6001"));
2245
2285
  console.log(feat("\u9879\u76EE\u4E0A\u4E0B\u6587\u6587\u4EF6\uFF1A\u81EA\u52A8\u52A0\u8F7D AICLI.md / CLAUDE.md\uFF08\u4E09\u5C42\u7EA7\uFF1A\u5168\u5C40/\u9879\u76EE/\u5B50\u76EE\u5F55\uFF09"));
2246
2286
  console.log(feat("\u8DE8 session \u5386\u53F2\u5168\u6587\u641C\u7D22\uFF08/search <\u5173\u952E\u8BCD>\uFF09"));
2247
- console.log(feat("\u6587\u4EF6\u64CD\u4F5C\u64A4\u9500\uFF08/undo\uFF0C\u652F\u6301 write_file / edit_file\uFF09"));
2287
+ console.log(feat("\u6587\u4EF6\u64CD\u4F5C\u64A4\u9500\uFF08/undo [list|<n>]\uFF0C\u652F\u6301 write_file / edit_file / bash \u521B\u5EFA\u7684\u6587\u4EF6/\u76EE\u5F55\uFF09"));
2248
2288
  console.log(feat("Thinking \u6A21\u5F0F\u6298\u53E0\uFF08<think> \u5757\u81EA\u52A8\u6298\u53E0\uFF0CGLM-5 \u7B49\uFF09"));
2249
2289
  console.log(feat("Token \u7528\u91CF\u8FFD\u8E2A\uFF08\u6BCF\u6B21\u56DE\u590D + session \u7D2F\u8BA1\uFF0C\u652F\u6301 Gemini/Claude/DeepSeek \u7B49\uFF09"));
2250
2290
  console.log(feat("MCP \u534F\u8BAE\u652F\u6301\uFF1A\u63A5\u5165\u5916\u90E8 MCP \u670D\u52A1\u5668\u5DE5\u5177\uFF08config.json mcpServers \u914D\u7F6E\uFF09"));
@@ -2286,6 +2326,7 @@ var Renderer = class {
2286
2326
  console.log(feat("/config set|get|show\uFF1AREPL \u5185\u5FEB\u6377\u8BFB\u5199\u914D\u7F6E\uFF0C\u65E0\u9700\u8FDB\u5165\u5411\u5BFC\uFF08\u70B9\u5206\u8DEF\u5F84 + \u81EA\u52A8\u7C7B\u578B\u8F6C\u6362\uFF09"));
2287
2327
  console.log(feat("\u5DE5\u5177\u8C03\u7528\u6700\u7EC8\u56DE\u7B54\u6D41\u5F0F\u5316\uFF1A\u6A21\u62DF\u6253\u5B57\u673A\u6548\u679C\u9010\u5757\u8F93\u51FA\uFF0C\u96F6\u989D\u5916 API \u8C03\u7528\uFF0C\u652F\u6301 Escape \u4E2D\u65AD"));
2288
2328
  console.log(feat("/diff \u547D\u4EE4\uFF1A\u663E\u793A\u5F53\u524D session \u5185\u6240\u6709\u6587\u4EF6\u4FEE\u6539\u7684\u6C47\u603B diff\uFF08\u5408\u5E76\u540C\u6587\u4EF6\u591A\u6B21\u4FEE\u6539\uFF09"));
2329
+ console.log(feat("/fork \u5BF9\u8BDD\u5206\u652F\uFF1A\u4ECE\u5F53\u524D\u4F4D\u7F6E\u6216\u6307\u5B9A checkpoint \u5206\u53C9\u4E3A\u65B0 session\uFF0C\u63A2\u7D22\u4E0D\u540C\u65B9\u6848"));
2289
2330
  console.log();
2290
2331
  }
2291
2332
  printPrompt(provider, _model) {
@@ -2385,8 +2426,8 @@ var Renderer = class {
2385
2426
  process.stdout.write("\n\n");
2386
2427
  }
2387
2428
  if (fileStream) {
2388
- await new Promise((resolve5, reject) => {
2389
- fileStream.end((err) => err ? reject(err) : resolve5());
2429
+ await new Promise((resolve6, reject) => {
2430
+ fileStream.end((err) => err ? reject(err) : resolve6());
2390
2431
  });
2391
2432
  const kb = (Buffer.byteLength(fullContent, "utf-8") / 1024).toFixed(1);
2392
2433
  process.stdout.write(theme.success(` \u2705 \u5DF2\u4FDD\u5B58: ${options.saveToFile} (${kb} KB)
@@ -2441,7 +2482,7 @@ var Renderer = class {
2441
2482
  process.stdout.write(displayed.slice(pos, end));
2442
2483
  pos = end;
2443
2484
  if (pos < displayed.length) {
2444
- await new Promise((resolve5) => setTimeout(resolve5, DELAY_MS));
2485
+ await new Promise((resolve6) => setTimeout(resolve6, DELAY_MS));
2445
2486
  }
2446
2487
  }
2447
2488
  process.stdout.write("\n\n");
@@ -2595,7 +2636,7 @@ function formatGitContextForPrompt(ctx) {
2595
2636
  }
2596
2637
 
2597
2638
  // src/tools/undo-stack.ts
2598
- import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, unlinkSync, existsSync as existsSync4 } from "fs";
2639
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, unlinkSync, rmdirSync, existsSync as existsSync4 } from "fs";
2599
2640
  var MAX_UNDO_DEPTH = 20;
2600
2641
  var UndoStack = class {
2601
2642
  stack = [];
@@ -2623,6 +2664,37 @@ var UndoStack = class {
2623
2664
  this.stack.shift();
2624
2665
  }
2625
2666
  }
2667
+ /**
2668
+ * 推入一个新建文件的条目(previousContent=null),undo 时删除该文件。
2669
+ * 用于 bash 工具执行后检测到的新建文件。
2670
+ */
2671
+ pushNewFile(filePath, description) {
2672
+ this.stack.push({
2673
+ filePath,
2674
+ previousContent: null,
2675
+ description,
2676
+ timestamp: /* @__PURE__ */ new Date()
2677
+ });
2678
+ if (this.stack.length > MAX_UNDO_DEPTH) {
2679
+ this.stack.shift();
2680
+ }
2681
+ }
2682
+ /**
2683
+ * 推入一个新建目录的条目(previousContent=null, isDirectory=true),
2684
+ * undo 时尝试 rmdir(仅空目录可删)。
2685
+ */
2686
+ pushNewDir(dirPath, description) {
2687
+ this.stack.push({
2688
+ filePath: dirPath,
2689
+ previousContent: null,
2690
+ description,
2691
+ timestamp: /* @__PURE__ */ new Date(),
2692
+ isDirectory: true
2693
+ });
2694
+ if (this.stack.length > MAX_UNDO_DEPTH) {
2695
+ this.stack.shift();
2696
+ }
2697
+ }
2626
2698
  /**
2627
2699
  * 弹出并执行最近一次撤销操作。
2628
2700
  * @returns 撤销结果描述,或 null(栈为空时)
@@ -2632,10 +2704,22 @@ var UndoStack = class {
2632
2704
  if (!entry) return null;
2633
2705
  try {
2634
2706
  if (entry.previousContent === null) {
2635
- if (existsSync4(entry.filePath)) {
2636
- unlinkSync(entry.filePath);
2707
+ if (entry.isDirectory) {
2708
+ if (existsSync4(entry.filePath)) {
2709
+ try {
2710
+ rmdirSync(entry.filePath);
2711
+ return { entry, result: `Removed newly created directory: ${entry.filePath}` };
2712
+ } catch {
2713
+ return { entry, result: `Cannot remove directory (not empty): ${entry.filePath}` };
2714
+ }
2715
+ }
2716
+ return { entry, result: `Directory already removed: ${entry.filePath}` };
2717
+ } else {
2718
+ if (existsSync4(entry.filePath)) {
2719
+ unlinkSync(entry.filePath);
2720
+ }
2721
+ return { entry, result: `Deleted newly created file: ${entry.filePath}` };
2637
2722
  }
2638
- return { entry, result: `Deleted newly created file: ${entry.filePath}` };
2639
2723
  } else {
2640
2724
  writeFileSync3(entry.filePath, entry.previousContent, "utf-8");
2641
2725
  const lines = entry.previousContent.split("\n").length;
@@ -3173,7 +3257,7 @@ function createDefaultCommands() {
3173
3257
  " /context - Show or reload hierarchical context layers",
3174
3258
  " /status - Show current status",
3175
3259
  " /search <keyword> - Search across all session histories",
3176
- " /undo - Undo the last file write/edit operation",
3260
+ " /undo [list|<n>] - Undo file ops (list stack, undo N times)",
3177
3261
  " /export [md|json] [file] - Export session to file (default: auto-named .md)",
3178
3262
  " /tools - List all AI tools available",
3179
3263
  " /plugins - Show plugin directory and loaded plugins",
@@ -3193,6 +3277,7 @@ function createDefaultCommands() {
3193
3277
  " /doctor - Health check (API keys, config, MCP status)",
3194
3278
  " /bug [--copy] - Generate bug report template (--copy to clipboard)",
3195
3279
  " /diff [--stats] - Show all file modifications in this session",
3280
+ " /fork [checkpoint] - Fork session from checkpoint or current position",
3196
3281
  " /exit - Exit"
3197
3282
  ] : [];
3198
3283
  console.log("\nAvailable commands:");
@@ -3581,23 +3666,53 @@ ${text}
3581
3666
  },
3582
3667
  {
3583
3668
  name: "undo",
3584
- description: "Undo the last file write or edit operation",
3585
- usage: "/undo",
3586
- execute(_args, ctx) {
3587
- const top = undoStack.peek();
3588
- if (!top) {
3669
+ description: "Undo file operations (supports: /undo, /undo list, /undo <n>)",
3670
+ usage: "/undo [list | <n>]",
3671
+ execute(args, ctx) {
3672
+ const sub = args.trim();
3673
+ if (sub === "list") {
3674
+ const history = undoStack.getHistory();
3675
+ if (history.length === 0) {
3676
+ ctx.renderer.printInfo("Undo stack is empty.");
3677
+ return;
3678
+ }
3679
+ console.log(theme.heading("\n Undo Stack (" + history.length + " entries, newest last):\n"));
3680
+ history.forEach((entry, i) => {
3681
+ const timeStr = entry.timestamp.toLocaleTimeString();
3682
+ const typeTag = entry.isDirectory ? theme.accent("[dir]") : entry.previousContent === null ? theme.accent("[new]") : theme.dim("[mod]");
3683
+ console.log(` ${theme.dim(String(i + 1).padStart(3))} ${typeTag} ${entry.description} ${theme.dim(timeStr)}`);
3684
+ });
3685
+ console.log("");
3686
+ return;
3687
+ }
3688
+ const n = sub ? parseInt(sub, 10) : 1;
3689
+ if (isNaN(n) || n < 1) {
3690
+ ctx.renderer.printInfo("Usage: /undo [list | <n>] \u2014 n must be a positive integer.");
3691
+ return;
3692
+ }
3693
+ if (undoStack.depth === 0) {
3589
3694
  ctx.renderer.printInfo("Nothing to undo.");
3590
3695
  return;
3591
3696
  }
3592
- const timeStr = top.timestamp.toLocaleTimeString();
3593
- ctx.renderer.printInfo(
3594
- `Undoing: ${top.description} (${timeStr})`
3595
- );
3596
- const undoResult = undoStack.undo();
3597
- if (undoResult) {
3598
- ctx.renderer.printSuccess(undoResult.result);
3599
- } else {
3697
+ const count = Math.min(n, undoStack.depth);
3698
+ const results = [];
3699
+ for (let i = 0; i < count; i++) {
3700
+ const top = undoStack.peek();
3701
+ if (!top) break;
3702
+ const timeStr = top.timestamp.toLocaleTimeString();
3703
+ ctx.renderer.printInfo(`Undoing [${i + 1}/${count}]: ${top.description} (${timeStr})`);
3704
+ const undoResult = undoStack.undo();
3705
+ if (undoResult) {
3706
+ results.push(undoResult.result);
3707
+ ctx.renderer.printSuccess(" " + undoResult.result);
3708
+ }
3709
+ }
3710
+ if (results.length === 0) {
3600
3711
  ctx.renderer.printInfo("Nothing to undo.");
3712
+ } else if (results.length > 1) {
3713
+ console.log(theme.success(`
3714
+ \u2713 ${results.length} operations undone.
3715
+ `));
3601
3716
  }
3602
3717
  }
3603
3718
  },
@@ -4173,7 +4288,7 @@ ${hint}` : "")
4173
4288
  description: "Run project tests and show structured report",
4174
4289
  usage: "/test [command|filter]",
4175
4290
  async execute(args, _ctx) {
4176
- const { executeTests } = await import("./run-tests-EM3QNRWV.js");
4291
+ const { executeTests } = await import("./run-tests-JJVZWQKI.js");
4177
4292
  const argStr = args.join(" ").trim();
4178
4293
  let testArgs = {};
4179
4294
  if (argStr) {
@@ -4500,6 +4615,57 @@ Summary: ${fileMap.size} file(s) \u2014 ${newFiles} new, ${modifiedFiles} modifi
4500
4615
  console.log();
4501
4616
  }
4502
4617
  },
4618
+ {
4619
+ name: "fork",
4620
+ description: "Fork current session from a checkpoint or current position",
4621
+ usage: "/fork [checkpoint-name]",
4622
+ async execute(args, ctx) {
4623
+ const session = ctx.sessions.current;
4624
+ if (!session) {
4625
+ ctx.renderer.printInfo("No active session to fork.");
4626
+ return;
4627
+ }
4628
+ const sub = args.join(" ").trim();
4629
+ let messageCount = session.messages.length;
4630
+ let fromLabel = "current position";
4631
+ if (sub) {
4632
+ const cp = session.checkpoints.find((c) => c.name === sub);
4633
+ if (!cp) {
4634
+ const available = session.checkpoints.map((c) => c.name);
4635
+ if (available.length > 0) {
4636
+ ctx.renderer.printInfo(
4637
+ `Checkpoint "${sub}" not found. Available: ${available.join(", ")}`
4638
+ );
4639
+ } else {
4640
+ ctx.renderer.printInfo(
4641
+ `Checkpoint "${sub}" not found. No checkpoints saved. Use /checkpoint save <name> first.`
4642
+ );
4643
+ }
4644
+ return;
4645
+ }
4646
+ messageCount = cp.messageIndex;
4647
+ fromLabel = `checkpoint "${cp.name}" (${cp.messageIndex} messages)`;
4648
+ }
4649
+ try {
4650
+ const originalId = session.id.slice(0, 8);
4651
+ const originalTitle = session.title ?? "(untitled)";
4652
+ const forked = await ctx.forkSession(messageCount);
4653
+ console.log(theme.heading("\n Session Forked\n"));
4654
+ console.log(` ${theme.dim("Original:")} ${theme.accent(originalId)} "${originalTitle}"`);
4655
+ console.log(` ${theme.dim("Forked: ")} ${theme.accent(forked.id.slice(0, 8))} "${forked.title ?? "(untitled)"}"`);
4656
+ console.log(` ${theme.dim("From: ")} ${fromLabel}`);
4657
+ console.log(` ${theme.dim("Messages:")} ${forked.messages.length} copied, ${forked.checkpoints.length} checkpoint(s) preserved`);
4658
+ console.log("");
4659
+ console.log(theme.dim(` Use /session load ${originalId} to switch back to original.`));
4660
+ console.log("");
4661
+ ctx.refreshPrompt();
4662
+ } catch (err) {
4663
+ ctx.renderer.printError(
4664
+ `Fork failed: ${err instanceof Error ? err.message : String(err)}`
4665
+ );
4666
+ }
4667
+ }
4668
+ },
4503
4669
  {
4504
4670
  name: "exit",
4505
4671
  description: "Exit the REPL",
@@ -4519,7 +4685,7 @@ var IGNORE_ENTER_MS = 80;
4519
4685
  function selectFromList(prompt, items, initialIndex = 0) {
4520
4686
  if (items.length === 0) return Promise.resolve(null);
4521
4687
  const PAGE = 12;
4522
- return new Promise((resolve5) => {
4688
+ return new Promise((resolve6) => {
4523
4689
  let selected = Math.max(0, Math.min(initialIndex, items.length - 1));
4524
4690
  let windowStart = Math.max(0, selected - Math.floor(PAGE / 2));
4525
4691
  let lastRenderedLines = 0;
@@ -4594,7 +4760,7 @@ function selectFromList(prompt, items, initialIndex = 0) {
4594
4760
  process.stdout.write(chalk5.dim(` \u2714 ${result}
4595
4761
  `));
4596
4762
  }
4597
- resolve5(result);
4763
+ resolve6(result);
4598
4764
  };
4599
4765
  const handleSequence = (seq) => {
4600
4766
  if (seq === "\x1B") {
@@ -4658,13 +4824,13 @@ function selectFromList(prompt, items, initialIndex = 0) {
4658
4824
  }
4659
4825
  };
4660
4826
  if (!process.stdin.isTTY) {
4661
- resolve5(items[0]?.value ?? null);
4827
+ resolve6(items[0]?.value ?? null);
4662
4828
  return;
4663
4829
  }
4664
4830
  try {
4665
4831
  process.stdin.setRawMode(true);
4666
4832
  } catch {
4667
- resolve5(items[0]?.value ?? null);
4833
+ resolve6(items[0]?.value ?? null);
4668
4834
  return;
4669
4835
  }
4670
4836
  savedDataListeners = process.stdin.rawListeners("data");
@@ -4677,7 +4843,7 @@ function selectFromList(prompt, items, initialIndex = 0) {
4677
4843
 
4678
4844
  // src/tools/builtin/bash.ts
4679
4845
  import { execSync as execSync4 } from "child_process";
4680
- import { existsSync as existsSync7 } from "fs";
4846
+ import { existsSync as existsSync7, readdirSync as readdirSync3, statSync as statSync3 } from "fs";
4681
4847
  import { platform as platform2 } from "os";
4682
4848
  import { resolve as resolve2 } from "path";
4683
4849
  var IS_WINDOWS = platform2() === "win32";
@@ -4749,6 +4915,12 @@ var bashTool = {
4749
4915
  } else {
4750
4916
  actualCommand = command;
4751
4917
  }
4918
+ const beforeSnapshot = snapshotDir(effectiveCwd);
4919
+ const parsedTargets = parseCreationTargets(command, effectiveCwd);
4920
+ const parsedTargetsBefore = /* @__PURE__ */ new Map();
4921
+ for (const t of parsedTargets) {
4922
+ parsedTargetsBefore.set(t, existsSync7(t));
4923
+ }
4752
4924
  try {
4753
4925
  const output = execSync4(actualCommand, {
4754
4926
  timeout,
@@ -4763,9 +4935,11 @@ var bashTool = {
4763
4935
  }
4764
4936
  });
4765
4937
  updateCwdFromCommand(command, effectiveCwd);
4938
+ pushBashUndoEntries(beforeSnapshot, parsedTargetsBefore, effectiveCwd);
4766
4939
  const result = IS_WINDOWS && Buffer.isBuffer(output) ? output.toString("utf-8") : output;
4767
4940
  return result || "(command completed with no output)";
4768
4941
  } catch (err) {
4942
+ pushBashUndoEntries(beforeSnapshot, parsedTargetsBefore, effectiveCwd);
4769
4943
  if (err && typeof err === "object" && "status" in err) {
4770
4944
  const execErr = err;
4771
4945
  const stderr = IS_WINDOWS && Buffer.isBuffer(execErr.stderr) ? execErr.stderr.toString("utf-8").trim() : execErr.stderr?.toString().trim() ?? "";
@@ -4798,6 +4972,69 @@ function fixWindowsDeleteCommand(command) {
4798
4972
  }
4799
4973
  );
4800
4974
  }
4975
+ function snapshotDir(dir) {
4976
+ try {
4977
+ return new Set(readdirSync3(dir).map((name) => resolve2(dir, name)));
4978
+ } catch {
4979
+ return /* @__PURE__ */ new Set();
4980
+ }
4981
+ }
4982
+ function parseCreationTargets(command, cwd) {
4983
+ const targets = [];
4984
+ for (const m of command.matchAll(/(?:echo|cat|printf)\s+[^>]*>\s*(['"]?)([^\s;&|'"]+)\1/g)) {
4985
+ if (m[2]) targets.push(resolve2(cwd, m[2]));
4986
+ }
4987
+ for (const m of command.matchAll(/\btouch\s+((?:['"]?[^\s;&|'"]+['"]?\s*)+)/g)) {
4988
+ for (const f of m[1].trim().split(/\s+/)) {
4989
+ const clean = f.replace(/^['"]|['"]$/g, "");
4990
+ if (clean && !clean.startsWith("-")) targets.push(resolve2(cwd, clean));
4991
+ }
4992
+ }
4993
+ for (const m of command.matchAll(/\bmkdir\s+(?:-\w+\s+)*((?:['"]?[^\s;&|'"]+['"]?\s*)+)/g)) {
4994
+ for (const d of m[1].trim().split(/\s+/)) {
4995
+ const clean = d.replace(/^['"]|['"]$/g, "");
4996
+ if (clean && !clean.startsWith("-")) targets.push(resolve2(cwd, clean));
4997
+ }
4998
+ }
4999
+ for (const m of command.matchAll(/\bcp\s+(?:-\w+\s+)*['"]?[^\s;&|'"]+['"]?\s+(['"]?)([^\s;&|'"]+)\1/g)) {
5000
+ if (m[2]) targets.push(resolve2(cwd, m[2]));
5001
+ }
5002
+ for (const m of command.matchAll(/\bNew-Item\s+(?:-(?:Path|ItemType)\s+\w+\s+)*['"]?([^\s;&|'"]+)['"]?/gi)) {
5003
+ if (m[1] && !m[1].startsWith("-")) targets.push(resolve2(cwd, m[1]));
5004
+ }
5005
+ return [...new Set(targets)];
5006
+ }
5007
+ function pushBashUndoEntries(beforeSnapshot, parsedTargetsBefore, cwd) {
5008
+ const tracked = /* @__PURE__ */ new Set();
5009
+ const afterSnapshot = snapshotDir(cwd);
5010
+ for (const absPath of afterSnapshot) {
5011
+ if (!beforeSnapshot.has(absPath)) {
5012
+ try {
5013
+ const st = statSync3(absPath);
5014
+ if (st.isDirectory()) {
5015
+ undoStack.pushNewDir(absPath, `bash (new dir): ${absPath}`);
5016
+ } else {
5017
+ undoStack.pushNewFile(absPath, `bash (new file): ${absPath}`);
5018
+ }
5019
+ tracked.add(absPath);
5020
+ } catch {
5021
+ }
5022
+ }
5023
+ }
5024
+ for (const [target, existedBefore] of parsedTargetsBefore) {
5025
+ if (!existedBefore && !tracked.has(target) && existsSync7(target)) {
5026
+ try {
5027
+ const st = statSync3(target);
5028
+ if (st.isDirectory()) {
5029
+ undoStack.pushNewDir(target, `bash (new dir): ${target}`);
5030
+ } else {
5031
+ undoStack.pushNewFile(target, `bash (new file): ${target}`);
5032
+ }
5033
+ } catch {
5034
+ }
5035
+ }
5036
+ }
5037
+ }
4801
5038
  function updateCwdFromCommand(command, baseCwd) {
4802
5039
  const cdMatches = [...command.matchAll(/(?:^|[;&|])\s*cd\s+(['"]?)([^\s;&|'"]+)\1/g)];
4803
5040
  if (cdMatches.length === 0) return;
@@ -4814,8 +5051,9 @@ function updateCwdFromCommand(command, baseCwd) {
4814
5051
  }
4815
5052
 
4816
5053
  // src/tools/builtin/read-file.ts
4817
- import { readFileSync as readFileSync5, existsSync as existsSync8, statSync as statSync3 } from "fs";
4818
- import { extname, resolve as resolve3, basename as basename2, sep } from "path";
5054
+ import { readFileSync as readFileSync5, existsSync as existsSync8, statSync as statSync4, readdirSync as readdirSync4 } from "fs";
5055
+ import { execSync as execSync5 } from "child_process";
5056
+ import { extname, resolve as resolve3, basename as basename2, sep, dirname as dirname3 } from "path";
4819
5057
  import { homedir as homedir2 } from "os";
4820
5058
  var MAX_FILE_BYTES = 10 * 1024 * 1024;
4821
5059
  function getSensitiveWarning(normalizedPath) {
@@ -4883,6 +5121,55 @@ function isBinaryBuffer(buf) {
4883
5121
  }
4884
5122
  return nullCount / sample.length > 0.1;
4885
5123
  }
5124
+ function findSimilarFiles(filePath) {
5125
+ const targetName = basename2(filePath).toLowerCase();
5126
+ if (!targetName) return [];
5127
+ const cwd = process.cwd();
5128
+ const candidates = [];
5129
+ try {
5130
+ for (const entry of readdirSync4(cwd, { withFileTypes: true })) {
5131
+ if (entry.isFile() && entry.name.toLowerCase() === targetName) {
5132
+ candidates.push(entry.name);
5133
+ }
5134
+ if (entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules") {
5135
+ try {
5136
+ const subDir = resolve3(cwd, entry.name);
5137
+ for (const sub of readdirSync4(subDir, { withFileTypes: true })) {
5138
+ if (sub.isFile() && sub.name.toLowerCase() === targetName) {
5139
+ candidates.push(`${entry.name}/${sub.name}`);
5140
+ }
5141
+ }
5142
+ } catch {
5143
+ }
5144
+ }
5145
+ }
5146
+ } catch {
5147
+ }
5148
+ return candidates.slice(0, 5);
5149
+ }
5150
+ function tryExtractPdfText(absPath) {
5151
+ try {
5152
+ const output = execSync5(`pdftotext "${absPath}" -`, {
5153
+ timeout: 15e3,
5154
+ encoding: "utf-8",
5155
+ stdio: ["pipe", "pipe", "pipe"]
5156
+ });
5157
+ if (output.trim().length > 0) return output;
5158
+ } catch {
5159
+ }
5160
+ try {
5161
+ const pyScript = `import sys; exec("try:\\n from pdfminer.high_level import extract_text\\n print(extract_text(sys.argv[1]))\\nexcept: pass")`;
5162
+ const output = execSync5(`python -c "${pyScript}" "${absPath}"`, {
5163
+ timeout: 15e3,
5164
+ encoding: "utf-8",
5165
+ stdio: ["pipe", "pipe", "pipe"],
5166
+ env: { ...process.env, PYTHONUTF8: "1", PYTHONIOENCODING: "utf-8" }
5167
+ });
5168
+ if (output.trim().length > 0) return output;
5169
+ } catch {
5170
+ }
5171
+ return null;
5172
+ }
4886
5173
  var readFileTool = {
4887
5174
  definition: {
4888
5175
  name: "read_file",
@@ -4907,8 +5194,24 @@ var readFileTool = {
4907
5194
  const encoding = args["encoding"] ?? "utf-8";
4908
5195
  if (!filePath) throw new Error("path is required");
4909
5196
  const normalizedPath = resolve3(filePath);
4910
- if (!existsSync8(normalizedPath)) throw new Error(`File not found: ${filePath}`);
4911
- const { size } = statSync3(normalizedPath);
5197
+ if (!existsSync8(normalizedPath)) {
5198
+ const suggestions = findSimilarFiles(filePath);
5199
+ if (suggestions.length > 0) {
5200
+ throw new Error(
5201
+ `File not found: ${filePath}
5202
+ \u5F53\u524D\u5DE5\u4F5C\u76EE\u5F55: ${process.cwd()}
5203
+ \u627E\u5230\u540C\u540D\u6587\u4EF6\uFF0C\u4F60\u662F\u5426\u8981\u627E\uFF1A
5204
+ ` + suggestions.map((s) => ` \u2192 ${s}`).join("\n") + `
5205
+ \u8BF7\u4F7F\u7528\u6B63\u786E\u7684\u76F8\u5BF9\u8DEF\u5F84\u91CD\u8BD5\u3002`
5206
+ );
5207
+ }
5208
+ throw new Error(
5209
+ `File not found: ${filePath}
5210
+ \u5F53\u524D\u5DE5\u4F5C\u76EE\u5F55: ${process.cwd()}
5211
+ \u8BF7\u4F7F\u7528 list_dir \u786E\u8BA4\u6587\u4EF6\u8DEF\u5F84\u540E\u91CD\u8BD5\u3002`
5212
+ );
5213
+ }
5214
+ const { size } = statSync4(normalizedPath);
4912
5215
  if (size > MAX_FILE_BYTES) {
4913
5216
  const mb = (size / 1024 / 1024).toFixed(1);
4914
5217
  return `[File too large: ${filePath} (${mb} MB)]
@@ -4920,12 +5223,31 @@ var readFileTool = {
4920
5223
  }
4921
5224
  const sensitiveWarning = getSensitiveWarning(normalizedPath);
4922
5225
  const ext = extname(normalizedPath).toLowerCase();
5226
+ if (ext === ".pdf") {
5227
+ const pdfText = tryExtractPdfText(normalizedPath);
5228
+ if (pdfText) {
5229
+ const lines2 = pdfText.split("\n").length;
5230
+ return `[PDF extracted: ${filePath} | ${lines2} lines]
5231
+
5232
+ ${pdfText}`;
5233
+ }
5234
+ const dir = dirname3(normalizedPath);
5235
+ const nameNoExt = basename2(normalizedPath, ext);
5236
+ const textAlts = [".md", ".txt", ".html"].map((e) => resolve3(dir, nameNoExt + e)).filter(existsSync8);
5237
+ return `[Binary file: ${filePath}]
5238
+ PDF \u6587\u672C\u63D0\u53D6\u5931\u8D25\uFF08pdftotext \u548C pdfminer \u5747\u4E0D\u53EF\u7528\uFF09\u3002
5239
+ ` + (textAlts.length > 0 ? `\u627E\u5230\u53EF\u66FF\u4EE3\u7684\u6587\u672C\u7248\u672C\uFF1A
5240
+ ${textAlts.map((p) => ` \u2192 ${basename2(p)}`).join("\n")}
5241
+ \u8BF7\u4F7F\u7528 read_file \u8BFB\u53D6\u4E0A\u8FF0\u6587\u4EF6\u3002` : `\u5982\u9700\u4F7F\u7528\u5176\u5185\u5BB9\uFF0C\u8BF7\u4F7F\u7528 bash \u5DE5\u5177\u5B89\u88C5\u5E76\u8C03\u7528 pdftotext\uFF1A
5242
+ pip install pdfminer.six # \u5B89\u88C5 Python PDF \u5E93
5243
+ \u6216\u5C06 PDF \u624B\u52A8\u8F6C\u6362\u4E3A .md / .txt \u6587\u4EF6\u540E\u518D\u8BFB\u53D6\u3002`);
5244
+ }
4923
5245
  if (BINARY_EXTENSIONS.has(ext)) {
4924
5246
  return `[Binary file: ${filePath}]
4925
5247
  \u6B64\u6587\u4EF6\u4E3A\u4E8C\u8FDB\u5236\u683C\u5F0F\uFF08${ext}\uFF09\uFF0C\u65E0\u6CD5\u4F5C\u4E3A\u6587\u672C\u8BFB\u53D6\u3002
4926
5248
  \u5982\u9700\u4F7F\u7528\u5176\u5185\u5BB9\uFF0C\u8BF7\u8003\u8651\uFF1A
4927
5249
  1. \u5C06\u6587\u4EF6\u8F6C\u6362\u4E3A\u6587\u672C\u683C\u5F0F\u540E\u518D\u8BFB\u53D6
4928
- 2. \u4F7F\u7528 bash \u5DE5\u5177\u8C03\u7528\u5916\u90E8\u8F6C\u6362\u7A0B\u5E8F\uFF08\u5982 pdftotext\u3001pandoc \u7B49\uFF09
5250
+ 2. \u4F7F\u7528 bash \u5DE5\u5177\u8C03\u7528\u5916\u90E8\u8F6C\u6362\u7A0B\u5E8F\uFF08\u5982 pandoc \u7B49\uFF09
4929
5251
  3. \u82E5\u6709\u5BF9\u5E94\u7684\u7EAF\u6587\u672C\u7248\u672C\uFF08.md / .txt\uFF09\uFF0C\u8BF7\u76F4\u63A5\u8BFB\u53D6\u90A3\u4E2A\u6587\u4EF6`;
4930
5252
  }
4931
5253
  const buf = readFileSync5(normalizedPath);
@@ -4949,7 +5271,7 @@ ${content}`;
4949
5271
 
4950
5272
  // src/tools/builtin/write-file.ts
4951
5273
  import { writeFileSync as writeFileSync5, appendFileSync as appendFileSync2, mkdirSync as mkdirSync5 } from "fs";
4952
- import { dirname as dirname3 } from "path";
5274
+ import { dirname as dirname4 } from "path";
4953
5275
  var writeFileTool = {
4954
5276
  definition: {
4955
5277
  name: "write_file",
@@ -4989,7 +5311,7 @@ var writeFileTool = {
4989
5311
  if (!appendMode) {
4990
5312
  undoStack.push(filePath, `write_file: ${filePath}`);
4991
5313
  }
4992
- mkdirSync5(dirname3(filePath), { recursive: true });
5314
+ mkdirSync5(dirname4(filePath), { recursive: true });
4993
5315
  if (appendMode) {
4994
5316
  appendFileSync2(filePath, content, encoding);
4995
5317
  } else {
@@ -5221,8 +5543,8 @@ function truncatePreview(str, maxLen = 80) {
5221
5543
  }
5222
5544
 
5223
5545
  // src/tools/builtin/list-dir.ts
5224
- import { readdirSync as readdirSync3, statSync as statSync4, existsSync as existsSync10 } from "fs";
5225
- import { join as join6 } from "path";
5546
+ import { readdirSync as readdirSync5, statSync as statSync5, existsSync as existsSync10 } from "fs";
5547
+ import { join as join6, basename as basename3 } from "path";
5226
5548
  var listDirTool = {
5227
5549
  definition: {
5228
5550
  name: "list_dir",
@@ -5244,7 +5566,33 @@ var listDirTool = {
5244
5566
  async execute(args) {
5245
5567
  const dirPath = String(args["path"] ?? process.cwd());
5246
5568
  const recursive = Boolean(args["recursive"] ?? false);
5247
- if (!existsSync10(dirPath)) throw new Error(`Directory not found: ${dirPath}`);
5569
+ if (!existsSync10(dirPath)) {
5570
+ const targetName = basename3(dirPath).toLowerCase();
5571
+ const cwd = process.cwd();
5572
+ const suggestions = [];
5573
+ try {
5574
+ for (const entry of readdirSync5(cwd, { withFileTypes: true })) {
5575
+ if (entry.isDirectory() && entry.name.toLowerCase() === targetName) {
5576
+ suggestions.push(entry.name);
5577
+ }
5578
+ }
5579
+ } catch {
5580
+ }
5581
+ if (suggestions.length > 0) {
5582
+ throw new Error(
5583
+ `Directory not found: ${dirPath}
5584
+ \u5F53\u524D\u5DE5\u4F5C\u76EE\u5F55: ${cwd}
5585
+ \u627E\u5230\u540C\u540D\u76EE\u5F55\uFF1A
5586
+ ` + suggestions.map((s) => ` \u2192 ${s}`).join("\n") + `
5587
+ \u8BF7\u4F7F\u7528\u6B63\u786E\u7684\u76F8\u5BF9\u8DEF\u5F84\u91CD\u8BD5\u3002`
5588
+ );
5589
+ }
5590
+ throw new Error(
5591
+ `Directory not found: ${dirPath}
5592
+ \u5F53\u524D\u5DE5\u4F5C\u76EE\u5F55: ${cwd}
5593
+ \u8BF7\u5148\u4F7F\u7528 list_dir\uFF08\u4E0D\u4F20 path\uFF09\u67E5\u770B\u5F53\u524D\u76EE\u5F55\u7ED3\u6784\u3002`
5594
+ );
5595
+ }
5248
5596
  const lines = [`Directory: ${dirPath}
5249
5597
  `];
5250
5598
  listRecursive(dirPath, "", recursive, lines);
@@ -5254,7 +5602,7 @@ var listDirTool = {
5254
5602
  function listRecursive(basePath, indent, recursive, lines) {
5255
5603
  let entries;
5256
5604
  try {
5257
- entries = readdirSync3(basePath, { withFileTypes: true });
5605
+ entries = readdirSync5(basePath, { withFileTypes: true });
5258
5606
  } catch {
5259
5607
  lines.push(`${indent}(permission denied)`);
5260
5608
  return;
@@ -5277,7 +5625,7 @@ function listRecursive(basePath, indent, recursive, lines) {
5277
5625
  }
5278
5626
  } else {
5279
5627
  try {
5280
- const stat = statSync4(join6(basePath, entry.name));
5628
+ const stat = statSync5(join6(basePath, entry.name));
5281
5629
  const size = formatSize(stat.size);
5282
5630
  lines.push(`${indent}\u{1F4C4} ${entry.name} (${size})`);
5283
5631
  } catch {
@@ -5293,7 +5641,7 @@ function formatSize(bytes) {
5293
5641
  }
5294
5642
 
5295
5643
  // src/tools/builtin/grep-files.ts
5296
- import { readdirSync as readdirSync4, readFileSync as readFileSync7, statSync as statSync5, existsSync as existsSync11 } from "fs";
5644
+ import { readdirSync as readdirSync6, readFileSync as readFileSync7, statSync as statSync6, existsSync as existsSync11 } from "fs";
5297
5645
  import { join as join7, relative } from "path";
5298
5646
  var grepFilesTool = {
5299
5647
  definition: {
@@ -5354,7 +5702,7 @@ var grepFilesTool = {
5354
5702
  regex = new RegExp(escaped, ignoreCase ? "gi" : "g");
5355
5703
  }
5356
5704
  const results = [];
5357
- const stat = statSync5(rootPath);
5705
+ const stat = statSync6(rootPath);
5358
5706
  if (stat.isFile()) {
5359
5707
  searchInFile(rootPath, rootPath, regex, contextLines, maxResults, results);
5360
5708
  } else {
@@ -5410,7 +5758,7 @@ function collectFiles(dirPath, filePattern, results, regex, contextLines, maxRes
5410
5758
  if (results.length >= maxResults) return;
5411
5759
  let entries;
5412
5760
  try {
5413
- entries = readdirSync4(dirPath, { withFileTypes: true });
5761
+ entries = readdirSync6(dirPath, { withFileTypes: true });
5414
5762
  } catch {
5415
5763
  return;
5416
5764
  }
@@ -5463,8 +5811,8 @@ function searchInFile(fullPath, displayPath, regex, contextLines, maxResults, re
5463
5811
  }
5464
5812
 
5465
5813
  // src/tools/builtin/glob-files.ts
5466
- import { readdirSync as readdirSync5, statSync as statSync6, existsSync as existsSync12 } from "fs";
5467
- import { join as join8, relative as relative2, basename as basename3 } from "path";
5814
+ import { readdirSync as readdirSync7, statSync as statSync7, existsSync as existsSync12 } from "fs";
5815
+ import { join as join8, relative as relative2, basename as basename4 } from "path";
5468
5816
  var globFilesTool = {
5469
5817
  definition: {
5470
5818
  name: "glob_files",
@@ -5568,7 +5916,7 @@ function collectMatchingFiles(dirPath, rootPath, regex, results, maxResults) {
5568
5916
  if (results.length >= maxResults) return;
5569
5917
  let entries;
5570
5918
  try {
5571
- entries = readdirSync5(dirPath, { withFileTypes: true });
5919
+ entries = readdirSync7(dirPath, { withFileTypes: true });
5572
5920
  } catch {
5573
5921
  return;
5574
5922
  }
@@ -5580,9 +5928,9 @@ function collectMatchingFiles(dirPath, rootPath, regex, results, maxResults) {
5580
5928
  collectMatchingFiles(fullPath, rootPath, regex, results, maxResults);
5581
5929
  } else if (entry.isFile()) {
5582
5930
  const relPath = relative2(rootPath, fullPath).replace(/\\/g, "/");
5583
- if (regex.test(relPath) || regex.test(basename3(relPath))) {
5931
+ if (regex.test(relPath) || regex.test(basename4(relPath))) {
5584
5932
  try {
5585
- const stat = statSync6(fullPath);
5933
+ const stat = statSync7(fullPath);
5586
5934
  results.push({ relPath, absPath: fullPath, mtime: stat.mtimeMs });
5587
5935
  } catch {
5588
5936
  results.push({ relPath, absPath: fullPath, mtime: 0 });
@@ -5656,7 +6004,7 @@ var runInteractiveTool = {
5656
6004
  PYTHONDONTWRITEBYTECODE: "1"
5657
6005
  };
5658
6006
  const prefixWarnings = [argsTypeWarning, stdinTypeWarning].filter(Boolean).join("");
5659
- return new Promise((resolve5) => {
6007
+ return new Promise((resolve6) => {
5660
6008
  const child = spawn(executable, cmdArgs.map(String), {
5661
6009
  cwd: process.cwd(),
5662
6010
  env,
@@ -5689,22 +6037,22 @@ var runInteractiveTool = {
5689
6037
  setTimeout(writeNextLine, 400);
5690
6038
  const timer = setTimeout(() => {
5691
6039
  child.kill();
5692
- resolve5(`${prefixWarnings}[Timeout after ${timeout}ms]
6040
+ resolve6(`${prefixWarnings}[Timeout after ${timeout}ms]
5693
6041
  ${buildOutput(stdout, stderr)}`);
5694
6042
  }, timeout);
5695
6043
  child.on("close", (code) => {
5696
6044
  clearTimeout(timer);
5697
6045
  const output = buildOutput(stdout, stderr);
5698
6046
  if (code !== 0 && code !== null) {
5699
- resolve5(`${prefixWarnings}Exit code ${code}:
6047
+ resolve6(`${prefixWarnings}Exit code ${code}:
5700
6048
  ${output}`);
5701
6049
  } else {
5702
- resolve5(`${prefixWarnings}${output || "(no output)"}`);
6050
+ resolve6(`${prefixWarnings}${output || "(no output)"}`);
5703
6051
  }
5704
6052
  });
5705
6053
  child.on("error", (err) => {
5706
6054
  clearTimeout(timer);
5707
- resolve5(
6055
+ resolve6(
5708
6056
  `${prefixWarnings}Failed to start process "${executable}": ${err.message}
5709
6057
  Hint: On Windows, use the full path to the executable, e.g.:
5710
6058
  C:\\Users\\Jinzd\\anaconda3\\envs\\python312\\python.exe`
@@ -5925,7 +6273,7 @@ var webFetchTool = {
5925
6273
 
5926
6274
  // src/tools/builtin/save-last-response.ts
5927
6275
  import { writeFileSync as writeFileSync7, mkdirSync as mkdirSync6 } from "fs";
5928
- import { dirname as dirname4 } from "path";
6276
+ import { dirname as dirname5 } from "path";
5929
6277
  var lastResponseStore = { content: "" };
5930
6278
  var saveLastResponseTool = {
5931
6279
  definition: {
@@ -5958,7 +6306,7 @@ var saveLastResponseTool = {
5958
6306
  throw new Error("\u6CA1\u6709\u53EF\u4FDD\u5B58\u7684\u5185\u5BB9\uFF1AAI \u5C1A\u672A\u4EA7\u751F\u4EFB\u4F55\u56DE\u590D\uFF0C\u6216\u4E0A\u6B21\u56DE\u590D\u4E3A\u7A7A\u3002");
5959
6307
  }
5960
6308
  undoStack.push(filePath, `save_last_response: ${filePath}`);
5961
- mkdirSync6(dirname4(filePath), { recursive: true });
6309
+ mkdirSync6(dirname5(filePath), { recursive: true });
5962
6310
  writeFileSync7(filePath, content, "utf-8");
5963
6311
  const lines = content.split("\n").length;
5964
6312
  return `File saved: ${filePath} (${lines} lines, ${content.length} bytes)`;
@@ -6051,7 +6399,7 @@ function promptUser(rl, question) {
6051
6399
  console.log();
6052
6400
  console.log(chalk6.cyan("\u2753 ") + chalk6.bold(question));
6053
6401
  process.stdout.write(chalk6.cyan("> "));
6054
- return new Promise((resolve5) => {
6402
+ return new Promise((resolve6) => {
6055
6403
  let completed = false;
6056
6404
  const cleanup = (answer) => {
6057
6405
  if (completed) return;
@@ -6061,7 +6409,7 @@ function promptUser(rl, question) {
6061
6409
  rl.pause();
6062
6410
  rlAny.output = savedOutput;
6063
6411
  askUserContext.prompting = false;
6064
- resolve5(answer);
6412
+ resolve6(answer);
6065
6413
  };
6066
6414
  const onLine = (line) => {
6067
6415
  cleanup(line);
@@ -6553,7 +6901,7 @@ var spawnAgentTool = {
6553
6901
 
6554
6902
  // src/tools/registry.ts
6555
6903
  import { pathToFileURL } from "url";
6556
- import { existsSync as existsSync14, mkdirSync as mkdirSync8, readdirSync as readdirSync6 } from "fs";
6904
+ import { existsSync as existsSync14, mkdirSync as mkdirSync8, readdirSync as readdirSync8 } from "fs";
6557
6905
  import { join as join10 } from "path";
6558
6906
  var ToolRegistry = class {
6559
6907
  tools = /* @__PURE__ */ new Map();
@@ -6636,7 +6984,7 @@ var ToolRegistry = class {
6636
6984
  }
6637
6985
  let files;
6638
6986
  try {
6639
- files = readdirSync6(pluginsDir).filter((f) => f.endsWith(".js"));
6987
+ files = readdirSync8(pluginsDir).filter((f) => f.endsWith(".js"));
6640
6988
  } catch {
6641
6989
  return 0;
6642
6990
  }
@@ -6690,7 +7038,7 @@ import chalk9 from "chalk";
6690
7038
  import { existsSync as existsSync15, readFileSync as readFileSync9 } from "fs";
6691
7039
 
6692
7040
  // src/tools/hooks.ts
6693
- import { execSync as execSync5 } from "child_process";
7041
+ import { execSync as execSync6 } from "child_process";
6694
7042
  function runHook(template, vars) {
6695
7043
  if (!template) return;
6696
7044
  let cmd = template;
@@ -6699,7 +7047,7 @@ function runHook(template, vars) {
6699
7047
  cmd = cmd.replace(/\{args\}/g, vars.args ?? "");
6700
7048
  cmd = cmd.replace(/\{status\}/g, vars.status ?? "");
6701
7049
  try {
6702
- execSync5(cmd, {
7050
+ execSync6(cmd, {
6703
7051
  timeout: 5e3,
6704
7052
  stdio: ["pipe", "pipe", "pipe"],
6705
7053
  encoding: "utf-8"
@@ -6960,7 +7308,7 @@ var ToolExecutor = class {
6960
7308
  rl.resume();
6961
7309
  process.stdout.write(prompt);
6962
7310
  this.confirming = true;
6963
- return new Promise((resolve5) => {
7311
+ return new Promise((resolve6) => {
6964
7312
  let completed = false;
6965
7313
  const cleanup = (result) => {
6966
7314
  if (completed) return;
@@ -6970,7 +7318,7 @@ var ToolExecutor = class {
6970
7318
  rl.pause();
6971
7319
  rlAny.output = savedOutput;
6972
7320
  this.confirming = false;
6973
- resolve5(result);
7321
+ resolve6(result);
6974
7322
  };
6975
7323
  const onLine = (line) => {
6976
7324
  const input2 = line.trim().toLowerCase();
@@ -7131,7 +7479,7 @@ var ToolExecutor = class {
7131
7479
  rl.resume();
7132
7480
  process.stdout.write(color("Proceed? [y/N] (type y + Enter to confirm) "));
7133
7481
  this.confirming = true;
7134
- return new Promise((resolve5) => {
7482
+ return new Promise((resolve6) => {
7135
7483
  let completed = false;
7136
7484
  const cleanup = (answer) => {
7137
7485
  if (completed) return;
@@ -7141,7 +7489,7 @@ var ToolExecutor = class {
7141
7489
  rl.pause();
7142
7490
  rlAny.output = savedOutput;
7143
7491
  this.confirming = false;
7144
- resolve5(answer === "y");
7492
+ resolve6(answer === "y");
7145
7493
  };
7146
7494
  const onLine = (line) => {
7147
7495
  cleanup(line.trim().toLowerCase());
@@ -7396,9 +7744,9 @@ Managing ${displayName} API Key`);
7396
7744
  };
7397
7745
 
7398
7746
  // src/repl/custom-commands.ts
7399
- import { existsSync as existsSync16, readFileSync as readFileSync10, readdirSync as readdirSync7, mkdirSync as mkdirSync9 } from "fs";
7747
+ import { existsSync as existsSync16, readFileSync as readFileSync10, readdirSync as readdirSync9, mkdirSync as mkdirSync9 } from "fs";
7400
7748
  import { join as join11, extname as extname3 } from "path";
7401
- import { execSync as execSync6 } from "child_process";
7749
+ import { execSync as execSync7 } from "child_process";
7402
7750
  function parseSimpleYaml(text) {
7403
7751
  const result = {};
7404
7752
  for (const line of text.split("\n")) {
@@ -7443,7 +7791,7 @@ function expandTemplate(template, args) {
7443
7791
  });
7444
7792
  result = result.replace(/\{\{git-diff\}\}/g, () => {
7445
7793
  try {
7446
- return execSync6("git diff", { encoding: "utf-8", timeout: 1e4 }).trim();
7794
+ return execSync7("git diff", { encoding: "utf-8", timeout: 1e4 }).trim();
7447
7795
  } catch {
7448
7796
  return "[No git diff available]";
7449
7797
  }
@@ -7466,7 +7814,7 @@ var CustomCommandManager = class {
7466
7814
  return 0;
7467
7815
  }
7468
7816
  let count = 0;
7469
- for (const file of readdirSync7(this.commandsDir)) {
7817
+ for (const file of readdirSync9(this.commandsDir)) {
7470
7818
  if (extname3(file) !== ".md") continue;
7471
7819
  const cmd = parseCommandFile(join11(this.commandsDir, file));
7472
7820
  if (cmd) {
@@ -7693,7 +8041,7 @@ var McpClient = class {
7693
8041
  // 内部方法:JSON-RPC 通信
7694
8042
  // ══════════════════════════════════════════════════════════════════
7695
8043
  sendRequest(method, params) {
7696
- return new Promise((resolve5, reject) => {
8044
+ return new Promise((resolve6, reject) => {
7697
8045
  if (!this.process?.stdin?.writable) {
7698
8046
  return reject(new Error(`MCP server [${this.serverId}] stdin not writable`));
7699
8047
  }
@@ -7716,7 +8064,7 @@ var McpClient = class {
7716
8064
  this.pendingRequests.set(id, {
7717
8065
  resolve: (result) => {
7718
8066
  cleanup();
7719
- resolve5(result);
8067
+ resolve6(result);
7720
8068
  },
7721
8069
  reject: (error) => {
7722
8070
  cleanup();
@@ -7787,13 +8135,13 @@ var McpClient = class {
7787
8135
  }
7788
8136
  /** Promise 超时包装 */
7789
8137
  withTimeout(promise, ms, label) {
7790
- return new Promise((resolve5, reject) => {
8138
+ return new Promise((resolve6, reject) => {
7791
8139
  const timer = setTimeout(() => {
7792
8140
  reject(new Error(`MCP [${this.serverId}] ${label} timed out after ${ms}ms`));
7793
8141
  }, ms);
7794
8142
  promise.then((val) => {
7795
8143
  clearTimeout(timer);
7796
- resolve5(val);
8144
+ resolve6(val);
7797
8145
  }).catch((err) => {
7798
8146
  clearTimeout(timer);
7799
8147
  reject(err);
@@ -8058,12 +8406,12 @@ var McpManager = class {
8058
8406
  };
8059
8407
 
8060
8408
  // src/skills/manager.ts
8061
- import { existsSync as existsSync18, readdirSync as readdirSync8, mkdirSync as mkdirSync11 } from "fs";
8409
+ import { existsSync as existsSync18, readdirSync as readdirSync10, mkdirSync as mkdirSync11 } from "fs";
8062
8410
  import { join as join13 } from "path";
8063
8411
 
8064
8412
  // src/skills/types.ts
8065
8413
  import { readFileSync as readFileSync12 } from "fs";
8066
- import { basename as basename4 } from "path";
8414
+ import { basename as basename5 } from "path";
8067
8415
  function parseSimpleYaml2(yaml) {
8068
8416
  const result = {};
8069
8417
  for (const line of yaml.split("\n")) {
@@ -8092,7 +8440,7 @@ function parseSkillFile(filePath) {
8092
8440
  if (!frontmatterMatch) {
8093
8441
  return {
8094
8442
  meta: {
8095
- name: basename4(filePath, ".md"),
8443
+ name: basename5(filePath, ".md"),
8096
8444
  description: ""
8097
8445
  },
8098
8446
  content: raw.trim(),
@@ -8103,7 +8451,7 @@ function parseSkillFile(filePath) {
8103
8451
  const parsed = parseSimpleYaml2(yaml);
8104
8452
  return {
8105
8453
  meta: {
8106
- name: parsed["name"] ?? basename4(filePath, ".md"),
8454
+ name: parsed["name"] ?? basename5(filePath, ".md"),
8107
8455
  description: parsed["description"] ?? "",
8108
8456
  tools: parsed["tools"] ? parseYamlArray(parsed["tools"]) : void 0
8109
8457
  },
@@ -8133,7 +8481,7 @@ var SkillManager = class {
8133
8481
  }
8134
8482
  let entries;
8135
8483
  try {
8136
- entries = readdirSync8(this.skillsDir);
8484
+ entries = readdirSync10(this.skillsDir);
8137
8485
  } catch {
8138
8486
  return 0;
8139
8487
  }
@@ -8244,7 +8592,7 @@ function parseAtReferences(input2, cwd) {
8244
8592
  let match;
8245
8593
  while ((match = atPattern.exec(input2)) !== null) {
8246
8594
  const rawPath = match[1] ?? match[2] ?? match[3] ?? "";
8247
- const absPath = resolve4(cwd, rawPath);
8595
+ const absPath = resolve5(cwd, rawPath);
8248
8596
  const ext = extname4(rawPath).toLowerCase();
8249
8597
  const mime = IMAGE_MIME[ext];
8250
8598
  if (!existsSync19(absPath)) {
@@ -8252,7 +8600,7 @@ function parseAtReferences(input2, cwd) {
8252
8600
  continue;
8253
8601
  }
8254
8602
  if (mime) {
8255
- const fileSize = statSync7(absPath).size;
8603
+ const fileSize = statSync8(absPath).size;
8256
8604
  if (fileSize > MAX_IMAGE_BYTES) {
8257
8605
  refs.push({ path: rawPath, type: "toolarge" });
8258
8606
  continue;
@@ -8450,7 +8798,7 @@ var Repl = class {
8450
8798
  if (depth > 2 || entryCount >= MAX_TREE_ENTRIES) return;
8451
8799
  let entries;
8452
8800
  try {
8453
- entries = readdirSync9(dir);
8801
+ entries = readdirSync11(dir);
8454
8802
  } catch {
8455
8803
  return;
8456
8804
  }
@@ -8462,7 +8810,7 @@ var Repl = class {
8462
8810
  const connector = isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
8463
8811
  let isDir;
8464
8812
  try {
8465
- isDir = statSync7(fullPath).isDirectory();
8813
+ isDir = statSync8(fullPath).isDirectory();
8466
8814
  } catch {
8467
8815
  continue;
8468
8816
  }
@@ -8484,7 +8832,7 @@ ${treeLines.join("\n")}`
8484
8832
  if (totalChars >= MAX_TOTAL_CHARS) return;
8485
8833
  let entries;
8486
8834
  try {
8487
- entries = readdirSync9(dir);
8835
+ entries = readdirSync11(dir);
8488
8836
  } catch {
8489
8837
  return;
8490
8838
  }
@@ -8494,7 +8842,7 @@ ${treeLines.join("\n")}`
8494
8842
  const fullPath = join14(dir, name);
8495
8843
  let st;
8496
8844
  try {
8497
- st = statSync7(fullPath);
8845
+ st = statSync8(fullPath);
8498
8846
  } catch {
8499
8847
  continue;
8500
8848
  }
@@ -8535,13 +8883,13 @@ ${content}
8535
8883
  * 已添加时返回 added=false(无重复添加)。
8536
8884
  */
8537
8885
  addExtraContextDir(dirPath) {
8538
- const absPath = resolve4(dirPath);
8886
+ const absPath = resolve5(dirPath);
8539
8887
  if (!existsSync19(absPath)) {
8540
8888
  return { success: false, charCount: 0, added: false, error: `Directory not found: ${dirPath}` };
8541
8889
  }
8542
8890
  let isDir;
8543
8891
  try {
8544
- isDir = statSync7(absPath).isDirectory();
8892
+ isDir = statSync8(absPath).isDirectory();
8545
8893
  } catch {
8546
8894
  return { success: false, charCount: 0, added: false, error: `Cannot access: ${dirPath}` };
8547
8895
  }
@@ -8558,7 +8906,7 @@ ${content}
8558
8906
  }
8559
8907
  /** 从额外上下文中移除目录。返回 true 表示成功移除,false 表示未找到。 */
8560
8908
  removeExtraContextDir(dirPath) {
8561
- const absPath = resolve4(dirPath);
8909
+ const absPath = resolve5(dirPath);
8562
8910
  const idx = this.extraContextDirs.findIndex((d) => d.dir === absPath);
8563
8911
  if (idx === -1) return false;
8564
8912
  this.extraContextDirs.splice(idx, 1);
@@ -8641,8 +8989,8 @@ ${content}
8641
8989
  if (setting === false) return { layers: [], mergedContent: "" };
8642
8990
  const cwd = process.cwd();
8643
8991
  if (setting !== "auto") {
8644
- const fullPath = resolve4(cwd, String(setting));
8645
- if (!fullPath.startsWith(resolve4(cwd))) {
8992
+ const fullPath = resolve5(cwd, String(setting));
8993
+ if (!fullPath.startsWith(resolve5(cwd))) {
8646
8994
  process.stderr.write(
8647
8995
  `[Warning] contextFile path "${setting}" is outside current directory, ignoring.
8648
8996
  `
@@ -8685,8 +9033,8 @@ ${content}
8685
9033
  charCount: projectCtx.content.length
8686
9034
  });
8687
9035
  }
8688
- const normalizedCwd = resolve4(cwd);
8689
- const normalizedRoot = resolve4(projectRoot);
9036
+ const normalizedCwd = resolve5(cwd);
9037
+ const normalizedRoot = resolve5(projectRoot);
8690
9038
  if (normalizedCwd !== normalizedRoot) {
8691
9039
  const localCtx = this.findContextFile(cwd);
8692
9040
  if (localCtx) {
@@ -9092,7 +9440,7 @@ Session '${this.resumeSessionId}' not found.
9092
9440
  this.handleExit();
9093
9441
  });
9094
9442
  this.showPrompt();
9095
- await new Promise((resolve5) => {
9443
+ await new Promise((resolve6) => {
9096
9444
  let processing = false;
9097
9445
  this.rl.on("line", async (line) => {
9098
9446
  if (this.toolExecutor.confirming) return;
@@ -9127,13 +9475,13 @@ Session '${this.resumeSessionId}' not found.
9127
9475
  process.stdin.resume();
9128
9476
  this.showPrompt();
9129
9477
  } else {
9130
- resolve5();
9478
+ resolve6();
9131
9479
  }
9132
9480
  }
9133
9481
  });
9134
9482
  this.rl.on("close", () => {
9135
9483
  if (!processing) {
9136
- resolve5();
9484
+ resolve6();
9137
9485
  }
9138
9486
  });
9139
9487
  });
@@ -9386,6 +9734,12 @@ Session '${this.resumeSessionId}' not found.
9386
9734
  return ["reload"];
9387
9735
  case "mcp":
9388
9736
  return ["reconnect"];
9737
+ case "undo":
9738
+ return ["list"];
9739
+ case "fork": {
9740
+ const cps = this.sessions.current?.checkpoints.map((c) => c.name) ?? [];
9741
+ return cps;
9742
+ }
9389
9743
  case "compact":
9390
9744
  case "review":
9391
9745
  case "init":
@@ -9402,18 +9756,18 @@ Session '${this.resumeSessionId}' not found.
9402
9756
  completeFilePath(partial) {
9403
9757
  try {
9404
9758
  const normalized = partial.replace(/\\/g, "/");
9405
- const dir = normalized.includes("/") ? dirname5(normalized) : ".";
9406
- const prefix = normalized.includes("/") ? basename5(normalized) : normalized;
9407
- const absDir = resolve4(process.cwd(), dir);
9759
+ const dir = normalized.includes("/") ? dirname6(normalized) : ".";
9760
+ const prefix = normalized.includes("/") ? basename6(normalized) : normalized;
9761
+ const absDir = resolve5(process.cwd(), dir);
9408
9762
  if (!existsSync19(absDir)) return [];
9409
- const entries = readdirSync9(absDir);
9763
+ const entries = readdirSync11(absDir);
9410
9764
  const results = [];
9411
9765
  for (const entry of entries) {
9412
9766
  if (entry.startsWith(".")) continue;
9413
9767
  if (!entry.toLowerCase().startsWith(prefix.toLowerCase())) continue;
9414
9768
  try {
9415
9769
  const fullPath = join14(absDir, entry);
9416
- const stat = statSync7(fullPath);
9770
+ const stat = statSync8(fullPath);
9417
9771
  const rel = dir === "." ? entry : `${dir}/${entry}`;
9418
9772
  results.push(stat.isDirectory() ? `${rel}/` : rel);
9419
9773
  } catch {
@@ -9943,6 +10297,7 @@ Tip: You can continue the conversation by asking the AI to proceed.`
9943
10297
  addContextDir: (dirPath) => this.addExtraContextDir(dirPath),
9944
10298
  removeContextDir: (dirPath) => this.removeExtraContextDir(dirPath),
9945
10299
  listContextDirs: () => [...this.extraContextDirs],
10300
+ forkSession: (messageCount, title) => this.sessions.forkSession(messageCount, title),
9946
10301
  exit: () => this.handleExit()
9947
10302
  };
9948
10303
  await cmd.execute(args, ctx);
@@ -10071,10 +10426,10 @@ async function setupProxy(configProxy) {
10071
10426
  }
10072
10427
  async function readStdin() {
10073
10428
  if (process.stdin.isTTY) return "";
10074
- return new Promise((resolve5, reject) => {
10429
+ return new Promise((resolve6, reject) => {
10075
10430
  const chunks = [];
10076
10431
  process.stdin.on("data", (chunk) => chunks.push(chunk));
10077
- process.stdin.on("end", () => resolve5(Buffer.concat(chunks).toString("utf-8").trimEnd()));
10432
+ process.stdin.on("end", () => resolve6(Buffer.concat(chunks).toString("utf-8").trimEnd()));
10078
10433
  process.stdin.on("error", reject);
10079
10434
  });
10080
10435
  }
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  executeTests,
4
4
  runTestsTool
5
- } from "./chunk-ZUZDWQVG.js";
5
+ } from "./chunk-P4XKHPXU.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.47",
3
+ "version": "0.1.49",
4
4
  "description": "Cross-platform REPL-style AI CLI with multi-provider support",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",