minimal-agent 0.1.6 → 0.1.7

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 +569 -134
  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
 
@@ -462,6 +462,141 @@ function toToolParameters(schema) {
462
462
  return rest;
463
463
  }
464
464
 
465
+ // src/tools/bash/semantics.ts
466
+ var DEFAULT_SEMANTIC = (exitCode) => ({
467
+ isError: exitCode !== 0,
468
+ message: exitCode !== 0 ? `Command failed with exit code ${exitCode}` : void 0
469
+ });
470
+ var COMMAND_SEMANTICS = /* @__PURE__ */ new Map([
471
+ // grep: 0=找到匹配, 1=无匹配(非错误), 2+=真错误
472
+ ["grep", (exitCode) => ({
473
+ isError: exitCode >= 2,
474
+ message: exitCode === 1 ? "No matches found" : void 0
475
+ })],
476
+ // ripgrep 与 grep 同义
477
+ ["rg", (exitCode) => ({
478
+ isError: exitCode >= 2,
479
+ message: exitCode === 1 ? "No matches found" : void 0
480
+ })],
481
+ // find: 1=部分目录不可达(仍有结果,非致命), 2+=错误
482
+ ["find", (exitCode) => ({
483
+ isError: exitCode >= 2,
484
+ message: exitCode === 1 ? "Some directories were inaccessible" : void 0
485
+ })],
486
+ // diff: 0=相同, 1=有差异(非错误), 2+=错误
487
+ ["diff", (exitCode) => ({
488
+ isError: exitCode >= 2,
489
+ message: exitCode === 1 ? "Files differ" : void 0
490
+ })],
491
+ // test: 0=真, 1=假(非错误), 2+=错误
492
+ ["test", (exitCode) => ({
493
+ isError: exitCode >= 2,
494
+ message: exitCode === 1 ? "Condition is false" : void 0
495
+ })],
496
+ // [ 是 test 的别名
497
+ ["[", (exitCode) => ({
498
+ isError: exitCode >= 2,
499
+ message: exitCode === 1 ? "Condition is false" : void 0
500
+ })]
501
+ ]);
502
+ function extractPrimaryCommand(command) {
503
+ let cmd = command.trim();
504
+ const wrapMatch = cmd.match(/^(?:bash|sh|zsh|dash)\s+-c\s+(['"])(.+)\1\s*$/s);
505
+ if (wrapMatch) cmd = wrapMatch[2].trim();
506
+ const segments = cmd.split(/\s*(?:\|\||&&|;|\|)\s*/).filter((s) => s.length > 0);
507
+ let last = segments[segments.length - 1] ?? cmd;
508
+ last = last.trim();
509
+ const tokens = last.split(/\s+/);
510
+ let i = 0;
511
+ if (tokens[i] === "env") i++;
512
+ while (i < tokens.length && /^[A-Za-z_][A-Za-z0-9_]*=/.test(tokens[i])) i++;
513
+ return tokens[i] ?? "";
514
+ }
515
+ function interpretCommandResult(command, exitCode, stdout, stderr) {
516
+ const base = extractPrimaryCommand(command);
517
+ const fn = COMMAND_SEMANTICS.get(base) ?? DEFAULT_SEMANTIC;
518
+ return fn(exitCode, stdout, stderr);
519
+ }
520
+
521
+ // src/tools/bash/warnings.ts
522
+ var DESTRUCTIVE_PATTERNS = [
523
+ // Git —— 数据丢失 / 难回退
524
+ {
525
+ pattern: /\bgit\s+reset\s+--hard\b/,
526
+ warning: "Note: may discard uncommitted changes"
527
+ },
528
+ {
529
+ pattern: /\bgit\s+push\b[^;&|\n]*[ \t](--force|--force-with-lease|-f)\b/,
530
+ warning: "Note: may overwrite remote history"
531
+ },
532
+ {
533
+ pattern: /\bgit\s+clean\b(?![^;&|\n]*(?:-[a-zA-Z]*n|--dry-run))[^;&|\n]*-[a-zA-Z]*f/,
534
+ warning: "Note: may permanently delete untracked files"
535
+ },
536
+ {
537
+ pattern: /\bgit\s+checkout\s+(--\s+)?\.[ \t]*($|[;&|\n])/,
538
+ warning: "Note: may discard all working tree changes"
539
+ },
540
+ {
541
+ pattern: /\bgit\s+restore\s+(--\s+)?\.[ \t]*($|[;&|\n])/,
542
+ warning: "Note: may discard all working tree changes"
543
+ },
544
+ {
545
+ pattern: /\bgit\s+stash[ \t]+(drop|clear)\b/,
546
+ warning: "Note: may permanently remove stashed changes"
547
+ },
548
+ {
549
+ pattern: /\bgit\s+branch\s+(-D[ \t]|--delete\s+--force|--force\s+--delete)\b/,
550
+ warning: "Note: may force-delete a branch"
551
+ },
552
+ // Git —— 安全绕过
553
+ {
554
+ pattern: /\bgit\s+(commit|push|merge)\b[^;&|\n]*--no-verify\b/,
555
+ warning: "Note: may skip safety hooks"
556
+ },
557
+ {
558
+ pattern: /\bgit\s+commit\b[^;&|\n]*--amend\b/,
559
+ warning: "Note: may rewrite the last commit"
560
+ },
561
+ // 文件删除(rm -rf / 之类的致命形式由 bash.ts 黑名单处理;这里只做"未到致命"的提醒)
562
+ {
563
+ 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]/,
564
+ warning: "Note: may recursively force-remove files"
565
+ },
566
+ {
567
+ pattern: /(^|[;&|\n]\s*)rm\s+-[a-zA-Z]*[rR]/,
568
+ warning: "Note: may recursively remove files"
569
+ },
570
+ {
571
+ pattern: /(^|[;&|\n]\s*)rm\s+-[a-zA-Z]*f/,
572
+ warning: "Note: may force-remove files"
573
+ },
574
+ // 数据库
575
+ {
576
+ pattern: /\b(DROP|TRUNCATE)\s+(TABLE|DATABASE|SCHEMA)\b/i,
577
+ warning: "Note: may drop or truncate database objects"
578
+ },
579
+ {
580
+ pattern: /\bDELETE\s+FROM\s+\w+[ \t]*(;|"|'|\n|$)/i,
581
+ warning: "Note: may delete all rows from a database table"
582
+ },
583
+ // 基础设施
584
+ {
585
+ pattern: /\bkubectl\s+delete\b/,
586
+ warning: "Note: may delete Kubernetes resources"
587
+ },
588
+ {
589
+ pattern: /\bterraform\s+destroy\b/,
590
+ warning: "Note: may destroy Terraform infrastructure"
591
+ }
592
+ ];
593
+ function scanDestructiveCommand(command) {
594
+ for (const { pattern, warning } of DESTRUCTIVE_PATTERNS) {
595
+ if (pattern.test(command)) return warning;
596
+ }
597
+ return null;
598
+ }
599
+
465
600
  // src/tools/bash/bash.ts
466
601
  var DEFAULT_TIMEOUT_MS = 12e4;
467
602
  var MAX_TIMEOUT_MS = 6e5;
@@ -599,6 +734,7 @@ async function call(input, signal) {
599
734
  \u547D\u4EE4\uFF1A${command}`
600
735
  };
601
736
  }
737
+ const destructiveWarning = scanDestructiveCommand(command);
602
738
  let stdout = "";
603
739
  let stderr = "";
604
740
  let exitCode = null;
@@ -669,16 +805,33 @@ ${stderr.replace(/\s+$/, "")}
669
805
  }
670
806
  parts.push(`
671
807
  [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) + `
808
+ let combinedOutput = parts.join("\n");
809
+ if (combinedOutput.length > DEFAULT_MAX_RESULT_SIZE_CHARS) {
810
+ combinedOutput = combinedOutput.slice(0, DEFAULT_MAX_RESULT_SIZE_CHARS) + `
675
811
 
676
812
  ... (\u8F93\u51FA\u8D85\u8FC7 ${DEFAULT_MAX_RESULT_SIZE_CHARS} \u5B57\u7B26\uFF0C\u5DF2\u622A\u65AD)`;
677
813
  }
678
- if (timedOut || exitCode !== null && exitCode !== 0 || killedBySignal) {
679
- return { ok: false, error: content };
814
+ if (timedOut || killedBySignal) {
815
+ return { ok: false, error: combinedOutput };
680
816
  }
681
- return { ok: true, content };
817
+ const semantic = interpretCommandResult(
818
+ command,
819
+ exitCode ?? 0,
820
+ stdout,
821
+ stderr
822
+ );
823
+ const finalContent = destructiveWarning ? `\u26A0\uFE0F \u8B66\u544A: ${destructiveWarning}
824
+
825
+ ${combinedOutput}` : combinedOutput;
826
+ if (semantic.isError) {
827
+ return {
828
+ ok: false,
829
+ error: `\u547D\u4EE4\u5931\u8D25 (exit ${exitCode}): ${stderr || stdout || semantic.message || ""}`.trim() + `
830
+
831
+ ${finalContent}`
832
+ };
833
+ }
834
+ return { ok: true, content: finalContent };
682
835
  }
683
836
  var bashTool = {
684
837
  name: "Bash",
@@ -694,14 +847,14 @@ var bashTool = {
694
847
 
695
848
  // src/tools/edit/edit.ts
696
849
  import { readFile as readFile6, writeFile as writeFile3, mkdir as mkdir4 } from "fs/promises";
697
- import { existsSync as existsSync2 } from "fs";
850
+ import { existsSync as existsSync3 } from "fs";
698
851
  import { dirname as dirname5 } from "path";
699
852
  import { z as z2 } from "zod";
700
853
 
701
854
  // src/tools/shared/fileUtils.ts
702
- import { readFile as readFile5 } from "fs/promises";
855
+ import { open, readFile as readFile5 } from "fs/promises";
703
856
  import { homedir as homedir4 } from "os";
704
- import { resolve as resolve4, normalize } from "path";
857
+ import { extname, resolve as resolve4 } from "path";
705
858
  var BLOCKED_DEVICE_PATHS = /* @__PURE__ */ new Set([
706
859
  "/dev/zero",
707
860
  "/dev/random",
@@ -718,12 +871,12 @@ var BLOCKED_DEVICE_PATHS = /* @__PURE__ */ new Set([
718
871
  ]);
719
872
  var WINDOWS_BLOCKED_NAMES = /* @__PURE__ */ new Set(["NUL", "CON", "PRN", "AUX", "COM1", "COM2", "LPT1"]);
720
873
  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"))) {
874
+ const slashed = filePath.replaceAll("\\", "/");
875
+ if (BLOCKED_DEVICE_PATHS.has(slashed)) return true;
876
+ if (slashed.startsWith("/proc/") && (slashed.endsWith("/fd/0") || slashed.endsWith("/fd/1") || slashed.endsWith("/fd/2"))) {
724
877
  return true;
725
878
  }
726
- const baseName = normalized.split(/[/\\]/).pop() ?? "";
879
+ const baseName = slashed.split("/").pop() ?? "";
727
880
  if (WINDOWS_BLOCKED_NAMES.has(baseName.toUpperCase())) {
728
881
  return true;
729
882
  }
@@ -733,6 +886,9 @@ function validateAndResolvePath(rawPath, workingDir) {
733
886
  if (rawPath.includes("\0")) {
734
887
  return { ok: false, error: "\u8DEF\u5F84\u5305\u542B\u975E\u6CD5\u5B57\u7B26\uFF08null byte\uFF09" };
735
888
  }
889
+ if (isBlockedDevicePath(rawPath)) {
890
+ 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` };
891
+ }
736
892
  const expanded = expandPath(rawPath);
737
893
  const resolved = resolve4(workingDir, expanded);
738
894
  if (isBlockedDevicePath(resolved)) {
@@ -778,6 +934,88 @@ function applyLineEnding(content, ending) {
778
934
  }
779
935
  return content;
780
936
  }
937
+ var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
938
+ ".png",
939
+ ".jpg",
940
+ ".jpeg",
941
+ ".gif",
942
+ ".webp",
943
+ ".bmp",
944
+ ".ico",
945
+ ".tiff",
946
+ ".tif",
947
+ ".pdf",
948
+ ".doc",
949
+ ".docx",
950
+ ".xls",
951
+ ".xlsx",
952
+ ".ppt",
953
+ ".pptx",
954
+ ".exe",
955
+ ".dll",
956
+ ".so",
957
+ ".dylib",
958
+ ".o",
959
+ ".a",
960
+ ".pyc",
961
+ ".pyo",
962
+ ".class",
963
+ ".jar",
964
+ ".zip",
965
+ ".tar",
966
+ ".gz",
967
+ ".bz2",
968
+ ".7z",
969
+ ".rar",
970
+ ".iso",
971
+ ".mp3",
972
+ ".mp4",
973
+ ".mov",
974
+ ".avi",
975
+ ".mkv",
976
+ ".wav",
977
+ ".flac",
978
+ ".ogg",
979
+ ".ttf",
980
+ ".otf",
981
+ ".woff",
982
+ ".woff2",
983
+ ".sqlite",
984
+ ".sqlite3",
985
+ ".db",
986
+ ".psd",
987
+ ".ai",
988
+ ".bin",
989
+ ".wasm"
990
+ ]);
991
+ function hasBinaryExtension(filePath) {
992
+ const ext = extname(filePath).toLowerCase();
993
+ return BINARY_EXTENSIONS.has(ext);
994
+ }
995
+ async function detectFileBomEncoding(filePath) {
996
+ let fh = null;
997
+ try {
998
+ fh = await open(filePath, "r");
999
+ const buf = Buffer.alloc(3);
1000
+ const { bytesRead } = await fh.read(buf, 0, 3, 0);
1001
+ if (bytesRead >= 3 && buf[0] === 239 && buf[1] === 187 && buf[2] === 191) {
1002
+ return "utf8-bom";
1003
+ }
1004
+ if (bytesRead >= 2 && buf[0] === 255 && buf[1] === 254) {
1005
+ return "utf16le";
1006
+ }
1007
+ return "utf8";
1008
+ } catch {
1009
+ return "utf8";
1010
+ } finally {
1011
+ if (fh) {
1012
+ try {
1013
+ await fh.close();
1014
+ } catch {
1015
+ }
1016
+ }
1017
+ }
1018
+ }
781
1019
  var LEFT_SINGLE_CURLY_QUOTE = "\u2018";
782
1020
  var RIGHT_SINGLE_CURLY_QUOTE = "\u2019";
783
1021
  var LEFT_DOUBLE_CURLY_QUOTE = "\u201C";
@@ -857,6 +1095,44 @@ function applyCurlySingleQuotes(str) {
857
1095
  return result.join("");
858
1096
  }
859
1097
 
1098
+ // src/tools/shared/fileState.ts
1099
+ import { existsSync as existsSync2, statSync } from "fs";
1100
+ var fileState = /* @__PURE__ */ new Map();
1101
+ function recordRead(absPath) {
1102
+ try {
1103
+ const st = statSync(absPath);
1104
+ fileState.set(absPath, { timestamp: st.mtimeMs, size: st.size });
1105
+ } catch {
1106
+ }
1107
+ }
1108
+ function assertFresh(absPath) {
1109
+ const entry = fileState.get(absPath);
1110
+ if (!entry) {
1111
+ if (existsSync2(absPath)) {
1112
+ return {
1113
+ ok: false,
1114
+ 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`
1115
+ };
1116
+ }
1117
+ return { ok: true };
1118
+ }
1119
+ try {
1120
+ const st = statSync(absPath);
1121
+ if (st.mtimeMs > entry.timestamp) {
1122
+ return {
1123
+ ok: false,
1124
+ 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`
1125
+ };
1126
+ }
1127
+ } catch {
1128
+ return { ok: true };
1129
+ }
1130
+ return { ok: true };
1131
+ }
1132
+ function clearFileState() {
1133
+ fileState.clear();
1134
+ }
1135
+
860
1136
  // src/tools/edit/edit.ts
861
1137
  var MAX_EDIT_FILE_SIZE_BYTES = 1024 * 1024 * 1024;
862
1138
  var inputSchema2 = z2.object({
@@ -885,10 +1161,14 @@ async function call2(input) {
885
1161
  const pathResult = validateAndResolvePath(input.file_path, getWorkingDir());
886
1162
  if (!pathResult.ok) return pathResult;
887
1163
  const filePath = pathResult.resolvedPath;
1164
+ const freshness = assertFresh(filePath);
1165
+ if (!freshness.ok) {
1166
+ return { ok: false, error: freshness.error };
1167
+ }
888
1168
  if (old_string === new_string) {
889
1169
  return { ok: false, error: "old_string \u4E0E new_string \u5B8C\u5168\u76F8\u540C\uFF0C\u6CA1\u6709\u53EF\u6539\u7684\u5185\u5BB9\u3002" };
890
1170
  }
891
- if (old_string === "" && !existsSync2(filePath)) {
1171
+ if (old_string === "" && !existsSync3(filePath)) {
892
1172
  try {
893
1173
  await mkdir4(dirname5(filePath), { recursive: true });
894
1174
  await writeFile3(filePath, new_string, "utf8");
@@ -900,7 +1180,7 @@ async function call2(input) {
900
1180
  return { ok: false, error: `\u521B\u5EFA\u6587\u4EF6\u5931\u8D25\uFF1A${e.message}` };
901
1181
  }
902
1182
  }
903
- if (!existsSync2(filePath)) {
1183
+ if (!existsSync3(filePath)) {
904
1184
  return {
905
1185
  ok: false,
906
1186
  error: `\u6587\u4EF6\u4E0D\u5B58\u5728\uFF1A${filePath}
@@ -1053,22 +1333,146 @@ var editTool = {
1053
1333
  call: call2
1054
1334
  };
1055
1335
 
1336
+ // src/tools/edit/multi-edit.ts
1337
+ import { readFile as readFile7, writeFile as writeFile4 } from "fs/promises";
1338
+ import { existsSync as existsSync4 } from "fs";
1339
+ import { z as z3 } from "zod";
1340
+ var editItemSchema = z3.object({
1341
+ 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"),
1342
+ new_string: z3.string().describe("\u66FF\u6362\u4E3A\u7684\u65B0\u6587\u672C"),
1343
+ 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")
1344
+ });
1345
+ var inputSchema3 = z3.object({
1346
+ file_path: z3.string().min(1).describe("\u8981\u7F16\u8F91\u7684\u6587\u4EF6\u8DEF\u5F84"),
1347
+ 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")
1348
+ });
1349
+ var parameters3 = toToolParameters(inputSchema3);
1350
+ var description3 = `Performs multiple exact string replacements in a single file, applied atomically (all-or-nothing).
1351
+
1352
+ Usage:
1353
+ - You MUST use your \`Read\` tool to read the current content of the file BEFORE calling MultiEdit.
1354
+ - Provide a list of \`edits\`, each with \`old_string\`, \`new_string\`, and optional \`replace_all\`.
1355
+ - 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.
1356
+ - 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).
1357
+ - Each \`old_string\` must be unique in the current content (after prior edits) unless \`replace_all=true\`.
1358
+ - Empty \`old_string\` is NOT allowed in MultiEdit. To create a new file, use the \`Edit\` tool with a single empty-old_string call.
1359
+ - Preserve exact indentation (tabs/spaces).`;
1360
+ function countOccurrences2(haystack, needle) {
1361
+ if (needle.length === 0) return 0;
1362
+ let count = 0;
1363
+ let pos = 0;
1364
+ while ((pos = haystack.indexOf(needle, pos)) !== -1) {
1365
+ count++;
1366
+ pos += needle.length;
1367
+ }
1368
+ return count;
1369
+ }
1370
+ function splitReplaceAll2(haystack, needle, replacement) {
1371
+ return haystack.split(needle).join(replacement);
1372
+ }
1373
+ async function call3(input) {
1374
+ const pathResult = validateAndResolvePath(input.file_path, getWorkingDir());
1375
+ if (!pathResult.ok) return pathResult;
1376
+ const filePath = pathResult.resolvedPath;
1377
+ const freshness = assertFresh(filePath);
1378
+ if (!freshness.ok) {
1379
+ return { ok: false, error: freshness.error };
1380
+ }
1381
+ if (!existsSync4(filePath)) {
1382
+ return {
1383
+ ok: false,
1384
+ error: `\u6587\u4EF6\u4E0D\u5B58\u5728\uFF1A${filePath}
1385
+ \uFF08MultiEdit \u4E0D\u652F\u6301\u521B\u5EFA\u65B0\u6587\u4EF6\uFF0C\u8BF7\u6539\u7528 Edit \u5DE5\u5177\uFF09`
1386
+ };
1387
+ }
1388
+ const edits = input.edits;
1389
+ for (let i = 0; i < edits.length; i++) {
1390
+ for (let j = 0; j < i; j++) {
1391
+ if (edits[j].new_string.includes(edits[i].old_string)) {
1392
+ return {
1393
+ ok: false,
1394
+ 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`
1395
+ };
1396
+ }
1397
+ }
1398
+ }
1399
+ let originalContent;
1400
+ try {
1401
+ originalContent = await readFile7(filePath, "utf8");
1402
+ } catch (e) {
1403
+ return { ok: false, error: `\u8BFB\u53D6\u5931\u8D25\uFF1A${e.message}` };
1404
+ }
1405
+ let currentContent = originalContent;
1406
+ for (let i = 0; i < edits.length; i++) {
1407
+ const edit = edits[i];
1408
+ const replaceAll = edit.replace_all ?? false;
1409
+ let searchTarget = edit.old_string;
1410
+ let processedNewString = edit.new_string;
1411
+ const actualOld = findActualString(currentContent, edit.old_string);
1412
+ if (actualOld === null) {
1413
+ return {
1414
+ ok: false,
1415
+ 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`
1416
+ };
1417
+ }
1418
+ if (actualOld !== edit.old_string) {
1419
+ searchTarget = actualOld;
1420
+ processedNewString = preserveQuoteStyle(edit.old_string, actualOld, edit.new_string);
1421
+ }
1422
+ const occurrences = countOccurrences2(currentContent, searchTarget);
1423
+ if (occurrences === 0) {
1424
+ return {
1425
+ ok: false,
1426
+ 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`
1427
+ };
1428
+ }
1429
+ if (occurrences > 1 && !replaceAll) {
1430
+ return {
1431
+ ok: false,
1432
+ 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`
1433
+ };
1434
+ }
1435
+ currentContent = replaceAll ? splitReplaceAll2(currentContent, searchTarget, processedNewString) : currentContent.replace(searchTarget, processedNewString);
1436
+ }
1437
+ const lineEnding = await detectFileLineEndings(filePath);
1438
+ const finalContent = applyLineEnding(currentContent, lineEnding);
1439
+ try {
1440
+ await writeFile4(filePath, finalContent, "utf8");
1441
+ } catch (e) {
1442
+ return { ok: false, error: `\u5199\u5165\u5931\u8D25\uFF1A${e.message}` };
1443
+ }
1444
+ return {
1445
+ ok: true,
1446
+ content: `\u5DF2\u5BF9 ${filePath} \u5E94\u7528 ${edits.length} \u5904\u4FEE\u6539`
1447
+ };
1448
+ }
1449
+ var multiEditTool = {
1450
+ name: "MultiEdit",
1451
+ description: description3,
1452
+ inputSchema: inputSchema3,
1453
+ parameters: parameters3,
1454
+ isReadOnly: false,
1455
+ isConcurrencySafe: false,
1456
+ maxResultSizeChars: DEFAULT_MAX_RESULT_SIZE_CHARS,
1457
+ call: call3
1458
+ };
1459
+
1056
1460
  // src/tools/glob/glob.ts
1057
1461
  import { stat as stat2 } from "fs/promises";
1058
1462
  import { isAbsolute, resolve as resolve5 } from "path";
1059
1463
  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')
1464
+ import { z as z4 } from "zod";
1465
+ var inputSchema4 = z4.object({
1466
+ pattern: z4.string().min(1).describe('glob \u6A21\u5F0F\uFF0C\u4F8B\u5982 "**/*.ts" \u6216 "src/components/**/*.tsx"'),
1467
+ 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
1468
  });
1065
- var parameters3 = toToolParameters(inputSchema3);
1066
- var description3 = `- Fast file pattern matching tool that works with any codebase size
1469
+ var parameters4 = toToolParameters(inputSchema4);
1470
+ var description4 = `- Fast file pattern matching tool that works with any codebase size
1067
1471
  - Supports glob patterns like "**/*.js" or "src/**/*.ts"
1068
1472
  - Returns matching file paths sorted by modification time (oldest first)
1069
1473
  - Use this tool when you need to find files by name patterns
1070
1474
  - 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) {
1475
+ async function call4(input) {
1072
1476
  const cwd = input.path ? resolve5(input.path) : getWorkingDir();
1073
1477
  const pattern = input.pattern.replace(/\\/g, "/");
1074
1478
  let matches;
@@ -1115,23 +1519,23 @@ async function call3(input) {
1115
1519
  }
1116
1520
  var globTool = {
1117
1521
  name: "Glob",
1118
- description: description3,
1119
- inputSchema: inputSchema3,
1120
- parameters: parameters3,
1522
+ description: description4,
1523
+ inputSchema: inputSchema4,
1524
+ parameters: parameters4,
1121
1525
  isReadOnly: true,
1122
1526
  isConcurrencySafe: true,
1123
1527
  maxResultSizeChars: DEFAULT_MAX_RESULT_SIZE_CHARS,
1124
- call: call3
1528
+ call: call4
1125
1529
  };
1126
1530
 
1127
1531
  // src/tools/grep/grep.ts
1128
1532
  import { spawn as spawn3 } from "child_process";
1129
1533
  import { resolve as resolve7 } from "path";
1130
- import { z as z4 } from "zod";
1534
+ import { z as z5 } from "zod";
1131
1535
 
1132
1536
  // src/tools/grep/rgPath.ts
1133
1537
  import { spawn as spawn2 } from "child_process";
1134
- import { chmodSync, existsSync as existsSync3 } from "fs";
1538
+ import { chmodSync, existsSync as existsSync5 } from "fs";
1135
1539
  import { resolve as resolve6 } from "path";
1136
1540
  var cached;
1137
1541
  async function resolveRgPath() {
@@ -1141,15 +1545,15 @@ async function resolveRgPath() {
1141
1545
  }
1142
1546
  async function detect() {
1143
1547
  const fromEnv = process.env.MINIMAL_AGENT_RIPGREP_PATH;
1144
- if (fromEnv && existsSync3(fromEnv)) return fromEnv;
1548
+ if (fromEnv && existsSync5(fromEnv)) return fromEnv;
1145
1549
  const vendored = vendoredRgPath();
1146
- if (vendored && existsSync3(vendored)) {
1550
+ if (vendored && existsSync5(vendored)) {
1147
1551
  ensureExecutable(vendored);
1148
1552
  return vendored;
1149
1553
  }
1150
1554
  if (await trySpawn("rg")) return "rg";
1151
1555
  for (const candidate of claudeCodeCandidates()) {
1152
- if (existsSync3(candidate)) {
1556
+ if (existsSync5(candidate)) {
1153
1557
  ensureExecutable(candidate);
1154
1558
  return candidate;
1155
1559
  }
@@ -1242,21 +1646,21 @@ function claudeCodeCandidates() {
1242
1646
  }
1243
1647
 
1244
1648
  // 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")
1649
+ var inputSchema5 = z5.object({
1650
+ pattern: z5.string().min(1).describe("\u6B63\u5219\u8868\u8FBE\u5F0F\uFF08ripgrep \u517C\u5BB9\u8BED\u6CD5\uFF09"),
1651
+ path: z5.string().optional().describe("\u641C\u7D22\u7684\u6839\u76EE\u5F55\u6216\u6587\u4EF6\uFF08\u9ED8\u8BA4\u5F53\u524D\u5DE5\u4F5C\u76EE\u5F55\uFF09"),
1652
+ glob: z5.string().optional().describe('\u6587\u4EF6\u540D glob \u8FC7\u6EE4\uFF0C\u5982 "*.ts"'),
1653
+ type: z5.string().optional().describe('rg \u7684\u6587\u4EF6\u7C7B\u578B\u5FEB\u6377\u540D\uFF0C\u5982 "py"\u3001"rust"\u3001"js"'),
1654
+ 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"),
1655
+ "-i": z5.boolean().optional().describe("\u5FFD\u7565\u5927\u5C0F\u5199"),
1656
+ "-n": z5.boolean().optional().describe("\u663E\u793A\u884C\u53F7\uFF08\u4EC5 content \u6A21\u5F0F\uFF09"),
1657
+ "-A": z5.number().int().min(0).optional().describe("\u5339\u914D\u540E\u5C55\u793A\u51E0\u884C\u4E0A\u4E0B\u6587"),
1658
+ "-B": z5.number().int().min(0).optional().describe("\u5339\u914D\u524D\u5C55\u793A\u51E0\u884C\u4E0A\u4E0B\u6587"),
1659
+ "-C": z5.number().int().min(0).optional().describe("\u5339\u914D\u524D\u540E\u5404\u5C55\u793A\u51E0\u884C\uFF08\u8986\u76D6 -A/-B\uFF09"),
1660
+ 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
1661
  });
1258
- var parameters4 = toToolParameters(inputSchema4);
1259
- var description4 = `A powerful search tool built on ripgrep.
1662
+ var parameters5 = toToolParameters(inputSchema5);
1663
+ var description5 = `A powerful search tool built on ripgrep.
1260
1664
 
1261
1665
  Usage:
1262
1666
  - ALWAYS use Grep for content search tasks. Do NOT invoke \`grep\` or \`rg\` directly via Bash.
@@ -1264,7 +1668,7 @@ Usage:
1264
1668
  - Filter files with glob parameter (e.g., "*.js", "**/*.tsx") or type parameter (e.g., "js", "py", "rust")
1265
1669
  - Output modes: "content" shows matching lines, "files_with_matches" shows only file paths (default), "count" shows match counts
1266
1670
  - Pattern syntax: Uses ripgrep (not classic grep)`;
1267
- async function call4(input, signal) {
1671
+ async function call5(input, signal) {
1268
1672
  const args = [];
1269
1673
  const mode = input.output_mode ?? "files_with_matches";
1270
1674
  if (mode === "files_with_matches") args.push("-l");
@@ -1341,27 +1745,27 @@ async function call4(input, signal) {
1341
1745
  }
1342
1746
  var grepTool = {
1343
1747
  name: "Grep",
1344
- description: description4,
1345
- inputSchema: inputSchema4,
1346
- parameters: parameters4,
1748
+ description: description5,
1749
+ inputSchema: inputSchema5,
1750
+ parameters: parameters5,
1347
1751
  isReadOnly: true,
1348
1752
  isConcurrencySafe: true,
1349
1753
  maxResultSizeChars: DEFAULT_MAX_RESULT_SIZE_CHARS,
1350
- call: call4
1754
+ call: call5
1351
1755
  };
1352
1756
 
1353
1757
  // src/tools/read/read.ts
1354
1758
  import { createReadStream } from "fs";
1355
- import { readFile as readFile7, stat as stat3 } from "fs/promises";
1759
+ import { readFile as readFile8, stat as stat3 } from "fs/promises";
1356
1760
  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}`)
1761
+ import { z as z6 } from "zod";
1762
+ var inputSchema6 = z6.object({
1763
+ 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"),
1764
+ offset: z6.number().int().positive().optional().describe("\u8D77\u59CB\u884C\u53F7\uFF081-indexed\uFF09\uFF1B\u4E0D\u586B\u5219\u4ECE\u6587\u4EF6\u5F00\u5934\u8BFB"),
1765
+ 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
1766
  });
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.
1767
+ var parameters6 = toToolParameters(inputSchema6);
1768
+ var description6 = `Reads a file from the local filesystem. You can access any file directly by using this tool.
1365
1769
  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
1770
 
1367
1771
  Usage:
@@ -1372,7 +1776,7 @@ Usage:
1372
1776
  - This tool can only read text files, not directories. To read a directory, use the Glob tool.
1373
1777
  - If you read a file that exists but has empty contents you will receive a warning in place of file contents.`;
1374
1778
  var STREAM_THRESHOLD = 1024 * 1024;
1375
- async function call5(input) {
1779
+ async function call6(input) {
1376
1780
  const offset = input.offset ?? 1;
1377
1781
  const limit = input.limit ?? MAX_LINES_TO_READ;
1378
1782
  const pathResult = validateAndResolvePath(input.file_path, getWorkingDir());
@@ -1383,6 +1787,12 @@ async function call5(input) {
1383
1787
  if (isBlockedDevicePath(filePath)) {
1384
1788
  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
1789
  }
1790
+ if (hasBinaryExtension(filePath)) {
1791
+ return {
1792
+ ok: false,
1793
+ 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`
1794
+ };
1795
+ }
1386
1796
  let st;
1387
1797
  try {
1388
1798
  st = await stat3(filePath);
@@ -1408,6 +1818,7 @@ async function call5(input) {
1408
1818
  numbered = result.numbered;
1409
1819
  totalLines = result.totalLines;
1410
1820
  if (result.isEmpty) {
1821
+ recordRead(filePath);
1411
1822
  return { ok: true, content: "<file is empty>" };
1412
1823
  }
1413
1824
  } else {
@@ -1416,6 +1827,7 @@ async function call5(input) {
1416
1827
  totalLines = result.totalLines;
1417
1828
  }
1418
1829
  if (totalLines === 0 || !numbered) {
1830
+ recordRead(filePath);
1419
1831
  return { ok: true, content: "<file is empty>" };
1420
1832
  }
1421
1833
  let content = numbered;
@@ -1438,10 +1850,11 @@ async function call5(input) {
1438
1850
 
1439
1851
  \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
1852
  }
1853
+ recordRead(filePath);
1441
1854
  return { ok: true, content };
1442
1855
  }
1443
1856
  async function readSmallFile(filePath, offset, limit) {
1444
- const raw = await readFile7(filePath, "utf8");
1857
+ const raw = await readFile8(filePath, "utf8");
1445
1858
  if (raw.length === 0) {
1446
1859
  return { numbered: "", totalLines: 0, isEmpty: true };
1447
1860
  }
@@ -1491,17 +1904,17 @@ async function readLargeFileStream(filePath, offset, limit) {
1491
1904
  }
1492
1905
  var readTool = {
1493
1906
  name: "Read",
1494
- description: description5,
1495
- inputSchema: inputSchema5,
1496
- parameters: parameters5,
1907
+ description: description6,
1908
+ inputSchema: inputSchema6,
1909
+ parameters: parameters6,
1497
1910
  isReadOnly: true,
1498
1911
  isConcurrencySafe: true,
1499
1912
  maxResultSizeChars: DEFAULT_MAX_RESULT_SIZE_CHARS,
1500
- call: call5
1913
+ call: call6
1501
1914
  };
1502
1915
 
1503
1916
  // src/tools/webfetch/webfetch.ts
1504
- import { z as z6 } from "zod";
1917
+ import { z as z7 } from "zod";
1505
1918
 
1506
1919
  // src/tools/webfetch/preapproved.ts
1507
1920
  var PREAPPROVED_HOSTS = /* @__PURE__ */ new Set([
@@ -1786,12 +2199,12 @@ function cleanCache() {
1786
2199
  }
1787
2200
  }
1788
2201
  }
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")
2202
+ var inputSchema7 = z7.object({
2203
+ url: z7.string().describe("\u8981\u83B7\u53D6\u5185\u5BB9\u7684 URL"),
2204
+ 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
2205
  });
1793
- var parameters6 = toToolParameters(inputSchema6);
1794
- var description6 = `- Fetches content from a specified URL and processes it using an AI model.
2206
+ var parameters7 = toToolParameters(inputSchema7);
2207
+ var description7 = `- Fetches content from a specified URL and processes it using an AI model.
1795
2208
  - Takes a URL and a prompt as input.
1796
2209
  - Fetches the URL content, converts HTML to markdown.
1797
2210
  - Processes the content with the prompt (e.g., extract summary, find specific info).
@@ -1900,7 +2313,7 @@ async function htmlToMarkdown(html) {
1900
2313
  const td = new TurndownService();
1901
2314
  return td.turndown(html);
1902
2315
  }
1903
- async function call6(input, signal) {
2316
+ async function call7(input, signal) {
1904
2317
  const { url } = input;
1905
2318
  const start = Date.now();
1906
2319
  const cacheKey = url;
@@ -1998,17 +2411,17 @@ ${content}`;
1998
2411
  }
1999
2412
  var webfetchTool = {
2000
2413
  name: "WebFetch",
2001
- description: description6,
2002
- inputSchema: inputSchema6,
2003
- parameters: parameters6,
2414
+ description: description7,
2415
+ inputSchema: inputSchema7,
2416
+ parameters: parameters7,
2004
2417
  isReadOnly: true,
2005
2418
  isConcurrencySafe: true,
2006
2419
  maxResultSizeChars: DEFAULT_MAX_RESULT_SIZE_CHARS,
2007
- call: call6
2420
+ call: call7
2008
2421
  };
2009
2422
 
2010
2423
  // src/tools/webbrowser/webbrowser.ts
2011
- import { z as z7 } from "zod";
2424
+ import { z as z8 } from "zod";
2012
2425
 
2013
2426
  // src/tools/webbrowser/browser.ts
2014
2427
  import os from "os";
@@ -2044,15 +2457,15 @@ function screenshotPath(prefix = "browser") {
2044
2457
  }
2045
2458
 
2046
2459
  // 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)")
2460
+ var inputSchema8 = z8.object({
2461
+ action: z8.enum(["navigate", "screenshot", "getContent", "click", "fill", "submit"]).describe("Browser action to perform"),
2462
+ url: z8.string().url().optional().describe("URL to navigate to (required for navigate action)"),
2463
+ selector: z8.string().optional().describe("CSS selector for click/fill/submit actions"),
2464
+ value: z8.string().optional().describe("Value to fill in input fields"),
2465
+ timeout: z8.number().int().positive().optional().describe("Timeout in milliseconds (default: 30000)")
2053
2466
  });
2054
- var parameters7 = toToolParameters(inputSchema7);
2055
- var description7 = `Control a headless web browser. Navigate to URLs, take screenshots, and interact with web pages.
2467
+ var parameters8 = toToolParameters(inputSchema8);
2468
+ var description8 = `Control a headless web browser. Navigate to URLs, take screenshots, and interact with web pages.
2056
2469
 
2057
2470
  When to use WebBrowser vs WebSearch:
2058
2471
  - WebSearch: When you need to find information or discover URLs through search
@@ -2083,7 +2496,7 @@ Example actions:
2083
2496
  - Take screenshot: { action: "screenshot" }
2084
2497
  - Click element: { action: "click", selector: "#submit-btn" }
2085
2498
  - Fill form field: { action: "fill", selector: "input[name='email']", value: "user@example.com" }`;
2086
- async function call7(input, signal) {
2499
+ async function call8(input, signal) {
2087
2500
  const { action, url, selector, value, timeout = 3e4 } = input;
2088
2501
  let page;
2089
2502
  try {
@@ -2187,30 +2600,30 @@ Current URL: ${page.url()}`
2187
2600
  }
2188
2601
  var webbrowserTool = {
2189
2602
  name: "WebBrowser",
2190
- description: description7,
2191
- inputSchema: inputSchema7,
2192
- parameters: parameters7,
2603
+ description: description8,
2604
+ inputSchema: inputSchema8,
2605
+ parameters: parameters8,
2193
2606
  isReadOnly: false,
2194
2607
  isConcurrencySafe: false,
2195
2608
  maxResultSizeChars: DEFAULT_MAX_RESULT_SIZE_CHARS,
2196
- call: call7
2609
+ call: call8
2197
2610
  };
2198
2611
 
2199
2612
  // 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")
2613
+ import { z as z9 } from "zod";
2614
+ var inputSchema9 = z9.object({
2615
+ 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"),
2616
+ max_results: z9.number().int().min(1).max(20).optional().describe("\u8FD4\u56DE\u7ED3\u679C\u6570\u91CF\uFF0C1-20\uFF0C\u9ED8\u8BA4 5"),
2617
+ 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"),
2618
+ topic: z9.enum(["general", "news"]).optional().describe("general=\u901A\u7528\u7F51\u9875\uFF1Bnews=\u504F\u65B0\u95FB\u6E90\uFF1B\u9ED8\u8BA4 general")
2206
2619
  });
2207
- var parameters8 = toToolParameters(inputSchema8);
2208
- var description8 = `- Searches the public web via the Tavily Search API and returns structured results.
2620
+ var parameters9 = toToolParameters(inputSchema9);
2621
+ var description9 = `- Searches the public web via the Tavily Search API and returns structured results.
2209
2622
  - 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
2623
  - 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
2624
  - Prefer specific natural-language queries over keyword soup (e.g. "how does Bun handle .env files in version 1.1").
2212
2625
  - Requires the TAVILY_API_KEY environment variable to be set; if missing the tool returns a friendly error.`;
2213
- async function call8(input, signal) {
2626
+ async function call9(input, signal) {
2214
2627
  const apiKey = process.env.TAVILY_API_KEY;
2215
2628
  if (!apiKey) {
2216
2629
  return {
@@ -2292,37 +2705,41 @@ ${(r.content ?? "").trim()}`
2292
2705
  }
2293
2706
  var webSearchTool = {
2294
2707
  name: "WebSearch",
2295
- description: description8,
2296
- inputSchema: inputSchema8,
2297
- parameters: parameters8,
2708
+ description: description9,
2709
+ inputSchema: inputSchema9,
2710
+ parameters: parameters9,
2298
2711
  isReadOnly: true,
2299
2712
  isConcurrencySafe: true,
2300
2713
  maxResultSizeChars: DEFAULT_MAX_RESULT_SIZE_CHARS,
2301
- call: call8
2714
+ call: call9
2302
2715
  };
2303
2716
 
2304
2717
  // 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";
2718
+ import { existsSync as existsSync6 } from "fs";
2719
+ import { mkdir as mkdir5, stat as stat4, writeFile as writeFile5 } from "fs/promises";
2307
2720
  import { dirname as dirname6 } from "path";
2308
- import { z as z9 } from "zod";
2721
+ import { z as z10 } from "zod";
2309
2722
  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")
2723
+ var inputSchema10 = z10.object({
2724
+ file_path: z10.string().min(1).describe("\u8981\u5199\u5165\u7684\u6587\u4EF6\u8DEF\u5F84"),
2725
+ content: z10.string().describe("\u6587\u4EF6\u5B8C\u6574\u5185\u5BB9\uFF08\u4F1A\u8986\u76D6\u65E2\u6709\u5185\u5BB9\uFF09")
2313
2726
  });
2314
- var parameters9 = toToolParameters(inputSchema9);
2315
- var description9 = `Writes a file to the local filesystem.
2727
+ var parameters10 = toToolParameters(inputSchema10);
2728
+ var description10 = `Writes a file to the local filesystem.
2316
2729
 
2317
2730
  Usage:
2318
2731
  - This tool will overwrite the existing file if there is one at the provided path.
2319
2732
  - If the parent directory does not exist, it will be created recursively.
2320
2733
  - ALWAYS prefer editing existing files in the codebase via the Edit tool. NEVER write new files unless explicitly required.
2321
2734
  - NEVER create documentation files (*.md) or README files unless explicitly requested by the User.`;
2322
- async function call9(input) {
2735
+ async function call10(input) {
2323
2736
  const pathResult = validateAndResolvePath(input.file_path, getWorkingDir());
2324
2737
  if (!pathResult.ok) return pathResult;
2325
2738
  const filePath = pathResult.resolvedPath;
2739
+ const freshness = assertFresh(filePath);
2740
+ if (!freshness.ok) {
2741
+ return { ok: false, error: freshness.error };
2742
+ }
2326
2743
  const contentSize = Buffer.byteLength(input.content, "utf8");
2327
2744
  if (contentSize > MAX_WRITE_SIZE_BYTES) {
2328
2745
  return {
@@ -2333,7 +2750,7 @@ async function call9(input) {
2333
2750
  try {
2334
2751
  await mkdir5(dirname6(filePath), { recursive: true });
2335
2752
  let originalSize = 0;
2336
- const fileExisted = existsSync4(filePath);
2753
+ const fileExisted = existsSync6(filePath);
2337
2754
  if (fileExisted) {
2338
2755
  try {
2339
2756
  const st = await stat4(filePath);
@@ -2342,16 +2759,32 @@ async function call9(input) {
2342
2759
  }
2343
2760
  }
2344
2761
  let contentToWrite = input.content;
2762
+ let bomEncoding = "utf8";
2345
2763
  if (fileExisted) {
2764
+ const detected = await detectFileBomEncoding(filePath);
2765
+ if (detected === "utf16le") {
2766
+ return {
2767
+ ok: false,
2768
+ 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"
2769
+ };
2770
+ }
2771
+ bomEncoding = detected;
2346
2772
  const lineEnding = await detectFileLineEndings(filePath);
2347
2773
  contentToWrite = applyLineEnding(input.content, lineEnding);
2348
2774
  }
2349
- await writeFile4(filePath, contentToWrite, "utf8");
2775
+ if (bomEncoding === "utf8-bom") {
2776
+ const bomBytes = Buffer.from([239, 187, 191]);
2777
+ const bodyBytes = Buffer.from(contentToWrite, "utf8");
2778
+ await writeFile5(filePath, Buffer.concat([bomBytes, bodyBytes]));
2779
+ } else {
2780
+ await writeFile5(filePath, contentToWrite, "utf8");
2781
+ }
2350
2782
  const action = fileExisted ? "\u5DF2\u8986\u76D6" : "\u5DF2\u521B\u5EFA\u65B0\u6587\u4EF6";
2351
2783
  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`;
2784
+ const bomInfo = bomEncoding === "utf8-bom" ? "\uFF0C\u5DF2\u4FDD\u7559 UTF-8 BOM" : "";
2352
2785
  return {
2353
2786
  ok: true,
2354
- content: `${action} ${filePath}${sizeInfo}`
2787
+ content: `${action} ${filePath}${sizeInfo}${bomInfo}`
2355
2788
  };
2356
2789
  } catch (e) {
2357
2790
  return { ok: false, error: `\u5199\u5165\u5931\u8D25\uFF1A${e.message}` };
@@ -2359,19 +2792,20 @@ async function call9(input) {
2359
2792
  }
2360
2793
  var writeTool = {
2361
2794
  name: "Write",
2362
- description: description9,
2363
- inputSchema: inputSchema9,
2364
- parameters: parameters9,
2795
+ description: description10,
2796
+ inputSchema: inputSchema10,
2797
+ parameters: parameters10,
2365
2798
  isReadOnly: false,
2366
2799
  isConcurrencySafe: false,
2367
2800
  maxResultSizeChars: DEFAULT_MAX_RESULT_SIZE_CHARS,
2368
- call: call9
2801
+ call: call10
2369
2802
  };
2370
2803
 
2371
2804
  // src/tools/index.ts
2372
2805
  var ALL_TOOLS = [
2373
2806
  readTool,
2374
2807
  editTool,
2808
+ multiEditTool,
2375
2809
  writeTool,
2376
2810
  globTool,
2377
2811
  grepTool,
@@ -3780,7 +4214,7 @@ async function reactiveCompactIfApplicable(messages, provider, error, state = de
3780
4214
  }
3781
4215
 
3782
4216
  // src/plugins/commandRouter.ts
3783
- import { readFile as readFile8, readdir as readdir3 } from "fs/promises";
4217
+ import { readFile as readFile9, readdir as readdir3 } from "fs/promises";
3784
4218
  import { join as join6 } from "path";
3785
4219
  var PLUGINS_DIR = join6(findPackageRoot(import.meta.url), "plugins");
3786
4220
  var pluginCache = /* @__PURE__ */ new Map();
@@ -3812,7 +4246,7 @@ async function loadPlugin(pluginDirPath) {
3812
4246
  let manifestVersion;
3813
4247
  let manifestDesc;
3814
4248
  try {
3815
- const raw = await readFile8(manifestPath, "utf8");
4249
+ const raw = await readFile9(manifestPath, "utf8");
3816
4250
  const parsed = JSON.parse(raw);
3817
4251
  manifestName = parsed.name ?? dirName;
3818
4252
  manifestVersion = parsed.version;
@@ -3827,7 +4261,7 @@ async function loadPlugin(pluginDirPath) {
3827
4261
  if (!entry.name.endsWith(".md")) continue;
3828
4262
  const cmdPath = join6(commandsDir, entry.name);
3829
4263
  try {
3830
- const content = await readFile8(cmdPath, "utf8");
4264
+ const content = await readFile9(cmdPath, "utf8");
3831
4265
  const fm = parseMarkdownFrontmatter(content);
3832
4266
  const sep2 = content.indexOf("\n---", 4);
3833
4267
  const body = sep2 >= 0 ? content.slice(sep2 + 4).trim() : content.trim();
@@ -3847,7 +4281,7 @@ async function loadPlugin(pluginDirPath) {
3847
4281
  const hooksJsonPath = join6(pluginDirPath, "hooks", "hooks.json");
3848
4282
  let hasStopHook = false;
3849
4283
  try {
3850
- const hooksRaw = await readFile8(hooksJsonPath, "utf8");
4284
+ const hooksRaw = await readFile9(hooksJsonPath, "utf8");
3851
4285
  const hooksParsed = JSON.parse(hooksRaw);
3852
4286
  const stopHooks = hooksParsed?.hooks?.Stop;
3853
4287
  if (Array.isArray(stopHooks) && stopHooks.length > 0) {
@@ -3940,13 +4374,13 @@ function getActiveStopHookPlugins() {
3940
4374
  }
3941
4375
 
3942
4376
  // src/plugins/stopHook.ts
3943
- import { readFile as readFile9 } from "fs/promises";
4377
+ import { readFile as readFile10 } from "fs/promises";
3944
4378
  import { join as join7 } from "path";
3945
4379
  import { spawn as spawn4 } from "child_process";
3946
4380
  async function loadStopHookConfig(pluginRoot) {
3947
4381
  const hooksJsonPath = join7(pluginRoot, "hooks", "hooks.json");
3948
4382
  try {
3949
- const raw = await readFile9(hooksJsonPath, "utf8");
4383
+ const raw = await readFile10(hooksJsonPath, "utf8");
3950
4384
  const parsed = JSON.parse(raw);
3951
4385
  const stopEntries = parsed?.hooks?.Stop;
3952
4386
  if (!Array.isArray(stopEntries)) return [];
@@ -4028,7 +4462,7 @@ function runSingleStopHook(hookConfig, pluginRoot, transcriptText) {
4028
4462
  }
4029
4463
 
4030
4464
  // src/plugins/verificationGate.ts
4031
- import { existsSync as existsSync5, readFileSync } from "fs";
4465
+ import { existsSync as existsSync7, readFileSync } from "fs";
4032
4466
  import { spawn as spawn5 } from "child_process";
4033
4467
  function parseVerifyArg(arg) {
4034
4468
  const colonIdx = arg.indexOf(":");
@@ -4105,7 +4539,7 @@ async function verifyShell(command, timeout) {
4105
4539
  };
4106
4540
  }
4107
4541
  function verifyFileExists(file) {
4108
- const exists = existsSync5(file);
4542
+ const exists = existsSync7(file);
4109
4543
  return {
4110
4544
  check: { type: "file_exists", file },
4111
4545
  passed: exists,
@@ -4205,8 +4639,8 @@ function formatCheckName(check) {
4205
4639
  }
4206
4640
 
4207
4641
  // 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";
4642
+ import { mkdir as mkdir6, appendFile, writeFile as writeFile6, unlink as unlink2, rmdir as rmdir2 } from "fs/promises";
4643
+ import { existsSync as existsSync8, readFileSync as readFileSync2 } from "fs";
4210
4644
  import { join as join8 } from "path";
4211
4645
  var Phase = /* @__PURE__ */ ((Phase2) => {
4212
4646
  Phase2["PLAN"] = "plan";
@@ -4275,8 +4709,8 @@ ${goal}
4275
4709
  };
4276
4710
  for (const [name, content] of Object.entries(files)) {
4277
4711
  const path2 = join8(this.dir, `${name}.md`);
4278
- if (!existsSync6(path2)) {
4279
- await writeFile5(path2, content);
4712
+ if (!existsSync8(path2)) {
4713
+ await writeFile6(path2, content);
4280
4714
  }
4281
4715
  }
4282
4716
  }
@@ -4320,14 +4754,14 @@ ${goal}
4320
4754
  `\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
4755
  );
4322
4756
  }
4323
- await writeFile5(join8(this.dir, "phase.md"), phase);
4757
+ await writeFile6(join8(this.dir, "phase.md"), phase);
4324
4758
  await this.appendProgress(`PHASE \u2192 ${phase}: ${reason}`);
4325
4759
  }
4326
4760
  async forceSetPhase(phase, reason) {
4327
4761
  if (!VALID_PHASES.has(phase)) {
4328
4762
  throw new Error(`Invalid phase: ${phase}`);
4329
4763
  }
4330
- await writeFile5(join8(this.dir, "phase.md"), phase);
4764
+ await writeFile6(join8(this.dir, "phase.md"), phase);
4331
4765
  await this.appendProgress(`PHASE \u2192 ${phase}: ${reason}`);
4332
4766
  }
4333
4767
  async appendProgress(line) {
@@ -4945,6 +5379,7 @@ function useChat(args) {
4945
5379
  setCompacting(false);
4946
5380
  bump();
4947
5381
  await clearContext();
5382
+ clearFileState();
4948
5383
  }, [bump, isLoading]);
4949
5384
  const compactNow = useCallback5(async () => {
4950
5385
  if (isLoading) return;
@@ -5298,7 +5733,7 @@ async function main() {
5298
5733
  const dirArg = extractCwdArg(args);
5299
5734
  if (dirArg) {
5300
5735
  const abs = resolve8(dirArg);
5301
- if (!existsSync7(abs)) {
5736
+ if (!existsSync9(abs)) {
5302
5737
  mkdirSync(abs, { recursive: true });
5303
5738
  }
5304
5739
  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.7",
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": {