jinzd-ai-cli 0.4.23 → 0.4.25

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/dist/index.js CHANGED
@@ -8,7 +8,6 @@ import {
8
8
  SessionManager,
9
9
  SkillManager,
10
10
  TOOL_CALL_REMINDER,
11
- checkPermission,
12
11
  clearDevState,
13
12
  detectsHallucinatedFileOp,
14
13
  formatGitContextForPrompt,
@@ -18,27 +17,26 @@ import {
18
17
  hadPreviousWriteToolCalls,
19
18
  loadDevState,
20
19
  parseSimpleYaml,
21
- renderDiff,
22
- runHook,
23
20
  saveDevState,
24
21
  sessionHasMeaningfulContent,
25
22
  setupProxy
26
- } from "./chunk-GBMVHLPA.js";
23
+ } from "./chunk-SS7BQZ5R.js";
27
24
  import {
25
+ ToolExecutor,
28
26
  ToolRegistry,
29
27
  askUserContext,
30
- getActiveMaxChars,
31
- getDangerLevel,
32
28
  googleSearchContext,
33
29
  initTheme,
34
- isFileWriteTool,
35
30
  lastResponseStore,
31
+ renderDiff,
36
32
  setContextWindow,
37
33
  spawnAgentContext,
38
34
  theme,
39
- truncateOutput,
40
35
  undoStack
41
- } from "./chunk-PDVX5QJA.js";
36
+ } from "./chunk-5GZQLJAY.js";
37
+ import {
38
+ fileCheckpoints
39
+ } from "./chunk-4BKXL7SM.js";
42
40
  import {
43
41
  AGENTIC_BEHAVIOR_GUIDELINE,
44
42
  AUTHOR,
@@ -59,16 +57,16 @@ import {
59
57
  SKILLS_DIR_NAME,
60
58
  VERSION,
61
59
  buildUserIdentityPrompt
62
- } from "./chunk-UA4BVWKV.js";
60
+ } from "./chunk-AHH5I2U6.js";
63
61
 
64
62
  // src/index.ts
65
63
  import { program } from "commander";
66
64
 
67
65
  // src/repl/repl.ts
68
66
  import * as readline from "readline";
69
- import { existsSync as existsSync5, readFileSync as readFileSync4, readdirSync as readdirSync3, statSync as statSync3 } from "fs";
67
+ import { existsSync as existsSync4, readFileSync as readFileSync3, readdirSync as readdirSync3, statSync as statSync3 } from "fs";
70
68
  import { join as join4, resolve as resolve2, extname as extname2, dirname as dirname3, basename as basename2 } from "path";
71
- import chalk5 from "chalk";
69
+ import chalk4 from "chalk";
72
70
 
73
71
  // src/repl/renderer.ts
74
72
  import chalk from "chalk";
@@ -836,6 +834,39 @@ Please structure your review as follows:
836
834
 
837
835
  Severity levels: \u{1F534} Critical / \u{1F7E1} Warning / \u{1F535} Info`;
838
836
  }
837
+ function buildSecurityReviewPrompt(diff, gitContextStr) {
838
+ return `# Security Vulnerability Review
839
+
840
+ Analyze the following code changes **exclusively for security vulnerabilities**.
841
+
842
+ ## Categories to check:
843
+ 1. **Injection** \u2014 SQL, command, path traversal, XSS, template injection
844
+ 2. **Authentication & Authorization** \u2014 hardcoded credentials, missing auth checks, privilege escalation
845
+ 3. **Secrets & Sensitive Data** \u2014 API keys, tokens, passwords in code, logging sensitive data
846
+ 4. **Input Validation** \u2014 missing validation, unsafe deserialization, buffer issues
847
+ 5. **Cryptography** \u2014 weak algorithms, improper random, hardcoded IVs/salts
848
+ 6. **Dependencies** \u2014 known vulnerable packages, unsafe dynamic imports
849
+ 7. **File System** \u2014 path traversal, unsafe file permissions, symlink attacks
850
+ 8. **Network** \u2014 SSRF, insecure protocols, missing TLS validation
851
+
852
+ ## Git Status
853
+ ${gitContextStr}
854
+
855
+ ## Code Changes (diff)
856
+ \`\`\`diff
857
+ ${diff}
858
+ \`\`\`
859
+
860
+ ## Output Format
861
+ For each finding:
862
+ - **Severity**: \u{1F534} CRITICAL / \u{1F7E0} HIGH / \u{1F7E1} MEDIUM / \u{1F535} LOW / \u2139\uFE0F INFO
863
+ - **Category**: (from list above)
864
+ - **File & location**: file:line
865
+ - **Description**: what the vulnerability is and how it could be exploited
866
+ - **Recommended fix**: specific code change to resolve
867
+
868
+ If no security issues found, state "\u2705 No security vulnerabilities detected" with a brief explanation of what was checked.`;
869
+ }
839
870
  var CommandRegistry = class {
840
871
  commands = /* @__PURE__ */ new Map();
841
872
  register(command) {
@@ -884,6 +915,8 @@ function createDefaultCommands() {
884
915
  " /skill [name|off|list] - Manage agent skills (reusable prompt packs)",
885
916
  " /checkpoint [save|restore|delete] <name> - Session checkpoints",
886
917
  " /review [--staged] [--detailed] - AI code review from git diff",
918
+ " /security-review [--staged] - Security vulnerability scan on git diff",
919
+ " /rewind [list|<n>] - Rewind to message N (restores files too)",
887
920
  " /commands [reload] - List/reload custom commands (~/.aicli/commands/)",
888
921
  " /test [command|filter] - Run project tests and show structured report",
889
922
  " /scaffold <description> - Generate project scaffolding with AI",
@@ -1908,6 +1941,108 @@ ${hint}` : "")
1908
1941
  }
1909
1942
  }
1910
1943
  },
1944
+ // ── /security-review ──────────────────────────────────────────
1945
+ {
1946
+ name: "security-review",
1947
+ description: "Security vulnerability scan on git diff",
1948
+ usage: "/security-review [--staged]",
1949
+ async execute(args, ctx) {
1950
+ const gitCtx = getGitContext();
1951
+ if (!gitCtx) {
1952
+ ctx.renderer.renderError("Not a git repository.");
1953
+ return;
1954
+ }
1955
+ const staged = args.includes("--staged");
1956
+ let diff;
1957
+ try {
1958
+ const cmd = staged ? "git diff --staged" : "git diff";
1959
+ diff = execSync2(cmd, { encoding: "utf-8", timeout: 1e4 }).trim();
1960
+ } catch {
1961
+ ctx.renderer.renderError("Failed to run git diff.");
1962
+ return;
1963
+ }
1964
+ if (!diff) {
1965
+ console.log(theme.dim(" No changes to review." + (staged ? "" : " Try --staged for staged changes.")));
1966
+ return;
1967
+ }
1968
+ const MAX_DIFF = 8e3;
1969
+ let truncated = false;
1970
+ if (diff.length > MAX_DIFF) {
1971
+ const head = diff.slice(0, Math.floor(MAX_DIFF * 0.7));
1972
+ const tail = diff.slice(diff.length - Math.floor(MAX_DIFF * 0.2));
1973
+ diff = head + "\n\n... [diff truncated, " + diff.length + " chars total] ...\n\n" + tail;
1974
+ truncated = true;
1975
+ }
1976
+ const prompt = buildSecurityReviewPrompt(diff, formatGitContextForPrompt(gitCtx));
1977
+ console.log(theme.dim(" \u{1F512} Scanning for security vulnerabilities..."));
1978
+ try {
1979
+ const review = await ctx.chatOnce(prompt, { temperature: 0.2, maxTokens: 8192 });
1980
+ console.log();
1981
+ console.log(review);
1982
+ console.log();
1983
+ if (truncated) {
1984
+ console.log(theme.warning(" \u26A0 Diff was truncated. Consider reviewing smaller changesets."));
1985
+ }
1986
+ } catch (err) {
1987
+ ctx.renderer.renderError(`Security review failed: ${err.message}`);
1988
+ }
1989
+ }
1990
+ },
1991
+ // ── /rewind ───────────────────────────────────────────────────
1992
+ {
1993
+ name: "rewind",
1994
+ description: "Rewind conversation to a previous message and restore file states",
1995
+ usage: "/rewind [list | <n>]",
1996
+ execute(args, ctx) {
1997
+ const session = ctx.sessions.current;
1998
+ if (!session || session.messages.length === 0) {
1999
+ console.log(theme.dim(" No messages to rewind."));
2000
+ return;
2001
+ }
2002
+ const sub = args[0];
2003
+ if (sub === "list" || !sub) {
2004
+ const msgs = session.messages;
2005
+ console.log(theme.primary(`
2006
+ Conversation messages (${msgs.length} total):
2007
+ `));
2008
+ for (let i = 0; i < msgs.length; i++) {
2009
+ const m = msgs[i];
2010
+ const role = m.role.padEnd(10);
2011
+ const text = getContentText(m.content).replace(/\n/g, " ").slice(0, 70);
2012
+ const hasCheckpoint = fileCheckpoints.getMessageIndices().includes(i) ? " \u{1F4CC}" : "";
2013
+ console.log(theme.dim(` [${String(i + 1).padStart(3)}]`) + ` ${theme.info(role)} ${text}${hasCheckpoint}`);
2014
+ }
2015
+ console.log();
2016
+ console.log(theme.dim(" Usage: /rewind <n> \u2014 rewind to message N (1-based)"));
2017
+ console.log(theme.dim(" \u{1F4CC} = file checkpoint exists at this point"));
2018
+ console.log();
2019
+ return;
2020
+ }
2021
+ const n = parseInt(sub, 10);
2022
+ if (isNaN(n) || n < 1 || n > session.messages.length) {
2023
+ ctx.renderer.renderError(`Invalid message number: ${sub}. Range: 1-${session.messages.length}`);
2024
+ return;
2025
+ }
2026
+ const targetIndex = n;
2027
+ const messagesRemoved = session.messages.length - targetIndex;
2028
+ const { restored, deleted, files } = fileCheckpoints.restoreToMessageIndex(targetIndex);
2029
+ session.messages = session.messages.slice(0, targetIndex);
2030
+ session.checkpoints = session.checkpoints.filter((c) => c.messageIndex <= targetIndex);
2031
+ session.updated = /* @__PURE__ */ new Date();
2032
+ console.log(theme.success(`
2033
+ \u2713 Rewound to message ${n}`));
2034
+ console.log(theme.dim(` Messages removed: ${messagesRemoved}`));
2035
+ if (restored > 0 || deleted > 0) {
2036
+ console.log(theme.dim(` Files restored: ${restored}, files deleted: ${deleted}`));
2037
+ for (const f of files) {
2038
+ console.log(theme.dim(` ${f}`));
2039
+ }
2040
+ } else {
2041
+ console.log(theme.dim(" No file changes to revert."));
2042
+ }
2043
+ console.log();
2044
+ }
2045
+ },
1911
2046
  // ── /commands ─────────────────────────────────────────────────
1912
2047
  {
1913
2048
  name: "commands",
@@ -1944,7 +2079,7 @@ ${hint}` : "")
1944
2079
  usage: "/test [command|filter]",
1945
2080
  async execute(args, ctx) {
1946
2081
  try {
1947
- const { executeTests } = await import("./run-tests-7ZBI4ZTU.js");
2082
+ const { executeTests } = await import("./run-tests-L3JNRB6X.js");
1948
2083
  const argStr = args.join(" ").trim();
1949
2084
  let testArgs = {};
1950
2085
  if (argStr) {
@@ -2576,472 +2711,6 @@ function selectFromList(prompt, items, initialIndex = 0) {
2576
2711
  });
2577
2712
  }
2578
2713
 
2579
- // src/tools/executor.ts
2580
- import chalk4 from "chalk";
2581
- import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
2582
- var ToolExecutor = class {
2583
- constructor(registry) {
2584
- this.registry = registry;
2585
- }
2586
- round = 0;
2587
- totalRounds = 0;
2588
- /** readline 接口引用,由 repl.ts 注入,用于 confirm() 读取用户输入 */
2589
- rl = null;
2590
- /**
2591
- * confirm() 进行中标志。
2592
- * repl.ts 的主循环 line handler 在此为 true 时忽略输入,
2593
- * 防止用户输入 "y"+Enter 被同时触发 once('line') 和主循环 on('line')。
2594
- */
2595
- confirming = false;
2596
- /** 在 confirm 期间用户输入的 slash 命令,由 repl.ts 主循环消费 */
2597
- pendingSlashCommand = null;
2598
- /** confirm() 的取消回调,由 SIGINT handler 调用 */
2599
- cancelConfirmFn = null;
2600
- /**
2601
- * 会话级 auto-approve:跳过所有 write/destructive 确认(仅当前会话有效)。
2602
- * 通过 /yolo 命令切换。destructive 操作仍会显示警告但不阻塞。
2603
- */
2604
- sessionAutoApprove = false;
2605
- /**
2606
- * 由外部(repl.ts SIGINT handler)调用,将当前 confirm() 等待视为用户按 N 取消。
2607
- * 若当前没有 confirm() 进行中,无操作。
2608
- */
2609
- cancelConfirm() {
2610
- if (this.cancelConfirmFn) {
2611
- this.cancelConfirmFn();
2612
- }
2613
- }
2614
- setRoundInfo(current, total) {
2615
- this.round = current;
2616
- this.totalRounds = total;
2617
- }
2618
- /**
2619
- * 注入 readline 接口,供 confirm() 使用。
2620
- * 必须在 start() 之前调用,rl 初始化后立即注入。
2621
- */
2622
- setReadline(rl) {
2623
- this.rl = rl;
2624
- }
2625
- /** 钩子配置(可选) */
2626
- hookConfig;
2627
- /** 权限规则(可选) */
2628
- permissionRules = [];
2629
- defaultPermission = "confirm";
2630
- /** 注入 hooks 和 permission rules 配置 */
2631
- setConfig(opts) {
2632
- this.hookConfig = opts.hookConfig;
2633
- if (opts.permissionRules) this.permissionRules = opts.permissionRules;
2634
- if (opts.defaultPermission) this.defaultPermission = opts.defaultPermission;
2635
- }
2636
- async execute(call) {
2637
- const tool = this.registry.get(call.name);
2638
- if (!tool) {
2639
- return {
2640
- callId: call.id,
2641
- content: `Unknown tool: ${call.name}`,
2642
- isError: true
2643
- };
2644
- }
2645
- const dangerLevel = getDangerLevel(call.name, call.arguments);
2646
- runHook(this.hookConfig?.preToolExecution, {
2647
- tool: call.name,
2648
- dangerLevel,
2649
- args: JSON.stringify(call.arguments).slice(0, 200)
2650
- });
2651
- if (this.permissionRules.length > 0) {
2652
- const action = checkPermission(call.name, call.arguments, dangerLevel, this.permissionRules, this.defaultPermission);
2653
- if (action === "deny") {
2654
- return { callId: call.id, content: `[Permission denied] Tool ${call.name} is blocked by permission rules. Do not retry.`, isError: true };
2655
- }
2656
- if (action === "auto-approve") {
2657
- this.printToolCall(call);
2658
- try {
2659
- const rawContent = await tool.execute(call.arguments);
2660
- const content = truncateOutput(rawContent, call.name);
2661
- const wasTruncated = content !== rawContent;
2662
- this.printToolResult(call.name, rawContent, false, wasTruncated);
2663
- runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "ok" });
2664
- return { callId: call.id, content, isError: false };
2665
- } catch (err) {
2666
- const message = err instanceof Error ? err.message : String(err);
2667
- this.printToolResult(call.name, message, true, false);
2668
- runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "error" });
2669
- return { callId: call.id, content: message, isError: true };
2670
- }
2671
- }
2672
- }
2673
- if (this.sessionAutoApprove && dangerLevel !== "safe") {
2674
- this.printToolCall(call);
2675
- if (dangerLevel === "write") this.printDiffPreview(call);
2676
- console.log(theme.warning(" \u26A1 Auto-approved (session /yolo mode)"));
2677
- try {
2678
- const rawContent = await tool.execute(call.arguments);
2679
- const content = truncateOutput(rawContent, call.name);
2680
- const wasTruncated = content !== rawContent;
2681
- this.printToolResult(call.name, rawContent, false, wasTruncated);
2682
- runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "ok" });
2683
- return { callId: call.id, content, isError: false };
2684
- } catch (err) {
2685
- const message = err instanceof Error ? err.message : String(err);
2686
- this.printToolResult(call.name, message, true, false);
2687
- runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "error" });
2688
- return { callId: call.id, content: message, isError: true };
2689
- }
2690
- }
2691
- if (dangerLevel === "write") {
2692
- this.printToolCall(call);
2693
- this.printDiffPreview(call);
2694
- const confirmed = await this.confirm(call, dangerLevel);
2695
- if (!confirmed) {
2696
- return {
2697
- callId: call.id,
2698
- content: `[User cancelled] The user declined the ${call.name} operation. Do not retry without asking.`,
2699
- isError: true
2700
- };
2701
- }
2702
- } else if (dangerLevel === "destructive") {
2703
- const confirmed = await this.confirm(call, dangerLevel);
2704
- if (!confirmed) {
2705
- return {
2706
- callId: call.id,
2707
- content: `[User cancelled] The user declined the destructive ${call.name} operation. Do not retry without asking.`,
2708
- isError: true
2709
- };
2710
- }
2711
- this.printToolCall(call);
2712
- } else {
2713
- this.printToolCall(call);
2714
- }
2715
- try {
2716
- const rawContent = await tool.execute(call.arguments);
2717
- const content = truncateOutput(rawContent, call.name);
2718
- const wasTruncated = content !== rawContent;
2719
- this.printToolResult(call.name, rawContent, false, wasTruncated);
2720
- runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "ok" });
2721
- return { callId: call.id, content, isError: false };
2722
- } catch (err) {
2723
- const message = err instanceof Error ? err.message : String(err);
2724
- this.printToolResult(call.name, message, true, false);
2725
- runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "error" });
2726
- return { callId: call.id, content: message, isError: true };
2727
- }
2728
- }
2729
- async executeAll(calls) {
2730
- const safeCalls = [];
2731
- const fileWriteCalls = [];
2732
- const otherCalls = [];
2733
- for (let i = 0; i < calls.length; i++) {
2734
- const call = calls[i];
2735
- const level = getDangerLevel(call.name, call.arguments);
2736
- if (level === "safe") {
2737
- safeCalls.push({ idx: i, call });
2738
- } else if (isFileWriteTool(call.name) && level === "write") {
2739
- fileWriteCalls.push({ idx: i, call });
2740
- } else {
2741
- otherCalls.push({ idx: i, call });
2742
- }
2743
- }
2744
- const results = new Array(calls.length);
2745
- await Promise.all(
2746
- safeCalls.map(async ({ idx, call }) => {
2747
- results[idx] = await this.execute(call);
2748
- })
2749
- );
2750
- if (fileWriteCalls.length === 1) {
2751
- const { idx, call } = fileWriteCalls[0];
2752
- results[idx] = await this.execute(call);
2753
- } else if (fileWriteCalls.length >= 2) {
2754
- const batchResults = await this.executeBatchFileWrites(fileWriteCalls.map((f) => f.call));
2755
- for (let i = 0; i < fileWriteCalls.length; i++) {
2756
- results[fileWriteCalls[i].idx] = batchResults[i];
2757
- }
2758
- }
2759
- for (const { idx, call } of otherCalls) {
2760
- results[idx] = await this.execute(call);
2761
- }
2762
- return results;
2763
- }
2764
- /**
2765
- * 批量文件写入:展示所有文件的编号列表 + diff 预览,
2766
- * 然后让用户 approve all / reject all / 选择性 approve。
2767
- */
2768
- async executeBatchFileWrites(calls) {
2769
- console.log();
2770
- console.log(theme.heading(`\u270E Batch file writes (${calls.length} files):`));
2771
- console.log(theme.dim("\u2500".repeat(50)));
2772
- for (let i = 0; i < calls.length; i++) {
2773
- const call = calls[i];
2774
- const filePath = String(call.arguments["path"] ?? "");
2775
- console.log(theme.warning(` [${i + 1}] `) + chalk4.white(call.name) + theme.dim(": ") + theme.accent(filePath));
2776
- this.printDiffPreview(call);
2777
- }
2778
- console.log(theme.dim("\u2500".repeat(50)));
2779
- const decision = this.sessionAutoApprove ? "all" : await this.batchConfirm(calls.length);
2780
- if (this.sessionAutoApprove) {
2781
- console.log(theme.warning(" \u26A1 All auto-approved (session /yolo mode)"));
2782
- }
2783
- const results = [];
2784
- for (let i = 0; i < calls.length; i++) {
2785
- const call = calls[i];
2786
- const approved = decision === "all" || decision !== "none" && decision.has(i + 1);
2787
- if (approved) {
2788
- const tool = this.registry.get(call.name);
2789
- if (!tool) {
2790
- results.push({ callId: call.id, content: `Unknown tool: ${call.name}`, isError: true });
2791
- continue;
2792
- }
2793
- try {
2794
- const rawContent = await tool.execute(call.arguments);
2795
- const content = truncateOutput(rawContent, call.name);
2796
- const wasTruncated = content !== rawContent;
2797
- this.printToolResult(call.name, rawContent, false, wasTruncated);
2798
- results.push({ callId: call.id, content, isError: false });
2799
- } catch (err) {
2800
- const message = err instanceof Error ? err.message : String(err);
2801
- this.printToolResult(call.name, message, true, false);
2802
- results.push({ callId: call.id, content: message, isError: true });
2803
- }
2804
- } else {
2805
- console.log(theme.dim(` [${i + 1}] `) + theme.dim("rejected"));
2806
- results.push({ callId: call.id, content: `[User rejected] The user rejected this ${call.name} operation. Do not retry without asking.`, isError: true });
2807
- }
2808
- }
2809
- return results;
2810
- }
2811
- /**
2812
- * 批量确认:让用户选择 approve all / reject all / 指定编号。
2813
- * 返回 'all' | 'none' | Set<number>(1-based 编号)
2814
- */
2815
- batchConfirm(count) {
2816
- const prompt = theme.warning(` [a]pprove all [r]eject all [1,${count > 1 ? count : "2"},..] approve specific: `);
2817
- if (!this.rl) {
2818
- process.stdout.write(theme.warning("No readline: auto-rejected.\n"));
2819
- return Promise.resolve("none");
2820
- }
2821
- const rl = this.rl;
2822
- const rlAny = rl;
2823
- const savedOutput = rlAny.output;
2824
- rlAny.output = process.stdout;
2825
- rl.resume();
2826
- process.stdout.write(prompt);
2827
- this.confirming = true;
2828
- return new Promise((resolve3) => {
2829
- let completed = false;
2830
- const cleanup = (result) => {
2831
- if (completed) return;
2832
- completed = true;
2833
- rl.removeListener("line", onLine);
2834
- this.cancelConfirmFn = null;
2835
- rl.pause();
2836
- rlAny.output = savedOutput;
2837
- this.confirming = false;
2838
- resolve3(result);
2839
- };
2840
- const onLine = (line) => {
2841
- const trimmed = line.trim();
2842
- if (trimmed.startsWith("/")) {
2843
- this.pendingSlashCommand = trimmed;
2844
- process.stdout.write(theme.dim(`
2845
- (command "${trimmed}" queued, will execute after current operation)
2846
- `));
2847
- cleanup("none");
2848
- return;
2849
- }
2850
- const input2 = trimmed.toLowerCase();
2851
- if (input2 === "a" || input2 === "all" || input2 === "y") {
2852
- cleanup("all");
2853
- } else if (input2 === "r" || input2 === "reject" || input2 === "n" || input2 === "") {
2854
- cleanup("none");
2855
- } else {
2856
- const nums = input2.split(/[,\s]+/).map(Number).filter((n) => !isNaN(n) && n >= 1 && n <= count);
2857
- if (nums.length > 0) {
2858
- cleanup(new Set(nums));
2859
- } else {
2860
- cleanup("none");
2861
- }
2862
- }
2863
- };
2864
- this.cancelConfirmFn = () => {
2865
- process.stdout.write(theme.dim("\n(cancelled)\n"));
2866
- cleanup("none");
2867
- };
2868
- try {
2869
- rl.once("line", onLine);
2870
- } catch {
2871
- cleanup("none");
2872
- }
2873
- });
2874
- }
2875
- printToolCall(call) {
2876
- const dangerLevel = getDangerLevel(call.name, call.arguments);
2877
- console.log();
2878
- const icon = dangerLevel === "write" ? theme.toolCall("\u270E Tool: ") : theme.heading(theme.accent("\u2699 Tool: "));
2879
- const roundBadge = this.totalRounds > 0 ? theme.dim(` [${this.round}/${this.totalRounds}]`) : "";
2880
- console.log(icon + chalk4.white(call.name) + roundBadge);
2881
- for (const [key, val] of Object.entries(call.arguments)) {
2882
- let valStr;
2883
- if (Array.isArray(val)) {
2884
- const json = JSON.stringify(val);
2885
- valStr = json.length > 160 ? json.slice(0, 160) + "..." : json;
2886
- } else if (typeof val === "string" && val.length > 120) {
2887
- valStr = val.slice(0, 120) + "...";
2888
- } else {
2889
- valStr = String(val);
2890
- }
2891
- console.log(theme.dim(` ${key}: `) + chalk4.white(valStr));
2892
- }
2893
- }
2894
- /**
2895
- * 对 write_file / edit_file 在执行前展示 diff 预览。
2896
- * - write_file:比较旧文件内容与新内容
2897
- * - edit_file (replace):比较旧字符串与新字符串
2898
- * - edit_file (insert/delete):显示操作摘要,不做 diff(变化明确)
2899
- */
2900
- printDiffPreview(call) {
2901
- if (call.name === "write_file") {
2902
- const filePath = String(call.arguments["path"] ?? "");
2903
- const newContent = String(call.arguments["content"] ?? "");
2904
- if (!filePath) return;
2905
- if (existsSync3(filePath)) {
2906
- let oldContent;
2907
- try {
2908
- oldContent = readFileSync2(filePath, "utf-8");
2909
- } catch {
2910
- return;
2911
- }
2912
- if (oldContent === newContent) {
2913
- console.log(theme.dim(" (file content unchanged)"));
2914
- return;
2915
- }
2916
- const diff = renderDiff(oldContent, newContent, { filePath, contextLines: 3 });
2917
- console.log(theme.dim(" \u2500\u2500 diff preview \u2500\u2500"));
2918
- console.log(diff);
2919
- console.log();
2920
- } else {
2921
- const lines = newContent.split("\n");
2922
- const preview = lines.slice(0, 20).map((l) => theme.success(`+ ${l}`)).join("\n");
2923
- const more = lines.length > 20 ? theme.dim(`
2924
- ... (+${lines.length - 20} more lines)`) : "";
2925
- console.log(theme.dim(" \u2500\u2500 new file preview \u2500\u2500"));
2926
- console.log(preview + more);
2927
- console.log();
2928
- }
2929
- } else if (call.name === "edit_file") {
2930
- const filePath = String(call.arguments["path"] ?? "");
2931
- if (!filePath || !existsSync3(filePath)) return;
2932
- const oldStr = call.arguments["old_str"];
2933
- const newStr = call.arguments["new_str"];
2934
- if (oldStr !== void 0) {
2935
- const diff = renderDiff(
2936
- String(oldStr),
2937
- String(newStr ?? ""),
2938
- { filePath, contextLines: 2 }
2939
- );
2940
- console.log(theme.dim(" \u2500\u2500 diff preview \u2500\u2500"));
2941
- console.log(diff);
2942
- console.log();
2943
- } else if (call.arguments["insert_after_line"] !== void 0) {
2944
- const line = Number(call.arguments["insert_after_line"]);
2945
- const insertContent = String(call.arguments["insert_content"] ?? "");
2946
- const insertLines = insertContent.split("\n");
2947
- const preview = insertLines.slice(0, 5).map((l) => theme.success(`+ ${l}`)).join("\n");
2948
- const more = insertLines.length > 5 ? theme.dim(`
2949
- ... (+${insertLines.length - 5} more lines)`) : "";
2950
- console.log(theme.dim(` \u2500\u2500 insert after line ${line} \u2500\u2500`));
2951
- console.log(preview + more);
2952
- console.log();
2953
- } else if (call.arguments["delete_from_line"] !== void 0) {
2954
- const from = Number(call.arguments["delete_from_line"]);
2955
- const to = Number(call.arguments["delete_to_line"] ?? from);
2956
- let fileContent;
2957
- try {
2958
- fileContent = readFileSync2(filePath, "utf-8");
2959
- } catch {
2960
- return;
2961
- }
2962
- const fileLines = fileContent.split("\n");
2963
- const deleted = fileLines.slice(from - 1, to);
2964
- const preview = deleted.slice(0, 5).map((l) => theme.error(`- ${l}`)).join("\n");
2965
- const more = deleted.length > 5 ? theme.dim(`
2966
- ... (-${deleted.length - 5} more lines)`) : "";
2967
- console.log(theme.dim(` \u2500\u2500 delete lines ${from}\u2013${to} \u2500\u2500`));
2968
- console.log(preview + more);
2969
- console.log();
2970
- }
2971
- }
2972
- }
2973
- printToolResult(name, content, isError, wasTruncated) {
2974
- if (isError) {
2975
- console.log(theme.error(`\u26A0 ${name} error: `) + theme.dim(content.slice(0, 300)));
2976
- } else {
2977
- const lines = content.split("\n");
2978
- const maxLines = name === "run_interactive" ? 40 : 8;
2979
- const preview = lines.slice(0, maxLines).join("\n");
2980
- const moreLines = lines.length > maxLines ? theme.dim(`
2981
- ... (${lines.length - maxLines} more lines)`) : "";
2982
- const truncatedNote = wasTruncated ? theme.warning(`
2983
- \u26A1 Output truncated to ${getActiveMaxChars()} chars before sending to AI`) : "";
2984
- console.log(theme.toolResult("\u2713 Result: ") + theme.dim(preview) + moreLines + truncatedNote);
2985
- }
2986
- console.log();
2987
- }
2988
- confirm(call, level) {
2989
- const color = level === "destructive" ? theme.error : theme.warning;
2990
- const label = level === "destructive" ? "\u26A0 DESTRUCTIVE" : "\u270E Write";
2991
- console.log();
2992
- console.log(color(`${label} operation: `) + theme.heading(call.name));
2993
- for (const [key, val] of Object.entries(call.arguments)) {
2994
- const valStr = typeof val === "string" && val.length > 200 ? val.slice(0, 200) + "..." : String(val);
2995
- console.log(theme.dim(` ${key}: `) + valStr);
2996
- }
2997
- if (!this.rl) {
2998
- process.stdout.write(theme.warning("No readline: auto-rejected.\n"));
2999
- return Promise.resolve(false);
3000
- }
3001
- const rl = this.rl;
3002
- const rlAny = rl;
3003
- const savedOutput = rlAny.output;
3004
- rlAny.output = process.stdout;
3005
- rl.resume();
3006
- process.stdout.write(color("Proceed? [y/N] (type y + Enter to confirm) "));
3007
- this.confirming = true;
3008
- return new Promise((resolve3) => {
3009
- let completed = false;
3010
- const cleanup = (answer) => {
3011
- if (completed) return;
3012
- completed = true;
3013
- rl.removeListener("line", onLine);
3014
- this.cancelConfirmFn = null;
3015
- rl.pause();
3016
- rlAny.output = savedOutput;
3017
- this.confirming = false;
3018
- resolve3(answer === "y");
3019
- };
3020
- const onLine = (line) => {
3021
- const trimmed = line.trim();
3022
- if (trimmed.startsWith("/")) {
3023
- this.pendingSlashCommand = trimmed;
3024
- process.stdout.write(theme.dim(`
3025
- (command "${trimmed}" queued, will execute after current operation)
3026
- `));
3027
- cleanup("n");
3028
- return;
3029
- }
3030
- cleanup(trimmed.toLowerCase());
3031
- };
3032
- this.cancelConfirmFn = () => {
3033
- process.stdout.write(theme.dim("\n(cancelled)\n"));
3034
- cleanup("n");
3035
- };
3036
- try {
3037
- rl.once("line", onLine);
3038
- } catch {
3039
- cleanup("n");
3040
- }
3041
- });
3042
- }
3043
- };
3044
-
3045
2714
  // src/tools/builtin/stream-to-file.ts
3046
2715
  var streamToFileContext = {
3047
2716
  provider: null,
@@ -3335,13 +3004,13 @@ Managing ${displayName} API Key`);
3335
3004
  };
3336
3005
 
3337
3006
  // src/repl/custom-commands.ts
3338
- import { existsSync as existsSync4, readFileSync as readFileSync3, readdirSync as readdirSync2, mkdirSync as mkdirSync3 } from "fs";
3007
+ import { existsSync as existsSync3, readFileSync as readFileSync2, readdirSync as readdirSync2, mkdirSync as mkdirSync3 } from "fs";
3339
3008
  import { join as join3, extname } from "path";
3340
3009
  import { execSync as execSync3 } from "child_process";
3341
3010
  function parseCommandFile(filePath) {
3342
3011
  let content;
3343
3012
  try {
3344
- content = readFileSync3(filePath, "utf-8");
3013
+ content = readFileSync2(filePath, "utf-8");
3345
3014
  } catch {
3346
3015
  return null;
3347
3016
  }
@@ -3365,7 +3034,7 @@ function expandTemplate(template, args) {
3365
3034
  result = result.replace(/\{\{input\}\}/g, args.join(" "));
3366
3035
  result = result.replace(/\{\{file:([^}]+)\}\}/g, (_m, p) => {
3367
3036
  try {
3368
- return readFileSync3(p.trim(), "utf-8");
3037
+ return readFileSync2(p.trim(), "utf-8");
3369
3038
  } catch {
3370
3039
  return `[Error: cannot read ${p.trim()}]`;
3371
3040
  }
@@ -3390,7 +3059,7 @@ var CustomCommandManager = class {
3390
3059
  commands = /* @__PURE__ */ new Map();
3391
3060
  loadCommands() {
3392
3061
  this.commands.clear();
3393
- if (!existsSync4(this.commandsDir)) {
3062
+ if (!existsSync3(this.commandsDir)) {
3394
3063
  mkdirSync3(this.commandsDir, { recursive: true });
3395
3064
  return 0;
3396
3065
  }
@@ -3473,7 +3142,7 @@ function parseAtReferences(input2, cwd) {
3473
3142
  const absPath = resolve2(cwd, rawPath);
3474
3143
  const ext = extname2(rawPath).toLowerCase();
3475
3144
  const mime = IMAGE_MIME[ext];
3476
- if (!existsSync5(absPath)) {
3145
+ if (!existsSync4(absPath)) {
3477
3146
  refs.push({ path: rawPath, type: "notfound" });
3478
3147
  continue;
3479
3148
  }
@@ -3483,7 +3152,7 @@ function parseAtReferences(input2, cwd) {
3483
3152
  refs.push({ path: rawPath, type: "toolarge" });
3484
3153
  continue;
3485
3154
  }
3486
- const data = readFileSync4(absPath).toString("base64");
3155
+ const data = readFileSync3(absPath).toString("base64");
3487
3156
  imageParts.push({
3488
3157
  type: "image_url",
3489
3158
  image_url: { url: `data:${mime};base64,${data}` }
@@ -3491,7 +3160,7 @@ function parseAtReferences(input2, cwd) {
3491
3160
  refs.push({ path: rawPath, type: "image" });
3492
3161
  textBody = textBody.replace(match[0], "").trim();
3493
3162
  } else {
3494
- const content = readFileSync4(absPath, "utf-8");
3163
+ const content = readFileSync3(absPath, "utf-8");
3495
3164
  const inlined = `
3496
3165
 
3497
3166
  [File: ${rawPath}]
@@ -3741,7 +3410,7 @@ ${treeLines.join("\n")}`
3741
3410
  if (!TEXT_EXTS.has(ext) && !isSpecial) continue;
3742
3411
  if (st.size > MAX_FILE_CHARS * 3) continue;
3743
3412
  try {
3744
- let content = readFileSync4(fullPath, "utf-8");
3413
+ let content = readFileSync3(fullPath, "utf-8");
3745
3414
  if (content.length > MAX_FILE_CHARS) {
3746
3415
  content = content.slice(0, MAX_FILE_CHARS) + `
3747
3416
  ... (truncated, ${content.length} chars total)`;
@@ -3771,7 +3440,7 @@ ${content}
3771
3440
  */
3772
3441
  addExtraContextDir(dirPath) {
3773
3442
  const absPath = resolve2(dirPath);
3774
- if (!existsSync5(absPath)) {
3443
+ if (!existsSync4(absPath)) {
3775
3444
  return { success: false, charCount: 0, added: false, error: `Directory not found: ${dirPath}` };
3776
3445
  }
3777
3446
  let isDir;
@@ -3806,8 +3475,8 @@ ${content}
3806
3475
  findContextFile(dir, candidates = CONTEXT_FILE_CANDIDATES) {
3807
3476
  for (const candidate of candidates) {
3808
3477
  const fullPath = join4(dir, candidate);
3809
- if (existsSync5(fullPath)) {
3810
- const content = readFileSync4(fullPath, "utf-8").trim();
3478
+ if (existsSync4(fullPath)) {
3479
+ const content = readFileSync3(fullPath, "utf-8").trim();
3811
3480
  if (content) return { filePath: fullPath, content };
3812
3481
  }
3813
3482
  }
@@ -3836,9 +3505,9 @@ ${content}
3836
3505
  const gitRoot = getGitRoot(cwd);
3837
3506
  const projectRoot = gitRoot ?? cwd;
3838
3507
  const mcpPath = join4(projectRoot, MCP_PROJECT_CONFIG_NAME);
3839
- if (!existsSync5(mcpPath)) return null;
3508
+ if (!existsSync4(mcpPath)) return null;
3840
3509
  try {
3841
- const raw = JSON.parse(readFileSync4(mcpPath, "utf-8"));
3510
+ const raw = JSON.parse(readFileSync3(mcpPath, "utf-8"));
3842
3511
  const servers = raw?.mcpServers;
3843
3512
  if (!servers || typeof servers !== "object") {
3844
3513
  process.stderr.write(
@@ -3884,8 +3553,8 @@ ${content}
3884
3553
  );
3885
3554
  return { layers: [], mergedContent: "" };
3886
3555
  }
3887
- if (existsSync5(fullPath)) {
3888
- const content = readFileSync4(fullPath, "utf-8").trim();
3556
+ if (existsSync4(fullPath)) {
3557
+ const content = readFileSync3(fullPath, "utf-8").trim();
3889
3558
  if (content) {
3890
3559
  const layer = {
3891
3560
  level: "project",
@@ -3943,8 +3612,8 @@ ${content}
3943
3612
  */
3944
3613
  loadMemoryContent() {
3945
3614
  const memoryPath = join4(this.config.getConfigDir(), MEMORY_FILE_NAME);
3946
- if (!existsSync5(memoryPath)) return null;
3947
- let content = readFileSync4(memoryPath, "utf-8").trim();
3615
+ if (!existsSync4(memoryPath)) return null;
3616
+ let content = readFileSync3(memoryPath, "utf-8").trim();
3948
3617
  if (!content) return null;
3949
3618
  if (content.length > MEMORY_MAX_CHARS) {
3950
3619
  content = content.slice(-MEMORY_MAX_CHARS);
@@ -4179,7 +3848,7 @@ ${response.content.trim()}
4179
3848
  const planTag = this.planMode ? theme.warning("[PLAN]") : "";
4180
3849
  const modelParams = this.getModelParams();
4181
3850
  const thinkTag = modelParams.thinking ? theme.accent("[THINK]") : "";
4182
- const promptStr = theme.success(`[${this.currentProvider}]`) + skillTag + planTag + thinkTag + chalk5.white(" > ");
3851
+ const promptStr = theme.success(`[${this.currentProvider}]`) + skillTag + planTag + thinkTag + chalk4.white(" > ");
4183
3852
  this.rl.setPrompt(promptStr);
4184
3853
  }
4185
3854
  showPrompt() {
@@ -4698,7 +4367,7 @@ Session '${this.resumeSessionId}' not found.
4698
4367
  const dir = normalized.includes("/") ? dirname3(normalized) : ".";
4699
4368
  const prefix = normalized.includes("/") ? basename2(normalized) : normalized;
4700
4369
  const absDir = resolve2(process.cwd(), dir);
4701
- if (!existsSync5(absDir)) return [];
4370
+ if (!existsSync4(absDir)) return [];
4702
4371
  const entries = readdirSync3(absDir);
4703
4372
  const results = [];
4704
4373
  for (const entry of entries) {
@@ -5244,6 +4913,7 @@ You have a maximum of ${MAX_TOOL_ROUNDS} tool call rounds for this task. Plan ef
5244
4913
  spawnAgentContext.systemPrompt = systemPrompt;
5245
4914
  spawnAgentContext.modelParams = modelParams;
5246
4915
  spawnAgentContext.configManager = this.config;
4916
+ ToolExecutor.currentMessageIndex = session.messages.length;
5247
4917
  const toolResults = await this.toolExecutor.executeAll(result.toolCalls);
5248
4918
  const thisRoundTools = result.toolCalls.map((tc) => tc.name);
5249
4919
  roundToolHistory.push({ round: round + 1, tools: thisRoundTools });
@@ -5691,7 +5361,7 @@ program.command("web").description("Start Web UI server with browser-based chat
5691
5361
  console.error("Error: Invalid port number. Must be between 1 and 65535.");
5692
5362
  process.exit(1);
5693
5363
  }
5694
- const { startWebServer } = await import("./server-SD5ICBFP.js");
5364
+ const { startWebServer } = await import("./server-V3IZSAMO.js");
5695
5365
  await startWebServer({ port, host: options.host });
5696
5366
  });
5697
5367
  program.command("user [action] [username]").description("Manage Web UI users (list | create <name> | delete <name> | reset-password <name> | migrate <name>)").action(async (action, username) => {
@@ -5924,7 +5594,7 @@ program.command("hub [topic]").description("Start multi-agent hub (discuss / bra
5924
5594
  }),
5925
5595
  config.get("customProviders")
5926
5596
  );
5927
- const { startHub } = await import("./hub-YN245LMP.js");
5597
+ const { startHub } = await import("./hub-JOYPSPR2.js");
5928
5598
  await startHub(
5929
5599
  {
5930
5600
  topic: topic ?? "",