minimal-agent 0.1.6 → 0.1.8

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.
Files changed (2) hide show
  1. package/dist/main.js +587 -136
  2. package/package.json +2 -2
package/dist/main.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  // src/main.tsx
4
4
  import { render } from "ink";
5
- import { existsSync as existsSync7, mkdirSync } from "fs";
5
+ import { existsSync as existsSync9, mkdirSync } from "fs";
6
6
  import { createRequire } from "module";
7
7
  import { resolve as resolve8 } from "path";
8
8
 
@@ -360,6 +360,8 @@ ${toolList}
360
360
  "\u6211\u8BB0\u5F97"\u3001"\u6211\u4E4B\u524D\u770B\u8FC7"\u3001"\u5E94\u8BE5\u5DEE\u4E0D\u591A"\u90FD\u4E0D\u7B97\u8BFB\u8FC7\u2014\u2014\u5FC5\u987B\u5728\u672C\u8F6E\u4EFB\u52A1\u4E2D\u5B9E\u9645\u8C03\u7528 Read\u3002
361
361
  \u5C24\u5176\u5F53\u4F60\u8981\u53C2\u8003\u67D0\u4E2A\u6587\u4EF6\u7684\u5199\u6CD5\u6765\u5B9E\u73B0\u7C7B\u4F3C\u529F\u80FD\u65F6\uFF0C\u5FC5\u987B\u5148\u91CD\u8BFB\u8BE5\u6587\u4EF6\u7684\u5173\u952E\u90E8\u5206\uFF08\u51FD\u6570\u5B9E\u73B0\u3001\u5224\u65AD\u903B\u8F91\u3001\u6570\u636E\u6D41\uFF09\uFF0C\u7406\u89E3\u6E05\u695A\u540E\u518D\u52A8\u624B\u3002
362
362
  - Edit \u5DE5\u5177\u7684 old_string \u5FC5\u987B\u5728\u6587\u4EF6\u4E2D\u552F\u4E00\uFF1B\u4E0D\u552F\u4E00\u65F6\u8BF7\u6269\u5927\u4E0A\u4E0B\u6587\u6216\u663E\u5F0F replace_all=true\u3002
363
+ - \u540C\u4E00\u6587\u4EF6\u9700\u8981\u4FEE\u6539\u591A\u5904\uFF083 \u5904\u53CA\u4EE5\u4E0A\uFF09\u65F6\u4F18\u5148\u7528 MultiEdit\uFF1A\u6240\u6709 edit \u6309\u987A\u5E8F\u5728\u5185\u5B58\u4E2D\u5E94\u7528\uFF0C**\u5168\u90E8\u6210\u529F\u624D\u843D\u76D8**\uFF1B\u4EFB\u4E00\u5931\u8D25\u78C1\u76D8\u4E0D\u52A8\uFF0C\u907F\u514D\u4E2D\u95F4\u72B6\u6001\u6C61\u67D3\u3002\u5355\u70B9\u4FEE\u6539\u7EE7\u7EED\u7528 Edit\u3002
364
+ - Write \u8986\u76D6\u65E2\u6709\u6587\u4EF6\u524D\u540C\u6837\u5FC5\u987B\u5148 Read\uFF08\u4E0E Edit \u5BF9\u79F0\uFF0C\u672A\u5148 Read \u4F1A\u88AB\u62D2\u7EDD\u5E76\u63D0\u793A"\u8BF7\u5148 Read"\uFF09\uFF1B\u5199\u65B0\u6587\u4EF6\u65E0\u6B64\u8981\u6C42\u3002
363
365
  - \u521B\u5EFA\u65B0\u6587\u4EF6\u7528 Write\uFF0C\u6216 Edit \u65F6 old_string \u4F20\u7A7A\u5B57\u7B26\u4E32\u3002
364
366
  - \u627E\u6587\u4EF6\u7528 Glob\uFF08"**/*.ts"\uFF09\uFF0C\u627E\u6587\u4EF6\u5185\u5BB9\u7528 Grep\uFF08\u57FA\u4E8E ripgrep\uFF09\u3002
365
367
  - \u5F53\u7528\u6237\u95EE\u5230\u8BAD\u7EC3\u622A\u6B62\u540E\u624D\u51FA\u73B0\u7684\u4FE1\u606F\uFF08\u6700\u65B0\u7248\u672C\u53F7\u3001\u8FD1\u671F\u65B0\u95FB\u3001\u7B2C\u4E09\u65B9 API \u6587\u6863\uFF09\u65F6\u7528 WebSearch\uFF1B\u4F18\u5148\u7CBE\u786E\u7684\u81EA\u7136\u8BED\u8A00\u67E5\u8BE2\u3002
@@ -369,6 +371,8 @@ ${toolList}
369
371
  Bash \u6709\u5B89\u5168\u9ED1\u540D\u5355\uFF08rm -rf /\u3001mkfs\u3001shutdown \u7B49\u5371\u9669\u547D\u4EE4\u4F1A\u88AB\u62E6\u622A\uFF09\uFF0C\u4F46\u4ECD\u9700\u8C28\u614E\uFF1A
370
372
  \u5148\u786E\u8BA4\u547D\u4EE4\u65E0\u5BB3\u518D\u6267\u884C\uFF0C\u907F\u514D\u4E0D\u53EF\u9006\u64CD\u4F5C\uFF08\u5982 git push --force\u3001git reset --hard\uFF09\u3002
371
373
  \u957F\u65F6\u95F4\u8FD0\u884C\u7684\u547D\u4EE4\uFF08npm install\u3001bun test\uFF09\u6CE8\u610F\u8D85\u65F6\u8BBE\u7F6E\uFF1B\u9700\u8981\u4EA4\u4E92\u8F93\u5165\u7684\u547D\u4EE4\u4E0D\u8981\u7528 Bash\uFF08\u7528 Write \u5199\u811A\u672C\u4EE3\u66FF\uFF09\u3002
374
+ Bash \u5DF2\u8BC6\u522B"\u4FE1\u606F\u6027\u9000\u51FA\u7801"\uFF1Agrep/rg/find/diff/test \u7684 exit=1\uFF08\u65E0\u5339\u914D / \u90E8\u5206\u4E0D\u53EF\u8BBF\u95EE / \u6709\u5DEE\u5F02 / \u6761\u4EF6\u5047\uFF09\u4F1A\u81EA\u52A8\u5224\u4E3A\u6210\u529F\uFF0C\u4E0D\u8981\u56E0 1 \u800C\u91CD\u8BD5\u3002
375
+ \u7834\u574F\u6027\u547D\u4EE4\uFF08git reset --hard / git push -f / rm -rf \u7B49\uFF09\u4F1A\u88AB Bash \u4E3B\u52A8\u5728\u8F93\u51FA\u5934\u90E8\u52A0 \u26A0\uFE0F \u8B66\u544A\uFF08\u4E0D\u62E6\u622A\uFF09\uFF0C\u770B\u5230\u65F6\u5E94\u5411\u7528\u6237\u786E\u8BA4\u610F\u56FE\u3002
372
376
  - \u5F53\u9700\u8981\u83B7\u53D6\u7F51\u9875\u9759\u6001\u6587\u672C\u5185\u5BB9\uFF08\u6293\u53D6\u6587\u6863\u3001\u8BFB\u53D6\u6587\u7AE0\uFF09\u65F6\u7528 WebFetch\u3002
373
377
  WebBrowser \u4F9D\u8D56\u53EF\u9009\u5305\uFF08playwright-core + chromium\uFF09\u2014\u2014 **\u9ED8\u8BA4\u5047\u5B9A\u672A\u5B89\u88C5**\uFF0C
374
378
  \u4EC5\u5728 WebFetch \u660E\u786E\u65E0\u6CD5\u6EE1\u8DB3\uFF08\u5982\u9700\u8981 JS \u6E32\u67D3\u540E\u7684\u5185\u5BB9\u3001\u70B9\u51FB\u6309\u94AE\u3001\u586B\u8868\u5355\u3001\u622A\u56FE\uFF09\u65F6\u518D\u5C1D\u8BD5\u3002
@@ -462,6 +466,141 @@ function toToolParameters(schema) {
462
466
  return rest;
463
467
  }
464
468
 
469
+ // src/tools/bash/semantics.ts
470
+ var DEFAULT_SEMANTIC = (exitCode) => ({
471
+ isError: exitCode !== 0,
472
+ message: exitCode !== 0 ? `Command failed with exit code ${exitCode}` : void 0
473
+ });
474
+ var COMMAND_SEMANTICS = /* @__PURE__ */ new Map([
475
+ // grep: 0=找到匹配, 1=无匹配(非错误), 2+=真错误
476
+ ["grep", (exitCode) => ({
477
+ isError: exitCode >= 2,
478
+ message: exitCode === 1 ? "No matches found" : void 0
479
+ })],
480
+ // ripgrep 与 grep 同义
481
+ ["rg", (exitCode) => ({
482
+ isError: exitCode >= 2,
483
+ message: exitCode === 1 ? "No matches found" : void 0
484
+ })],
485
+ // find: 1=部分目录不可达(仍有结果,非致命), 2+=错误
486
+ ["find", (exitCode) => ({
487
+ isError: exitCode >= 2,
488
+ message: exitCode === 1 ? "Some directories were inaccessible" : void 0
489
+ })],
490
+ // diff: 0=相同, 1=有差异(非错误), 2+=错误
491
+ ["diff", (exitCode) => ({
492
+ isError: exitCode >= 2,
493
+ message: exitCode === 1 ? "Files differ" : void 0
494
+ })],
495
+ // test: 0=真, 1=假(非错误), 2+=错误
496
+ ["test", (exitCode) => ({
497
+ isError: exitCode >= 2,
498
+ message: exitCode === 1 ? "Condition is false" : void 0
499
+ })],
500
+ // [ 是 test 的别名
501
+ ["[", (exitCode) => ({
502
+ isError: exitCode >= 2,
503
+ message: exitCode === 1 ? "Condition is false" : void 0
504
+ })]
505
+ ]);
506
+ function extractPrimaryCommand(command) {
507
+ let cmd = command.trim();
508
+ const wrapMatch = cmd.match(/^(?:bash|sh|zsh|dash)\s+-c\s+(['"])(.+)\1\s*$/s);
509
+ if (wrapMatch) cmd = wrapMatch[2].trim();
510
+ const segments = cmd.split(/\s*(?:\|\||&&|;|\|)\s*/).filter((s) => s.length > 0);
511
+ let last = segments[segments.length - 1] ?? cmd;
512
+ last = last.trim();
513
+ const tokens = last.split(/\s+/);
514
+ let i = 0;
515
+ if (tokens[i] === "env") i++;
516
+ while (i < tokens.length && /^[A-Za-z_][A-Za-z0-9_]*=/.test(tokens[i])) i++;
517
+ return tokens[i] ?? "";
518
+ }
519
+ function interpretCommandResult(command, exitCode, stdout, stderr) {
520
+ const base = extractPrimaryCommand(command);
521
+ const fn = COMMAND_SEMANTICS.get(base) ?? DEFAULT_SEMANTIC;
522
+ return fn(exitCode, stdout, stderr);
523
+ }
524
+
525
+ // src/tools/bash/warnings.ts
526
+ var DESTRUCTIVE_PATTERNS = [
527
+ // Git —— 数据丢失 / 难回退
528
+ {
529
+ pattern: /\bgit\s+reset\s+--hard\b/,
530
+ warning: "Note: may discard uncommitted changes"
531
+ },
532
+ {
533
+ pattern: /\bgit\s+push\b[^;&|\n]*[ \t](--force|--force-with-lease|-f)\b/,
534
+ warning: "Note: may overwrite remote history"
535
+ },
536
+ {
537
+ pattern: /\bgit\s+clean\b(?![^;&|\n]*(?:-[a-zA-Z]*n|--dry-run))[^;&|\n]*-[a-zA-Z]*f/,
538
+ warning: "Note: may permanently delete untracked files"
539
+ },
540
+ {
541
+ pattern: /\bgit\s+checkout\s+(--\s+)?\.[ \t]*($|[;&|\n])/,
542
+ warning: "Note: may discard all working tree changes"
543
+ },
544
+ {
545
+ pattern: /\bgit\s+restore\s+(--\s+)?\.[ \t]*($|[;&|\n])/,
546
+ warning: "Note: may discard all working tree changes"
547
+ },
548
+ {
549
+ pattern: /\bgit\s+stash[ \t]+(drop|clear)\b/,
550
+ warning: "Note: may permanently remove stashed changes"
551
+ },
552
+ {
553
+ pattern: /\bgit\s+branch\s+(-D[ \t]|--delete\s+--force|--force\s+--delete)\b/,
554
+ warning: "Note: may force-delete a branch"
555
+ },
556
+ // Git —— 安全绕过
557
+ {
558
+ pattern: /\bgit\s+(commit|push|merge)\b[^;&|\n]*--no-verify\b/,
559
+ warning: "Note: may skip safety hooks"
560
+ },
561
+ {
562
+ pattern: /\bgit\s+commit\b[^;&|\n]*--amend\b/,
563
+ warning: "Note: may rewrite the last commit"
564
+ },
565
+ // 文件删除(rm -rf / 之类的致命形式由 bash.ts 黑名单处理;这里只做"未到致命"的提醒)
566
+ {
567
+ pattern: /(^|[;&|\n]\s*)rm\s+-[a-zA-Z]*[rR][a-zA-Z]*f|(^|[;&|\n]\s*)rm\s+-[a-zA-Z]*f[a-zA-Z]*[rR]/,
568
+ warning: "Note: may recursively force-remove files"
569
+ },
570
+ {
571
+ pattern: /(^|[;&|\n]\s*)rm\s+-[a-zA-Z]*[rR]/,
572
+ warning: "Note: may recursively remove files"
573
+ },
574
+ {
575
+ pattern: /(^|[;&|\n]\s*)rm\s+-[a-zA-Z]*f/,
576
+ warning: "Note: may force-remove files"
577
+ },
578
+ // 数据库
579
+ {
580
+ pattern: /\b(DROP|TRUNCATE)\s+(TABLE|DATABASE|SCHEMA)\b/i,
581
+ warning: "Note: may drop or truncate database objects"
582
+ },
583
+ {
584
+ pattern: /\bDELETE\s+FROM\s+\w+[ \t]*(;|"|'|\n|$)/i,
585
+ warning: "Note: may delete all rows from a database table"
586
+ },
587
+ // 基础设施
588
+ {
589
+ pattern: /\bkubectl\s+delete\b/,
590
+ warning: "Note: may delete Kubernetes resources"
591
+ },
592
+ {
593
+ pattern: /\bterraform\s+destroy\b/,
594
+ warning: "Note: may destroy Terraform infrastructure"
595
+ }
596
+ ];
597
+ function scanDestructiveCommand(command) {
598
+ for (const { pattern, warning } of DESTRUCTIVE_PATTERNS) {
599
+ if (pattern.test(command)) return warning;
600
+ }
601
+ return null;
602
+ }
603
+
465
604
  // src/tools/bash/bash.ts
466
605
  var DEFAULT_TIMEOUT_MS = 12e4;
467
606
  var MAX_TIMEOUT_MS = 6e5;
@@ -578,6 +717,14 @@ While the Bash tool can do similar things, it's better to use the built-in tools
578
717
  - Never skip hooks (--no-verify) or bypass signing (--no-gpg-sign, -c commit.gpgsign=false) unless the user has explicitly asked for it. If a hook fails, investigate and fix the underlying issue.
579
718
  - Avoid unnecessary \`sleep\` commands; do not retry failing commands in a sleep loop \u2014 diagnose the root cause.
580
719
 
720
+ # Exit code semantics
721
+ Some commands return non-zero exit codes for informational (non-error) reasons. Bash recognizes these and reports them as success (ok=true) \u2014 do NOT retry just because exit code is 1:
722
+ - \`grep\` / \`rg\` exit 1 \u2192 no match found (not an error)
723
+ - \`find\` exit 1 \u2192 some directories inaccessible (non-fatal, partial results still returned)
724
+ - \`diff\` / \`cmp\` exit 1 \u2192 files differ (informational, not an error)
725
+ - \`test\` / \`[\` exit 1 \u2192 condition is false (the answer to a question, not a failure)
726
+ Only exit codes \u2265 2 from these commands indicate a real failure. For all other commands, non-zero exit codes are treated as failures normally.
727
+
581
728
  # Safety
582
729
  The following command patterns are blocked at the tool level and will fail before execution (no need to try them):
583
730
  - \`rm -rf /\` and variants targeting root, $HOME, ~, or system directories (/etc, /usr, /bin, /Windows, /Users, /home, ...)
@@ -587,7 +734,9 @@ The following command patterns are blocked at the tool level and will fail befor
587
734
  - Pipe-to-shell from network: \`curl ... | sh\`, \`wget ... | bash\`, etc.
588
735
  - \`chmod 777 /\`, Windows full-disk \`del /s\` / \`rmdir /s\`, \`diskpart\`
589
736
 
590
- If you have a legitimate use case that requires one of the above patterns, ask the user to run the command themselves in their terminal \u2014 do not try to bypass the check.`;
737
+ If you have a legitimate use case that requires one of the above patterns, ask the user to run the command themselves in their terminal \u2014 do not try to bypass the check.
738
+
739
+ Separately, Bash scans for common destructive-but-recoverable patterns (\`git reset --hard\`, \`git push -f\` / \`--force\` / \`--force-with-lease\`, \`git checkout .\`, \`git restore .\`, \`git clean -f\`, \`git stash drop/clear\`, \`git branch -D\`, \`git commit --amend\` / \`--no-verify\`, \`rm -rf <path>\`, \`DROP TABLE\`, \`TRUNCATE\`, \`DELETE FROM\`, \`kubectl delete\`, \`terraform destroy\`, etc.) and prepends a \`\u26A0\uFE0F \u8B66\u544A:\` line to the output. These commands are NOT blocked \u2014 the warning is informational. Treat it as a signal to double-check intent and surface the warning to the user when relevant.`;
591
740
  async function call(input, signal) {
592
741
  const command = input.command;
593
742
  const timeoutMs = Math.min(input.timeout ?? DEFAULT_TIMEOUT_MS, MAX_TIMEOUT_MS);
@@ -599,6 +748,7 @@ async function call(input, signal) {
599
748
  \u547D\u4EE4\uFF1A${command}`
600
749
  };
601
750
  }
751
+ const destructiveWarning = scanDestructiveCommand(command);
602
752
  let stdout = "";
603
753
  let stderr = "";
604
754
  let exitCode = null;
@@ -669,16 +819,33 @@ ${stderr.replace(/\s+$/, "")}
669
819
  }
670
820
  parts.push(`
671
821
  [exit code: ${exitCode === null ? "killed" : exitCode}]`);
672
- let content = parts.join("\n");
673
- if (content.length > DEFAULT_MAX_RESULT_SIZE_CHARS) {
674
- content = content.slice(0, DEFAULT_MAX_RESULT_SIZE_CHARS) + `
822
+ let combinedOutput = parts.join("\n");
823
+ if (combinedOutput.length > DEFAULT_MAX_RESULT_SIZE_CHARS) {
824
+ combinedOutput = combinedOutput.slice(0, DEFAULT_MAX_RESULT_SIZE_CHARS) + `
675
825
 
676
826
  ... (\u8F93\u51FA\u8D85\u8FC7 ${DEFAULT_MAX_RESULT_SIZE_CHARS} \u5B57\u7B26\uFF0C\u5DF2\u622A\u65AD)`;
677
827
  }
678
- if (timedOut || exitCode !== null && exitCode !== 0 || killedBySignal) {
679
- return { ok: false, error: content };
828
+ if (timedOut || killedBySignal) {
829
+ return { ok: false, error: combinedOutput };
680
830
  }
681
- return { ok: true, content };
831
+ const semantic = interpretCommandResult(
832
+ command,
833
+ exitCode ?? 0,
834
+ stdout,
835
+ stderr
836
+ );
837
+ const finalContent = destructiveWarning ? `\u26A0\uFE0F \u8B66\u544A: ${destructiveWarning}
838
+
839
+ ${combinedOutput}` : combinedOutput;
840
+ if (semantic.isError) {
841
+ return {
842
+ ok: false,
843
+ error: `\u547D\u4EE4\u5931\u8D25 (exit ${exitCode}): ${stderr || stdout || semantic.message || ""}`.trim() + `
844
+
845
+ ${finalContent}`
846
+ };
847
+ }
848
+ return { ok: true, content: finalContent };
682
849
  }
683
850
  var bashTool = {
684
851
  name: "Bash",
@@ -694,14 +861,14 @@ var bashTool = {
694
861
 
695
862
  // src/tools/edit/edit.ts
696
863
  import { readFile as readFile6, writeFile as writeFile3, mkdir as mkdir4 } from "fs/promises";
697
- import { existsSync as existsSync2 } from "fs";
864
+ import { existsSync as existsSync3 } from "fs";
698
865
  import { dirname as dirname5 } from "path";
699
866
  import { z as z2 } from "zod";
700
867
 
701
868
  // src/tools/shared/fileUtils.ts
702
- import { readFile as readFile5 } from "fs/promises";
869
+ import { open, readFile as readFile5 } from "fs/promises";
703
870
  import { homedir as homedir4 } from "os";
704
- import { resolve as resolve4, normalize } from "path";
871
+ import { extname, resolve as resolve4 } from "path";
705
872
  var BLOCKED_DEVICE_PATHS = /* @__PURE__ */ new Set([
706
873
  "/dev/zero",
707
874
  "/dev/random",
@@ -718,12 +885,12 @@ var BLOCKED_DEVICE_PATHS = /* @__PURE__ */ new Set([
718
885
  ]);
719
886
  var WINDOWS_BLOCKED_NAMES = /* @__PURE__ */ new Set(["NUL", "CON", "PRN", "AUX", "COM1", "COM2", "LPT1"]);
720
887
  function isBlockedDevicePath(filePath) {
721
- const normalized = normalize(filePath);
722
- if (BLOCKED_DEVICE_PATHS.has(normalized)) return true;
723
- if (normalized.startsWith("/proc/") && (normalized.endsWith("/fd/0") || normalized.endsWith("/fd/1") || normalized.endsWith("/fd/2"))) {
888
+ const slashed = filePath.replaceAll("\\", "/");
889
+ if (BLOCKED_DEVICE_PATHS.has(slashed)) return true;
890
+ if (slashed.startsWith("/proc/") && (slashed.endsWith("/fd/0") || slashed.endsWith("/fd/1") || slashed.endsWith("/fd/2"))) {
724
891
  return true;
725
892
  }
726
- const baseName = normalized.split(/[/\\]/).pop() ?? "";
893
+ const baseName = slashed.split("/").pop() ?? "";
727
894
  if (WINDOWS_BLOCKED_NAMES.has(baseName.toUpperCase())) {
728
895
  return true;
729
896
  }
@@ -733,6 +900,9 @@ function validateAndResolvePath(rawPath, workingDir) {
733
900
  if (rawPath.includes("\0")) {
734
901
  return { ok: false, error: "\u8DEF\u5F84\u5305\u542B\u975E\u6CD5\u5B57\u7B26\uFF08null byte\uFF09" };
735
902
  }
903
+ if (isBlockedDevicePath(rawPath)) {
904
+ return { ok: false, error: `\u4E0D\u5141\u8BB8\u8BFB\u53D6\u8BBE\u5907\u6587\u4EF6\uFF1A${rawPath}\u3002\u8BE5\u8DEF\u5F84\u53EF\u80FD\u4EA7\u751F\u65E0\u9650\u8F93\u51FA\u6216\u963B\u585E\u8FDB\u7A0B\u3002` };
905
+ }
736
906
  const expanded = expandPath(rawPath);
737
907
  const resolved = resolve4(workingDir, expanded);
738
908
  if (isBlockedDevicePath(resolved)) {
@@ -778,6 +948,88 @@ function applyLineEnding(content, ending) {
778
948
  }
779
949
  return content;
780
950
  }
951
+ var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
952
+ ".png",
953
+ ".jpg",
954
+ ".jpeg",
955
+ ".gif",
956
+ ".webp",
957
+ ".bmp",
958
+ ".ico",
959
+ ".tiff",
960
+ ".tif",
961
+ ".pdf",
962
+ ".doc",
963
+ ".docx",
964
+ ".xls",
965
+ ".xlsx",
966
+ ".ppt",
967
+ ".pptx",
968
+ ".exe",
969
+ ".dll",
970
+ ".so",
971
+ ".dylib",
972
+ ".o",
973
+ ".a",
974
+ ".pyc",
975
+ ".pyo",
976
+ ".class",
977
+ ".jar",
978
+ ".zip",
979
+ ".tar",
980
+ ".gz",
981
+ ".bz2",
982
+ ".7z",
983
+ ".rar",
984
+ ".iso",
985
+ ".mp3",
986
+ ".mp4",
987
+ ".mov",
988
+ ".avi",
989
+ ".mkv",
990
+ ".wav",
991
+ ".flac",
992
+ ".ogg",
993
+ ".ttf",
994
+ ".otf",
995
+ ".woff",
996
+ ".woff2",
997
+ ".sqlite",
998
+ ".sqlite3",
999
+ ".db",
1000
+ ".psd",
1001
+ ".ai",
1002
+ ".bin",
1003
+ ".wasm"
1004
+ ]);
1005
+ function hasBinaryExtension(filePath) {
1006
+ const ext = extname(filePath).toLowerCase();
1007
+ return BINARY_EXTENSIONS.has(ext);
1008
+ }
1009
+ async function detectFileBomEncoding(filePath) {
1010
+ let fh = null;
1011
+ try {
1012
+ fh = await open(filePath, "r");
1013
+ const buf = Buffer.alloc(3);
1014
+ const { bytesRead } = await fh.read(buf, 0, 3, 0);
1015
+ if (bytesRead >= 3 && buf[0] === 239 && buf[1] === 187 && buf[2] === 191) {
1016
+ return "utf8-bom";
1017
+ }
1018
+ if (bytesRead >= 2 && buf[0] === 255 && buf[1] === 254) {
1019
+ return "utf16le";
1020
+ }
1021
+ return "utf8";
1022
+ } catch {
1023
+ return "utf8";
1024
+ } finally {
1025
+ if (fh) {
1026
+ try {
1027
+ await fh.close();
1028
+ } catch {
1029
+ }
1030
+ }
1031
+ }
1032
+ }
781
1033
  var LEFT_SINGLE_CURLY_QUOTE = "\u2018";
782
1034
  var RIGHT_SINGLE_CURLY_QUOTE = "\u2019";
783
1035
  var LEFT_DOUBLE_CURLY_QUOTE = "\u201C";
@@ -857,6 +1109,44 @@ function applyCurlySingleQuotes(str) {
857
1109
  return result.join("");
858
1110
  }
859
1111
 
1112
+ // src/tools/shared/fileState.ts
1113
+ import { existsSync as existsSync2, statSync } from "fs";
1114
+ var fileState = /* @__PURE__ */ new Map();
1115
+ function recordRead(absPath) {
1116
+ try {
1117
+ const st = statSync(absPath);
1118
+ fileState.set(absPath, { timestamp: st.mtimeMs, size: st.size });
1119
+ } catch {
1120
+ }
1121
+ }
1122
+ function assertFresh(absPath) {
1123
+ const entry = fileState.get(absPath);
1124
+ if (!entry) {
1125
+ if (existsSync2(absPath)) {
1126
+ return {
1127
+ ok: false,
1128
+ error: `\u6587\u4EF6 ${absPath} \u5DF2\u5B58\u5728\u4F46\u672A\u5728\u672C\u4F1A\u8BDD Read \u8FC7\u3002\u8BF7\u5148\u7528 Read \u5DE5\u5177\u8BFB\u53D6\uFF0C\u786E\u8BA4\u5185\u5BB9\u540E\u518D\u4FEE\u6539\u3002`
1129
+ };
1130
+ }
1131
+ return { ok: true };
1132
+ }
1133
+ try {
1134
+ const st = statSync(absPath);
1135
+ if (st.mtimeMs > entry.timestamp) {
1136
+ return {
1137
+ ok: false,
1138
+ error: `${absPath} \u5728 Read \u540E\u88AB\u5916\u90E8\u4FEE\u6539\uFF08mtime \u6F02\u79FB\uFF09\u3002\u8BF7\u91CD\u65B0\u7528 Read \u5DE5\u5177\u8BFB\u53D6\u6700\u65B0\u5185\u5BB9\u3002`
1139
+ };
1140
+ }
1141
+ } catch {
1142
+ return { ok: true };
1143
+ }
1144
+ return { ok: true };
1145
+ }
1146
+ function clearFileState() {
1147
+ fileState.clear();
1148
+ }
1149
+
860
1150
  // src/tools/edit/edit.ts
861
1151
  var MAX_EDIT_FILE_SIZE_BYTES = 1024 * 1024 * 1024;
862
1152
  var inputSchema2 = z2.object({
@@ -885,10 +1175,14 @@ async function call2(input) {
885
1175
  const pathResult = validateAndResolvePath(input.file_path, getWorkingDir());
886
1176
  if (!pathResult.ok) return pathResult;
887
1177
  const filePath = pathResult.resolvedPath;
1178
+ const freshness = assertFresh(filePath);
1179
+ if (!freshness.ok) {
1180
+ return { ok: false, error: freshness.error };
1181
+ }
888
1182
  if (old_string === new_string) {
889
1183
  return { ok: false, error: "old_string \u4E0E new_string \u5B8C\u5168\u76F8\u540C\uFF0C\u6CA1\u6709\u53EF\u6539\u7684\u5185\u5BB9\u3002" };
890
1184
  }
891
- if (old_string === "" && !existsSync2(filePath)) {
1185
+ if (old_string === "" && !existsSync3(filePath)) {
892
1186
  try {
893
1187
  await mkdir4(dirname5(filePath), { recursive: true });
894
1188
  await writeFile3(filePath, new_string, "utf8");
@@ -900,7 +1194,7 @@ async function call2(input) {
900
1194
  return { ok: false, error: `\u521B\u5EFA\u6587\u4EF6\u5931\u8D25\uFF1A${e.message}` };
901
1195
  }
902
1196
  }
903
- if (!existsSync2(filePath)) {
1197
+ if (!existsSync3(filePath)) {
904
1198
  return {
905
1199
  ok: false,
906
1200
  error: `\u6587\u4EF6\u4E0D\u5B58\u5728\uFF1A${filePath}
@@ -1053,22 +1347,146 @@ var editTool = {
1053
1347
  call: call2
1054
1348
  };
1055
1349
 
1350
+ // src/tools/edit/multi-edit.ts
1351
+ import { readFile as readFile7, writeFile as writeFile4 } from "fs/promises";
1352
+ import { existsSync as existsSync4 } from "fs";
1353
+ import { z as z3 } from "zod";
1354
+ var editItemSchema = z3.object({
1355
+ old_string: z3.string().min(1).describe("\u8981\u66FF\u6362\u7684\u539F\u6587\u672C\uFF08\u4E0D\u5141\u8BB8\u4E3A\u7A7A \u2014\u2014 \u521B\u5EFA\u65B0\u6587\u4EF6\u8BF7\u7528 Edit \u5DE5\u5177\uFF09"),
1356
+ new_string: z3.string().describe("\u66FF\u6362\u4E3A\u7684\u65B0\u6587\u672C"),
1357
+ replace_all: z3.boolean().optional().describe("\u662F\u5426\u66FF\u6362\u6240\u6709\u51FA\u73B0\u4F4D\u7F6E\uFF08\u9ED8\u8BA4 false\uFF0C\u8981\u6C42 old_string \u5728\u5F53\u524D\u5185\u5BB9\u4E2D\u552F\u4E00\uFF09")
1358
+ });
1359
+ var inputSchema3 = z3.object({
1360
+ file_path: z3.string().min(1).describe("\u8981\u7F16\u8F91\u7684\u6587\u4EF6\u8DEF\u5F84"),
1361
+ edits: z3.array(editItemSchema).min(1).max(50).describe("\u6309\u987A\u5E8F\u5E94\u7528\u7684 edit \u5217\u8868\uFF081-50 \u6761\uFF09\uFF0C\u539F\u5B50\u5316\u6267\u884C\uFF1A\u5168\u90E8\u6210\u529F\u624D\u843D\u76D8")
1362
+ });
1363
+ var parameters3 = toToolParameters(inputSchema3);
1364
+ var description3 = `Performs multiple exact string replacements in a single file, applied atomically (all-or-nothing).
1365
+
1366
+ Usage:
1367
+ - You MUST use your \`Read\` tool to read the current content of the file BEFORE calling MultiEdit.
1368
+ - Provide a list of \`edits\`, each with \`old_string\`, \`new_string\`, and optional \`replace_all\`.
1369
+ - All edits are applied sequentially in memory; if ANY edit fails (string not found, ambiguous match, or dependency conflict), the file on disk is UNTOUCHED.
1370
+ - Order matters: later edits operate on the result of earlier edits. If a later edit's \`old_string\` is a substring of an earlier edit's \`new_string\`, MultiEdit refuses (reorder or merge instead).
1371
+ - Each \`old_string\` must be unique in the current content (after prior edits) unless \`replace_all=true\`.
1372
+ - Empty \`old_string\` is NOT allowed in MultiEdit. To create a new file, use the \`Edit\` tool with a single empty-old_string call.
1373
+ - Preserve exact indentation (tabs/spaces).`;
1374
+ function countOccurrences2(haystack, needle) {
1375
+ if (needle.length === 0) return 0;
1376
+ let count = 0;
1377
+ let pos = 0;
1378
+ while ((pos = haystack.indexOf(needle, pos)) !== -1) {
1379
+ count++;
1380
+ pos += needle.length;
1381
+ }
1382
+ return count;
1383
+ }
1384
+ function splitReplaceAll2(haystack, needle, replacement) {
1385
+ return haystack.split(needle).join(replacement);
1386
+ }
1387
+ async function call3(input) {
1388
+ const pathResult = validateAndResolvePath(input.file_path, getWorkingDir());
1389
+ if (!pathResult.ok) return pathResult;
1390
+ const filePath = pathResult.resolvedPath;
1391
+ const freshness = assertFresh(filePath);
1392
+ if (!freshness.ok) {
1393
+ return { ok: false, error: freshness.error };
1394
+ }
1395
+ if (!existsSync4(filePath)) {
1396
+ return {
1397
+ ok: false,
1398
+ error: `\u6587\u4EF6\u4E0D\u5B58\u5728\uFF1A${filePath}
1399
+ \uFF08MultiEdit \u4E0D\u652F\u6301\u521B\u5EFA\u65B0\u6587\u4EF6\uFF0C\u8BF7\u6539\u7528 Edit \u5DE5\u5177\uFF09`
1400
+ };
1401
+ }
1402
+ const edits = input.edits;
1403
+ for (let i = 0; i < edits.length; i++) {
1404
+ for (let j = 0; j < i; j++) {
1405
+ if (edits[j].new_string.includes(edits[i].old_string)) {
1406
+ return {
1407
+ ok: false,
1408
+ error: `edits[${i}].old_string \u662F edits[${j}].new_string \u7684\u5B50\u4E32\uFF0C\u4F1A\u5BFC\u81F4\u540E\u7EED edit \u547D\u4E2D\u524D\u5E8F\u4EA7\u7269\uFF0C\u8BF7\u91CD\u65B0\u6392\u5E8F\u6216\u5408\u5E76`
1409
+ };
1410
+ }
1411
+ }
1412
+ }
1413
+ let originalContent;
1414
+ try {
1415
+ originalContent = await readFile7(filePath, "utf8");
1416
+ } catch (e) {
1417
+ return { ok: false, error: `\u8BFB\u53D6\u5931\u8D25\uFF1A${e.message}` };
1418
+ }
1419
+ let currentContent = originalContent;
1420
+ for (let i = 0; i < edits.length; i++) {
1421
+ const edit = edits[i];
1422
+ const replaceAll = edit.replace_all ?? false;
1423
+ let searchTarget = edit.old_string;
1424
+ let processedNewString = edit.new_string;
1425
+ const actualOld = findActualString(currentContent, edit.old_string);
1426
+ if (actualOld === null) {
1427
+ return {
1428
+ ok: false,
1429
+ error: `edits[${i}] \u672A\u5339\u914D\u5230 old_string\uFF08\u5728\u5DF2\u5E94\u7528\u524D\u5E8F ${i} \u5904\u4FEE\u6539\u540E\u7684\u5185\u5BB9\u4E2D\u627E\u4E0D\u5230\uFF09\u3002\u8BF7\u5148\u7528 Read \u5DE5\u5177\u6838\u5BF9\u5F53\u524D\u5185\u5BB9\uFF08\u6CE8\u610F\u7A7A\u683C/\u7F29\u8FDB/\u6362\u884C\uFF09\uFF0C\u5E76\u68C0\u67E5 edit \u987A\u5E8F\u3002`
1430
+ };
1431
+ }
1432
+ if (actualOld !== edit.old_string) {
1433
+ searchTarget = actualOld;
1434
+ processedNewString = preserveQuoteStyle(edit.old_string, actualOld, edit.new_string);
1435
+ }
1436
+ const occurrences = countOccurrences2(currentContent, searchTarget);
1437
+ if (occurrences === 0) {
1438
+ return {
1439
+ ok: false,
1440
+ error: `edits[${i}] \u672A\u5339\u914D\u5230 old_string\uFF08\u5DF2\u5E94\u7528\u524D\u5E8F ${i} \u5904\u4FEE\u6539\u540E\u5185\u5BB9\u4E2D\u51FA\u73B0 0 \u6B21\uFF09\u3002`
1441
+ };
1442
+ }
1443
+ if (occurrences > 1 && !replaceAll) {
1444
+ return {
1445
+ ok: false,
1446
+ error: `edits[${i}].old_string \u5728\u5F53\u524D\u5185\u5BB9\u4E2D\u51FA\u73B0 ${occurrences} \u6B21\uFF0C\u4E0D\u552F\u4E00\u3002\u8BF7\u6269\u5927 old_string \u5305\u542B\u66F4\u591A\u4E0A\u4E0B\u6587\uFF0C\u6216\u663E\u5F0F\u4F20 replace_all=true\u3002`
1447
+ };
1448
+ }
1449
+ currentContent = replaceAll ? splitReplaceAll2(currentContent, searchTarget, processedNewString) : currentContent.replace(searchTarget, processedNewString);
1450
+ }
1451
+ const lineEnding = await detectFileLineEndings(filePath);
1452
+ const finalContent = applyLineEnding(currentContent, lineEnding);
1453
+ try {
1454
+ await writeFile4(filePath, finalContent, "utf8");
1455
+ } catch (e) {
1456
+ return { ok: false, error: `\u5199\u5165\u5931\u8D25\uFF1A${e.message}` };
1457
+ }
1458
+ return {
1459
+ ok: true,
1460
+ content: `\u5DF2\u5BF9 ${filePath} \u5E94\u7528 ${edits.length} \u5904\u4FEE\u6539`
1461
+ };
1462
+ }
1463
+ var multiEditTool = {
1464
+ name: "MultiEdit",
1465
+ description: description3,
1466
+ inputSchema: inputSchema3,
1467
+ parameters: parameters3,
1468
+ isReadOnly: false,
1469
+ isConcurrencySafe: false,
1470
+ maxResultSizeChars: DEFAULT_MAX_RESULT_SIZE_CHARS,
1471
+ call: call3
1472
+ };
1473
+
1056
1474
  // src/tools/glob/glob.ts
1057
1475
  import { stat as stat2 } from "fs/promises";
1058
1476
  import { isAbsolute, resolve as resolve5 } from "path";
1059
1477
  import fg from "fast-glob";
1060
- import { z as z3 } from "zod";
1061
- var inputSchema3 = z3.object({
1062
- pattern: z3.string().min(1).describe('glob \u6A21\u5F0F\uFF0C\u4F8B\u5982 "**/*.ts" \u6216 "src/components/**/*.tsx"'),
1063
- path: z3.string().optional().describe('\u641C\u7D22\u7684\u6839\u76EE\u5F55\uFF08\u9ED8\u8BA4\u5F53\u524D\u5DE5\u4F5C\u76EE\u5F55\uFF09\uFF1B\u7701\u7565\u65F6\u4E0D\u8981\u4F20 "undefined" \u5B57\u7B26\u4E32')
1478
+ import { z as z4 } from "zod";
1479
+ var inputSchema4 = z4.object({
1480
+ pattern: z4.string().min(1).describe('glob \u6A21\u5F0F\uFF0C\u4F8B\u5982 "**/*.ts" \u6216 "src/components/**/*.tsx"'),
1481
+ path: z4.string().optional().describe('\u641C\u7D22\u7684\u6839\u76EE\u5F55\uFF08\u9ED8\u8BA4\u5F53\u524D\u5DE5\u4F5C\u76EE\u5F55\uFF09\uFF1B\u7701\u7565\u65F6\u4E0D\u8981\u4F20 "undefined" \u5B57\u7B26\u4E32')
1064
1482
  });
1065
- var parameters3 = toToolParameters(inputSchema3);
1066
- var description3 = `- Fast file pattern matching tool that works with any codebase size
1483
+ var parameters4 = toToolParameters(inputSchema4);
1484
+ var description4 = `- Fast file pattern matching tool that works with any codebase size
1067
1485
  - Supports glob patterns like "**/*.js" or "src/**/*.ts"
1068
1486
  - Returns matching file paths sorted by modification time (oldest first)
1069
1487
  - Use this tool when you need to find files by name patterns
1070
1488
  - When you need to do an open ended search that may require multiple rounds, prefer the Grep tool for content search`;
1071
- async function call3(input) {
1489
+ async function call4(input) {
1072
1490
  const cwd = input.path ? resolve5(input.path) : getWorkingDir();
1073
1491
  const pattern = input.pattern.replace(/\\/g, "/");
1074
1492
  let matches;
@@ -1115,23 +1533,23 @@ async function call3(input) {
1115
1533
  }
1116
1534
  var globTool = {
1117
1535
  name: "Glob",
1118
- description: description3,
1119
- inputSchema: inputSchema3,
1120
- parameters: parameters3,
1536
+ description: description4,
1537
+ inputSchema: inputSchema4,
1538
+ parameters: parameters4,
1121
1539
  isReadOnly: true,
1122
1540
  isConcurrencySafe: true,
1123
1541
  maxResultSizeChars: DEFAULT_MAX_RESULT_SIZE_CHARS,
1124
- call: call3
1542
+ call: call4
1125
1543
  };
1126
1544
 
1127
1545
  // src/tools/grep/grep.ts
1128
1546
  import { spawn as spawn3 } from "child_process";
1129
1547
  import { resolve as resolve7 } from "path";
1130
- import { z as z4 } from "zod";
1548
+ import { z as z5 } from "zod";
1131
1549
 
1132
1550
  // src/tools/grep/rgPath.ts
1133
1551
  import { spawn as spawn2 } from "child_process";
1134
- import { chmodSync, existsSync as existsSync3 } from "fs";
1552
+ import { chmodSync, existsSync as existsSync5 } from "fs";
1135
1553
  import { resolve as resolve6 } from "path";
1136
1554
  var cached;
1137
1555
  async function resolveRgPath() {
@@ -1141,15 +1559,15 @@ async function resolveRgPath() {
1141
1559
  }
1142
1560
  async function detect() {
1143
1561
  const fromEnv = process.env.MINIMAL_AGENT_RIPGREP_PATH;
1144
- if (fromEnv && existsSync3(fromEnv)) return fromEnv;
1562
+ if (fromEnv && existsSync5(fromEnv)) return fromEnv;
1145
1563
  const vendored = vendoredRgPath();
1146
- if (vendored && existsSync3(vendored)) {
1564
+ if (vendored && existsSync5(vendored)) {
1147
1565
  ensureExecutable(vendored);
1148
1566
  return vendored;
1149
1567
  }
1150
1568
  if (await trySpawn("rg")) return "rg";
1151
1569
  for (const candidate of claudeCodeCandidates()) {
1152
- if (existsSync3(candidate)) {
1570
+ if (existsSync5(candidate)) {
1153
1571
  ensureExecutable(candidate);
1154
1572
  return candidate;
1155
1573
  }
@@ -1242,21 +1660,21 @@ function claudeCodeCandidates() {
1242
1660
  }
1243
1661
 
1244
1662
  // src/tools/grep/grep.ts
1245
- var inputSchema4 = z4.object({
1246
- pattern: z4.string().min(1).describe("\u6B63\u5219\u8868\u8FBE\u5F0F\uFF08ripgrep \u517C\u5BB9\u8BED\u6CD5\uFF09"),
1247
- path: z4.string().optional().describe("\u641C\u7D22\u7684\u6839\u76EE\u5F55\u6216\u6587\u4EF6\uFF08\u9ED8\u8BA4\u5F53\u524D\u5DE5\u4F5C\u76EE\u5F55\uFF09"),
1248
- glob: z4.string().optional().describe('\u6587\u4EF6\u540D glob \u8FC7\u6EE4\uFF0C\u5982 "*.ts"'),
1249
- type: z4.string().optional().describe('rg \u7684\u6587\u4EF6\u7C7B\u578B\u5FEB\u6377\u540D\uFF0C\u5982 "py"\u3001"rust"\u3001"js"'),
1250
- output_mode: z4.enum(["content", "files_with_matches", "count"]).optional().describe("\u8F93\u51FA\u6A21\u5F0F\uFF1Acontent=\u5339\u914D\u884C\uFF1Bfiles_with_matches=\u53EA\u5217\u6587\u4EF6\uFF1Bcount=\u6BCF\u6587\u4EF6\u8BA1\u6570"),
1251
- "-i": z4.boolean().optional().describe("\u5FFD\u7565\u5927\u5C0F\u5199"),
1252
- "-n": z4.boolean().optional().describe("\u663E\u793A\u884C\u53F7\uFF08\u4EC5 content \u6A21\u5F0F\uFF09"),
1253
- "-A": z4.number().int().min(0).optional().describe("\u5339\u914D\u540E\u5C55\u793A\u51E0\u884C\u4E0A\u4E0B\u6587"),
1254
- "-B": z4.number().int().min(0).optional().describe("\u5339\u914D\u524D\u5C55\u793A\u51E0\u884C\u4E0A\u4E0B\u6587"),
1255
- "-C": z4.number().int().min(0).optional().describe("\u5339\u914D\u524D\u540E\u5404\u5C55\u793A\u51E0\u884C\uFF08\u8986\u76D6 -A/-B\uFF09"),
1256
- head_limit: z4.number().int().positive().optional().describe("\u8F93\u51FA\u6700\u591A\u4FDD\u7559\u524D N \u884C\uFF08\u9632\u6B62\u7ED3\u679C\u8FC7\u5927\uFF09")
1663
+ var inputSchema5 = z5.object({
1664
+ pattern: z5.string().min(1).describe("\u6B63\u5219\u8868\u8FBE\u5F0F\uFF08ripgrep \u517C\u5BB9\u8BED\u6CD5\uFF09"),
1665
+ path: z5.string().optional().describe("\u641C\u7D22\u7684\u6839\u76EE\u5F55\u6216\u6587\u4EF6\uFF08\u9ED8\u8BA4\u5F53\u524D\u5DE5\u4F5C\u76EE\u5F55\uFF09"),
1666
+ glob: z5.string().optional().describe('\u6587\u4EF6\u540D glob \u8FC7\u6EE4\uFF0C\u5982 "*.ts"'),
1667
+ type: z5.string().optional().describe('rg \u7684\u6587\u4EF6\u7C7B\u578B\u5FEB\u6377\u540D\uFF0C\u5982 "py"\u3001"rust"\u3001"js"'),
1668
+ output_mode: z5.enum(["content", "files_with_matches", "count"]).optional().describe("\u8F93\u51FA\u6A21\u5F0F\uFF1Acontent=\u5339\u914D\u884C\uFF1Bfiles_with_matches=\u53EA\u5217\u6587\u4EF6\uFF1Bcount=\u6BCF\u6587\u4EF6\u8BA1\u6570"),
1669
+ "-i": z5.boolean().optional().describe("\u5FFD\u7565\u5927\u5C0F\u5199"),
1670
+ "-n": z5.boolean().optional().describe("\u663E\u793A\u884C\u53F7\uFF08\u4EC5 content \u6A21\u5F0F\uFF09"),
1671
+ "-A": z5.number().int().min(0).optional().describe("\u5339\u914D\u540E\u5C55\u793A\u51E0\u884C\u4E0A\u4E0B\u6587"),
1672
+ "-B": z5.number().int().min(0).optional().describe("\u5339\u914D\u524D\u5C55\u793A\u51E0\u884C\u4E0A\u4E0B\u6587"),
1673
+ "-C": z5.number().int().min(0).optional().describe("\u5339\u914D\u524D\u540E\u5404\u5C55\u793A\u51E0\u884C\uFF08\u8986\u76D6 -A/-B\uFF09"),
1674
+ head_limit: z5.number().int().positive().optional().describe("\u8F93\u51FA\u6700\u591A\u4FDD\u7559\u524D N \u884C\uFF08\u9632\u6B62\u7ED3\u679C\u8FC7\u5927\uFF09")
1257
1675
  });
1258
- var parameters4 = toToolParameters(inputSchema4);
1259
- var description4 = `A powerful search tool built on ripgrep.
1676
+ var parameters5 = toToolParameters(inputSchema5);
1677
+ var description5 = `A powerful search tool built on ripgrep.
1260
1678
 
1261
1679
  Usage:
1262
1680
  - ALWAYS use Grep for content search tasks. Do NOT invoke \`grep\` or \`rg\` directly via Bash.
@@ -1264,7 +1682,7 @@ Usage:
1264
1682
  - Filter files with glob parameter (e.g., "*.js", "**/*.tsx") or type parameter (e.g., "js", "py", "rust")
1265
1683
  - Output modes: "content" shows matching lines, "files_with_matches" shows only file paths (default), "count" shows match counts
1266
1684
  - Pattern syntax: Uses ripgrep (not classic grep)`;
1267
- async function call4(input, signal) {
1685
+ async function call5(input, signal) {
1268
1686
  const args = [];
1269
1687
  const mode = input.output_mode ?? "files_with_matches";
1270
1688
  if (mode === "files_with_matches") args.push("-l");
@@ -1341,27 +1759,27 @@ async function call4(input, signal) {
1341
1759
  }
1342
1760
  var grepTool = {
1343
1761
  name: "Grep",
1344
- description: description4,
1345
- inputSchema: inputSchema4,
1346
- parameters: parameters4,
1762
+ description: description5,
1763
+ inputSchema: inputSchema5,
1764
+ parameters: parameters5,
1347
1765
  isReadOnly: true,
1348
1766
  isConcurrencySafe: true,
1349
1767
  maxResultSizeChars: DEFAULT_MAX_RESULT_SIZE_CHARS,
1350
- call: call4
1768
+ call: call5
1351
1769
  };
1352
1770
 
1353
1771
  // src/tools/read/read.ts
1354
1772
  import { createReadStream } from "fs";
1355
- import { readFile as readFile7, stat as stat3 } from "fs/promises";
1773
+ import { readFile as readFile8, stat as stat3 } from "fs/promises";
1356
1774
  import { createInterface } from "readline";
1357
- import { z as z5 } from "zod";
1358
- var inputSchema5 = z5.object({
1359
- file_path: z5.string().min(1, "\u5FC5\u987B\u63D0\u4F9B file_path").describe("\u8981\u8BFB\u53D6\u7684\u6587\u4EF6\u8DEF\u5F84\uFF0C\u7EDD\u5BF9\u8DEF\u5F84\u4F18\u5148"),
1360
- offset: z5.number().int().positive().optional().describe("\u8D77\u59CB\u884C\u53F7\uFF081-indexed\uFF09\uFF1B\u4E0D\u586B\u5219\u4ECE\u6587\u4EF6\u5F00\u5934\u8BFB"),
1361
- limit: z5.number().int().positive().optional().describe(`\u6700\u591A\u8BFB\u591A\u5C11\u884C\uFF1B\u4E0D\u586B\u5219\u7528\u9ED8\u8BA4\u503C ${MAX_LINES_TO_READ}`)
1775
+ import { z as z6 } from "zod";
1776
+ var inputSchema6 = z6.object({
1777
+ file_path: z6.string().min(1, "\u5FC5\u987B\u63D0\u4F9B file_path").describe("\u8981\u8BFB\u53D6\u7684\u6587\u4EF6\u8DEF\u5F84\uFF0C\u7EDD\u5BF9\u8DEF\u5F84\u4F18\u5148"),
1778
+ offset: z6.number().int().positive().optional().describe("\u8D77\u59CB\u884C\u53F7\uFF081-indexed\uFF09\uFF1B\u4E0D\u586B\u5219\u4ECE\u6587\u4EF6\u5F00\u5934\u8BFB"),
1779
+ limit: z6.number().int().positive().optional().describe(`\u6700\u591A\u8BFB\u591A\u5C11\u884C\uFF1B\u4E0D\u586B\u5219\u7528\u9ED8\u8BA4\u503C ${MAX_LINES_TO_READ}`)
1362
1780
  });
1363
- var parameters5 = toToolParameters(inputSchema5);
1364
- var description5 = `Reads a file from the local filesystem. You can access any file directly by using this tool.
1781
+ var parameters6 = toToolParameters(inputSchema6);
1782
+ var description6 = `Reads a file from the local filesystem. You can access any file directly by using this tool.
1365
1783
  Assume this tool is able to read all files on the machine. If the User provides a path to a file assume that path is valid. It is okay to read a file that does not exist; an error will be returned.
1366
1784
 
1367
1785
  Usage:
@@ -1370,9 +1788,10 @@ Usage:
1370
1788
  - You can optionally specify a line offset and limit (especially handy for long files)
1371
1789
  - Results are returned using cat -n format, with line numbers starting at 1
1372
1790
  - This tool can only read text files, not directories. To read a directory, use the Glob tool.
1373
- - If you read a file that exists but has empty contents you will receive a warning in place of file contents.`;
1791
+ - If you read a file that exists but has empty contents you will receive a warning in place of file contents.
1792
+ - This tool cannot read binary files. Files whose extensions are on the binary blocklist (images: .png/.jpg/.gif/.webp/..., documents: .pdf/.docx/.xlsx/..., executables: .exe/.dll/.so/..., archives: .zip/.tar/.gz/..., and others) will be rejected with a clear error. If the file is actually text despite the extension (e.g., a misnamed log), rename it or use Bash \`cat\` to read it directly \u2014 do not retry Read with the same path.`;
1374
1793
  var STREAM_THRESHOLD = 1024 * 1024;
1375
- async function call5(input) {
1794
+ async function call6(input) {
1376
1795
  const offset = input.offset ?? 1;
1377
1796
  const limit = input.limit ?? MAX_LINES_TO_READ;
1378
1797
  const pathResult = validateAndResolvePath(input.file_path, getWorkingDir());
@@ -1383,6 +1802,12 @@ async function call5(input) {
1383
1802
  if (isBlockedDevicePath(filePath)) {
1384
1803
  return { ok: false, error: `\u4E0D\u5141\u8BB8\u8BFB\u53D6\u8BBE\u5907\u6587\u4EF6\uFF1A${filePath}\u3002\u8BE5\u8DEF\u5F84\u53EF\u80FD\u4EA7\u751F\u65E0\u9650\u8F93\u51FA\u6216\u963B\u585E\u8FDB\u7A0B\u3002` };
1385
1804
  }
1805
+ if (hasBinaryExtension(filePath)) {
1806
+ return {
1807
+ ok: false,
1808
+ error: `\u4E0D\u652F\u6301\u8BFB\u53D6\u4E8C\u8FDB\u5236\u6587\u4EF6\uFF1A${filePath}\uFF08\u6269\u5C55\u540D\u547D\u4E2D\u4E8C\u8FDB\u5236\u9ED1\u540D\u5355\uFF09\u3002\u82E5\u8BE5\u6587\u4EF6\u5B9E\u9645\u4E3A\u6587\u672C\uFF0C\u53EF\u6539\u540E\u7F00\u6216\u7528 Bash cat \u65C1\u8DEF\u3002`
1809
+ };
1810
+ }
1386
1811
  let st;
1387
1812
  try {
1388
1813
  st = await stat3(filePath);
@@ -1408,6 +1833,7 @@ async function call5(input) {
1408
1833
  numbered = result.numbered;
1409
1834
  totalLines = result.totalLines;
1410
1835
  if (result.isEmpty) {
1836
+ recordRead(filePath);
1411
1837
  return { ok: true, content: "<file is empty>" };
1412
1838
  }
1413
1839
  } else {
@@ -1416,6 +1842,7 @@ async function call5(input) {
1416
1842
  totalLines = result.totalLines;
1417
1843
  }
1418
1844
  if (totalLines === 0 || !numbered) {
1845
+ recordRead(filePath);
1419
1846
  return { ok: true, content: "<file is empty>" };
1420
1847
  }
1421
1848
  let content = numbered;
@@ -1438,10 +1865,11 @@ async function call5(input) {
1438
1865
 
1439
1866
  \u26A0\uFE0F \u6CE8\u610F\uFF1A\u8FD9\u662F\u4E00\u4E2A\u5927\u6587\u4EF6\uFF08${(st.size / 1024).toFixed(1)} KB\uFF09\u3002\u5EFA\u8BAE\u7528 offset/limit \u5206\u6BB5\u8BFB\u53D6\uFF0C\u4F8B\u5982\u5148\u8BFB\u5173\u952E\u90E8\u5206\uFF08imports\u3001exports\u3001\u51FD\u6570\u7B7E\u540D\uFF09\u3002`;
1440
1867
  }
1868
+ recordRead(filePath);
1441
1869
  return { ok: true, content };
1442
1870
  }
1443
1871
  async function readSmallFile(filePath, offset, limit) {
1444
- const raw = await readFile7(filePath, "utf8");
1872
+ const raw = await readFile8(filePath, "utf8");
1445
1873
  if (raw.length === 0) {
1446
1874
  return { numbered: "", totalLines: 0, isEmpty: true };
1447
1875
  }
@@ -1491,17 +1919,17 @@ async function readLargeFileStream(filePath, offset, limit) {
1491
1919
  }
1492
1920
  var readTool = {
1493
1921
  name: "Read",
1494
- description: description5,
1495
- inputSchema: inputSchema5,
1496
- parameters: parameters5,
1922
+ description: description6,
1923
+ inputSchema: inputSchema6,
1924
+ parameters: parameters6,
1497
1925
  isReadOnly: true,
1498
1926
  isConcurrencySafe: true,
1499
1927
  maxResultSizeChars: DEFAULT_MAX_RESULT_SIZE_CHARS,
1500
- call: call5
1928
+ call: call6
1501
1929
  };
1502
1930
 
1503
1931
  // src/tools/webfetch/webfetch.ts
1504
- import { z as z6 } from "zod";
1932
+ import { z as z7 } from "zod";
1505
1933
 
1506
1934
  // src/tools/webfetch/preapproved.ts
1507
1935
  var PREAPPROVED_HOSTS = /* @__PURE__ */ new Set([
@@ -1786,12 +2214,12 @@ function cleanCache() {
1786
2214
  }
1787
2215
  }
1788
2216
  }
1789
- var inputSchema6 = z6.object({
1790
- url: z6.string().describe("\u8981\u83B7\u53D6\u5185\u5BB9\u7684 URL"),
1791
- prompt: z6.string().describe("\u5BF9\u5185\u5BB9\u8FDB\u884C\u5904\u7406\u7684\u6307\u4EE4\uFF0C\u63CF\u8FF0\u4F60\u60F3\u4ECE\u9875\u9762\u63D0\u53D6\u4EC0\u4E48\u4FE1\u606F")
2217
+ var inputSchema7 = z7.object({
2218
+ url: z7.string().describe("\u8981\u83B7\u53D6\u5185\u5BB9\u7684 URL"),
2219
+ prompt: z7.string().describe("\u5BF9\u5185\u5BB9\u8FDB\u884C\u5904\u7406\u7684\u6307\u4EE4\uFF0C\u63CF\u8FF0\u4F60\u60F3\u4ECE\u9875\u9762\u63D0\u53D6\u4EC0\u4E48\u4FE1\u606F")
1792
2220
  });
1793
- var parameters6 = toToolParameters(inputSchema6);
1794
- var description6 = `- Fetches content from a specified URL and processes it using an AI model.
2221
+ var parameters7 = toToolParameters(inputSchema7);
2222
+ var description7 = `- Fetches content from a specified URL and processes it using an AI model.
1795
2223
  - Takes a URL and a prompt as input.
1796
2224
  - Fetches the URL content, converts HTML to markdown.
1797
2225
  - Processes the content with the prompt (e.g., extract summary, find specific info).
@@ -1900,7 +2328,7 @@ async function htmlToMarkdown(html) {
1900
2328
  const td = new TurndownService();
1901
2329
  return td.turndown(html);
1902
2330
  }
1903
- async function call6(input, signal) {
2331
+ async function call7(input, signal) {
1904
2332
  const { url } = input;
1905
2333
  const start = Date.now();
1906
2334
  const cacheKey = url;
@@ -1998,17 +2426,17 @@ ${content}`;
1998
2426
  }
1999
2427
  var webfetchTool = {
2000
2428
  name: "WebFetch",
2001
- description: description6,
2002
- inputSchema: inputSchema6,
2003
- parameters: parameters6,
2429
+ description: description7,
2430
+ inputSchema: inputSchema7,
2431
+ parameters: parameters7,
2004
2432
  isReadOnly: true,
2005
2433
  isConcurrencySafe: true,
2006
2434
  maxResultSizeChars: DEFAULT_MAX_RESULT_SIZE_CHARS,
2007
- call: call6
2435
+ call: call7
2008
2436
  };
2009
2437
 
2010
2438
  // src/tools/webbrowser/webbrowser.ts
2011
- import { z as z7 } from "zod";
2439
+ import { z as z8 } from "zod";
2012
2440
 
2013
2441
  // src/tools/webbrowser/browser.ts
2014
2442
  import os from "os";
@@ -2044,15 +2472,15 @@ function screenshotPath(prefix = "browser") {
2044
2472
  }
2045
2473
 
2046
2474
  // src/tools/webbrowser/webbrowser.ts
2047
- var inputSchema7 = z7.object({
2048
- action: z7.enum(["navigate", "screenshot", "getContent", "click", "fill", "submit"]).describe("Browser action to perform"),
2049
- url: z7.string().url().optional().describe("URL to navigate to (required for navigate action)"),
2050
- selector: z7.string().optional().describe("CSS selector for click/fill/submit actions"),
2051
- value: z7.string().optional().describe("Value to fill in input fields"),
2052
- timeout: z7.number().int().positive().optional().describe("Timeout in milliseconds (default: 30000)")
2475
+ var inputSchema8 = z8.object({
2476
+ action: z8.enum(["navigate", "screenshot", "getContent", "click", "fill", "submit"]).describe("Browser action to perform"),
2477
+ url: z8.string().url().optional().describe("URL to navigate to (required for navigate action)"),
2478
+ selector: z8.string().optional().describe("CSS selector for click/fill/submit actions"),
2479
+ value: z8.string().optional().describe("Value to fill in input fields"),
2480
+ timeout: z8.number().int().positive().optional().describe("Timeout in milliseconds (default: 30000)")
2053
2481
  });
2054
- var parameters7 = toToolParameters(inputSchema7);
2055
- var description7 = `Control a headless web browser. Navigate to URLs, take screenshots, and interact with web pages.
2482
+ var parameters8 = toToolParameters(inputSchema8);
2483
+ var description8 = `Control a headless web browser. Navigate to URLs, take screenshots, and interact with web pages.
2056
2484
 
2057
2485
  When to use WebBrowser vs WebSearch:
2058
2486
  - WebSearch: When you need to find information or discover URLs through search
@@ -2083,7 +2511,7 @@ Example actions:
2083
2511
  - Take screenshot: { action: "screenshot" }
2084
2512
  - Click element: { action: "click", selector: "#submit-btn" }
2085
2513
  - Fill form field: { action: "fill", selector: "input[name='email']", value: "user@example.com" }`;
2086
- async function call7(input, signal) {
2514
+ async function call8(input, signal) {
2087
2515
  const { action, url, selector, value, timeout = 3e4 } = input;
2088
2516
  let page;
2089
2517
  try {
@@ -2187,30 +2615,30 @@ Current URL: ${page.url()}`
2187
2615
  }
2188
2616
  var webbrowserTool = {
2189
2617
  name: "WebBrowser",
2190
- description: description7,
2191
- inputSchema: inputSchema7,
2192
- parameters: parameters7,
2618
+ description: description8,
2619
+ inputSchema: inputSchema8,
2620
+ parameters: parameters8,
2193
2621
  isReadOnly: false,
2194
2622
  isConcurrencySafe: false,
2195
2623
  maxResultSizeChars: DEFAULT_MAX_RESULT_SIZE_CHARS,
2196
- call: call7
2624
+ call: call8
2197
2625
  };
2198
2626
 
2199
2627
  // src/tools/websearch/websearch.ts
2200
- import { z as z8 } from "zod";
2201
- var inputSchema8 = z8.object({
2202
- query: z8.string().min(1, "\u5FC5\u987B\u63D0\u4F9B\u641C\u7D22\u5173\u952E\u8BCD").max(400, "\u641C\u7D22\u5173\u952E\u8BCD\u592A\u957F\uFF08>400 \u5B57\uFF09").describe("\u641C\u7D22\u5173\u952E\u8BCD\uFF0C\u5EFA\u8BAE\u81EA\u7136\u8BED\u8A00\u63CF\u8FF0\u9700\u8981\u67E5\u7684\u4FE1\u606F"),
2203
- max_results: z8.number().int().min(1).max(20).optional().describe("\u8FD4\u56DE\u7ED3\u679C\u6570\u91CF\uFF0C1-20\uFF0C\u9ED8\u8BA4 5"),
2204
- search_depth: z8.enum(["basic", "advanced"]).optional().describe("basic \u5FEB\u4F46\u6D45\uFF1Badvanced \u6162\u4F46\u6DF1\uFF08\u542B answer \u6458\u8981\uFF09\uFF0C\u9ED8\u8BA4 basic"),
2205
- topic: z8.enum(["general", "news"]).optional().describe("general=\u901A\u7528\u7F51\u9875\uFF1Bnews=\u504F\u65B0\u95FB\u6E90\uFF1B\u9ED8\u8BA4 general")
2628
+ import { z as z9 } from "zod";
2629
+ var inputSchema9 = z9.object({
2630
+ query: z9.string().min(1, "\u5FC5\u987B\u63D0\u4F9B\u641C\u7D22\u5173\u952E\u8BCD").max(400, "\u641C\u7D22\u5173\u952E\u8BCD\u592A\u957F\uFF08>400 \u5B57\uFF09").describe("\u641C\u7D22\u5173\u952E\u8BCD\uFF0C\u5EFA\u8BAE\u81EA\u7136\u8BED\u8A00\u63CF\u8FF0\u9700\u8981\u67E5\u7684\u4FE1\u606F"),
2631
+ max_results: z9.number().int().min(1).max(20).optional().describe("\u8FD4\u56DE\u7ED3\u679C\u6570\u91CF\uFF0C1-20\uFF0C\u9ED8\u8BA4 5"),
2632
+ search_depth: z9.enum(["basic", "advanced"]).optional().describe("basic \u5FEB\u4F46\u6D45\uFF1Badvanced \u6162\u4F46\u6DF1\uFF08\u542B answer \u6458\u8981\uFF09\uFF0C\u9ED8\u8BA4 basic"),
2633
+ topic: z9.enum(["general", "news"]).optional().describe("general=\u901A\u7528\u7F51\u9875\uFF1Bnews=\u504F\u65B0\u95FB\u6E90\uFF1B\u9ED8\u8BA4 general")
2206
2634
  });
2207
- var parameters8 = toToolParameters(inputSchema8);
2208
- var description8 = `- Searches the public web via the Tavily Search API and returns structured results.
2635
+ var parameters9 = toToolParameters(inputSchema9);
2636
+ var description9 = `- Searches the public web via the Tavily Search API and returns structured results.
2209
2637
  - Use this when you need up-to-date information that is not in your training data, or when the user asks for recent news / docs / API references.
2210
2638
  - Returns the top N results, each with a title, URL, and content snippet. With \`search_depth: "advanced"\` Tavily also returns a synthesized answer at the top.
2211
2639
  - Prefer specific natural-language queries over keyword soup (e.g. "how does Bun handle .env files in version 1.1").
2212
2640
  - Requires the TAVILY_API_KEY environment variable to be set; if missing the tool returns a friendly error.`;
2213
- async function call8(input, signal) {
2641
+ async function call9(input, signal) {
2214
2642
  const apiKey = process.env.TAVILY_API_KEY;
2215
2643
  if (!apiKey) {
2216
2644
  return {
@@ -2292,37 +2720,42 @@ ${(r.content ?? "").trim()}`
2292
2720
  }
2293
2721
  var webSearchTool = {
2294
2722
  name: "WebSearch",
2295
- description: description8,
2296
- inputSchema: inputSchema8,
2297
- parameters: parameters8,
2723
+ description: description9,
2724
+ inputSchema: inputSchema9,
2725
+ parameters: parameters9,
2298
2726
  isReadOnly: true,
2299
2727
  isConcurrencySafe: true,
2300
2728
  maxResultSizeChars: DEFAULT_MAX_RESULT_SIZE_CHARS,
2301
- call: call8
2729
+ call: call9
2302
2730
  };
2303
2731
 
2304
2732
  // src/tools/write/write.ts
2305
- import { existsSync as existsSync4 } from "fs";
2306
- import { mkdir as mkdir5, stat as stat4, writeFile as writeFile4 } from "fs/promises";
2733
+ import { existsSync as existsSync6 } from "fs";
2734
+ import { mkdir as mkdir5, stat as stat4, writeFile as writeFile5 } from "fs/promises";
2307
2735
  import { dirname as dirname6 } from "path";
2308
- import { z as z9 } from "zod";
2736
+ import { z as z10 } from "zod";
2309
2737
  var MAX_WRITE_SIZE_BYTES = 1024 * 1024 * 1024;
2310
- var inputSchema9 = z9.object({
2311
- file_path: z9.string().min(1).describe("\u8981\u5199\u5165\u7684\u6587\u4EF6\u8DEF\u5F84"),
2312
- content: z9.string().describe("\u6587\u4EF6\u5B8C\u6574\u5185\u5BB9\uFF08\u4F1A\u8986\u76D6\u65E2\u6709\u5185\u5BB9\uFF09")
2738
+ var inputSchema10 = z10.object({
2739
+ file_path: z10.string().min(1).describe("\u8981\u5199\u5165\u7684\u6587\u4EF6\u8DEF\u5F84"),
2740
+ content: z10.string().describe("\u6587\u4EF6\u5B8C\u6574\u5185\u5BB9\uFF08\u4F1A\u8986\u76D6\u65E2\u6709\u5185\u5BB9\uFF09")
2313
2741
  });
2314
- var parameters9 = toToolParameters(inputSchema9);
2315
- var description9 = `Writes a file to the local filesystem.
2742
+ var parameters10 = toToolParameters(inputSchema10);
2743
+ var description10 = `Writes a file to the local filesystem.
2316
2744
 
2317
2745
  Usage:
2318
2746
  - This tool will overwrite the existing file if there is one at the provided path.
2747
+ - If you intend to overwrite an existing file, you MUST use your \`Read\` tool to read its current content at least once in the current session BEFORE calling Write. This tool will error with a "\u8BF7\u5148 Read" message if you attempt to overwrite a file that has not been read. To create a new file (path does not yet exist), you can call Write directly without a prior Read.
2319
2748
  - If the parent directory does not exist, it will be created recursively.
2320
2749
  - ALWAYS prefer editing existing files in the codebase via the Edit tool. NEVER write new files unless explicitly required.
2321
2750
  - NEVER create documentation files (*.md) or README files unless explicitly requested by the User.`;
2322
- async function call9(input) {
2751
+ async function call10(input) {
2323
2752
  const pathResult = validateAndResolvePath(input.file_path, getWorkingDir());
2324
2753
  if (!pathResult.ok) return pathResult;
2325
2754
  const filePath = pathResult.resolvedPath;
2755
+ const freshness = assertFresh(filePath);
2756
+ if (!freshness.ok) {
2757
+ return { ok: false, error: freshness.error };
2758
+ }
2326
2759
  const contentSize = Buffer.byteLength(input.content, "utf8");
2327
2760
  if (contentSize > MAX_WRITE_SIZE_BYTES) {
2328
2761
  return {
@@ -2333,7 +2766,7 @@ async function call9(input) {
2333
2766
  try {
2334
2767
  await mkdir5(dirname6(filePath), { recursive: true });
2335
2768
  let originalSize = 0;
2336
- const fileExisted = existsSync4(filePath);
2769
+ const fileExisted = existsSync6(filePath);
2337
2770
  if (fileExisted) {
2338
2771
  try {
2339
2772
  const st = await stat4(filePath);
@@ -2342,16 +2775,32 @@ async function call9(input) {
2342
2775
  }
2343
2776
  }
2344
2777
  let contentToWrite = input.content;
2778
+ let bomEncoding = "utf8";
2345
2779
  if (fileExisted) {
2780
+ const detected = await detectFileBomEncoding(filePath);
2781
+ if (detected === "utf16le") {
2782
+ return {
2783
+ ok: false,
2784
+ error: "\u6682\u4E0D\u652F\u6301\u6539\u5199 UTF-16 LE \u6587\u4EF6\uFF08BOM=FF FE\uFF09\u3002\u8BF7\u6539\u7528 UTF-8 \u7F16\u7801\u3002"
2785
+ };
2786
+ }
2787
+ bomEncoding = detected;
2346
2788
  const lineEnding = await detectFileLineEndings(filePath);
2347
2789
  contentToWrite = applyLineEnding(input.content, lineEnding);
2348
2790
  }
2349
- await writeFile4(filePath, contentToWrite, "utf8");
2791
+ if (bomEncoding === "utf8-bom") {
2792
+ const bomBytes = Buffer.from([239, 187, 191]);
2793
+ const bodyBytes = Buffer.from(contentToWrite, "utf8");
2794
+ await writeFile5(filePath, Buffer.concat([bomBytes, bodyBytes]));
2795
+ } else {
2796
+ await writeFile5(filePath, contentToWrite, "utf8");
2797
+ }
2350
2798
  const action = fileExisted ? "\u5DF2\u8986\u76D6" : "\u5DF2\u521B\u5EFA\u65B0\u6587\u4EF6";
2351
2799
  const sizeInfo = fileExisted ? `\uFF08\u539F\u6587\u4EF6 ${originalSize} \u5B57\u7B26 \u2192 \u65B0\u5185\u5BB9 ${contentToWrite.length} \u5B57\u7B26\uFF09` : `\uFF08${contentToWrite.length} \u5B57\u7B26\uFF09`;
2800
+ const bomInfo = bomEncoding === "utf8-bom" ? "\uFF0C\u5DF2\u4FDD\u7559 UTF-8 BOM" : "";
2352
2801
  return {
2353
2802
  ok: true,
2354
- content: `${action} ${filePath}${sizeInfo}`
2803
+ content: `${action} ${filePath}${sizeInfo}${bomInfo}`
2355
2804
  };
2356
2805
  } catch (e) {
2357
2806
  return { ok: false, error: `\u5199\u5165\u5931\u8D25\uFF1A${e.message}` };
@@ -2359,19 +2808,20 @@ async function call9(input) {
2359
2808
  }
2360
2809
  var writeTool = {
2361
2810
  name: "Write",
2362
- description: description9,
2363
- inputSchema: inputSchema9,
2364
- parameters: parameters9,
2811
+ description: description10,
2812
+ inputSchema: inputSchema10,
2813
+ parameters: parameters10,
2365
2814
  isReadOnly: false,
2366
2815
  isConcurrencySafe: false,
2367
2816
  maxResultSizeChars: DEFAULT_MAX_RESULT_SIZE_CHARS,
2368
- call: call9
2817
+ call: call10
2369
2818
  };
2370
2819
 
2371
2820
  // src/tools/index.ts
2372
2821
  var ALL_TOOLS = [
2373
2822
  readTool,
2374
2823
  editTool,
2824
+ multiEditTool,
2375
2825
  writeTool,
2376
2826
  globTool,
2377
2827
  grepTool,
@@ -3780,7 +4230,7 @@ async function reactiveCompactIfApplicable(messages, provider, error, state = de
3780
4230
  }
3781
4231
 
3782
4232
  // src/plugins/commandRouter.ts
3783
- import { readFile as readFile8, readdir as readdir3 } from "fs/promises";
4233
+ import { readFile as readFile9, readdir as readdir3 } from "fs/promises";
3784
4234
  import { join as join6 } from "path";
3785
4235
  var PLUGINS_DIR = join6(findPackageRoot(import.meta.url), "plugins");
3786
4236
  var pluginCache = /* @__PURE__ */ new Map();
@@ -3812,7 +4262,7 @@ async function loadPlugin(pluginDirPath) {
3812
4262
  let manifestVersion;
3813
4263
  let manifestDesc;
3814
4264
  try {
3815
- const raw = await readFile8(manifestPath, "utf8");
4265
+ const raw = await readFile9(manifestPath, "utf8");
3816
4266
  const parsed = JSON.parse(raw);
3817
4267
  manifestName = parsed.name ?? dirName;
3818
4268
  manifestVersion = parsed.version;
@@ -3827,7 +4277,7 @@ async function loadPlugin(pluginDirPath) {
3827
4277
  if (!entry.name.endsWith(".md")) continue;
3828
4278
  const cmdPath = join6(commandsDir, entry.name);
3829
4279
  try {
3830
- const content = await readFile8(cmdPath, "utf8");
4280
+ const content = await readFile9(cmdPath, "utf8");
3831
4281
  const fm = parseMarkdownFrontmatter(content);
3832
4282
  const sep2 = content.indexOf("\n---", 4);
3833
4283
  const body = sep2 >= 0 ? content.slice(sep2 + 4).trim() : content.trim();
@@ -3847,7 +4297,7 @@ async function loadPlugin(pluginDirPath) {
3847
4297
  const hooksJsonPath = join6(pluginDirPath, "hooks", "hooks.json");
3848
4298
  let hasStopHook = false;
3849
4299
  try {
3850
- const hooksRaw = await readFile8(hooksJsonPath, "utf8");
4300
+ const hooksRaw = await readFile9(hooksJsonPath, "utf8");
3851
4301
  const hooksParsed = JSON.parse(hooksRaw);
3852
4302
  const stopHooks = hooksParsed?.hooks?.Stop;
3853
4303
  if (Array.isArray(stopHooks) && stopHooks.length > 0) {
@@ -3940,13 +4390,13 @@ function getActiveStopHookPlugins() {
3940
4390
  }
3941
4391
 
3942
4392
  // src/plugins/stopHook.ts
3943
- import { readFile as readFile9 } from "fs/promises";
4393
+ import { readFile as readFile10 } from "fs/promises";
3944
4394
  import { join as join7 } from "path";
3945
4395
  import { spawn as spawn4 } from "child_process";
3946
4396
  async function loadStopHookConfig(pluginRoot) {
3947
4397
  const hooksJsonPath = join7(pluginRoot, "hooks", "hooks.json");
3948
4398
  try {
3949
- const raw = await readFile9(hooksJsonPath, "utf8");
4399
+ const raw = await readFile10(hooksJsonPath, "utf8");
3950
4400
  const parsed = JSON.parse(raw);
3951
4401
  const stopEntries = parsed?.hooks?.Stop;
3952
4402
  if (!Array.isArray(stopEntries)) return [];
@@ -4028,7 +4478,7 @@ function runSingleStopHook(hookConfig, pluginRoot, transcriptText) {
4028
4478
  }
4029
4479
 
4030
4480
  // src/plugins/verificationGate.ts
4031
- import { existsSync as existsSync5, readFileSync } from "fs";
4481
+ import { existsSync as existsSync7, readFileSync } from "fs";
4032
4482
  import { spawn as spawn5 } from "child_process";
4033
4483
  function parseVerifyArg(arg) {
4034
4484
  const colonIdx = arg.indexOf(":");
@@ -4105,7 +4555,7 @@ async function verifyShell(command, timeout) {
4105
4555
  };
4106
4556
  }
4107
4557
  function verifyFileExists(file) {
4108
- const exists = existsSync5(file);
4558
+ const exists = existsSync7(file);
4109
4559
  return {
4110
4560
  check: { type: "file_exists", file },
4111
4561
  passed: exists,
@@ -4205,8 +4655,8 @@ function formatCheckName(check) {
4205
4655
  }
4206
4656
 
4207
4657
  // src/plugins/goalState.ts
4208
- import { mkdir as mkdir6, appendFile, writeFile as writeFile5, unlink as unlink2, rmdir as rmdir2 } from "fs/promises";
4209
- import { existsSync as existsSync6, readFileSync as readFileSync2 } from "fs";
4658
+ import { mkdir as mkdir6, appendFile, writeFile as writeFile6, unlink as unlink2, rmdir as rmdir2 } from "fs/promises";
4659
+ import { existsSync as existsSync8, readFileSync as readFileSync2 } from "fs";
4210
4660
  import { join as join8 } from "path";
4211
4661
  var Phase = /* @__PURE__ */ ((Phase2) => {
4212
4662
  Phase2["PLAN"] = "plan";
@@ -4275,8 +4725,8 @@ ${goal}
4275
4725
  };
4276
4726
  for (const [name, content] of Object.entries(files)) {
4277
4727
  const path2 = join8(this.dir, `${name}.md`);
4278
- if (!existsSync6(path2)) {
4279
- await writeFile5(path2, content);
4728
+ if (!existsSync8(path2)) {
4729
+ await writeFile6(path2, content);
4280
4730
  }
4281
4731
  }
4282
4732
  }
@@ -4320,14 +4770,14 @@ ${goal}
4320
4770
  `\u975E\u6CD5\u9636\u6BB5\u5207\u6362: ${current} \u2192 ${phase}\u3002\u8BE5\u76EE\u6807\u9636\u6BB5\u4E0D\u5728 PHASE_TRANSITIONS[${current}] \u7684\u53EF\u8FBE\u96C6\u5408\u5185\uFF0C\u9700\u8981\u8D70 forceSetPhase\u3002`
4321
4771
  );
4322
4772
  }
4323
- await writeFile5(join8(this.dir, "phase.md"), phase);
4773
+ await writeFile6(join8(this.dir, "phase.md"), phase);
4324
4774
  await this.appendProgress(`PHASE \u2192 ${phase}: ${reason}`);
4325
4775
  }
4326
4776
  async forceSetPhase(phase, reason) {
4327
4777
  if (!VALID_PHASES.has(phase)) {
4328
4778
  throw new Error(`Invalid phase: ${phase}`);
4329
4779
  }
4330
- await writeFile5(join8(this.dir, "phase.md"), phase);
4780
+ await writeFile6(join8(this.dir, "phase.md"), phase);
4331
4781
  await this.appendProgress(`PHASE \u2192 ${phase}: ${reason}`);
4332
4782
  }
4333
4783
  async appendProgress(line) {
@@ -4945,6 +5395,7 @@ function useChat(args) {
4945
5395
  setCompacting(false);
4946
5396
  bump();
4947
5397
  await clearContext();
5398
+ clearFileState();
4948
5399
  }, [bump, isLoading]);
4949
5400
  const compactNow = useCallback5(async () => {
4950
5401
  if (isLoading) return;
@@ -5298,7 +5749,7 @@ async function main() {
5298
5749
  const dirArg = extractCwdArg(args);
5299
5750
  if (dirArg) {
5300
5751
  const abs = resolve8(dirArg);
5301
- if (!existsSync7(abs)) {
5752
+ if (!existsSync9(abs)) {
5302
5753
  mkdirSync(abs, { recursive: true });
5303
5754
  }
5304
5755
  process.chdir(abs);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "minimal-agent",
3
- "version": "0.1.6",
4
- "description": "最小化 Agent 系统 —— 单对话 + 9 工具 + 自动压缩 + OpenAI 兼容 + Ink TUI(学习/教学用)",
3
+ "version": "0.1.8",
4
+ "description": "最小化 Agent 系统 —— 单对话 + 10 工具 + MultiEdit + Pre-read Guard + 自动压缩 + OpenAI 兼容 + Ink TUI",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "author": "Bill Wang <leiwang0359@gmail.com>",
7
7
  "repository": {