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.
@@ -1,4 +1,7 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ fileCheckpoints
4
+ } from "./chunk-4BKXL7SM.js";
2
5
  import {
3
6
  CONFIG_DIR_NAME,
4
7
  MEMORY_FILE_NAME,
@@ -6,7 +9,7 @@ import {
6
9
  SUBAGENT_DEFAULT_MAX_ROUNDS,
7
10
  SUBAGENT_MAX_ROUNDS_LIMIT,
8
11
  runTestsTool
9
- } from "./chunk-UA4BVWKV.js";
12
+ } from "./chunk-AHH5I2U6.js";
10
13
 
11
14
  // src/tools/builtin/bash.ts
12
15
  import { execSync } from "child_process";
@@ -586,37 +589,849 @@ Cannot extract text from this PDF, but found alternative text versions:
586
589
  ` + textAlts.map((p) => ` \u2192 ${basename(p)}`).join("\n") + `
587
590
  Please use read_file to read the above files.`;
588
591
  }
589
- return `[PDF file: ${filePath}]
590
- Cannot extract text from this PDF (requires pdftotext or pdfminer.six).
591
- Suggestion: read existing text versions (.md / .txt) in the project, or install extraction tools via bash and retry.`;
592
- }
593
- if (BINARY_EXTENSIONS.has(ext)) {
594
- return `[Binary file: ${filePath} (${ext})]
595
- This is a binary file and cannot be read as text. If there is a text version (.md / .txt) in the project, please read that instead.`;
596
- }
597
- const { readFile: readFile2 } = await import("fs/promises");
598
- const buf = size > 1048576 ? await readFile2(normalizedPath) : readFileSync2(normalizedPath);
599
- if (encoding === "base64") {
600
- return `[File: ${filePath} | base64]
601
-
602
- ${buf.toString("base64")}`;
592
+ return `[PDF file: ${filePath}]
593
+ Cannot extract text from this PDF (requires pdftotext or pdfminer.six).
594
+ Suggestion: read existing text versions (.md / .txt) in the project, or install extraction tools via bash and retry.`;
595
+ }
596
+ if (BINARY_EXTENSIONS.has(ext)) {
597
+ return `[Binary file: ${filePath} (${ext})]
598
+ This is a binary file and cannot be read as text. If there is a text version (.md / .txt) in the project, please read that instead.`;
599
+ }
600
+ const { readFile: readFile2 } = await import("fs/promises");
601
+ const buf = size > 1048576 ? await readFile2(normalizedPath) : readFileSync2(normalizedPath);
602
+ if (encoding === "base64") {
603
+ return `[File: ${filePath} | base64]
604
+
605
+ ${buf.toString("base64")}`;
606
+ }
607
+ if (isBinaryBuffer(buf)) {
608
+ return `[Binary file: ${filePath}]
609
+ This file contains binary data and cannot be read as text.
610
+ If needed, use the bash tool to run an appropriate conversion program.`;
611
+ }
612
+ const content = buf.toString(encoding);
613
+ const lines = content.split("\n").length;
614
+ return `${sensitiveWarning}[File: ${filePath} | ${lines} lines]
615
+
616
+ ${content}`;
617
+ }
618
+ };
619
+
620
+ // src/tools/builtin/write-file.ts
621
+ import { writeFileSync as writeFileSync2, appendFileSync, mkdirSync } from "fs";
622
+ import { dirname as dirname2 } from "path";
623
+
624
+ // src/tools/executor.ts
625
+ import chalk3 from "chalk";
626
+ import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
627
+
628
+ // src/tools/types.ts
629
+ function isFileWriteTool(name) {
630
+ return name === "write_file" || name === "edit_file";
631
+ }
632
+ function getDangerLevel(toolName, args) {
633
+ if (toolName.startsWith("mcp__")) return "safe";
634
+ if (toolName === "bash") {
635
+ const cmd = String(args["command"] ?? "");
636
+ if (/\brm\s+[^\n]*(?:-\w*[rRfF]\w*|--recursive|--force)\b/.test(cmd)) return "destructive";
637
+ if (/\brm\s+\S/.test(cmd)) return "destructive";
638
+ if (/\brmdir\b|\bformat\b|\bmkfs\b/.test(cmd)) return "destructive";
639
+ if (/\bRemove-Item\b.*(?:-Recurse|-Force)|\bri\s+.*-(?:Recurse|Force)\b|\brd\s+\/s\b|\brmdir\s+\/s\b/i.test(cmd)) return "destructive";
640
+ if (/\bdel\s+\S/.test(cmd)) return "destructive";
641
+ if (/\becho\b.*>>?|\btee\b|\bcp\b|\bmv\b/.test(cmd)) return "write";
642
+ if (/\bSet-Content\b|\bOut-File\b|\bAdd-Content\b|\bCopy-Item\b|\bMove-Item\b/i.test(cmd)) return "write";
643
+ return "safe";
644
+ }
645
+ if (toolName === "write_file") return "write";
646
+ if (toolName === "edit_file") return "write";
647
+ if (toolName === "save_last_response") return "write";
648
+ if (toolName === "run_interactive") {
649
+ const exe = String(args["executable"] ?? "").toLowerCase();
650
+ if (/\b(rm|rmdir|del|format|mkfs|Remove-Item)\b/i.test(exe)) return "destructive";
651
+ if (/\b(bash|sh|zsh|cmd|powershell|pwsh|python|node|ruby|perl)\b/i.test(exe)) return "write";
652
+ return "write";
653
+ }
654
+ if (toolName === "task_create" || toolName === "task_stop") return "write";
655
+ if (toolName === "task_list") return "safe";
656
+ if (toolName === "read_file" || toolName === "list_dir" || toolName === "grep_files" || toolName === "glob_files" || toolName === "web_fetch" || toolName === "save_memory" || toolName === "ask_user" || toolName === "write_todos" || toolName === "google_search" || toolName === "spawn_agent" || toolName === "run_tests") return "safe";
657
+ return "write";
658
+ }
659
+ function schemaToJsonSchema(schema) {
660
+ const result = {
661
+ type: schema.type,
662
+ description: schema.description
663
+ };
664
+ if (schema.enum) result["enum"] = schema.enum;
665
+ if (schema.items) result["items"] = schemaToJsonSchema(schema.items);
666
+ if (schema.properties) {
667
+ result["properties"] = Object.fromEntries(
668
+ Object.entries(schema.properties).map(([k, v]) => [k, schemaToJsonSchema(v)])
669
+ );
670
+ }
671
+ return result;
672
+ }
673
+
674
+ // src/tools/diff-utils.ts
675
+ import chalk from "chalk";
676
+ function renderDiff(oldText, newText, opts = {}) {
677
+ const contextLines = opts.contextLines ?? 3;
678
+ const maxLines = opts.maxLines ?? 120;
679
+ const filePath = opts.filePath ?? "";
680
+ const oldLines = oldText.split("\n");
681
+ const newLines = newText.split("\n");
682
+ const hunks = computeHunks(oldLines, newLines, contextLines);
683
+ if (hunks.length === 0) {
684
+ return chalk.dim(" (no changes)");
685
+ }
686
+ const output = [];
687
+ if (filePath) {
688
+ output.push(chalk.bold.white(`--- ${filePath} (before)`));
689
+ output.push(chalk.bold.white(`+++ ${filePath} (after)`));
690
+ }
691
+ let totalDisplayed = 0;
692
+ for (const hunk of hunks) {
693
+ if (totalDisplayed >= maxLines) {
694
+ output.push(chalk.dim(` ... (diff truncated, too many changes)`));
695
+ break;
696
+ }
697
+ output.push(
698
+ chalk.cyan(
699
+ `@@ -${hunk.oldStart + 1},${hunk.oldCount} +${hunk.newStart + 1},${hunk.newCount} @@`
700
+ )
701
+ );
702
+ for (const line of hunk.lines) {
703
+ if (totalDisplayed >= maxLines) break;
704
+ totalDisplayed++;
705
+ if (line.type === "context") {
706
+ output.push(chalk.dim(` ${line.text}`));
707
+ } else if (line.type === "remove") {
708
+ output.push(chalk.red(`- ${line.text}`));
709
+ } else {
710
+ output.push(chalk.green(`+ ${line.text}`));
711
+ }
712
+ }
713
+ }
714
+ return output.join("\n");
715
+ }
716
+ function computeHunks(oldLines, newLines, contextLines) {
717
+ const edits = diffLines(oldLines, newLines);
718
+ if (edits.every((e) => e.type === "context")) return [];
719
+ const hunks = [];
720
+ let i = 0;
721
+ while (i < edits.length) {
722
+ if (edits[i].type === "context") {
723
+ i++;
724
+ continue;
725
+ }
726
+ const start = Math.max(0, i - contextLines);
727
+ let end = i;
728
+ while (end < edits.length) {
729
+ if (edits[end].type !== "context") {
730
+ end++;
731
+ } else {
732
+ let hasMoreChange = false;
733
+ for (let j = end + 1; j < Math.min(edits.length, end + contextLines * 2 + 1); j++) {
734
+ if (edits[j].type !== "context") {
735
+ hasMoreChange = true;
736
+ break;
737
+ }
738
+ }
739
+ if (hasMoreChange) {
740
+ end++;
741
+ } else {
742
+ break;
743
+ }
744
+ }
745
+ }
746
+ end = Math.min(edits.length, end + contextLines);
747
+ const hunkEdits = edits.slice(start, end);
748
+ let oldStart = 0;
749
+ let newStart = 0;
750
+ for (let k = 0; k < start; k++) {
751
+ if (edits[k].type !== "add") oldStart++;
752
+ if (edits[k].type !== "remove") newStart++;
753
+ }
754
+ let oldCount = 0;
755
+ let newCount = 0;
756
+ for (const e of hunkEdits) {
757
+ if (e.type !== "add") oldCount++;
758
+ if (e.type !== "remove") newCount++;
759
+ }
760
+ hunks.push({
761
+ oldStart,
762
+ oldCount,
763
+ newStart,
764
+ newCount,
765
+ lines: hunkEdits.map((e) => ({ type: e.type, text: e.text }))
766
+ });
767
+ i = end;
768
+ }
769
+ return hunks;
770
+ }
771
+ function diffLines(oldLines, newLines) {
772
+ const n = oldLines.length;
773
+ const m = newLines.length;
774
+ if (n * m > 25e4) {
775
+ return simpleDiff(oldLines, newLines);
776
+ }
777
+ const dp = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
778
+ for (let i2 = 1; i2 <= n; i2++) {
779
+ for (let j2 = 1; j2 <= m; j2++) {
780
+ if (oldLines[i2 - 1] === newLines[j2 - 1]) {
781
+ dp[i2][j2] = dp[i2 - 1][j2 - 1] + 1;
782
+ } else {
783
+ dp[i2][j2] = Math.max(dp[i2 - 1][j2], dp[i2][j2 - 1]);
784
+ }
785
+ }
786
+ }
787
+ const result = [];
788
+ let i = n;
789
+ let j = m;
790
+ while (i > 0 || j > 0) {
791
+ if (i > 0 && j > 0 && oldLines[i - 1] === newLines[j - 1]) {
792
+ result.unshift({ type: "context", text: oldLines[i - 1] });
793
+ i--;
794
+ j--;
795
+ } else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) {
796
+ result.unshift({ type: "add", text: newLines[j - 1] });
797
+ j--;
798
+ } else {
799
+ result.unshift({ type: "remove", text: oldLines[i - 1] });
800
+ i--;
801
+ }
802
+ }
803
+ return result;
804
+ }
805
+ function simpleDiff(oldLines, newLines) {
806
+ const result = [];
807
+ const maxLen = Math.max(oldLines.length, newLines.length);
808
+ for (let i = 0; i < maxLen; i++) {
809
+ const o = oldLines[i];
810
+ const n = newLines[i];
811
+ if (o !== void 0 && n !== void 0) {
812
+ if (o === n) {
813
+ result.push({ type: "context", text: o });
814
+ } else {
815
+ result.push({ type: "remove", text: o });
816
+ result.push({ type: "add", text: n });
817
+ }
818
+ } else if (o !== void 0) {
819
+ result.push({ type: "remove", text: o });
820
+ } else if (n !== void 0) {
821
+ result.push({ type: "add", text: n });
822
+ }
823
+ }
824
+ return result;
825
+ }
826
+
827
+ // src/tools/hooks.ts
828
+ import { execSync as execSync3 } from "child_process";
829
+ function shellEscape(value) {
830
+ return "'" + value.replace(/'/g, "'\\''") + "'";
831
+ }
832
+ function runHook(template, vars) {
833
+ if (!template) return;
834
+ let cmd = template;
835
+ cmd = cmd.replace(/\{tool\}/g, shellEscape(vars.tool));
836
+ cmd = cmd.replace(/\{dangerLevel\}/g, shellEscape(vars.dangerLevel ?? ""));
837
+ cmd = cmd.replace(/\{args\}/g, shellEscape(vars.args ?? ""));
838
+ cmd = cmd.replace(/\{status\}/g, shellEscape(vars.status ?? ""));
839
+ try {
840
+ execSync3(cmd, {
841
+ timeout: 5e3,
842
+ stdio: ["pipe", "pipe", "pipe"],
843
+ encoding: "utf-8"
844
+ });
845
+ } catch {
846
+ process.stderr.write(`\u26A0 Hook failed: ${cmd.slice(0, 100)}
847
+ `);
848
+ }
849
+ }
850
+
851
+ // src/tools/permissions.ts
852
+ function checkPermission(toolName, args, dangerLevel, rules, defaultAction = "confirm") {
853
+ for (const rule of rules) {
854
+ if (rule.tool !== "*" && rule.tool !== toolName) continue;
855
+ if (rule.when) {
856
+ if (rule.when.dangerLevel && rule.when.dangerLevel !== dangerLevel) continue;
857
+ if (rule.when.pathPattern) {
858
+ const path = String(args["path"] ?? args["command"] ?? "");
859
+ if (!path.includes(rule.when.pathPattern)) continue;
860
+ }
861
+ }
862
+ return rule.action;
863
+ }
864
+ return defaultAction;
865
+ }
866
+
867
+ // src/tools/truncate.ts
868
+ var DEFAULT_MAX_TOOL_OUTPUT_CHARS = 12e3;
869
+ function getMaxOutputChars(contextWindow) {
870
+ if (!contextWindow || contextWindow <= 0) return DEFAULT_MAX_TOOL_OUTPUT_CHARS;
871
+ return Math.max(DEFAULT_MAX_TOOL_OUTPUT_CHARS, Math.min(Math.floor(contextWindow / 4), 12e4));
872
+ }
873
+ var activeMaxChars = DEFAULT_MAX_TOOL_OUTPUT_CHARS;
874
+ function setContextWindow(contextWindow) {
875
+ activeMaxChars = getMaxOutputChars(contextWindow);
876
+ }
877
+ function getActiveMaxChars() {
878
+ return activeMaxChars;
879
+ }
880
+ function truncateOutput(content, toolName, maxChars) {
881
+ const limit = maxChars ?? activeMaxChars;
882
+ if (content.length <= limit) return content;
883
+ const keepHead = Math.floor(limit * 0.7);
884
+ const keepTail = Math.floor(limit * 0.2);
885
+ const omitted = content.length - keepHead - keepTail;
886
+ const lines = content.split("\n").length;
887
+ const head = content.slice(0, keepHead);
888
+ const tail = content.slice(content.length - keepTail);
889
+ return head + `
890
+
891
+ ... [Output truncated: ${content.length} chars / ${lines} lines total, ${omitted} chars omitted. Use read_file to view full content in segments` + (toolName === "bash" ? ", or narrow the command scope" : "") + `] ...
892
+
893
+ ` + tail;
894
+ }
895
+
896
+ // src/repl/theme.ts
897
+ import chalk2 from "chalk";
898
+ var DARK_THEME = {
899
+ prompt: chalk2.green,
900
+ info: chalk2.cyan,
901
+ warning: chalk2.yellow,
902
+ error: chalk2.red,
903
+ success: chalk2.green,
904
+ dim: chalk2.dim,
905
+ accent: chalk2.cyan,
906
+ toolCall: chalk2.yellow,
907
+ toolResult: chalk2.green,
908
+ heading: chalk2.bold.cyan
909
+ };
910
+ var LIGHT_THEME = {
911
+ prompt: chalk2.blue,
912
+ info: chalk2.blueBright,
913
+ warning: chalk2.yellow,
914
+ error: chalk2.red,
915
+ success: chalk2.green,
916
+ dim: chalk2.gray,
917
+ accent: chalk2.blueBright,
918
+ toolCall: chalk2.magenta,
919
+ toolResult: chalk2.green,
920
+ heading: chalk2.bold.blue
921
+ };
922
+ function resolveColor(name) {
923
+ if (name.startsWith("#")) return chalk2.hex(name);
924
+ const parts = name.split(".");
925
+ let result = chalk2;
926
+ for (const part of parts) {
927
+ const obj = result;
928
+ if (obj && typeof obj[part] !== "undefined") {
929
+ result = obj[part];
930
+ }
931
+ }
932
+ if (typeof result !== "function") {
933
+ process.stderr.write(`[theme] Warning: unrecognized color "${name}", using default.
934
+ `);
935
+ return chalk2;
936
+ }
937
+ return result;
938
+ }
939
+ function buildCustomTheme(base, overrides) {
940
+ if (!overrides) return base;
941
+ const result = { ...base };
942
+ for (const [key, colorName] of Object.entries(overrides)) {
943
+ if (key in result && colorName) {
944
+ result[key] = resolveColor(colorName);
945
+ }
946
+ }
947
+ return result;
948
+ }
949
+ var _currentTheme = DARK_THEME;
950
+ function initTheme(themeId = "dark", customColors) {
951
+ switch (themeId) {
952
+ case "light":
953
+ _currentTheme = LIGHT_THEME;
954
+ break;
955
+ case "custom":
956
+ _currentTheme = buildCustomTheme(DARK_THEME, customColors);
957
+ break;
958
+ default:
959
+ _currentTheme = DARK_THEME;
960
+ }
961
+ }
962
+ var theme = new Proxy(DARK_THEME, {
963
+ get(_target, prop) {
964
+ return _currentTheme[prop];
965
+ }
966
+ });
967
+
968
+ // src/tools/executor.ts
969
+ var ToolExecutor = class {
970
+ constructor(registry) {
971
+ this.registry = registry;
972
+ }
973
+ /** 当前 session 消息索引,由 repl.ts / session-handler.ts 在每轮工具执行前设置 */
974
+ static currentMessageIndex = 0;
975
+ round = 0;
976
+ totalRounds = 0;
977
+ /** readline 接口引用,由 repl.ts 注入,用于 confirm() 读取用户输入 */
978
+ rl = null;
979
+ /**
980
+ * confirm() 进行中标志。
981
+ * repl.ts 的主循环 line handler 在此为 true 时忽略输入,
982
+ * 防止用户输入 "y"+Enter 被同时触发 once('line') 和主循环 on('line')。
983
+ */
984
+ confirming = false;
985
+ /** 在 confirm 期间用户输入的 slash 命令,由 repl.ts 主循环消费 */
986
+ pendingSlashCommand = null;
987
+ /** confirm() 的取消回调,由 SIGINT handler 调用 */
988
+ cancelConfirmFn = null;
989
+ /**
990
+ * 会话级 auto-approve:跳过所有 write/destructive 确认(仅当前会话有效)。
991
+ * 通过 /yolo 命令切换。destructive 操作仍会显示警告但不阻塞。
992
+ */
993
+ sessionAutoApprove = false;
994
+ /**
995
+ * 由外部(repl.ts SIGINT handler)调用,将当前 confirm() 等待视为用户按 N 取消。
996
+ * 若当前没有 confirm() 进行中,无操作。
997
+ */
998
+ cancelConfirm() {
999
+ if (this.cancelConfirmFn) {
1000
+ this.cancelConfirmFn();
1001
+ }
1002
+ }
1003
+ setRoundInfo(current, total) {
1004
+ this.round = current;
1005
+ this.totalRounds = total;
1006
+ }
1007
+ /**
1008
+ * 注入 readline 接口,供 confirm() 使用。
1009
+ * 必须在 start() 之前调用,rl 初始化后立即注入。
1010
+ */
1011
+ setReadline(rl) {
1012
+ this.rl = rl;
1013
+ }
1014
+ /** 钩子配置(可选) */
1015
+ hookConfig;
1016
+ /** 权限规则(可选) */
1017
+ permissionRules = [];
1018
+ defaultPermission = "confirm";
1019
+ /** 注入 hooks 和 permission rules 配置 */
1020
+ setConfig(opts) {
1021
+ this.hookConfig = opts.hookConfig;
1022
+ if (opts.permissionRules) this.permissionRules = opts.permissionRules;
1023
+ if (opts.defaultPermission) this.defaultPermission = opts.defaultPermission;
1024
+ }
1025
+ async execute(call) {
1026
+ const tool = this.registry.get(call.name);
1027
+ if (!tool) {
1028
+ return {
1029
+ callId: call.id,
1030
+ content: `Unknown tool: ${call.name}`,
1031
+ isError: true
1032
+ };
1033
+ }
1034
+ const dangerLevel = getDangerLevel(call.name, call.arguments);
1035
+ runHook(this.hookConfig?.preToolExecution, {
1036
+ tool: call.name,
1037
+ dangerLevel,
1038
+ args: JSON.stringify(call.arguments).slice(0, 200)
1039
+ });
1040
+ if (this.permissionRules.length > 0) {
1041
+ const action = checkPermission(call.name, call.arguments, dangerLevel, this.permissionRules, this.defaultPermission);
1042
+ if (action === "deny") {
1043
+ return { callId: call.id, content: `[Permission denied] Tool ${call.name} is blocked by permission rules. Do not retry.`, isError: true };
1044
+ }
1045
+ if (action === "auto-approve") {
1046
+ this.printToolCall(call);
1047
+ try {
1048
+ const rawContent = await tool.execute(call.arguments);
1049
+ const content = truncateOutput(rawContent, call.name);
1050
+ const wasTruncated = content !== rawContent;
1051
+ this.printToolResult(call.name, rawContent, false, wasTruncated);
1052
+ runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "ok" });
1053
+ return { callId: call.id, content, isError: false };
1054
+ } catch (err) {
1055
+ const message = err instanceof Error ? err.message : String(err);
1056
+ this.printToolResult(call.name, message, true, false);
1057
+ runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "error" });
1058
+ return { callId: call.id, content: message, isError: true };
1059
+ }
1060
+ }
1061
+ }
1062
+ if (this.sessionAutoApprove && dangerLevel !== "safe") {
1063
+ this.printToolCall(call);
1064
+ if (dangerLevel === "write") this.printDiffPreview(call);
1065
+ console.log(theme.warning(" \u26A1 Auto-approved (session /yolo mode)"));
1066
+ try {
1067
+ const rawContent = await tool.execute(call.arguments);
1068
+ const content = truncateOutput(rawContent, call.name);
1069
+ const wasTruncated = content !== rawContent;
1070
+ this.printToolResult(call.name, rawContent, false, wasTruncated);
1071
+ runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "ok" });
1072
+ return { callId: call.id, content, isError: false };
1073
+ } catch (err) {
1074
+ const message = err instanceof Error ? err.message : String(err);
1075
+ this.printToolResult(call.name, message, true, false);
1076
+ runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "error" });
1077
+ return { callId: call.id, content: message, isError: true };
1078
+ }
1079
+ }
1080
+ if (dangerLevel === "write") {
1081
+ this.printToolCall(call);
1082
+ this.printDiffPreview(call);
1083
+ const confirmed = await this.confirm(call, dangerLevel);
1084
+ if (!confirmed) {
1085
+ return {
1086
+ callId: call.id,
1087
+ content: `[User cancelled] The user declined the ${call.name} operation. Do not retry without asking.`,
1088
+ isError: true
1089
+ };
1090
+ }
1091
+ } else if (dangerLevel === "destructive") {
1092
+ const confirmed = await this.confirm(call, dangerLevel);
1093
+ if (!confirmed) {
1094
+ return {
1095
+ callId: call.id,
1096
+ content: `[User cancelled] The user declined the destructive ${call.name} operation. Do not retry without asking.`,
1097
+ isError: true
1098
+ };
1099
+ }
1100
+ this.printToolCall(call);
1101
+ } else {
1102
+ this.printToolCall(call);
1103
+ }
1104
+ try {
1105
+ const rawContent = await tool.execute(call.arguments);
1106
+ const content = truncateOutput(rawContent, call.name);
1107
+ const wasTruncated = content !== rawContent;
1108
+ this.printToolResult(call.name, rawContent, false, wasTruncated);
1109
+ runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "ok" });
1110
+ return { callId: call.id, content, isError: false };
1111
+ } catch (err) {
1112
+ const message = err instanceof Error ? err.message : String(err);
1113
+ this.printToolResult(call.name, message, true, false);
1114
+ runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "error" });
1115
+ return { callId: call.id, content: message, isError: true };
1116
+ }
1117
+ }
1118
+ async executeAll(calls) {
1119
+ const safeCalls = [];
1120
+ const fileWriteCalls = [];
1121
+ const otherCalls = [];
1122
+ for (let i = 0; i < calls.length; i++) {
1123
+ const call = calls[i];
1124
+ const level = getDangerLevel(call.name, call.arguments);
1125
+ if (level === "safe") {
1126
+ safeCalls.push({ idx: i, call });
1127
+ } else if (isFileWriteTool(call.name) && level === "write") {
1128
+ fileWriteCalls.push({ idx: i, call });
1129
+ } else {
1130
+ otherCalls.push({ idx: i, call });
1131
+ }
1132
+ }
1133
+ const results = new Array(calls.length);
1134
+ await Promise.all(
1135
+ safeCalls.map(async ({ idx, call }) => {
1136
+ results[idx] = await this.execute(call);
1137
+ })
1138
+ );
1139
+ if (fileWriteCalls.length === 1) {
1140
+ const { idx, call } = fileWriteCalls[0];
1141
+ results[idx] = await this.execute(call);
1142
+ } else if (fileWriteCalls.length >= 2) {
1143
+ const batchResults = await this.executeBatchFileWrites(fileWriteCalls.map((f) => f.call));
1144
+ for (let i = 0; i < fileWriteCalls.length; i++) {
1145
+ results[fileWriteCalls[i].idx] = batchResults[i];
1146
+ }
1147
+ }
1148
+ for (const { idx, call } of otherCalls) {
1149
+ results[idx] = await this.execute(call);
1150
+ }
1151
+ return results;
1152
+ }
1153
+ /**
1154
+ * 批量文件写入:展示所有文件的编号列表 + diff 预览,
1155
+ * 然后让用户 approve all / reject all / 选择性 approve。
1156
+ */
1157
+ async executeBatchFileWrites(calls) {
1158
+ console.log();
1159
+ console.log(theme.heading(`\u270E Batch file writes (${calls.length} files):`));
1160
+ console.log(theme.dim("\u2500".repeat(50)));
1161
+ for (let i = 0; i < calls.length; i++) {
1162
+ const call = calls[i];
1163
+ const filePath = String(call.arguments["path"] ?? "");
1164
+ console.log(theme.warning(` [${i + 1}] `) + chalk3.white(call.name) + theme.dim(": ") + theme.accent(filePath));
1165
+ this.printDiffPreview(call);
1166
+ }
1167
+ console.log(theme.dim("\u2500".repeat(50)));
1168
+ const decision = this.sessionAutoApprove ? "all" : await this.batchConfirm(calls.length);
1169
+ if (this.sessionAutoApprove) {
1170
+ console.log(theme.warning(" \u26A1 All auto-approved (session /yolo mode)"));
1171
+ }
1172
+ const results = [];
1173
+ for (let i = 0; i < calls.length; i++) {
1174
+ const call = calls[i];
1175
+ const approved = decision === "all" || decision !== "none" && decision.has(i + 1);
1176
+ if (approved) {
1177
+ const tool = this.registry.get(call.name);
1178
+ if (!tool) {
1179
+ results.push({ callId: call.id, content: `Unknown tool: ${call.name}`, isError: true });
1180
+ continue;
1181
+ }
1182
+ try {
1183
+ const rawContent = await tool.execute(call.arguments);
1184
+ const content = truncateOutput(rawContent, call.name);
1185
+ const wasTruncated = content !== rawContent;
1186
+ this.printToolResult(call.name, rawContent, false, wasTruncated);
1187
+ results.push({ callId: call.id, content, isError: false });
1188
+ } catch (err) {
1189
+ const message = err instanceof Error ? err.message : String(err);
1190
+ this.printToolResult(call.name, message, true, false);
1191
+ results.push({ callId: call.id, content: message, isError: true });
1192
+ }
1193
+ } else {
1194
+ console.log(theme.dim(` [${i + 1}] `) + theme.dim("rejected"));
1195
+ results.push({ callId: call.id, content: `[User rejected] The user rejected this ${call.name} operation. Do not retry without asking.`, isError: true });
1196
+ }
1197
+ }
1198
+ return results;
1199
+ }
1200
+ /**
1201
+ * 批量确认:让用户选择 approve all / reject all / 指定编号。
1202
+ * 返回 'all' | 'none' | Set<number>(1-based 编号)
1203
+ */
1204
+ batchConfirm(count) {
1205
+ const prompt = theme.warning(` [a]pprove all [r]eject all [1,${count > 1 ? count : "2"},..] approve specific: `);
1206
+ if (!this.rl) {
1207
+ process.stdout.write(theme.warning("No readline: auto-rejected.\n"));
1208
+ return Promise.resolve("none");
1209
+ }
1210
+ const rl = this.rl;
1211
+ const rlAny = rl;
1212
+ const savedOutput = rlAny.output;
1213
+ rlAny.output = process.stdout;
1214
+ rl.resume();
1215
+ process.stdout.write(prompt);
1216
+ this.confirming = true;
1217
+ return new Promise((resolve4) => {
1218
+ let completed = false;
1219
+ const cleanup = (result) => {
1220
+ if (completed) return;
1221
+ completed = true;
1222
+ rl.removeListener("line", onLine);
1223
+ this.cancelConfirmFn = null;
1224
+ rl.pause();
1225
+ rlAny.output = savedOutput;
1226
+ this.confirming = false;
1227
+ resolve4(result);
1228
+ };
1229
+ const onLine = (line) => {
1230
+ const trimmed = line.trim();
1231
+ if (trimmed.startsWith("/")) {
1232
+ this.pendingSlashCommand = trimmed;
1233
+ process.stdout.write(theme.dim(`
1234
+ (command "${trimmed}" queued, will execute after current operation)
1235
+ `));
1236
+ cleanup("none");
1237
+ return;
1238
+ }
1239
+ const input = trimmed.toLowerCase();
1240
+ if (input === "a" || input === "all" || input === "y") {
1241
+ cleanup("all");
1242
+ } else if (input === "r" || input === "reject" || input === "n" || input === "") {
1243
+ cleanup("none");
1244
+ } else {
1245
+ const nums = input.split(/[,\s]+/).map(Number).filter((n) => !isNaN(n) && n >= 1 && n <= count);
1246
+ if (nums.length > 0) {
1247
+ cleanup(new Set(nums));
1248
+ } else {
1249
+ cleanup("none");
1250
+ }
1251
+ }
1252
+ };
1253
+ this.cancelConfirmFn = () => {
1254
+ process.stdout.write(theme.dim("\n(cancelled)\n"));
1255
+ cleanup("none");
1256
+ };
1257
+ try {
1258
+ rl.once("line", onLine);
1259
+ } catch {
1260
+ cleanup("none");
1261
+ }
1262
+ });
1263
+ }
1264
+ printToolCall(call) {
1265
+ const dangerLevel = getDangerLevel(call.name, call.arguments);
1266
+ console.log();
1267
+ const icon = dangerLevel === "write" ? theme.toolCall("\u270E Tool: ") : theme.heading(theme.accent("\u2699 Tool: "));
1268
+ const roundBadge = this.totalRounds > 0 ? theme.dim(` [${this.round}/${this.totalRounds}]`) : "";
1269
+ console.log(icon + chalk3.white(call.name) + roundBadge);
1270
+ for (const [key, val] of Object.entries(call.arguments)) {
1271
+ let valStr;
1272
+ if (Array.isArray(val)) {
1273
+ const json = JSON.stringify(val);
1274
+ valStr = json.length > 160 ? json.slice(0, 160) + "..." : json;
1275
+ } else if (typeof val === "string" && val.length > 120) {
1276
+ valStr = val.slice(0, 120) + "...";
1277
+ } else {
1278
+ valStr = String(val);
1279
+ }
1280
+ console.log(theme.dim(` ${key}: `) + chalk3.white(valStr));
603
1281
  }
604
- if (isBinaryBuffer(buf)) {
605
- return `[Binary file: ${filePath}]
606
- This file contains binary data and cannot be read as text.
607
- If needed, use the bash tool to run an appropriate conversion program.`;
1282
+ }
1283
+ /**
1284
+ * write_file / edit_file 在执行前展示 diff 预览。
1285
+ * - write_file:比较旧文件内容与新内容
1286
+ * - edit_file (replace):比较旧字符串与新字符串
1287
+ * - edit_file (insert/delete):显示操作摘要,不做 diff(变化明确)
1288
+ */
1289
+ printDiffPreview(call) {
1290
+ if (call.name === "write_file") {
1291
+ const filePath = String(call.arguments["path"] ?? "");
1292
+ const newContent = String(call.arguments["content"] ?? "");
1293
+ if (!filePath) return;
1294
+ if (existsSync4(filePath)) {
1295
+ let oldContent;
1296
+ try {
1297
+ oldContent = readFileSync3(filePath, "utf-8");
1298
+ } catch {
1299
+ return;
1300
+ }
1301
+ if (oldContent === newContent) {
1302
+ console.log(theme.dim(" (file content unchanged)"));
1303
+ return;
1304
+ }
1305
+ const diff = renderDiff(oldContent, newContent, { filePath, contextLines: 3 });
1306
+ console.log(theme.dim(" \u2500\u2500 diff preview \u2500\u2500"));
1307
+ console.log(diff);
1308
+ console.log();
1309
+ } else {
1310
+ const lines = newContent.split("\n");
1311
+ const preview = lines.slice(0, 20).map((l) => theme.success(`+ ${l}`)).join("\n");
1312
+ const more = lines.length > 20 ? theme.dim(`
1313
+ ... (+${lines.length - 20} more lines)`) : "";
1314
+ console.log(theme.dim(" \u2500\u2500 new file preview \u2500\u2500"));
1315
+ console.log(preview + more);
1316
+ console.log();
1317
+ }
1318
+ } else if (call.name === "edit_file") {
1319
+ const filePath = String(call.arguments["path"] ?? "");
1320
+ if (!filePath || !existsSync4(filePath)) return;
1321
+ const oldStr = call.arguments["old_str"];
1322
+ const newStr = call.arguments["new_str"];
1323
+ if (oldStr !== void 0) {
1324
+ const diff = renderDiff(
1325
+ String(oldStr),
1326
+ String(newStr ?? ""),
1327
+ { filePath, contextLines: 2 }
1328
+ );
1329
+ console.log(theme.dim(" \u2500\u2500 diff preview \u2500\u2500"));
1330
+ console.log(diff);
1331
+ console.log();
1332
+ } else if (call.arguments["insert_after_line"] !== void 0) {
1333
+ const line = Number(call.arguments["insert_after_line"]);
1334
+ const insertContent = String(call.arguments["insert_content"] ?? "");
1335
+ const insertLines = insertContent.split("\n");
1336
+ const preview = insertLines.slice(0, 5).map((l) => theme.success(`+ ${l}`)).join("\n");
1337
+ const more = insertLines.length > 5 ? theme.dim(`
1338
+ ... (+${insertLines.length - 5} more lines)`) : "";
1339
+ console.log(theme.dim(` \u2500\u2500 insert after line ${line} \u2500\u2500`));
1340
+ console.log(preview + more);
1341
+ console.log();
1342
+ } else if (call.arguments["delete_from_line"] !== void 0) {
1343
+ const from = Number(call.arguments["delete_from_line"]);
1344
+ const to = Number(call.arguments["delete_to_line"] ?? from);
1345
+ let fileContent;
1346
+ try {
1347
+ fileContent = readFileSync3(filePath, "utf-8");
1348
+ } catch {
1349
+ return;
1350
+ }
1351
+ const fileLines = fileContent.split("\n");
1352
+ const deleted = fileLines.slice(from - 1, to);
1353
+ const preview = deleted.slice(0, 5).map((l) => theme.error(`- ${l}`)).join("\n");
1354
+ const more = deleted.length > 5 ? theme.dim(`
1355
+ ... (-${deleted.length - 5} more lines)`) : "";
1356
+ console.log(theme.dim(` \u2500\u2500 delete lines ${from}\u2013${to} \u2500\u2500`));
1357
+ console.log(preview + more);
1358
+ console.log();
1359
+ }
608
1360
  }
609
- const content = buf.toString(encoding);
610
- const lines = content.split("\n").length;
611
- return `${sensitiveWarning}[File: ${filePath} | ${lines} lines]
612
-
613
- ${content}`;
1361
+ }
1362
+ printToolResult(name, content, isError, wasTruncated) {
1363
+ if (isError) {
1364
+ console.log(theme.error(`\u26A0 ${name} error: `) + theme.dim(content.slice(0, 300)));
1365
+ } else {
1366
+ const lines = content.split("\n");
1367
+ const maxLines = name === "run_interactive" ? 40 : 8;
1368
+ const preview = lines.slice(0, maxLines).join("\n");
1369
+ const moreLines = lines.length > maxLines ? theme.dim(`
1370
+ ... (${lines.length - maxLines} more lines)`) : "";
1371
+ const truncatedNote = wasTruncated ? theme.warning(`
1372
+ \u26A1 Output truncated to ${getActiveMaxChars()} chars before sending to AI`) : "";
1373
+ console.log(theme.toolResult("\u2713 Result: ") + theme.dim(preview) + moreLines + truncatedNote);
1374
+ }
1375
+ console.log();
1376
+ }
1377
+ confirm(call, level) {
1378
+ const color = level === "destructive" ? theme.error : theme.warning;
1379
+ const label = level === "destructive" ? "\u26A0 DESTRUCTIVE" : "\u270E Write";
1380
+ console.log();
1381
+ console.log(color(`${label} operation: `) + theme.heading(call.name));
1382
+ for (const [key, val] of Object.entries(call.arguments)) {
1383
+ const valStr = typeof val === "string" && val.length > 200 ? val.slice(0, 200) + "..." : String(val);
1384
+ console.log(theme.dim(` ${key}: `) + valStr);
1385
+ }
1386
+ if (!this.rl) {
1387
+ process.stdout.write(theme.warning("No readline: auto-rejected.\n"));
1388
+ return Promise.resolve(false);
1389
+ }
1390
+ const rl = this.rl;
1391
+ const rlAny = rl;
1392
+ const savedOutput = rlAny.output;
1393
+ rlAny.output = process.stdout;
1394
+ rl.resume();
1395
+ process.stdout.write(color("Proceed? [y/N] (type y + Enter to confirm) "));
1396
+ this.confirming = true;
1397
+ return new Promise((resolve4) => {
1398
+ let completed = false;
1399
+ const cleanup = (answer) => {
1400
+ if (completed) return;
1401
+ completed = true;
1402
+ rl.removeListener("line", onLine);
1403
+ this.cancelConfirmFn = null;
1404
+ rl.pause();
1405
+ rlAny.output = savedOutput;
1406
+ this.confirming = false;
1407
+ resolve4(answer === "y");
1408
+ };
1409
+ const onLine = (line) => {
1410
+ const trimmed = line.trim();
1411
+ if (trimmed.startsWith("/")) {
1412
+ this.pendingSlashCommand = trimmed;
1413
+ process.stdout.write(theme.dim(`
1414
+ (command "${trimmed}" queued, will execute after current operation)
1415
+ `));
1416
+ cleanup("n");
1417
+ return;
1418
+ }
1419
+ cleanup(trimmed.toLowerCase());
1420
+ };
1421
+ this.cancelConfirmFn = () => {
1422
+ process.stdout.write(theme.dim("\n(cancelled)\n"));
1423
+ cleanup("n");
1424
+ };
1425
+ try {
1426
+ rl.once("line", onLine);
1427
+ } catch {
1428
+ cleanup("n");
1429
+ }
1430
+ });
614
1431
  }
615
1432
  };
616
1433
 
617
1434
  // src/tools/builtin/write-file.ts
618
- import { writeFileSync as writeFileSync2, appendFileSync, mkdirSync } from "fs";
619
- import { dirname as dirname2 } from "path";
620
1435
  var writeFileTool = {
621
1436
  definition: {
622
1437
  name: "write_file",
@@ -654,6 +1469,7 @@ Important: For long content (over 500 lines or 3000 chars), you MUST split into
654
1469
  const appendMode = String(args["append"] ?? "false").toLowerCase() === "true";
655
1470
  if (!filePath) throw new ToolError("write_file", "path is required");
656
1471
  undoStack.push(filePath, `write_file${appendMode ? " (append)" : ""}: ${filePath}`);
1472
+ fileCheckpoints.snapshot(filePath, ToolExecutor.currentMessageIndex);
657
1473
  mkdirSync(dirname2(filePath), { recursive: true });
658
1474
  if (appendMode) {
659
1475
  appendFileSync(filePath, content, encoding);
@@ -667,7 +1483,7 @@ Important: For long content (over 500 lines or 3000 chars), you MUST split into
667
1483
  };
668
1484
 
669
1485
  // src/tools/builtin/edit-file.ts
670
- import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, existsSync as existsSync4 } from "fs";
1486
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, existsSync as existsSync5 } from "fs";
671
1487
  function similarityScore(a, b) {
672
1488
  if (a === b) return 1;
673
1489
  if (a.length < 2 || b.length < 2) return 0;
@@ -793,8 +1609,8 @@ Note: Path can be absolute or relative to the current working directory.`,
793
1609
  const filePath = String(args["path"] ?? "");
794
1610
  const encoding = args["encoding"] ?? "utf-8";
795
1611
  if (!filePath) throw new ToolError("edit_file", "path is required");
796
- if (!existsSync4(filePath)) throw new ToolError("edit_file", `File not found: ${filePath}`);
797
- const original = readFileSync3(filePath, encoding);
1612
+ if (!existsSync5(filePath)) throw new ToolError("edit_file", `File not found: ${filePath}`);
1613
+ const original = readFileSync4(filePath, encoding);
798
1614
  if (args["old_str"] !== void 0) {
799
1615
  const oldStr = String(args["old_str"]);
800
1616
  const newStr = String(args["new_str"] ?? "");
@@ -818,6 +1634,7 @@ Please read the file first and use exact text.`;
818
1634
  return `ERROR: old_str matches multiple locations with whitespace-tolerant matching. Please include more surrounding context to make it unique.`;
819
1635
  }
820
1636
  undoStack.push(filePath, `edit_file (ws-replace): ${filePath}`);
1637
+ fileCheckpoints.snapshot(filePath, ToolExecutor.currentMessageIndex);
821
1638
  const before = fileLines.slice(0, matchStart);
822
1639
  const after = fileLines.slice(matchStart + searchLines.length);
823
1640
  const updated2 = [...before, newStr, ...after].join("\n");
@@ -839,6 +1656,7 @@ ${similar.join("\n")}` : "";
839
1656
  Please read the file first and use exact text.`;
840
1657
  }
841
1658
  undoStack.push(filePath, `edit_file (replace_all): ${filePath}`);
1659
+ fileCheckpoints.snapshot(filePath, ToolExecutor.currentMessageIndex);
842
1660
  const updated2 = original.split(oldStr).join(newStr);
843
1661
  writeFileSync3(filePath, updated2, encoding);
844
1662
  return `Successfully edited ${filePath} (replace_all)
@@ -862,6 +1680,7 @@ Tip: You can also try ignore_whitespace: true to match ignoring indentation diff
862
1680
  return `ERROR: old_str appears multiple times in file (at least at positions ${firstIndex} and ${secondIndex}). Please include more surrounding context to make it unique.`;
863
1681
  }
864
1682
  undoStack.push(filePath, `edit_file (replace): ${filePath}`);
1683
+ fileCheckpoints.snapshot(filePath, ToolExecutor.currentMessageIndex);
865
1684
  const updated = original.slice(0, firstIndex) + newStr + original.slice(firstIndex + oldStr.length);
866
1685
  writeFileSync3(filePath, updated, encoding);
867
1686
  const oldLines = oldStr.split("\n").length;
@@ -881,6 +1700,7 @@ Tip: You can also try ignore_whitespace: true to match ignoring indentation diff
881
1700
  throw new ToolError("edit_file", `insert_after_line ${afterLine} is out of range (file has ${lines.length} lines)`);
882
1701
  }
883
1702
  undoStack.push(filePath, `edit_file (insert): ${filePath}`);
1703
+ fileCheckpoints.snapshot(filePath, ToolExecutor.currentMessageIndex);
884
1704
  lines.splice(afterLine, 0, content);
885
1705
  writeFileSync3(filePath, lines.join("\n"), encoding);
886
1706
  return `Successfully inserted ${content.split("\n").length} line(s) after line ${afterLine} in ${filePath}`;
@@ -896,6 +1716,7 @@ Tip: You can also try ignore_whitespace: true to match ignoring indentation diff
896
1716
  );
897
1717
  }
898
1718
  undoStack.push(filePath, `edit_file (delete): ${filePath}`);
1719
+ fileCheckpoints.snapshot(filePath, ToolExecutor.currentMessageIndex);
899
1720
  const deleted = lines.splice(fromLine - 1, toLine - fromLine + 1);
900
1721
  writeFileSync3(filePath, lines.join("\n"), encoding);
901
1722
  return `Successfully deleted lines ${fromLine}-${toLine} (${deleted.length} lines) from ${filePath}`;
@@ -913,7 +1734,7 @@ function truncatePreview(str, maxLen = 80) {
913
1734
  }
914
1735
 
915
1736
  // src/tools/builtin/list-dir.ts
916
- import { readdirSync as readdirSync3, statSync as statSync3, existsSync as existsSync5 } from "fs";
1737
+ import { readdirSync as readdirSync3, statSync as statSync3, existsSync as existsSync6 } from "fs";
917
1738
  import { join, basename as basename2 } from "path";
918
1739
  var listDirTool = {
919
1740
  definition: {
@@ -936,7 +1757,7 @@ var listDirTool = {
936
1757
  async execute(args) {
937
1758
  const dirPath = String(args["path"] ?? process.cwd());
938
1759
  const recursive = Boolean(args["recursive"] ?? false);
939
- if (!existsSync5(dirPath)) {
1760
+ if (!existsSync6(dirPath)) {
940
1761
  const targetName = basename2(dirPath).toLowerCase();
941
1762
  const cwd = process.cwd();
942
1763
  const suggestions = [];
@@ -1014,7 +1835,7 @@ function formatSize(bytes) {
1014
1835
  }
1015
1836
 
1016
1837
  // src/tools/builtin/grep-files.ts
1017
- import { readdirSync as readdirSync4, readFileSync as readFileSync4, statSync as statSync4, existsSync as existsSync6 } from "fs";
1838
+ import { readdirSync as readdirSync4, readFileSync as readFileSync5, statSync as statSync4, existsSync as existsSync7 } from "fs";
1018
1839
  import { readFile } from "fs/promises";
1019
1840
  import { join as join2, relative } from "path";
1020
1841
  var grepFilesTool = {
@@ -1067,7 +1888,7 @@ Supports regex. Automatically skips node_modules, dist, .git directories.`,
1067
1888
  const contextLines = Math.max(0, Number(args["context_lines"] ?? 0));
1068
1889
  const maxResults = Math.max(1, Number(args["max_results"] ?? 50));
1069
1890
  if (!pattern) throw new ToolError("grep_files", "pattern is required");
1070
- if (!existsSync6(rootPath)) throw new ToolError("grep_files", `Path not found: ${rootPath}`);
1891
+ if (!existsSync7(rootPath)) throw new ToolError("grep_files", `Path not found: ${rootPath}`);
1071
1892
  const MAX_PATTERN_LENGTH = 1e3;
1072
1893
  if (pattern.length > MAX_PATTERN_LENGTH) {
1073
1894
  throw new ToolError("grep_files", `Pattern too long (${pattern.length} chars, max ${MAX_PATTERN_LENGTH}). Use a shorter pattern.`);
@@ -1220,7 +2041,7 @@ function searchInFile(fullPath, displayPath, regex, contextLines, maxResults, re
1220
2041
  }
1221
2042
  let content;
1222
2043
  try {
1223
- content = readFileSync4(fullPath, "utf-8");
2044
+ content = readFileSync5(fullPath, "utf-8");
1224
2045
  } catch {
1225
2046
  return;
1226
2047
  }
@@ -1251,7 +2072,7 @@ function searchInFile(fullPath, displayPath, regex, contextLines, maxResults, re
1251
2072
  }
1252
2073
 
1253
2074
  // src/tools/builtin/glob-files.ts
1254
- import { readdirSync as readdirSync5, statSync as statSync5, existsSync as existsSync7 } from "fs";
2075
+ import { readdirSync as readdirSync5, statSync as statSync5, existsSync as existsSync8 } from "fs";
1255
2076
  import { join as join3, relative as relative2, basename as basename3 } from "path";
1256
2077
  var globFilesTool = {
1257
2078
  definition: {
@@ -1285,7 +2106,7 @@ Results sorted by most recent modification time. Automatically skips node_module
1285
2106
  const rootPath = String(args["path"] ?? process.cwd());
1286
2107
  const maxResults = Math.max(1, Number(args["max_results"] ?? 100));
1287
2108
  if (!pattern) throw new ToolError("glob_files", "pattern is required");
1288
- if (!existsSync7(rootPath)) throw new ToolError("glob_files", `Path not found: ${rootPath}`);
2109
+ if (!existsSync8(rootPath)) throw new ToolError("glob_files", `Path not found: ${rootPath}`);
1289
2110
  const regex = globToRegex(pattern);
1290
2111
  const matches = [];
1291
2112
  collectMatchingFiles(rootPath, rootPath, regex, matches, maxResults);
@@ -1754,7 +2575,7 @@ var saveLastResponseTool = {
1754
2575
  };
1755
2576
 
1756
2577
  // src/tools/builtin/save-memory.ts
1757
- import { existsSync as existsSync8, statSync as statSync6, appendFileSync as appendFileSync2, mkdirSync as mkdirSync3 } from "fs";
2578
+ import { existsSync as existsSync9, statSync as statSync6, appendFileSync as appendFileSync2, mkdirSync as mkdirSync3 } from "fs";
1758
2579
  import { join as join4 } from "path";
1759
2580
  import { homedir as homedir2 } from "os";
1760
2581
  function getMemoryFilePath() {
@@ -1783,7 +2604,7 @@ var saveMemoryTool = {
1783
2604
  if (!content) throw new ToolError("save_memory", "content is required");
1784
2605
  const memoryPath = getMemoryFilePath();
1785
2606
  const configDir = join4(homedir2(), CONFIG_DIR_NAME);
1786
- if (!existsSync8(configDir)) {
2607
+ if (!existsSync9(configDir)) {
1787
2608
  mkdirSync3(configDir, { recursive: true });
1788
2609
  }
1789
2610
  const timestamp = formatTimestamp();
@@ -1798,7 +2619,7 @@ ${content}
1798
2619
  };
1799
2620
 
1800
2621
  // src/tools/builtin/ask-user.ts
1801
- import chalk from "chalk";
2622
+ import chalk4 from "chalk";
1802
2623
  var askUserContext = {
1803
2624
  prompting: false
1804
2625
  };
@@ -1835,8 +2656,8 @@ function promptUser(rl, question) {
1835
2656
  rl.resume();
1836
2657
  askUserContext.prompting = true;
1837
2658
  console.log();
1838
- console.log(chalk.cyan("\u2753 ") + chalk.bold(question));
1839
- process.stdout.write(chalk.cyan("> "));
2659
+ console.log(chalk4.cyan("\u2753 ") + chalk4.bold(question));
2660
+ process.stdout.write(chalk4.cyan("> "));
1840
2661
  return new Promise((resolve4) => {
1841
2662
  let completed = false;
1842
2663
  const cleanup = (answer) => {
@@ -1853,7 +2674,7 @@ function promptUser(rl, question) {
1853
2674
  cleanup(line);
1854
2675
  };
1855
2676
  askUserContext.cancelFn = () => {
1856
- process.stdout.write(chalk.gray("\n(cancelled)\n"));
2677
+ process.stdout.write(chalk4.gray("\n(cancelled)\n"));
1857
2678
  cleanup(null);
1858
2679
  };
1859
2680
  rl.once("line", onLine);
@@ -1861,7 +2682,7 @@ function promptUser(rl, question) {
1861
2682
  }
1862
2683
 
1863
2684
  // src/tools/builtin/write-todos.ts
1864
- import chalk2 from "chalk";
2685
+ import chalk5 from "chalk";
1865
2686
  var VALID_STATUSES = /* @__PURE__ */ new Set(["pending", "in_progress", "completed"]);
1866
2687
  var currentTodos = [];
1867
2688
  var writeTodosTool = {
@@ -1924,25 +2745,25 @@ function renderTodoList(todos) {
1924
2745
  const total = todos.length;
1925
2746
  console.log();
1926
2747
  console.log(
1927
- chalk2.bold.cyan("\u{1F4CB} Todo List") + chalk2.dim(` (${completed}/${total} completed)`)
2748
+ chalk5.bold.cyan("\u{1F4CB} Todo List") + chalk5.dim(` (${completed}/${total} completed)`)
1928
2749
  );
1929
- console.log(chalk2.dim(" " + "\u2500".repeat(40)));
2750
+ console.log(chalk5.dim(" " + "\u2500".repeat(40)));
1930
2751
  for (const todo of todos) {
1931
2752
  let icon;
1932
2753
  let text;
1933
2754
  switch (todo.status) {
1934
2755
  case "completed":
1935
- icon = chalk2.green(" \u2713 ");
1936
- text = chalk2.strikethrough.gray(todo.title);
2756
+ icon = chalk5.green(" \u2713 ");
2757
+ text = chalk5.strikethrough.gray(todo.title);
1937
2758
  break;
1938
2759
  case "in_progress":
1939
- icon = chalk2.yellow(" \u2192 ");
1940
- text = chalk2.white(todo.title);
2760
+ icon = chalk5.yellow(" \u2192 ");
2761
+ text = chalk5.white(todo.title);
1941
2762
  break;
1942
2763
  case "pending":
1943
2764
  default:
1944
- icon = chalk2.gray(" \u25CB ");
1945
- text = chalk2.gray(todo.title);
2765
+ icon = chalk5.gray(" \u25CB ");
2766
+ text = chalk5.gray(todo.title);
1946
2767
  break;
1947
2768
  }
1948
2769
  console.log(icon + text);
@@ -2117,151 +2938,6 @@ function formatResults(query, data, requested) {
2117
2938
  return header + "\n" + results.join("\n\n");
2118
2939
  }
2119
2940
 
2120
- // src/tools/types.ts
2121
- function isFileWriteTool(name) {
2122
- return name === "write_file" || name === "edit_file";
2123
- }
2124
- function getDangerLevel(toolName, args) {
2125
- if (toolName.startsWith("mcp__")) return "safe";
2126
- if (toolName === "bash") {
2127
- const cmd = String(args["command"] ?? "");
2128
- if (/\brm\s+[^\n]*(?:-\w*[rRfF]\w*|--recursive|--force)\b/.test(cmd)) return "destructive";
2129
- if (/\brm\s+\S/.test(cmd)) return "destructive";
2130
- if (/\brmdir\b|\bformat\b|\bmkfs\b/.test(cmd)) return "destructive";
2131
- if (/\bRemove-Item\b.*(?:-Recurse|-Force)|\bri\s+.*-(?:Recurse|Force)\b|\brd\s+\/s\b|\brmdir\s+\/s\b/i.test(cmd)) return "destructive";
2132
- if (/\bdel\s+\S/.test(cmd)) return "destructive";
2133
- if (/\becho\b.*>>?|\btee\b|\bcp\b|\bmv\b/.test(cmd)) return "write";
2134
- if (/\bSet-Content\b|\bOut-File\b|\bAdd-Content\b|\bCopy-Item\b|\bMove-Item\b/i.test(cmd)) return "write";
2135
- return "safe";
2136
- }
2137
- if (toolName === "write_file") return "write";
2138
- if (toolName === "edit_file") return "write";
2139
- if (toolName === "save_last_response") return "write";
2140
- if (toolName === "run_interactive") {
2141
- const exe = String(args["executable"] ?? "").toLowerCase();
2142
- if (/\b(rm|rmdir|del|format|mkfs|Remove-Item)\b/i.test(exe)) return "destructive";
2143
- if (/\b(bash|sh|zsh|cmd|powershell|pwsh|python|node|ruby|perl)\b/i.test(exe)) return "write";
2144
- return "write";
2145
- }
2146
- if (toolName === "read_file" || toolName === "list_dir" || toolName === "grep_files" || toolName === "glob_files" || toolName === "web_fetch" || toolName === "save_memory" || toolName === "ask_user" || toolName === "write_todos" || toolName === "google_search" || toolName === "spawn_agent" || toolName === "run_tests") return "safe";
2147
- return "write";
2148
- }
2149
- function schemaToJsonSchema(schema) {
2150
- const result = {
2151
- type: schema.type,
2152
- description: schema.description
2153
- };
2154
- if (schema.enum) result["enum"] = schema.enum;
2155
- if (schema.items) result["items"] = schemaToJsonSchema(schema.items);
2156
- if (schema.properties) {
2157
- result["properties"] = Object.fromEntries(
2158
- Object.entries(schema.properties).map(([k, v]) => [k, schemaToJsonSchema(v)])
2159
- );
2160
- }
2161
- return result;
2162
- }
2163
-
2164
- // src/tools/truncate.ts
2165
- var DEFAULT_MAX_TOOL_OUTPUT_CHARS = 12e3;
2166
- function getMaxOutputChars(contextWindow) {
2167
- if (!contextWindow || contextWindow <= 0) return DEFAULT_MAX_TOOL_OUTPUT_CHARS;
2168
- return Math.max(DEFAULT_MAX_TOOL_OUTPUT_CHARS, Math.min(Math.floor(contextWindow / 4), 12e4));
2169
- }
2170
- var activeMaxChars = DEFAULT_MAX_TOOL_OUTPUT_CHARS;
2171
- function setContextWindow(contextWindow) {
2172
- activeMaxChars = getMaxOutputChars(contextWindow);
2173
- }
2174
- function getActiveMaxChars() {
2175
- return activeMaxChars;
2176
- }
2177
- function truncateOutput(content, toolName, maxChars) {
2178
- const limit = maxChars ?? activeMaxChars;
2179
- if (content.length <= limit) return content;
2180
- const keepHead = Math.floor(limit * 0.7);
2181
- const keepTail = Math.floor(limit * 0.2);
2182
- const omitted = content.length - keepHead - keepTail;
2183
- const lines = content.split("\n").length;
2184
- const head = content.slice(0, keepHead);
2185
- const tail = content.slice(content.length - keepTail);
2186
- return head + `
2187
-
2188
- ... [Output truncated: ${content.length} chars / ${lines} lines total, ${omitted} chars omitted. Use read_file to view full content in segments` + (toolName === "bash" ? ", or narrow the command scope" : "") + `] ...
2189
-
2190
- ` + tail;
2191
- }
2192
-
2193
- // src/repl/theme.ts
2194
- import chalk3 from "chalk";
2195
- var DARK_THEME = {
2196
- prompt: chalk3.green,
2197
- info: chalk3.cyan,
2198
- warning: chalk3.yellow,
2199
- error: chalk3.red,
2200
- success: chalk3.green,
2201
- dim: chalk3.dim,
2202
- accent: chalk3.cyan,
2203
- toolCall: chalk3.yellow,
2204
- toolResult: chalk3.green,
2205
- heading: chalk3.bold.cyan
2206
- };
2207
- var LIGHT_THEME = {
2208
- prompt: chalk3.blue,
2209
- info: chalk3.blueBright,
2210
- warning: chalk3.yellow,
2211
- error: chalk3.red,
2212
- success: chalk3.green,
2213
- dim: chalk3.gray,
2214
- accent: chalk3.blueBright,
2215
- toolCall: chalk3.magenta,
2216
- toolResult: chalk3.green,
2217
- heading: chalk3.bold.blue
2218
- };
2219
- function resolveColor(name) {
2220
- if (name.startsWith("#")) return chalk3.hex(name);
2221
- const parts = name.split(".");
2222
- let result = chalk3;
2223
- for (const part of parts) {
2224
- const obj = result;
2225
- if (obj && typeof obj[part] !== "undefined") {
2226
- result = obj[part];
2227
- }
2228
- }
2229
- if (typeof result !== "function") {
2230
- process.stderr.write(`[theme] Warning: unrecognized color "${name}", using default.
2231
- `);
2232
- return chalk3;
2233
- }
2234
- return result;
2235
- }
2236
- function buildCustomTheme(base, overrides) {
2237
- if (!overrides) return base;
2238
- const result = { ...base };
2239
- for (const [key, colorName] of Object.entries(overrides)) {
2240
- if (key in result && colorName) {
2241
- result[key] = resolveColor(colorName);
2242
- }
2243
- }
2244
- return result;
2245
- }
2246
- var _currentTheme = DARK_THEME;
2247
- function initTheme(themeId = "dark", customColors) {
2248
- switch (themeId) {
2249
- case "light":
2250
- _currentTheme = LIGHT_THEME;
2251
- break;
2252
- case "custom":
2253
- _currentTheme = buildCustomTheme(DARK_THEME, customColors);
2254
- break;
2255
- default:
2256
- _currentTheme = DARK_THEME;
2257
- }
2258
- }
2259
- var theme = new Proxy(DARK_THEME, {
2260
- get(_target, prop) {
2261
- return _currentTheme[prop];
2262
- }
2263
- });
2264
-
2265
2941
  // src/tools/builtin/spawn-agent.ts
2266
2942
  var spawnAgentContext = {
2267
2943
  provider: null,
@@ -2516,9 +3192,194 @@ var spawnAgentTool = {
2516
3192
  }
2517
3193
  };
2518
3194
 
3195
+ // src/tools/builtin/task-manager.ts
3196
+ import { spawn as spawn2 } from "child_process";
3197
+ import { randomUUID } from "crypto";
3198
+ import { platform as platform3 } from "os";
3199
+ var MAX_OUTPUT_CHARS = 1e4;
3200
+ var MAX_TASKS = 20;
3201
+ var tasks = /* @__PURE__ */ new Map();
3202
+ function appendOutput(task, field, chunk) {
3203
+ task[field] += chunk;
3204
+ if (task[field].length > MAX_OUTPUT_CHARS) {
3205
+ task[field] = "... [truncated] ...\n" + task[field].slice(-MAX_OUTPUT_CHARS);
3206
+ }
3207
+ }
3208
+ function createTask(command, description) {
3209
+ if (tasks.size >= MAX_TASKS) {
3210
+ for (const [id2, t] of tasks) {
3211
+ if (t.status !== "running") {
3212
+ tasks.delete(id2);
3213
+ break;
3214
+ }
3215
+ }
3216
+ }
3217
+ const id = randomUUID().slice(0, 8);
3218
+ const isWin = platform3() === "win32";
3219
+ const shell = isWin ? "cmd" : "sh";
3220
+ const shellFlag = isWin ? "/c" : "-c";
3221
+ const proc = spawn2(shell, [shellFlag, command], {
3222
+ stdio: ["ignore", "pipe", "pipe"],
3223
+ detached: false,
3224
+ env: { ...process.env, PYTHONUTF8: "1" }
3225
+ });
3226
+ const task = {
3227
+ id,
3228
+ command,
3229
+ description,
3230
+ process: proc,
3231
+ startTime: Date.now(),
3232
+ endTime: null,
3233
+ status: "running",
3234
+ exitCode: null,
3235
+ stdout: "",
3236
+ stderr: ""
3237
+ };
3238
+ proc.stdout?.on("data", (chunk) => appendOutput(task, "stdout", chunk.toString("utf-8")));
3239
+ proc.stderr?.on("data", (chunk) => appendOutput(task, "stderr", chunk.toString("utf-8")));
3240
+ proc.on("close", (code) => {
3241
+ task.endTime = Date.now();
3242
+ task.exitCode = code;
3243
+ task.status = code === 0 ? "completed" : "failed";
3244
+ });
3245
+ proc.on("error", (err) => {
3246
+ task.endTime = Date.now();
3247
+ task.status = "failed";
3248
+ task.stderr += `
3249
+ Process error: ${err.message}`;
3250
+ });
3251
+ tasks.set(id, task);
3252
+ return task;
3253
+ }
3254
+ function listTasks() {
3255
+ return [...tasks.values()].map(({ process: _p, ...rest }) => rest);
3256
+ }
3257
+ function getTaskOutput(id) {
3258
+ const task = tasks.get(id);
3259
+ if (!task) return null;
3260
+ return { stdout: task.stdout, stderr: task.stderr, status: task.status };
3261
+ }
3262
+ function stopTask(id) {
3263
+ const task = tasks.get(id);
3264
+ if (!task || task.status !== "running") return false;
3265
+ try {
3266
+ task.process.kill("SIGTERM");
3267
+ setTimeout(() => {
3268
+ if (task.status === "running") {
3269
+ try {
3270
+ task.process.kill("SIGKILL");
3271
+ } catch {
3272
+ }
3273
+ }
3274
+ }, 3e3);
3275
+ task.status = "stopped";
3276
+ task.endTime = Date.now();
3277
+ return true;
3278
+ } catch {
3279
+ return false;
3280
+ }
3281
+ }
3282
+
3283
+ // src/tools/builtin/task-create.ts
3284
+ var taskCreateTool = {
3285
+ definition: {
3286
+ name: "task_create",
3287
+ description: `Start a command running in the background. Returns a task ID for monitoring with task_list. Use this to run long-running processes (dev servers, builds, tests) while continuing other work.`,
3288
+ parameters: {
3289
+ command: {
3290
+ type: "string",
3291
+ description: "Shell command to run in the background",
3292
+ required: true
3293
+ },
3294
+ description: {
3295
+ type: "string",
3296
+ description: "Brief description of what this task does",
3297
+ required: true
3298
+ }
3299
+ },
3300
+ dangerous: false
3301
+ },
3302
+ async execute(args) {
3303
+ const command = String(args["command"] ?? "");
3304
+ const description = String(args["description"] ?? command);
3305
+ if (!command) throw new ToolError("task_create", "command is required");
3306
+ const task = createTask(command, description);
3307
+ return `Background task started.
3308
+ ID: ${task.id}
3309
+ Command: ${command}
3310
+ Description: ${description}
3311
+ Use task_list to check status, task_stop to terminate.`;
3312
+ }
3313
+ };
3314
+
3315
+ // src/tools/builtin/task-list.ts
3316
+ var taskListTool = {
3317
+ definition: {
3318
+ name: "task_list",
3319
+ description: `List all background tasks with their status. Provide an ID to get detailed output for a specific task.`,
3320
+ parameters: {
3321
+ id: {
3322
+ type: "string",
3323
+ description: "Optional: specific task ID to get detailed stdout/stderr output",
3324
+ required: false
3325
+ }
3326
+ },
3327
+ dangerous: false
3328
+ },
3329
+ async execute(args) {
3330
+ const id = args["id"] ? String(args["id"]) : void 0;
3331
+ if (id) {
3332
+ const output = getTaskOutput(id);
3333
+ if (!output) return `Task not found: ${id}`;
3334
+ const parts = [`Task ${id} (${output.status})`];
3335
+ if (output.stdout) parts.push(`
3336
+ \u2500\u2500 stdout \u2500\u2500
3337
+ ${output.stdout}`);
3338
+ if (output.stderr) parts.push(`
3339
+ \u2500\u2500 stderr \u2500\u2500
3340
+ ${output.stderr}`);
3341
+ if (!output.stdout && !output.stderr) parts.push("\n(no output yet)");
3342
+ return parts.join("");
3343
+ }
3344
+ const all = listTasks();
3345
+ if (all.length === 0) return "No background tasks.";
3346
+ const lines = [`Background tasks (${all.length}):
3347
+ `];
3348
+ for (const t of all) {
3349
+ const elapsed = ((t.endTime ?? Date.now()) - t.startTime) / 1e3;
3350
+ const status = t.status === "running" ? "\u{1F7E2} running" : t.status === "completed" ? "\u2705 completed" : t.status === "stopped" ? "\u23F9 stopped" : "\u274C failed";
3351
+ lines.push(` [${t.id}] ${status} (${elapsed.toFixed(1)}s) \u2014 ${t.description}`);
3352
+ if (t.exitCode !== null) lines.push(` exit code: ${t.exitCode}`);
3353
+ }
3354
+ return lines.join("\n");
3355
+ }
3356
+ };
3357
+
3358
+ // src/tools/builtin/task-stop.ts
3359
+ var taskStopTool = {
3360
+ definition: {
3361
+ name: "task_stop",
3362
+ description: `Stop a running background task by its ID. Use task_list to find the ID.`,
3363
+ parameters: {
3364
+ id: {
3365
+ type: "string",
3366
+ description: "Task ID to stop",
3367
+ required: true
3368
+ }
3369
+ },
3370
+ dangerous: false
3371
+ },
3372
+ async execute(args) {
3373
+ const id = String(args["id"] ?? "");
3374
+ if (!id) throw new ToolError("task_stop", "id is required");
3375
+ const success = stopTask(id);
3376
+ return success ? `Task ${id} stopped.` : `Task ${id} not found or already completed.`;
3377
+ }
3378
+ };
3379
+
2519
3380
  // src/tools/registry.ts
2520
3381
  import { pathToFileURL } from "url";
2521
- import { existsSync as existsSync9, mkdirSync as mkdirSync4, readdirSync as readdirSync6 } from "fs";
3382
+ import { existsSync as existsSync10, mkdirSync as mkdirSync4, readdirSync as readdirSync6 } from "fs";
2522
3383
  import { join as join5 } from "path";
2523
3384
  var ToolRegistry = class {
2524
3385
  tools = /* @__PURE__ */ new Map();
@@ -2541,6 +3402,9 @@ var ToolRegistry = class {
2541
3402
  this.register(googleSearchTool);
2542
3403
  this.register(spawnAgentTool);
2543
3404
  this.register(runTestsTool);
3405
+ this.register(taskCreateTool);
3406
+ this.register(taskListTool);
3407
+ this.register(taskStopTool);
2544
3408
  }
2545
3409
  register(tool) {
2546
3410
  this.tools.set(tool.definition.name, tool);
@@ -2592,7 +3456,7 @@ var ToolRegistry = class {
2592
3456
  * Returns the number of successfully loaded plugins.
2593
3457
  */
2594
3458
  async loadPlugins(pluginsDir, allowPlugins = false) {
2595
- if (!existsSync9(pluginsDir)) {
3459
+ if (!existsSync10(pluginsDir)) {
2596
3460
  try {
2597
3461
  mkdirSync4(pluginsDir, { recursive: true });
2598
3462
  } catch {
@@ -2663,12 +3527,15 @@ export {
2663
3527
  initTheme,
2664
3528
  theme,
2665
3529
  undoStack,
3530
+ renderDiff,
3531
+ runHook,
3532
+ checkPermission,
3533
+ setContextWindow,
3534
+ truncateOutput,
3535
+ ToolExecutor,
2666
3536
  lastResponseStore,
2667
3537
  askUserContext,
2668
3538
  googleSearchContext,
2669
- setContextWindow,
2670
- getActiveMaxChars,
2671
- truncateOutput,
2672
3539
  spawnAgentContext,
2673
3540
  ToolRegistry
2674
3541
  };