jinzd-ai-cli 0.1.46 → 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.46";
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-LRU55UAO.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 崩溃。
@@ -1924,7 +1944,12 @@ var SessionManager = class {
1924
1944
  if (!existsSync2(filePath)) {
1925
1945
  throw new Error(`Session ${id} not found`);
1926
1946
  }
1927
- const data = JSON.parse(readFileSync2(filePath, "utf-8"));
1947
+ let data;
1948
+ try {
1949
+ data = JSON.parse(readFileSync2(filePath, "utf-8"));
1950
+ } catch (err) {
1951
+ throw new Error(`Session ${id} is corrupted: ${err instanceof Error ? err.message : String(err)}`);
1952
+ }
1928
1953
  const session = Session.fromJSON(data);
1929
1954
  this._current = session;
1930
1955
  return session;
@@ -1956,6 +1981,26 @@ var SessionManager = class {
1956
1981
  }
1957
1982
  return metas.sort((a, b) => b.updated.getTime() - a.updated.getTime());
1958
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
+ }
1959
2004
  /**
1960
2005
  * 跨 session 全文搜索。
1961
2006
  * 遍历所有历史 JSON 文件,逐条匹配消息内容(不区分大小写),
@@ -2016,7 +2061,7 @@ var SessionManager = class {
2016
2061
 
2017
2062
  // src/repl/repl.ts
2018
2063
  import * as readline from "readline";
2019
- 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";
2020
2065
  import { join as join14, resolve as resolve4, extname as extname4, dirname as dirname5, basename as basename5 } from "path";
2021
2066
  import chalk10 from "chalk";
2022
2067
 
@@ -2226,12 +2271,12 @@ var Renderer = class {
2226
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"));
2227
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"));
2228
2273
  console.log(HR);
2229
- console.log(theme.dim(" REPL \u547D\u4EE4\uFF0834\u4E2A\uFF09\uFF1A"));
2274
+ console.log(theme.dim(" REPL \u547D\u4EE4\uFF0835\u4E2A\uFF09\uFF1A"));
2230
2275
  console.log(theme.dim(" /help /about /provider /model /clear /compact /plan /session"));
2231
2276
  console.log(theme.dim(" /system /context /status /search /undo /export /copy /cost"));
2232
2277
  console.log(theme.dim(" /init /skill /tools /plugins /mcp /config /checkpoint /review"));
2233
2278
  console.log(theme.dim(" /commands /test /scaffold /add-dir /memory /doctor /bug /think"));
2234
- console.log(theme.dim(" /diff /exit"));
2279
+ console.log(theme.dim(" /diff /fork /exit"));
2235
2280
  console.log(HR);
2236
2281
  console.log(theme.dim(" \u4E3B\u8981\u7279\u6027\uFF1A"));
2237
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"));
@@ -2239,7 +2284,7 @@ var Renderer = class {
2239
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"));
2240
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"));
2241
2286
  console.log(feat("\u8DE8 session \u5386\u53F2\u5168\u6587\u641C\u7D22\uFF08/search <\u5173\u952E\u8BCD>\uFF09"));
2242
- 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"));
2243
2288
  console.log(feat("Thinking \u6A21\u5F0F\u6298\u53E0\uFF08<think> \u5757\u81EA\u52A8\u6298\u53E0\uFF0CGLM-5 \u7B49\uFF09"));
2244
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"));
2245
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"));
@@ -2281,6 +2326,7 @@ var Renderer = class {
2281
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"));
2282
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"));
2283
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"));
2284
2330
  console.log();
2285
2331
  }
2286
2332
  printPrompt(provider, _model) {
@@ -2590,7 +2636,7 @@ function formatGitContextForPrompt(ctx) {
2590
2636
  }
2591
2637
 
2592
2638
  // src/tools/undo-stack.ts
2593
- 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";
2594
2640
  var MAX_UNDO_DEPTH = 20;
2595
2641
  var UndoStack = class {
2596
2642
  stack = [];
@@ -2618,6 +2664,37 @@ var UndoStack = class {
2618
2664
  this.stack.shift();
2619
2665
  }
2620
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
+ }
2621
2698
  /**
2622
2699
  * 弹出并执行最近一次撤销操作。
2623
2700
  * @returns 撤销结果描述,或 null(栈为空时)
@@ -2627,10 +2704,22 @@ var UndoStack = class {
2627
2704
  if (!entry) return null;
2628
2705
  try {
2629
2706
  if (entry.previousContent === null) {
2630
- if (existsSync4(entry.filePath)) {
2631
- 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}` };
2632
2722
  }
2633
- return { entry, result: `Deleted newly created file: ${entry.filePath}` };
2634
2723
  } else {
2635
2724
  writeFileSync3(entry.filePath, entry.previousContent, "utf-8");
2636
2725
  const lines = entry.previousContent.split("\n").length;
@@ -2950,8 +3039,15 @@ function scanDirTree(dir, maxDepth = 2, maxEntries = 80) {
2950
3039
  }
2951
3040
  const filtered = entries.filter((e) => !e.startsWith(".") && !SCAN_SKIP_DIRS.has(e));
2952
3041
  const sorted = filtered.sort((a, b) => {
2953
- const aIsDir = statSync2(join5(d, a)).isDirectory();
2954
- const bIsDir = statSync2(join5(d, b)).isDirectory();
3042
+ let aIsDir = false, bIsDir = false;
3043
+ try {
3044
+ aIsDir = statSync2(join5(d, a)).isDirectory();
3045
+ } catch {
3046
+ }
3047
+ try {
3048
+ bIsDir = statSync2(join5(d, b)).isDirectory();
3049
+ } catch {
3050
+ }
2955
3051
  if (aIsDir !== bIsDir) return aIsDir ? -1 : 1;
2956
3052
  return a.localeCompare(b);
2957
3053
  });
@@ -3161,7 +3257,7 @@ function createDefaultCommands() {
3161
3257
  " /context - Show or reload hierarchical context layers",
3162
3258
  " /status - Show current status",
3163
3259
  " /search <keyword> - Search across all session histories",
3164
- " /undo - Undo the last file write/edit operation",
3260
+ " /undo [list|<n>] - Undo file ops (list stack, undo N times)",
3165
3261
  " /export [md|json] [file] - Export session to file (default: auto-named .md)",
3166
3262
  " /tools - List all AI tools available",
3167
3263
  " /plugins - Show plugin directory and loaded plugins",
@@ -3181,6 +3277,7 @@ function createDefaultCommands() {
3181
3277
  " /doctor - Health check (API keys, config, MCP status)",
3182
3278
  " /bug [--copy] - Generate bug report template (--copy to clipboard)",
3183
3279
  " /diff [--stats] - Show all file modifications in this session",
3280
+ " /fork [checkpoint] - Fork session from checkpoint or current position",
3184
3281
  " /exit - Exit"
3185
3282
  ] : [];
3186
3283
  console.log("\nAvailable commands:");
@@ -3569,23 +3666,53 @@ ${text}
3569
3666
  },
3570
3667
  {
3571
3668
  name: "undo",
3572
- description: "Undo the last file write or edit operation",
3573
- usage: "/undo",
3574
- execute(_args, ctx) {
3575
- const top = undoStack.peek();
3576
- 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) {
3577
3694
  ctx.renderer.printInfo("Nothing to undo.");
3578
3695
  return;
3579
3696
  }
3580
- const timeStr = top.timestamp.toLocaleTimeString();
3581
- ctx.renderer.printInfo(
3582
- `Undoing: ${top.description} (${timeStr})`
3583
- );
3584
- const undoResult = undoStack.undo();
3585
- if (undoResult) {
3586
- ctx.renderer.printSuccess(undoResult.result);
3587
- } 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) {
3588
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
+ `));
3589
3716
  }
3590
3717
  }
3591
3718
  },
@@ -4161,7 +4288,7 @@ ${hint}` : "")
4161
4288
  description: "Run project tests and show structured report",
4162
4289
  usage: "/test [command|filter]",
4163
4290
  async execute(args, _ctx) {
4164
- const { executeTests } = await import("./run-tests-V62QUKVZ.js");
4291
+ const { executeTests } = await import("./run-tests-2TCRQBHR.js");
4165
4292
  const argStr = args.join(" ").trim();
4166
4293
  let testArgs = {};
4167
4294
  if (argStr) {
@@ -4488,6 +4615,57 @@ Summary: ${fileMap.size} file(s) \u2014 ${newFiles} new, ${modifiedFiles} modifi
4488
4615
  console.log();
4489
4616
  }
4490
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
+ },
4491
4669
  {
4492
4670
  name: "exit",
4493
4671
  description: "Exit the REPL",
@@ -4665,7 +4843,7 @@ function selectFromList(prompt, items, initialIndex = 0) {
4665
4843
 
4666
4844
  // src/tools/builtin/bash.ts
4667
4845
  import { execSync as execSync4 } from "child_process";
4668
- import { existsSync as existsSync7 } from "fs";
4846
+ import { existsSync as existsSync7, readdirSync as readdirSync3, statSync as statSync3 } from "fs";
4669
4847
  import { platform as platform2 } from "os";
4670
4848
  import { resolve as resolve2 } from "path";
4671
4849
  var IS_WINDOWS = platform2() === "win32";
@@ -4737,6 +4915,12 @@ var bashTool = {
4737
4915
  } else {
4738
4916
  actualCommand = command;
4739
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
+ }
4740
4924
  try {
4741
4925
  const output = execSync4(actualCommand, {
4742
4926
  timeout,
@@ -4751,9 +4935,11 @@ var bashTool = {
4751
4935
  }
4752
4936
  });
4753
4937
  updateCwdFromCommand(command, effectiveCwd);
4938
+ pushBashUndoEntries(beforeSnapshot, parsedTargetsBefore, effectiveCwd);
4754
4939
  const result = IS_WINDOWS && Buffer.isBuffer(output) ? output.toString("utf-8") : output;
4755
4940
  return result || "(command completed with no output)";
4756
4941
  } catch (err) {
4942
+ pushBashUndoEntries(beforeSnapshot, parsedTargetsBefore, effectiveCwd);
4757
4943
  if (err && typeof err === "object" && "status" in err) {
4758
4944
  const execErr = err;
4759
4945
  const stderr = IS_WINDOWS && Buffer.isBuffer(execErr.stderr) ? execErr.stderr.toString("utf-8").trim() : execErr.stderr?.toString().trim() ?? "";
@@ -4786,6 +4972,69 @@ function fixWindowsDeleteCommand(command) {
4786
4972
  }
4787
4973
  );
4788
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
+ }
4789
5038
  function updateCwdFromCommand(command, baseCwd) {
4790
5039
  const cdMatches = [...command.matchAll(/(?:^|[;&|])\s*cd\s+(['"]?)([^\s;&|'"]+)\1/g)];
4791
5040
  if (cdMatches.length === 0) return;
@@ -4802,7 +5051,7 @@ function updateCwdFromCommand(command, baseCwd) {
4802
5051
  }
4803
5052
 
4804
5053
  // src/tools/builtin/read-file.ts
4805
- 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";
4806
5055
  import { extname, resolve as resolve3, basename as basename2, sep } from "path";
4807
5056
  import { homedir as homedir2 } from "os";
4808
5057
  var MAX_FILE_BYTES = 10 * 1024 * 1024;
@@ -4896,7 +5145,7 @@ var readFileTool = {
4896
5145
  if (!filePath) throw new Error("path is required");
4897
5146
  const normalizedPath = resolve3(filePath);
4898
5147
  if (!existsSync8(normalizedPath)) throw new Error(`File not found: ${filePath}`);
4899
- const { size } = statSync3(normalizedPath);
5148
+ const { size } = statSync4(normalizedPath);
4900
5149
  if (size > MAX_FILE_BYTES) {
4901
5150
  const mb = (size / 1024 / 1024).toFixed(1);
4902
5151
  return `[File too large: ${filePath} (${mb} MB)]
@@ -5209,7 +5458,7 @@ function truncatePreview(str, maxLen = 80) {
5209
5458
  }
5210
5459
 
5211
5460
  // src/tools/builtin/list-dir.ts
5212
- 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";
5213
5462
  import { join as join6 } from "path";
5214
5463
  var listDirTool = {
5215
5464
  definition: {
@@ -5242,7 +5491,7 @@ var listDirTool = {
5242
5491
  function listRecursive(basePath, indent, recursive, lines) {
5243
5492
  let entries;
5244
5493
  try {
5245
- entries = readdirSync3(basePath, { withFileTypes: true });
5494
+ entries = readdirSync4(basePath, { withFileTypes: true });
5246
5495
  } catch {
5247
5496
  lines.push(`${indent}(permission denied)`);
5248
5497
  return;
@@ -5265,7 +5514,7 @@ function listRecursive(basePath, indent, recursive, lines) {
5265
5514
  }
5266
5515
  } else {
5267
5516
  try {
5268
- const stat = statSync4(join6(basePath, entry.name));
5517
+ const stat = statSync5(join6(basePath, entry.name));
5269
5518
  const size = formatSize(stat.size);
5270
5519
  lines.push(`${indent}\u{1F4C4} ${entry.name} (${size})`);
5271
5520
  } catch {
@@ -5281,7 +5530,7 @@ function formatSize(bytes) {
5281
5530
  }
5282
5531
 
5283
5532
  // src/tools/builtin/grep-files.ts
5284
- 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";
5285
5534
  import { join as join7, relative } from "path";
5286
5535
  var grepFilesTool = {
5287
5536
  definition: {
@@ -5342,7 +5591,7 @@ var grepFilesTool = {
5342
5591
  regex = new RegExp(escaped, ignoreCase ? "gi" : "g");
5343
5592
  }
5344
5593
  const results = [];
5345
- const stat = statSync5(rootPath);
5594
+ const stat = statSync6(rootPath);
5346
5595
  if (stat.isFile()) {
5347
5596
  searchInFile(rootPath, rootPath, regex, contextLines, maxResults, results);
5348
5597
  } else {
@@ -5398,7 +5647,7 @@ function collectFiles(dirPath, filePattern, results, regex, contextLines, maxRes
5398
5647
  if (results.length >= maxResults) return;
5399
5648
  let entries;
5400
5649
  try {
5401
- entries = readdirSync4(dirPath, { withFileTypes: true });
5650
+ entries = readdirSync5(dirPath, { withFileTypes: true });
5402
5651
  } catch {
5403
5652
  return;
5404
5653
  }
@@ -5451,7 +5700,7 @@ function searchInFile(fullPath, displayPath, regex, contextLines, maxResults, re
5451
5700
  }
5452
5701
 
5453
5702
  // src/tools/builtin/glob-files.ts
5454
- 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";
5455
5704
  import { join as join8, relative as relative2, basename as basename3 } from "path";
5456
5705
  var globFilesTool = {
5457
5706
  definition: {
@@ -5556,7 +5805,7 @@ function collectMatchingFiles(dirPath, rootPath, regex, results, maxResults) {
5556
5805
  if (results.length >= maxResults) return;
5557
5806
  let entries;
5558
5807
  try {
5559
- entries = readdirSync5(dirPath, { withFileTypes: true });
5808
+ entries = readdirSync6(dirPath, { withFileTypes: true });
5560
5809
  } catch {
5561
5810
  return;
5562
5811
  }
@@ -5570,7 +5819,7 @@ function collectMatchingFiles(dirPath, rootPath, regex, results, maxResults) {
5570
5819
  const relPath = relative2(rootPath, fullPath).replace(/\\/g, "/");
5571
5820
  if (regex.test(relPath) || regex.test(basename3(relPath))) {
5572
5821
  try {
5573
- const stat = statSync6(fullPath);
5822
+ const stat = statSync7(fullPath);
5574
5823
  results.push({ relPath, absPath: fullPath, mtime: stat.mtimeMs });
5575
5824
  } catch {
5576
5825
  results.push({ relPath, absPath: fullPath, mtime: 0 });
@@ -6541,7 +6790,7 @@ var spawnAgentTool = {
6541
6790
 
6542
6791
  // src/tools/registry.ts
6543
6792
  import { pathToFileURL } from "url";
6544
- 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";
6545
6794
  import { join as join10 } from "path";
6546
6795
  var ToolRegistry = class {
6547
6796
  tools = /* @__PURE__ */ new Map();
@@ -6624,7 +6873,7 @@ var ToolRegistry = class {
6624
6873
  }
6625
6874
  let files;
6626
6875
  try {
6627
- files = readdirSync6(pluginsDir).filter((f) => f.endsWith(".js"));
6876
+ files = readdirSync7(pluginsDir).filter((f) => f.endsWith(".js"));
6628
6877
  } catch {
6629
6878
  return 0;
6630
6879
  }
@@ -7384,7 +7633,7 @@ Managing ${displayName} API Key`);
7384
7633
  };
7385
7634
 
7386
7635
  // src/repl/custom-commands.ts
7387
- 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";
7388
7637
  import { join as join11, extname as extname3 } from "path";
7389
7638
  import { execSync as execSync6 } from "child_process";
7390
7639
  function parseSimpleYaml(text) {
@@ -7454,7 +7703,7 @@ var CustomCommandManager = class {
7454
7703
  return 0;
7455
7704
  }
7456
7705
  let count = 0;
7457
- for (const file of readdirSync7(this.commandsDir)) {
7706
+ for (const file of readdirSync8(this.commandsDir)) {
7458
7707
  if (extname3(file) !== ".md") continue;
7459
7708
  const cmd = parseCommandFile(join11(this.commandsDir, file));
7460
7709
  if (cmd) {
@@ -8046,7 +8295,7 @@ var McpManager = class {
8046
8295
  };
8047
8296
 
8048
8297
  // src/skills/manager.ts
8049
- 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";
8050
8299
  import { join as join13 } from "path";
8051
8300
 
8052
8301
  // src/skills/types.ts
@@ -8121,7 +8370,7 @@ var SkillManager = class {
8121
8370
  }
8122
8371
  let entries;
8123
8372
  try {
8124
- entries = readdirSync8(this.skillsDir);
8373
+ entries = readdirSync9(this.skillsDir);
8125
8374
  } catch {
8126
8375
  return 0;
8127
8376
  }
@@ -8240,7 +8489,7 @@ function parseAtReferences(input2, cwd) {
8240
8489
  continue;
8241
8490
  }
8242
8491
  if (mime) {
8243
- const fileSize = statSync7(absPath).size;
8492
+ const fileSize = statSync8(absPath).size;
8244
8493
  if (fileSize > MAX_IMAGE_BYTES) {
8245
8494
  refs.push({ path: rawPath, type: "toolarge" });
8246
8495
  continue;
@@ -8438,7 +8687,7 @@ var Repl = class {
8438
8687
  if (depth > 2 || entryCount >= MAX_TREE_ENTRIES) return;
8439
8688
  let entries;
8440
8689
  try {
8441
- entries = readdirSync9(dir);
8690
+ entries = readdirSync10(dir);
8442
8691
  } catch {
8443
8692
  return;
8444
8693
  }
@@ -8450,7 +8699,7 @@ var Repl = class {
8450
8699
  const connector = isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
8451
8700
  let isDir;
8452
8701
  try {
8453
- isDir = statSync7(fullPath).isDirectory();
8702
+ isDir = statSync8(fullPath).isDirectory();
8454
8703
  } catch {
8455
8704
  continue;
8456
8705
  }
@@ -8472,7 +8721,7 @@ ${treeLines.join("\n")}`
8472
8721
  if (totalChars >= MAX_TOTAL_CHARS) return;
8473
8722
  let entries;
8474
8723
  try {
8475
- entries = readdirSync9(dir);
8724
+ entries = readdirSync10(dir);
8476
8725
  } catch {
8477
8726
  return;
8478
8727
  }
@@ -8482,7 +8731,7 @@ ${treeLines.join("\n")}`
8482
8731
  const fullPath = join14(dir, name);
8483
8732
  let st;
8484
8733
  try {
8485
- st = statSync7(fullPath);
8734
+ st = statSync8(fullPath);
8486
8735
  } catch {
8487
8736
  continue;
8488
8737
  }
@@ -8529,7 +8778,7 @@ ${content}
8529
8778
  }
8530
8779
  let isDir;
8531
8780
  try {
8532
- isDir = statSync7(absPath).isDirectory();
8781
+ isDir = statSync8(absPath).isDirectory();
8533
8782
  } catch {
8534
8783
  return { success: false, charCount: 0, added: false, error: `Cannot access: ${dirPath}` };
8535
8784
  }
@@ -9106,17 +9355,17 @@ Session '${this.resumeSessionId}' not found.
9106
9355
  this.renderer.renderError(err);
9107
9356
  } finally {
9108
9357
  processing = false;
9109
- }
9110
- if (this.running) {
9111
- rlAny.output = savedOutput;
9112
- const rlInternal = this.rl;
9113
- rlInternal.line = "";
9114
- rlInternal.cursor = 0;
9115
- rlInternal.paused = false;
9116
- process.stdin.resume();
9117
- this.showPrompt();
9118
- } else {
9119
- resolve5();
9358
+ if (this.running) {
9359
+ rlAny.output = savedOutput;
9360
+ const rlInternal = this.rl;
9361
+ rlInternal.line = "";
9362
+ rlInternal.cursor = 0;
9363
+ rlInternal.paused = false;
9364
+ process.stdin.resume();
9365
+ this.showPrompt();
9366
+ } else {
9367
+ resolve5();
9368
+ }
9120
9369
  }
9121
9370
  });
9122
9371
  this.rl.on("close", () => {
@@ -9374,6 +9623,12 @@ Session '${this.resumeSessionId}' not found.
9374
9623
  return ["reload"];
9375
9624
  case "mcp":
9376
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
+ }
9377
9632
  case "compact":
9378
9633
  case "review":
9379
9634
  case "init":
@@ -9394,14 +9649,14 @@ Session '${this.resumeSessionId}' not found.
9394
9649
  const prefix = normalized.includes("/") ? basename5(normalized) : normalized;
9395
9650
  const absDir = resolve4(process.cwd(), dir);
9396
9651
  if (!existsSync19(absDir)) return [];
9397
- const entries = readdirSync9(absDir);
9652
+ const entries = readdirSync10(absDir);
9398
9653
  const results = [];
9399
9654
  for (const entry of entries) {
9400
9655
  if (entry.startsWith(".")) continue;
9401
9656
  if (!entry.toLowerCase().startsWith(prefix.toLowerCase())) continue;
9402
9657
  try {
9403
9658
  const fullPath = join14(absDir, entry);
9404
- const stat = statSync7(fullPath);
9659
+ const stat = statSync8(fullPath);
9405
9660
  const rel = dir === "." ? entry : `${dir}/${entry}`;
9406
9661
  results.push(stat.isDirectory() ? `${rel}/` : rel);
9407
9662
  } catch {
@@ -9931,6 +10186,7 @@ Tip: You can continue the conversation by asking the AI to proceed.`
9931
10186
  addContextDir: (dirPath) => this.addExtraContextDir(dirPath),
9932
10187
  removeContextDir: (dirPath) => this.removeExtraContextDir(dirPath),
9933
10188
  listContextDirs: () => [...this.extraContextDirs],
10189
+ forkSession: (messageCount, title) => this.sessions.forkSession(messageCount, title),
9934
10190
  exit: () => this.handleExit()
9935
10191
  };
9936
10192
  await cmd.execute(args, ctx);
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  executeTests,
4
4
  runTestsTool
5
- } from "./chunk-LRU55UAO.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.46",
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",