jinzd-ai-cli 0.1.47 → 0.1.48

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.
@@ -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.48";
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-XDSNK7WW.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,7 +2061,7 @@ 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";
2064
+ import { existsSync as existsSync19, readFileSync as readFileSync13, readdirSync as readdirSync10, statSync as statSync8 } from "fs";
2025
2065
  import { join as join14, resolve as resolve4, extname as extname4, dirname as dirname5, basename as basename5 } from "path";
2026
2066
  import chalk10 from "chalk";
2027
2067
 
@@ -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) {
@@ -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-2TCRQBHR.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",
@@ -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,7 +5051,7 @@ 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";
5054
+ import { readFileSync as readFileSync5, existsSync as existsSync8, statSync as statSync4 } from "fs";
4818
5055
  import { extname, resolve as resolve3, basename as basename2, sep } from "path";
4819
5056
  import { homedir as homedir2 } from "os";
4820
5057
  var MAX_FILE_BYTES = 10 * 1024 * 1024;
@@ -4908,7 +5145,7 @@ var readFileTool = {
4908
5145
  if (!filePath) throw new Error("path is required");
4909
5146
  const normalizedPath = resolve3(filePath);
4910
5147
  if (!existsSync8(normalizedPath)) throw new Error(`File not found: ${filePath}`);
4911
- const { size } = statSync3(normalizedPath);
5148
+ const { size } = statSync4(normalizedPath);
4912
5149
  if (size > MAX_FILE_BYTES) {
4913
5150
  const mb = (size / 1024 / 1024).toFixed(1);
4914
5151
  return `[File too large: ${filePath} (${mb} MB)]
@@ -5221,7 +5458,7 @@ function truncatePreview(str, maxLen = 80) {
5221
5458
  }
5222
5459
 
5223
5460
  // src/tools/builtin/list-dir.ts
5224
- import { readdirSync as readdirSync3, statSync as statSync4, existsSync as existsSync10 } from "fs";
5461
+ import { readdirSync as readdirSync4, statSync as statSync5, existsSync as existsSync10 } from "fs";
5225
5462
  import { join as join6 } from "path";
5226
5463
  var listDirTool = {
5227
5464
  definition: {
@@ -5254,7 +5491,7 @@ var listDirTool = {
5254
5491
  function listRecursive(basePath, indent, recursive, lines) {
5255
5492
  let entries;
5256
5493
  try {
5257
- entries = readdirSync3(basePath, { withFileTypes: true });
5494
+ entries = readdirSync4(basePath, { withFileTypes: true });
5258
5495
  } catch {
5259
5496
  lines.push(`${indent}(permission denied)`);
5260
5497
  return;
@@ -5277,7 +5514,7 @@ function listRecursive(basePath, indent, recursive, lines) {
5277
5514
  }
5278
5515
  } else {
5279
5516
  try {
5280
- const stat = statSync4(join6(basePath, entry.name));
5517
+ const stat = statSync5(join6(basePath, entry.name));
5281
5518
  const size = formatSize(stat.size);
5282
5519
  lines.push(`${indent}\u{1F4C4} ${entry.name} (${size})`);
5283
5520
  } catch {
@@ -5293,7 +5530,7 @@ function formatSize(bytes) {
5293
5530
  }
5294
5531
 
5295
5532
  // src/tools/builtin/grep-files.ts
5296
- import { readdirSync as readdirSync4, readFileSync as readFileSync7, statSync as statSync5, existsSync as existsSync11 } from "fs";
5533
+ import { readdirSync as readdirSync5, readFileSync as readFileSync7, statSync as statSync6, existsSync as existsSync11 } from "fs";
5297
5534
  import { join as join7, relative } from "path";
5298
5535
  var grepFilesTool = {
5299
5536
  definition: {
@@ -5354,7 +5591,7 @@ var grepFilesTool = {
5354
5591
  regex = new RegExp(escaped, ignoreCase ? "gi" : "g");
5355
5592
  }
5356
5593
  const results = [];
5357
- const stat = statSync5(rootPath);
5594
+ const stat = statSync6(rootPath);
5358
5595
  if (stat.isFile()) {
5359
5596
  searchInFile(rootPath, rootPath, regex, contextLines, maxResults, results);
5360
5597
  } else {
@@ -5410,7 +5647,7 @@ function collectFiles(dirPath, filePattern, results, regex, contextLines, maxRes
5410
5647
  if (results.length >= maxResults) return;
5411
5648
  let entries;
5412
5649
  try {
5413
- entries = readdirSync4(dirPath, { withFileTypes: true });
5650
+ entries = readdirSync5(dirPath, { withFileTypes: true });
5414
5651
  } catch {
5415
5652
  return;
5416
5653
  }
@@ -5463,7 +5700,7 @@ function searchInFile(fullPath, displayPath, regex, contextLines, maxResults, re
5463
5700
  }
5464
5701
 
5465
5702
  // src/tools/builtin/glob-files.ts
5466
- import { readdirSync as readdirSync5, statSync as statSync6, existsSync as existsSync12 } from "fs";
5703
+ import { readdirSync as readdirSync6, statSync as statSync7, existsSync as existsSync12 } from "fs";
5467
5704
  import { join as join8, relative as relative2, basename as basename3 } from "path";
5468
5705
  var globFilesTool = {
5469
5706
  definition: {
@@ -5568,7 +5805,7 @@ function collectMatchingFiles(dirPath, rootPath, regex, results, maxResults) {
5568
5805
  if (results.length >= maxResults) return;
5569
5806
  let entries;
5570
5807
  try {
5571
- entries = readdirSync5(dirPath, { withFileTypes: true });
5808
+ entries = readdirSync6(dirPath, { withFileTypes: true });
5572
5809
  } catch {
5573
5810
  return;
5574
5811
  }
@@ -5582,7 +5819,7 @@ function collectMatchingFiles(dirPath, rootPath, regex, results, maxResults) {
5582
5819
  const relPath = relative2(rootPath, fullPath).replace(/\\/g, "/");
5583
5820
  if (regex.test(relPath) || regex.test(basename3(relPath))) {
5584
5821
  try {
5585
- const stat = statSync6(fullPath);
5822
+ const stat = statSync7(fullPath);
5586
5823
  results.push({ relPath, absPath: fullPath, mtime: stat.mtimeMs });
5587
5824
  } catch {
5588
5825
  results.push({ relPath, absPath: fullPath, mtime: 0 });
@@ -6553,7 +6790,7 @@ var spawnAgentTool = {
6553
6790
 
6554
6791
  // src/tools/registry.ts
6555
6792
  import { pathToFileURL } from "url";
6556
- import { existsSync as existsSync14, mkdirSync as mkdirSync8, readdirSync as readdirSync6 } from "fs";
6793
+ import { existsSync as existsSync14, mkdirSync as mkdirSync8, readdirSync as readdirSync7 } from "fs";
6557
6794
  import { join as join10 } from "path";
6558
6795
  var ToolRegistry = class {
6559
6796
  tools = /* @__PURE__ */ new Map();
@@ -6636,7 +6873,7 @@ var ToolRegistry = class {
6636
6873
  }
6637
6874
  let files;
6638
6875
  try {
6639
- files = readdirSync6(pluginsDir).filter((f) => f.endsWith(".js"));
6876
+ files = readdirSync7(pluginsDir).filter((f) => f.endsWith(".js"));
6640
6877
  } catch {
6641
6878
  return 0;
6642
6879
  }
@@ -7396,7 +7633,7 @@ Managing ${displayName} API Key`);
7396
7633
  };
7397
7634
 
7398
7635
  // src/repl/custom-commands.ts
7399
- import { existsSync as existsSync16, readFileSync as readFileSync10, readdirSync as readdirSync7, mkdirSync as mkdirSync9 } from "fs";
7636
+ import { existsSync as existsSync16, readFileSync as readFileSync10, readdirSync as readdirSync8, mkdirSync as mkdirSync9 } from "fs";
7400
7637
  import { join as join11, extname as extname3 } from "path";
7401
7638
  import { execSync as execSync6 } from "child_process";
7402
7639
  function parseSimpleYaml(text) {
@@ -7466,7 +7703,7 @@ var CustomCommandManager = class {
7466
7703
  return 0;
7467
7704
  }
7468
7705
  let count = 0;
7469
- for (const file of readdirSync7(this.commandsDir)) {
7706
+ for (const file of readdirSync8(this.commandsDir)) {
7470
7707
  if (extname3(file) !== ".md") continue;
7471
7708
  const cmd = parseCommandFile(join11(this.commandsDir, file));
7472
7709
  if (cmd) {
@@ -8058,7 +8295,7 @@ var McpManager = class {
8058
8295
  };
8059
8296
 
8060
8297
  // src/skills/manager.ts
8061
- import { existsSync as existsSync18, readdirSync as readdirSync8, mkdirSync as mkdirSync11 } from "fs";
8298
+ import { existsSync as existsSync18, readdirSync as readdirSync9, mkdirSync as mkdirSync11 } from "fs";
8062
8299
  import { join as join13 } from "path";
8063
8300
 
8064
8301
  // src/skills/types.ts
@@ -8133,7 +8370,7 @@ var SkillManager = class {
8133
8370
  }
8134
8371
  let entries;
8135
8372
  try {
8136
- entries = readdirSync8(this.skillsDir);
8373
+ entries = readdirSync9(this.skillsDir);
8137
8374
  } catch {
8138
8375
  return 0;
8139
8376
  }
@@ -8252,7 +8489,7 @@ function parseAtReferences(input2, cwd) {
8252
8489
  continue;
8253
8490
  }
8254
8491
  if (mime) {
8255
- const fileSize = statSync7(absPath).size;
8492
+ const fileSize = statSync8(absPath).size;
8256
8493
  if (fileSize > MAX_IMAGE_BYTES) {
8257
8494
  refs.push({ path: rawPath, type: "toolarge" });
8258
8495
  continue;
@@ -8450,7 +8687,7 @@ var Repl = class {
8450
8687
  if (depth > 2 || entryCount >= MAX_TREE_ENTRIES) return;
8451
8688
  let entries;
8452
8689
  try {
8453
- entries = readdirSync9(dir);
8690
+ entries = readdirSync10(dir);
8454
8691
  } catch {
8455
8692
  return;
8456
8693
  }
@@ -8462,7 +8699,7 @@ var Repl = class {
8462
8699
  const connector = isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
8463
8700
  let isDir;
8464
8701
  try {
8465
- isDir = statSync7(fullPath).isDirectory();
8702
+ isDir = statSync8(fullPath).isDirectory();
8466
8703
  } catch {
8467
8704
  continue;
8468
8705
  }
@@ -8484,7 +8721,7 @@ ${treeLines.join("\n")}`
8484
8721
  if (totalChars >= MAX_TOTAL_CHARS) return;
8485
8722
  let entries;
8486
8723
  try {
8487
- entries = readdirSync9(dir);
8724
+ entries = readdirSync10(dir);
8488
8725
  } catch {
8489
8726
  return;
8490
8727
  }
@@ -8494,7 +8731,7 @@ ${treeLines.join("\n")}`
8494
8731
  const fullPath = join14(dir, name);
8495
8732
  let st;
8496
8733
  try {
8497
- st = statSync7(fullPath);
8734
+ st = statSync8(fullPath);
8498
8735
  } catch {
8499
8736
  continue;
8500
8737
  }
@@ -8541,7 +8778,7 @@ ${content}
8541
8778
  }
8542
8779
  let isDir;
8543
8780
  try {
8544
- isDir = statSync7(absPath).isDirectory();
8781
+ isDir = statSync8(absPath).isDirectory();
8545
8782
  } catch {
8546
8783
  return { success: false, charCount: 0, added: false, error: `Cannot access: ${dirPath}` };
8547
8784
  }
@@ -9386,6 +9623,12 @@ Session '${this.resumeSessionId}' not found.
9386
9623
  return ["reload"];
9387
9624
  case "mcp":
9388
9625
  return ["reconnect"];
9626
+ case "undo":
9627
+ return ["list"];
9628
+ case "fork": {
9629
+ const cps = this.sessions.current?.checkpoints.map((c) => c.name) ?? [];
9630
+ return cps;
9631
+ }
9389
9632
  case "compact":
9390
9633
  case "review":
9391
9634
  case "init":
@@ -9406,14 +9649,14 @@ Session '${this.resumeSessionId}' not found.
9406
9649
  const prefix = normalized.includes("/") ? basename5(normalized) : normalized;
9407
9650
  const absDir = resolve4(process.cwd(), dir);
9408
9651
  if (!existsSync19(absDir)) return [];
9409
- const entries = readdirSync9(absDir);
9652
+ const entries = readdirSync10(absDir);
9410
9653
  const results = [];
9411
9654
  for (const entry of entries) {
9412
9655
  if (entry.startsWith(".")) continue;
9413
9656
  if (!entry.toLowerCase().startsWith(prefix.toLowerCase())) continue;
9414
9657
  try {
9415
9658
  const fullPath = join14(absDir, entry);
9416
- const stat = statSync7(fullPath);
9659
+ const stat = statSync8(fullPath);
9417
9660
  const rel = dir === "." ? entry : `${dir}/${entry}`;
9418
9661
  results.push(stat.isDirectory() ? `${rel}/` : rel);
9419
9662
  } catch {
@@ -9943,6 +10186,7 @@ Tip: You can continue the conversation by asking the AI to proceed.`
9943
10186
  addContextDir: (dirPath) => this.addExtraContextDir(dirPath),
9944
10187
  removeContextDir: (dirPath) => this.removeExtraContextDir(dirPath),
9945
10188
  listContextDirs: () => [...this.extraContextDirs],
10189
+ forkSession: (messageCount, title) => this.sessions.forkSession(messageCount, title),
9946
10190
  exit: () => this.handleExit()
9947
10191
  };
9948
10192
  await cmd.execute(args, ctx);
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  executeTests,
4
4
  runTestsTool
5
- } from "./chunk-ZUZDWQVG.js";
5
+ } from "./chunk-XDSNK7WW.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.48",
4
4
  "description": "Cross-platform REPL-style AI CLI with multi-provider support",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",