minimal-agent 0.1.5 → 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.
package/dist/main.js CHANGED
@@ -2,7 +2,19 @@
2
2
 
3
3
  // src/main.tsx
4
4
  import { render } from "ink";
5
+ import { existsSync as existsSync9, mkdirSync } from "fs";
5
6
  import { createRequire } from "module";
7
+ import { resolve as resolve8 } from "path";
8
+
9
+ // src/bootstrap/cwdArg.ts
10
+ function extractCwdArg(argv) {
11
+ for (let i = 0; i < argv.length; i++) {
12
+ if (argv[i] === "-d" || argv[i] === "--cwd") {
13
+ return argv[i + 1] ?? null;
14
+ }
15
+ }
16
+ return null;
17
+ }
6
18
 
7
19
  // src/bootstrap/workingDir.ts
8
20
  import { resolve } from "path";
@@ -57,42 +69,6 @@ async function saveConfig(cfg) {
57
69
 
58
70
  // src/config.ts
59
71
  var DEFAULT_CONTEXT_WINDOW = 128e3;
60
- async function loadProvider() {
61
- const baseURL = process.env.MINIMAL_AGENT_BASE_URL;
62
- const apiKey = process.env.MINIMAL_AGENT_API_KEY;
63
- const model = process.env.MINIMAL_AGENT_MODEL;
64
- if (!baseURL || !apiKey || !model) {
65
- const missing = [];
66
- if (!baseURL) missing.push("MINIMAL_AGENT_BASE_URL");
67
- if (!apiKey) missing.push("MINIMAL_AGENT_API_KEY");
68
- if (!model) missing.push("MINIMAL_AGENT_MODEL");
69
- throw new Error(
70
- `\u7F3A\u5C11\u5FC5\u9700\u7684\u73AF\u5883\u53D8\u91CF\uFF1A${missing.join(", ")}
71
-
72
- \u8BF7\u5728 .env \u4E2D\u914D\u7F6E\uFF1A
73
- MINIMAL_AGENT_BASE_URL=https://api.example.com/v1
74
- MINIMAL_AGENT_API_KEY=your-api-key
75
- MINIMAL_AGENT_MODEL=your-model
76
-
77
- \u53C2\u8003 .env.example`
78
- );
79
- }
80
- const contextWindowRaw = process.env.MINIMAL_AGENT_CONTEXT_WINDOW;
81
- let contextWindow = DEFAULT_CONTEXT_WINDOW;
82
- if (contextWindowRaw) {
83
- const n = parseInt(contextWindowRaw, 10);
84
- if (!Number.isNaN(n) && n > 0) {
85
- contextWindow = n;
86
- }
87
- }
88
- return {
89
- name: process.env.MINIMAL_AGENT_PROVIDER ?? "env",
90
- baseURL,
91
- apiKey,
92
- model,
93
- contextWindow
94
- };
95
- }
96
72
  async function loadProviderLayered() {
97
73
  const envBaseURL = process.env.MINIMAL_AGENT_BASE_URL;
98
74
  const envApiKey = process.env.MINIMAL_AGENT_API_KEY;
@@ -124,8 +100,8 @@ async function loadProviderLayered() {
124
100
  }
125
101
 
126
102
  // src/context/persistContext.ts
127
- import { mkdir as mkdir3, readFile as readFile2, unlink, writeFile as writeFile2 } from "fs/promises";
128
- import { dirname as dirname3 } from "path";
103
+ import { mkdir as mkdir3, readFile as readFile2, readdir, rmdir, unlink, writeFile as writeFile2 } from "fs/promises";
104
+ import { dirname as dirname3, join as join3 } from "path";
129
105
 
130
106
  // src/context/sessionPath.ts
131
107
  import { createHash } from "crypto";
@@ -191,6 +167,30 @@ async function clearContext(file) {
191
167
  await unlink(target);
192
168
  } catch {
193
169
  }
170
+ const cwd = getWorkingDir();
171
+ let topEntries;
172
+ try {
173
+ topEntries = await readdir(cwd);
174
+ } catch {
175
+ return;
176
+ }
177
+ const stateDirs = topEntries.filter(
178
+ (name) => name === ".minimal-agent" || name.startsWith(".minimal-agent-")
179
+ );
180
+ for (const name of stateDirs) {
181
+ const dir = join3(cwd, name);
182
+ try {
183
+ const entries = await readdir(dir);
184
+ for (const entry of entries) {
185
+ try {
186
+ await unlink(join3(dir, entry));
187
+ } catch {
188
+ }
189
+ }
190
+ await rmdir(dir);
191
+ } catch {
192
+ }
193
+ }
194
194
  }
195
195
 
196
196
  // src/prompts/system.ts
@@ -198,10 +198,10 @@ import { homedir as homedir3 } from "os";
198
198
 
199
199
  // src/prompts/projectInstructions.ts
200
200
  import { readFile as readFile3 } from "fs/promises";
201
- import { join as join3 } from "path";
201
+ import { join as join4 } from "path";
202
202
  var FILENAME = "minimal-agent.md";
203
203
  async function loadProjectInstructions(cwd) {
204
- const filePath = join3(cwd, FILENAME);
204
+ const filePath = join4(cwd, FILENAME);
205
205
  try {
206
206
  const content = await readFile3(filePath, "utf-8");
207
207
  const trimmed = content.trim();
@@ -220,8 +220,8 @@ async function loadProjectInstructions(cwd) {
220
220
  }
221
221
 
222
222
  // src/prompts/skillList.ts
223
- import { readFile as readFile4, readdir } from "fs/promises";
224
- import { join as join4 } from "path";
223
+ import { readFile as readFile4, readdir as readdir2 } from "fs/promises";
224
+ import { join as join5 } from "path";
225
225
 
226
226
  // src/utils/packageRoot.ts
227
227
  import { existsSync } from "fs";
@@ -246,7 +246,7 @@ function findPackageRoot(metaUrl) {
246
246
  }
247
247
 
248
248
  // src/prompts/skillList.ts
249
- var SKILLS_DIR = join4(findPackageRoot(import.meta.url), "skills");
249
+ var SKILLS_DIR = join5(findPackageRoot(import.meta.url), "skills");
250
250
  function stripQuotes(s) {
251
251
  const trimmed = s.trim();
252
252
  if (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'")) {
@@ -273,10 +273,10 @@ function parseFrontmatter(content) {
273
273
  async function getSkillList() {
274
274
  const skills = [];
275
275
  try {
276
- const entries = await readdir(SKILLS_DIR, { withFileTypes: true });
276
+ const entries = await readdir2(SKILLS_DIR, { withFileTypes: true });
277
277
  for (const entry of entries) {
278
278
  if (!entry.isDirectory()) continue;
279
- const skillPath = join4(SKILLS_DIR, entry.name, "SKILL.md");
279
+ const skillPath = join5(SKILLS_DIR, entry.name, "SKILL.md");
280
280
  try {
281
281
  const content = await readFile4(skillPath, "utf8");
282
282
  const meta = parseFrontmatter(content);
@@ -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",
@@ -693,10 +846,294 @@ var bashTool = {
693
846
  };
694
847
 
695
848
  // src/tools/edit/edit.ts
696
- import { readFile as readFile5, writeFile as writeFile3, mkdir as mkdir4 } from "fs/promises";
697
- import { existsSync as existsSync2 } from "fs";
698
- import { dirname as dirname5, resolve as resolve4 } from "path";
849
+ import { readFile as readFile6, writeFile as writeFile3, mkdir as mkdir4 } from "fs/promises";
850
+ import { existsSync as existsSync3 } from "fs";
851
+ import { dirname as dirname5 } from "path";
699
852
  import { z as z2 } from "zod";
853
+
854
+ // src/tools/shared/fileUtils.ts
855
+ import { open, readFile as readFile5 } from "fs/promises";
856
+ import { homedir as homedir4 } from "os";
857
+ import { extname, resolve as resolve4 } from "path";
858
+ var BLOCKED_DEVICE_PATHS = /* @__PURE__ */ new Set([
859
+ "/dev/zero",
860
+ "/dev/random",
861
+ "/dev/urandom",
862
+ "/dev/full",
863
+ "/dev/stdin",
864
+ "/dev/tty",
865
+ "/dev/console",
866
+ "/dev/stdout",
867
+ "/dev/stderr",
868
+ "/dev/fd/0",
869
+ "/dev/fd/1",
870
+ "/dev/fd/2"
871
+ ]);
872
+ var WINDOWS_BLOCKED_NAMES = /* @__PURE__ */ new Set(["NUL", "CON", "PRN", "AUX", "COM1", "COM2", "LPT1"]);
873
+ function isBlockedDevicePath(filePath) {
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"))) {
877
+ return true;
878
+ }
879
+ const baseName = slashed.split("/").pop() ?? "";
880
+ if (WINDOWS_BLOCKED_NAMES.has(baseName.toUpperCase())) {
881
+ return true;
882
+ }
883
+ return false;
884
+ }
885
+ function validateAndResolvePath(rawPath, workingDir) {
886
+ if (rawPath.includes("\0")) {
887
+ return { ok: false, error: "\u8DEF\u5F84\u5305\u542B\u975E\u6CD5\u5B57\u7B26\uFF08null byte\uFF09" };
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
+ }
892
+ const expanded = expandPath(rawPath);
893
+ const resolved = resolve4(workingDir, expanded);
894
+ if (isBlockedDevicePath(resolved)) {
895
+ return { ok: false, error: `\u4E0D\u5141\u8BB8\u8BFB\u53D6\u8BBE\u5907\u6587\u4EF6\uFF1A${resolved}\u3002\u8BE5\u8DEF\u5F84\u53EF\u80FD\u4EA7\u751F\u65E0\u9650\u8F93\u51FA\u6216\u963B\u585E\u8FDB\u7A0B\u3002` };
896
+ }
897
+ if (process.platform === "win32" && /^\\\\/.test(resolved)) {
898
+ return { ok: false, error: "\u4E0D\u652F\u6301 UNC \u8DEF\u5F84\uFF08\\\\server\\share \u683C\u5F0F\uFF09\uFF0C\u8BF7\u4F7F\u7528\u672C\u5730\u8DEF\u5F84" };
899
+ }
900
+ return { ok: true, resolvedPath: resolved };
901
+ }
902
+ function expandPath(p) {
903
+ if (p.startsWith("~/") || p === "~") {
904
+ return resolve4(homedir4(), p.slice(2));
905
+ }
906
+ return p;
907
+ }
908
+ function detectLineEndingsForString(content) {
909
+ let crlfCount = 0;
910
+ let lfCount = 0;
911
+ for (let i = 0; i < content.length; i++) {
912
+ if (content[i] === "\n") {
913
+ if (i > 0 && content[i - 1] === "\r") {
914
+ crlfCount++;
915
+ } else {
916
+ lfCount++;
917
+ }
918
+ }
919
+ }
920
+ return crlfCount > lfCount ? "CRLF" : "LF";
921
+ }
922
+ async function detectFileLineEndings(filePath) {
923
+ try {
924
+ const handle = await readFile5(filePath, { encoding: "utf8" });
925
+ const head = handle.slice(0, 4096);
926
+ return detectLineEndingsForString(head);
927
+ } catch {
928
+ return "LF";
929
+ }
930
+ }
931
+ function applyLineEnding(content, ending) {
932
+ if (ending === "CRLF") {
933
+ return content.replaceAll("\r\n", "\n").split("\n").join("\r\n");
934
+ }
935
+ return content;
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
+ }
1019
+ var LEFT_SINGLE_CURLY_QUOTE = "\u2018";
1020
+ var RIGHT_SINGLE_CURLY_QUOTE = "\u2019";
1021
+ var LEFT_DOUBLE_CURLY_QUOTE = "\u201C";
1022
+ var RIGHT_DOUBLE_CURLY_QUOTE = "\u201D";
1023
+ function normalizeQuotes(str) {
1024
+ return str.replaceAll(LEFT_SINGLE_CURLY_QUOTE, "'").replaceAll(RIGHT_SINGLE_CURLY_QUOTE, "'").replaceAll(LEFT_DOUBLE_CURLY_QUOTE, '"').replaceAll(RIGHT_DOUBLE_CURLY_QUOTE, '"');
1025
+ }
1026
+ function findActualString(fileContent, searchString) {
1027
+ if (fileContent.includes(searchString)) {
1028
+ return searchString;
1029
+ }
1030
+ const normalizedSearch = normalizeQuotes(searchString);
1031
+ const normalizedFile = normalizeQuotes(fileContent);
1032
+ const searchIndex = normalizedFile.indexOf(normalizedSearch);
1033
+ if (searchIndex !== -1) {
1034
+ return fileContent.substring(searchIndex, searchIndex + searchString.length);
1035
+ }
1036
+ return null;
1037
+ }
1038
+ function preserveQuoteStyle(oldString, actualOldString, newString) {
1039
+ if (oldString === actualOldString) {
1040
+ return newString;
1041
+ }
1042
+ const hasDoubleQuotes = actualOldString.includes(LEFT_DOUBLE_CURLY_QUOTE) || actualOldString.includes(RIGHT_DOUBLE_CURLY_QUOTE);
1043
+ const hasSingleQuotes = actualOldString.includes(LEFT_SINGLE_CURLY_QUOTE) || actualOldString.includes(RIGHT_SINGLE_CURLY_QUOTE);
1044
+ if (!hasDoubleQuotes && !hasSingleQuotes) {
1045
+ return newString;
1046
+ }
1047
+ let result = newString;
1048
+ if (hasDoubleQuotes) {
1049
+ result = applyCurlyDoubleQuotes(result);
1050
+ }
1051
+ if (hasSingleQuotes) {
1052
+ result = applyCurlySingleQuotes(result);
1053
+ }
1054
+ return result;
1055
+ }
1056
+ function isOpeningContext(chars, index) {
1057
+ if (index === 0) return true;
1058
+ const prev = chars[index - 1];
1059
+ return prev === " " || prev === " " || prev === "\n" || prev === "\r" || prev === "(" || prev === "[" || prev === "{" || prev === "\u2014" || prev === "\u2013";
1060
+ }
1061
+ function applyCurlyDoubleQuotes(str) {
1062
+ const chars = [...str];
1063
+ const result = [];
1064
+ for (let i = 0; i < chars.length; i++) {
1065
+ if (chars[i] === '"') {
1066
+ result.push(
1067
+ isOpeningContext(chars, i) ? LEFT_DOUBLE_CURLY_QUOTE : RIGHT_DOUBLE_CURLY_QUOTE
1068
+ );
1069
+ } else {
1070
+ result.push(chars[i]);
1071
+ }
1072
+ }
1073
+ return result.join("");
1074
+ }
1075
+ function applyCurlySingleQuotes(str) {
1076
+ const chars = [...str];
1077
+ const result = [];
1078
+ for (let i = 0; i < chars.length; i++) {
1079
+ if (chars[i] === "'") {
1080
+ const prev = i > 0 ? chars[i - 1] : void 0;
1081
+ const next = i < chars.length - 1 ? chars[i + 1] : void 0;
1082
+ const prevIsLetter = prev !== void 0 && new RegExp("\\p{L}", "u").test(prev);
1083
+ const nextIsLetter = next !== void 0 && new RegExp("\\p{L}", "u").test(next);
1084
+ if (prevIsLetter && nextIsLetter) {
1085
+ result.push(RIGHT_SINGLE_CURLY_QUOTE);
1086
+ } else {
1087
+ result.push(
1088
+ isOpeningContext(chars, i) ? LEFT_SINGLE_CURLY_QUOTE : RIGHT_SINGLE_CURLY_QUOTE
1089
+ );
1090
+ }
1091
+ } else {
1092
+ result.push(chars[i]);
1093
+ }
1094
+ }
1095
+ return result.join("");
1096
+ }
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
+
1136
+ // src/tools/edit/edit.ts
700
1137
  var MAX_EDIT_FILE_SIZE_BYTES = 1024 * 1024 * 1024;
701
1138
  var inputSchema2 = z2.object({
702
1139
  file_path: z2.string().min(1).describe("\u8981\u7F16\u8F91\u7684\u6587\u4EF6\u8DEF\u5F84"),
@@ -718,25 +1155,20 @@ Usage:
718
1155
  - The edit will FAIL if \`old_string\` is not unique in the file. Either provide a larger string with more surrounding context to make it unique or use \`replace_all\` to change every instance of \`old_string\`.
719
1156
  - Use \`replace_all\` for replacing and renaming strings across the file. This parameter is useful if you want to rename a variable for instance.
720
1157
  - Preserve exact indentation (tabs/spaces).`;
721
- function validatePath(filePath) {
722
- if (filePath.includes("\0")) {
723
- return { ok: false, error: "\u8DEF\u5F84\u5305\u542B\u975E\u6CD5\u5B57\u7B26\uFF08null byte\uFF09" };
724
- }
725
- if (process.platform === "win32" && filePath.includes("\\\\")) {
726
- return { ok: false, error: "\u4E0D\u652F\u6301 UNC \u8DEF\u5F84\uFF08\\\\server\\share \u683C\u5F0F\uFF09\uFF0C\u8BF7\u4F7F\u7528\u672C\u5730\u8DEF\u5F84" };
727
- }
728
- return { ok: true };
729
- }
730
1158
  async function call2(input) {
731
- const filePath = resolve4(input.file_path);
732
1159
  const { old_string, new_string } = input;
733
1160
  const replaceAll = input.replace_all ?? false;
734
- const pathCheck = validatePath(filePath);
735
- if (!pathCheck.ok) return pathCheck;
1161
+ const pathResult = validateAndResolvePath(input.file_path, getWorkingDir());
1162
+ if (!pathResult.ok) return pathResult;
1163
+ const filePath = pathResult.resolvedPath;
1164
+ const freshness = assertFresh(filePath);
1165
+ if (!freshness.ok) {
1166
+ return { ok: false, error: freshness.error };
1167
+ }
736
1168
  if (old_string === new_string) {
737
1169
  return { ok: false, error: "old_string \u4E0E new_string \u5B8C\u5168\u76F8\u540C\uFF0C\u6CA1\u6709\u53EF\u6539\u7684\u5185\u5BB9\u3002" };
738
1170
  }
739
- if (old_string === "" && !existsSync2(filePath)) {
1171
+ if (old_string === "" && !existsSync3(filePath)) {
740
1172
  try {
741
1173
  await mkdir4(dirname5(filePath), { recursive: true });
742
1174
  await writeFile3(filePath, new_string, "utf8");
@@ -748,7 +1180,7 @@ async function call2(input) {
748
1180
  return { ok: false, error: `\u521B\u5EFA\u6587\u4EF6\u5931\u8D25\uFF1A${e.message}` };
749
1181
  }
750
1182
  }
751
- if (!existsSync2(filePath)) {
1183
+ if (!existsSync3(filePath)) {
752
1184
  return {
753
1185
  ok: false,
754
1186
  error: `\u6587\u4EF6\u4E0D\u5B58\u5728\uFF1A${filePath}
@@ -757,10 +1189,11 @@ async function call2(input) {
757
1189
  }
758
1190
  let original;
759
1191
  try {
760
- original = await readFile5(filePath, "utf8");
1192
+ original = await readFile6(filePath, "utf8");
761
1193
  } catch (e) {
762
1194
  return { ok: false, error: `\u8BFB\u53D6\u5931\u8D25\uFF1A${e.message}` };
763
1195
  }
1196
+ const originalLineEnding = detectLineEndingsForString(original);
764
1197
  const fileSize = Buffer.byteLength(original, "utf8");
765
1198
  if (fileSize > MAX_EDIT_FILE_SIZE_BYTES) {
766
1199
  return {
@@ -774,9 +1207,16 @@ async function call2(input) {
774
1207
  error: "old_string \u4E3A\u7A7A\u4F46\u6587\u4EF6\u5DF2\u5B58\u5728 \u2014\u2014 \u8FD9\u901A\u5E38\u662F\u9519\u8BEF\u7684\u3002\u8981\u66FF\u6362\u5168\u6587\u8BF7\u7528 Write \u5DE5\u5177\u3002"
775
1208
  };
776
1209
  }
777
- const occurrences = countOccurrences(original, old_string);
1210
+ let searchTarget = old_string;
1211
+ let processedNewString = new_string;
1212
+ const actualOld = findActualString(original, old_string);
1213
+ if (actualOld !== null && actualOld !== old_string) {
1214
+ searchTarget = actualOld;
1215
+ processedNewString = preserveQuoteStyle(old_string, actualOld, new_string);
1216
+ }
1217
+ const occurrences = countOccurrences(original, searchTarget);
778
1218
  if (occurrences === 0) {
779
- const hint = findFuzzyMatchHint(original, old_string);
1219
+ const hint = findFuzzyMatchHint(original, searchTarget);
780
1220
  const extraMsg = hint ? `
781
1221
 
782
1222
  \u{1F4A1} \u63D0\u793A\uFF1A${hint}` : "";
@@ -792,9 +1232,10 @@ async function call2(input) {
792
1232
  \u8BF7\u6269\u5927 old_string \u5305\u542B\u66F4\u591A\u4E0A\u4E0B\u6587\uFF0C\u6216\u663E\u5F0F\u4F20 replace_all=true\u3002`
793
1233
  };
794
1234
  }
795
- const replaced = replaceAll ? splitReplaceAll(original, old_string, new_string) : original.replace(old_string, new_string);
1235
+ const replaced = replaceAll ? splitReplaceAll(original, searchTarget, processedNewString) : original.replace(searchTarget, processedNewString);
1236
+ const normalizedReplaced = applyLineEnding(replaced, originalLineEnding);
796
1237
  try {
797
- await writeFile3(filePath, replaced, "utf8");
1238
+ await writeFile3(filePath, normalizedReplaced, "utf8");
798
1239
  } catch (e) {
799
1240
  return { ok: false, error: `\u5199\u5165\u5931\u8D25\uFF1A${e.message}` };
800
1241
  }
@@ -892,22 +1333,146 @@ var editTool = {
892
1333
  call: call2
893
1334
  };
894
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
+
895
1460
  // src/tools/glob/glob.ts
896
1461
  import { stat as stat2 } from "fs/promises";
897
1462
  import { isAbsolute, resolve as resolve5 } from "path";
898
1463
  import fg from "fast-glob";
899
- import { z as z3 } from "zod";
900
- var inputSchema3 = z3.object({
901
- pattern: z3.string().min(1).describe('glob \u6A21\u5F0F\uFF0C\u4F8B\u5982 "**/*.ts" \u6216 "src/components/**/*.tsx"'),
902
- 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')
903
1468
  });
904
- var parameters3 = toToolParameters(inputSchema3);
905
- 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
906
1471
  - Supports glob patterns like "**/*.js" or "src/**/*.ts"
907
1472
  - Returns matching file paths sorted by modification time (oldest first)
908
1473
  - Use this tool when you need to find files by name patterns
909
1474
  - When you need to do an open ended search that may require multiple rounds, prefer the Grep tool for content search`;
910
- async function call3(input) {
1475
+ async function call4(input) {
911
1476
  const cwd = input.path ? resolve5(input.path) : getWorkingDir();
912
1477
  const pattern = input.pattern.replace(/\\/g, "/");
913
1478
  let matches;
@@ -954,23 +1519,23 @@ async function call3(input) {
954
1519
  }
955
1520
  var globTool = {
956
1521
  name: "Glob",
957
- description: description3,
958
- inputSchema: inputSchema3,
959
- parameters: parameters3,
1522
+ description: description4,
1523
+ inputSchema: inputSchema4,
1524
+ parameters: parameters4,
960
1525
  isReadOnly: true,
961
1526
  isConcurrencySafe: true,
962
1527
  maxResultSizeChars: DEFAULT_MAX_RESULT_SIZE_CHARS,
963
- call: call3
1528
+ call: call4
964
1529
  };
965
1530
 
966
1531
  // src/tools/grep/grep.ts
967
1532
  import { spawn as spawn3 } from "child_process";
968
1533
  import { resolve as resolve7 } from "path";
969
- import { z as z4 } from "zod";
1534
+ import { z as z5 } from "zod";
970
1535
 
971
1536
  // src/tools/grep/rgPath.ts
972
1537
  import { spawn as spawn2 } from "child_process";
973
- import { chmodSync, existsSync as existsSync3 } from "fs";
1538
+ import { chmodSync, existsSync as existsSync5 } from "fs";
974
1539
  import { resolve as resolve6 } from "path";
975
1540
  var cached;
976
1541
  async function resolveRgPath() {
@@ -980,15 +1545,15 @@ async function resolveRgPath() {
980
1545
  }
981
1546
  async function detect() {
982
1547
  const fromEnv = process.env.MINIMAL_AGENT_RIPGREP_PATH;
983
- if (fromEnv && existsSync3(fromEnv)) return fromEnv;
1548
+ if (fromEnv && existsSync5(fromEnv)) return fromEnv;
984
1549
  const vendored = vendoredRgPath();
985
- if (vendored && existsSync3(vendored)) {
1550
+ if (vendored && existsSync5(vendored)) {
986
1551
  ensureExecutable(vendored);
987
1552
  return vendored;
988
1553
  }
989
1554
  if (await trySpawn("rg")) return "rg";
990
1555
  for (const candidate of claudeCodeCandidates()) {
991
- if (existsSync3(candidate)) {
1556
+ if (existsSync5(candidate)) {
992
1557
  ensureExecutable(candidate);
993
1558
  return candidate;
994
1559
  }
@@ -1081,21 +1646,21 @@ function claudeCodeCandidates() {
1081
1646
  }
1082
1647
 
1083
1648
  // src/tools/grep/grep.ts
1084
- var inputSchema4 = z4.object({
1085
- pattern: z4.string().min(1).describe("\u6B63\u5219\u8868\u8FBE\u5F0F\uFF08ripgrep \u517C\u5BB9\u8BED\u6CD5\uFF09"),
1086
- path: z4.string().optional().describe("\u641C\u7D22\u7684\u6839\u76EE\u5F55\u6216\u6587\u4EF6\uFF08\u9ED8\u8BA4\u5F53\u524D\u5DE5\u4F5C\u76EE\u5F55\uFF09"),
1087
- glob: z4.string().optional().describe('\u6587\u4EF6\u540D glob \u8FC7\u6EE4\uFF0C\u5982 "*.ts"'),
1088
- type: z4.string().optional().describe('rg \u7684\u6587\u4EF6\u7C7B\u578B\u5FEB\u6377\u540D\uFF0C\u5982 "py"\u3001"rust"\u3001"js"'),
1089
- 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"),
1090
- "-i": z4.boolean().optional().describe("\u5FFD\u7565\u5927\u5C0F\u5199"),
1091
- "-n": z4.boolean().optional().describe("\u663E\u793A\u884C\u53F7\uFF08\u4EC5 content \u6A21\u5F0F\uFF09"),
1092
- "-A": z4.number().int().min(0).optional().describe("\u5339\u914D\u540E\u5C55\u793A\u51E0\u884C\u4E0A\u4E0B\u6587"),
1093
- "-B": z4.number().int().min(0).optional().describe("\u5339\u914D\u524D\u5C55\u793A\u51E0\u884C\u4E0A\u4E0B\u6587"),
1094
- "-C": z4.number().int().min(0).optional().describe("\u5339\u914D\u524D\u540E\u5404\u5C55\u793A\u51E0\u884C\uFF08\u8986\u76D6 -A/-B\uFF09"),
1095
- 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")
1096
1661
  });
1097
- var parameters4 = toToolParameters(inputSchema4);
1098
- var description4 = `A powerful search tool built on ripgrep.
1662
+ var parameters5 = toToolParameters(inputSchema5);
1663
+ var description5 = `A powerful search tool built on ripgrep.
1099
1664
 
1100
1665
  Usage:
1101
1666
  - ALWAYS use Grep for content search tasks. Do NOT invoke \`grep\` or \`rg\` directly via Bash.
@@ -1103,7 +1668,7 @@ Usage:
1103
1668
  - Filter files with glob parameter (e.g., "*.js", "**/*.tsx") or type parameter (e.g., "js", "py", "rust")
1104
1669
  - Output modes: "content" shows matching lines, "files_with_matches" shows only file paths (default), "count" shows match counts
1105
1670
  - Pattern syntax: Uses ripgrep (not classic grep)`;
1106
- async function call4(input, signal) {
1671
+ async function call5(input, signal) {
1107
1672
  const args = [];
1108
1673
  const mode = input.output_mode ?? "files_with_matches";
1109
1674
  if (mode === "files_with_matches") args.push("-l");
@@ -1180,26 +1745,27 @@ async function call4(input, signal) {
1180
1745
  }
1181
1746
  var grepTool = {
1182
1747
  name: "Grep",
1183
- description: description4,
1184
- inputSchema: inputSchema4,
1185
- parameters: parameters4,
1748
+ description: description5,
1749
+ inputSchema: inputSchema5,
1750
+ parameters: parameters5,
1186
1751
  isReadOnly: true,
1187
1752
  isConcurrencySafe: true,
1188
1753
  maxResultSizeChars: DEFAULT_MAX_RESULT_SIZE_CHARS,
1189
- call: call4
1754
+ call: call5
1190
1755
  };
1191
1756
 
1192
1757
  // src/tools/read/read.ts
1193
- import { readFile as readFile6, stat as stat3 } from "fs/promises";
1194
- import { resolve as resolve8 } from "path";
1195
- import { z as z5 } from "zod";
1196
- var inputSchema5 = z5.object({
1197
- 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"),
1198
- offset: z5.number().int().positive().optional().describe("\u8D77\u59CB\u884C\u53F7\uFF081-indexed\uFF09\uFF1B\u4E0D\u586B\u5219\u4ECE\u6587\u4EF6\u5F00\u5934\u8BFB"),
1199
- 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}`)
1758
+ import { createReadStream } from "fs";
1759
+ import { readFile as readFile8, stat as stat3 } from "fs/promises";
1760
+ import { createInterface } from "readline";
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}`)
1200
1766
  });
1201
- var parameters5 = toToolParameters(inputSchema5);
1202
- 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.
1203
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.
1204
1770
 
1205
1771
  Usage:
@@ -1209,10 +1775,24 @@ Usage:
1209
1775
  - Results are returned using cat -n format, with line numbers starting at 1
1210
1776
  - This tool can only read text files, not directories. To read a directory, use the Glob tool.
1211
1777
  - If you read a file that exists but has empty contents you will receive a warning in place of file contents.`;
1212
- async function call5(input) {
1213
- const filePath = resolve8(input.file_path);
1778
+ var STREAM_THRESHOLD = 1024 * 1024;
1779
+ async function call6(input) {
1214
1780
  const offset = input.offset ?? 1;
1215
1781
  const limit = input.limit ?? MAX_LINES_TO_READ;
1782
+ const pathResult = validateAndResolvePath(input.file_path, getWorkingDir());
1783
+ if (!pathResult.ok) {
1784
+ return { ok: false, error: pathResult.error };
1785
+ }
1786
+ const filePath = pathResult.resolvedPath;
1787
+ if (isBlockedDevicePath(filePath)) {
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` };
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
+ }
1216
1796
  let st;
1217
1797
  try {
1218
1798
  st = await stat3(filePath);
@@ -1231,54 +1811,110 @@ async function call5(input) {
1231
1811
  error: `\u6587\u4EF6\u8FC7\u5927\uFF08${st.size} \u5B57\u8282 > ${MAX_FILE_SIZE_BYTES}\uFF09\u3002\u8BF7\u7528 offset/limit \u5206\u6BB5\u8BFB\u3002`
1232
1812
  };
1233
1813
  }
1234
- let raw;
1235
- try {
1236
- raw = await readFile6(filePath, "utf8");
1237
- } catch (e) {
1238
- return { ok: false, error: `\u8BFB\u53D6\u5931\u8D25\uFF1A${e.message}` };
1814
+ let numbered;
1815
+ let totalLines;
1816
+ if (st.size <= STREAM_THRESHOLD) {
1817
+ const result = await readSmallFile(filePath, offset, limit);
1818
+ numbered = result.numbered;
1819
+ totalLines = result.totalLines;
1820
+ if (result.isEmpty) {
1821
+ recordRead(filePath);
1822
+ return { ok: true, content: "<file is empty>" };
1823
+ }
1824
+ } else {
1825
+ const result = await readLargeFileStream(filePath, offset, limit);
1826
+ numbered = result.numbered;
1827
+ totalLines = result.totalLines;
1239
1828
  }
1240
- if (raw.length === 0) {
1829
+ if (totalLines === 0 || !numbered) {
1830
+ recordRead(filePath);
1241
1831
  return { ok: true, content: "<file is empty>" };
1242
1832
  }
1243
- const allLines = raw.split("\n");
1244
- const totalLines = allLines.length;
1245
- const startIdx = Math.max(0, offset - 1);
1246
- const endIdx = Math.min(totalLines, startIdx + limit);
1247
- const slice = allLines.slice(startIdx, endIdx);
1248
- const numbered = slice.map((line, i) => `${(startIdx + i + 1).toString()} ${line}`).join("\n");
1249
1833
  let content = numbered;
1250
- const contentLength = content.length;
1251
- if (contentLength > DEFAULT_MAX_RESULT_SIZE_CHARS) {
1834
+ if (content.length > DEFAULT_MAX_RESULT_SIZE_CHARS) {
1252
1835
  content = content.slice(0, DEFAULT_MAX_RESULT_SIZE_CHARS) + `
1253
1836
 
1254
1837
  ... (\u8F93\u51FA\u8D85\u8FC7 ${DEFAULT_MAX_RESULT_SIZE_CHARS} \u5B57\u7B26\uFF0C\u5DF2\u622A\u65AD)`;
1255
1838
  }
1839
+ const startIdx = Math.max(0, offset - 1);
1840
+ const endIdx = Math.min(totalLines, startIdx + limit);
1256
1841
  if (endIdx < totalLines) {
1257
1842
  const nextOffset = endIdx + 1;
1843
+ const returnedLines = content.split("\n").filter((l) => l.trim()).length;
1258
1844
  content += `
1259
1845
 
1260
- ... (\u672C\u6B21\u8FD4\u56DE ${slice.length} \u884C / \u6587\u4EF6\u5171 ${totalLines} \u884C\uFF1B\u7528 offset=${nextOffset} \u7EE7\u7EED\u8BFB)`;
1846
+ ... (\u672C\u6B21\u8FD4\u56DE ${returnedLines} \u884C / \u6587\u4EF6\u5171 ${totalLines} \u884C\uFF1B\u7528 offset=${nextOffset} \u7EE7\u7EED\u8BFB)`;
1261
1847
  }
1262
1848
  if (st.size > 100 * 1024 && offset === 1) {
1263
1849
  content += `
1264
1850
 
1265
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`;
1266
1852
  }
1853
+ recordRead(filePath);
1267
1854
  return { ok: true, content };
1268
1855
  }
1856
+ async function readSmallFile(filePath, offset, limit) {
1857
+ const raw = await readFile8(filePath, "utf8");
1858
+ if (raw.length === 0) {
1859
+ return { numbered: "", totalLines: 0, isEmpty: true };
1860
+ }
1861
+ const allLines = raw.split("\n");
1862
+ const totalLines = allLines.length;
1863
+ const startIdx = Math.max(0, offset - 1);
1864
+ const endIdx = Math.min(totalLines, startIdx + limit);
1865
+ const slice = allLines.slice(startIdx, endIdx);
1866
+ const numbered = slice.map((line, i) => `${(startIdx + i + 1).toString()} ${line}`).join("\n");
1867
+ return { numbered, totalLines, isEmpty: false };
1868
+ }
1869
+ async function readLargeFileStream(filePath, offset, limit) {
1870
+ return new Promise((resolvePromise, reject) => {
1871
+ const lines = [];
1872
+ let currentLine = 0;
1873
+ const startIdx = Math.max(0, offset - 1);
1874
+ const endLine = startIdx + limit;
1875
+ const input = createReadStream(filePath, { encoding: "utf8" });
1876
+ const rl = createInterface({
1877
+ input,
1878
+ crlfDelay: Infinity
1879
+ });
1880
+ input.on("error", (err) => {
1881
+ reject(err);
1882
+ });
1883
+ rl.on("line", (line) => {
1884
+ currentLine++;
1885
+ if (currentLine > endLine) {
1886
+ rl.close();
1887
+ rl.removeAllListeners();
1888
+ return;
1889
+ }
1890
+ if (currentLine >= offset) {
1891
+ lines.push(`${currentLine} ${line}`);
1892
+ }
1893
+ });
1894
+ rl.on("close", () => {
1895
+ resolvePromise({
1896
+ numbered: lines.join("\n"),
1897
+ totalLines: currentLine
1898
+ });
1899
+ });
1900
+ rl.on("error", (err) => {
1901
+ reject(err);
1902
+ });
1903
+ });
1904
+ }
1269
1905
  var readTool = {
1270
1906
  name: "Read",
1271
- description: description5,
1272
- inputSchema: inputSchema5,
1273
- parameters: parameters5,
1907
+ description: description6,
1908
+ inputSchema: inputSchema6,
1909
+ parameters: parameters6,
1274
1910
  isReadOnly: true,
1275
1911
  isConcurrencySafe: true,
1276
1912
  maxResultSizeChars: DEFAULT_MAX_RESULT_SIZE_CHARS,
1277
- call: call5
1913
+ call: call6
1278
1914
  };
1279
1915
 
1280
1916
  // src/tools/webfetch/webfetch.ts
1281
- import { z as z6 } from "zod";
1917
+ import { z as z7 } from "zod";
1282
1918
 
1283
1919
  // src/tools/webfetch/preapproved.ts
1284
1920
  var PREAPPROVED_HOSTS = /* @__PURE__ */ new Set([
@@ -1563,12 +2199,12 @@ function cleanCache() {
1563
2199
  }
1564
2200
  }
1565
2201
  }
1566
- var inputSchema6 = z6.object({
1567
- url: z6.string().describe("\u8981\u83B7\u53D6\u5185\u5BB9\u7684 URL"),
1568
- 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")
1569
2205
  });
1570
- var parameters6 = toToolParameters(inputSchema6);
1571
- 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.
1572
2208
  - Takes a URL and a prompt as input.
1573
2209
  - Fetches the URL content, converts HTML to markdown.
1574
2210
  - Processes the content with the prompt (e.g., extract summary, find specific info).
@@ -1677,7 +2313,7 @@ async function htmlToMarkdown(html) {
1677
2313
  const td = new TurndownService();
1678
2314
  return td.turndown(html);
1679
2315
  }
1680
- async function call6(input, signal) {
2316
+ async function call7(input, signal) {
1681
2317
  const { url } = input;
1682
2318
  const start = Date.now();
1683
2319
  const cacheKey = url;
@@ -1775,17 +2411,17 @@ ${content}`;
1775
2411
  }
1776
2412
  var webfetchTool = {
1777
2413
  name: "WebFetch",
1778
- description: description6,
1779
- inputSchema: inputSchema6,
1780
- parameters: parameters6,
2414
+ description: description7,
2415
+ inputSchema: inputSchema7,
2416
+ parameters: parameters7,
1781
2417
  isReadOnly: true,
1782
2418
  isConcurrencySafe: true,
1783
2419
  maxResultSizeChars: DEFAULT_MAX_RESULT_SIZE_CHARS,
1784
- call: call6
2420
+ call: call7
1785
2421
  };
1786
2422
 
1787
2423
  // src/tools/webbrowser/webbrowser.ts
1788
- import { z as z7 } from "zod";
2424
+ import { z as z8 } from "zod";
1789
2425
 
1790
2426
  // src/tools/webbrowser/browser.ts
1791
2427
  import os from "os";
@@ -1821,15 +2457,15 @@ function screenshotPath(prefix = "browser") {
1821
2457
  }
1822
2458
 
1823
2459
  // src/tools/webbrowser/webbrowser.ts
1824
- var inputSchema7 = z7.object({
1825
- action: z7.enum(["navigate", "screenshot", "getContent", "click", "fill", "submit"]).describe("Browser action to perform"),
1826
- url: z7.string().url().optional().describe("URL to navigate to (required for navigate action)"),
1827
- selector: z7.string().optional().describe("CSS selector for click/fill/submit actions"),
1828
- value: z7.string().optional().describe("Value to fill in input fields"),
1829
- 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)")
1830
2466
  });
1831
- var parameters7 = toToolParameters(inputSchema7);
1832
- 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.
1833
2469
 
1834
2470
  When to use WebBrowser vs WebSearch:
1835
2471
  - WebSearch: When you need to find information or discover URLs through search
@@ -1860,7 +2496,7 @@ Example actions:
1860
2496
  - Take screenshot: { action: "screenshot" }
1861
2497
  - Click element: { action: "click", selector: "#submit-btn" }
1862
2498
  - Fill form field: { action: "fill", selector: "input[name='email']", value: "user@example.com" }`;
1863
- async function call7(input, signal) {
2499
+ async function call8(input, signal) {
1864
2500
  const { action, url, selector, value, timeout = 3e4 } = input;
1865
2501
  let page;
1866
2502
  try {
@@ -1964,30 +2600,30 @@ Current URL: ${page.url()}`
1964
2600
  }
1965
2601
  var webbrowserTool = {
1966
2602
  name: "WebBrowser",
1967
- description: description7,
1968
- inputSchema: inputSchema7,
1969
- parameters: parameters7,
2603
+ description: description8,
2604
+ inputSchema: inputSchema8,
2605
+ parameters: parameters8,
1970
2606
  isReadOnly: false,
1971
2607
  isConcurrencySafe: false,
1972
2608
  maxResultSizeChars: DEFAULT_MAX_RESULT_SIZE_CHARS,
1973
- call: call7
2609
+ call: call8
1974
2610
  };
1975
2611
 
1976
2612
  // src/tools/websearch/websearch.ts
1977
- import { z as z8 } from "zod";
1978
- var inputSchema8 = z8.object({
1979
- 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"),
1980
- max_results: z8.number().int().min(1).max(20).optional().describe("\u8FD4\u56DE\u7ED3\u679C\u6570\u91CF\uFF0C1-20\uFF0C\u9ED8\u8BA4 5"),
1981
- 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"),
1982
- 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")
1983
2619
  });
1984
- var parameters8 = toToolParameters(inputSchema8);
1985
- 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.
1986
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.
1987
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.
1988
2624
  - Prefer specific natural-language queries over keyword soup (e.g. "how does Bun handle .env files in version 1.1").
1989
2625
  - Requires the TAVILY_API_KEY environment variable to be set; if missing the tool returns a friendly error.`;
1990
- async function call8(input, signal) {
2626
+ async function call9(input, signal) {
1991
2627
  const apiKey = process.env.TAVILY_API_KEY;
1992
2628
  if (!apiKey) {
1993
2629
  return {
@@ -2069,46 +2705,41 @@ ${(r.content ?? "").trim()}`
2069
2705
  }
2070
2706
  var webSearchTool = {
2071
2707
  name: "WebSearch",
2072
- description: description8,
2073
- inputSchema: inputSchema8,
2074
- parameters: parameters8,
2708
+ description: description9,
2709
+ inputSchema: inputSchema9,
2710
+ parameters: parameters9,
2075
2711
  isReadOnly: true,
2076
2712
  isConcurrencySafe: true,
2077
2713
  maxResultSizeChars: DEFAULT_MAX_RESULT_SIZE_CHARS,
2078
- call: call8
2714
+ call: call9
2079
2715
  };
2080
2716
 
2081
2717
  // src/tools/write/write.ts
2082
- import { existsSync as existsSync4 } from "fs";
2083
- import { mkdir as mkdir5, stat as stat4, writeFile as writeFile4 } from "fs/promises";
2084
- import { dirname as dirname6, resolve as resolve9 } from "path";
2085
- import { z as z9 } from "zod";
2718
+ import { existsSync as existsSync6 } from "fs";
2719
+ import { mkdir as mkdir5, stat as stat4, writeFile as writeFile5 } from "fs/promises";
2720
+ import { dirname as dirname6 } from "path";
2721
+ import { z as z10 } from "zod";
2086
2722
  var MAX_WRITE_SIZE_BYTES = 1024 * 1024 * 1024;
2087
- var inputSchema9 = z9.object({
2088
- file_path: z9.string().min(1).describe("\u8981\u5199\u5165\u7684\u6587\u4EF6\u8DEF\u5F84"),
2089
- 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")
2090
2726
  });
2091
- var parameters9 = toToolParameters(inputSchema9);
2092
- var description9 = `Writes a file to the local filesystem.
2727
+ var parameters10 = toToolParameters(inputSchema10);
2728
+ var description10 = `Writes a file to the local filesystem.
2093
2729
 
2094
2730
  Usage:
2095
2731
  - This tool will overwrite the existing file if there is one at the provided path.
2096
2732
  - If the parent directory does not exist, it will be created recursively.
2097
2733
  - ALWAYS prefer editing existing files in the codebase via the Edit tool. NEVER write new files unless explicitly required.
2098
2734
  - NEVER create documentation files (*.md) or README files unless explicitly requested by the User.`;
2099
- function validatePath2(filePath) {
2100
- if (filePath.includes("\0")) {
2101
- return { ok: false, error: "\u8DEF\u5F84\u5305\u542B\u975E\u6CD5\u5B57\u7B26\uFF08null byte\uFF09" };
2735
+ async function call10(input) {
2736
+ const pathResult = validateAndResolvePath(input.file_path, getWorkingDir());
2737
+ if (!pathResult.ok) return pathResult;
2738
+ const filePath = pathResult.resolvedPath;
2739
+ const freshness = assertFresh(filePath);
2740
+ if (!freshness.ok) {
2741
+ return { ok: false, error: freshness.error };
2102
2742
  }
2103
- if (process.platform === "win32" && filePath.includes("\\\\")) {
2104
- return { ok: false, error: "\u4E0D\u652F\u6301 UNC \u8DEF\u5F84\uFF08\\\\server\\share \u683C\u5F0F\uFF09\uFF0C\u8BF7\u4F7F\u7528\u672C\u5730\u8DEF\u5F84" };
2105
- }
2106
- return { ok: true };
2107
- }
2108
- async function call9(input) {
2109
- const filePath = resolve9(input.file_path);
2110
- const pathCheck = validatePath2(filePath);
2111
- if (!pathCheck.ok) return pathCheck;
2112
2743
  const contentSize = Buffer.byteLength(input.content, "utf8");
2113
2744
  if (contentSize > MAX_WRITE_SIZE_BYTES) {
2114
2745
  return {
@@ -2119,7 +2750,7 @@ async function call9(input) {
2119
2750
  try {
2120
2751
  await mkdir5(dirname6(filePath), { recursive: true });
2121
2752
  let originalSize = 0;
2122
- const fileExisted = existsSync4(filePath);
2753
+ const fileExisted = existsSync6(filePath);
2123
2754
  if (fileExisted) {
2124
2755
  try {
2125
2756
  const st = await stat4(filePath);
@@ -2127,12 +2758,33 @@ async function call9(input) {
2127
2758
  } catch {
2128
2759
  }
2129
2760
  }
2130
- await writeFile4(filePath, input.content, "utf8");
2761
+ let contentToWrite = input.content;
2762
+ let bomEncoding = "utf8";
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;
2772
+ const lineEnding = await detectFileLineEndings(filePath);
2773
+ contentToWrite = applyLineEnding(input.content, lineEnding);
2774
+ }
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
+ }
2131
2782
  const action = fileExisted ? "\u5DF2\u8986\u76D6" : "\u5DF2\u521B\u5EFA\u65B0\u6587\u4EF6";
2132
- const sizeInfo = fileExisted ? `\uFF08\u539F\u6587\u4EF6 ${originalSize} \u5B57\u7B26 \u2192 \u65B0\u5185\u5BB9 ${input.content.length} \u5B57\u7B26\uFF09` : `\uFF08${input.content.length} \u5B57\u7B26\uFF09`;
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" : "";
2133
2785
  return {
2134
2786
  ok: true,
2135
- content: `${action} ${filePath}${sizeInfo}`
2787
+ content: `${action} ${filePath}${sizeInfo}${bomInfo}`
2136
2788
  };
2137
2789
  } catch (e) {
2138
2790
  return { ok: false, error: `\u5199\u5165\u5931\u8D25\uFF1A${e.message}` };
@@ -2140,19 +2792,20 @@ async function call9(input) {
2140
2792
  }
2141
2793
  var writeTool = {
2142
2794
  name: "Write",
2143
- description: description9,
2144
- inputSchema: inputSchema9,
2145
- parameters: parameters9,
2795
+ description: description10,
2796
+ inputSchema: inputSchema10,
2797
+ parameters: parameters10,
2146
2798
  isReadOnly: false,
2147
2799
  isConcurrencySafe: false,
2148
2800
  maxResultSizeChars: DEFAULT_MAX_RESULT_SIZE_CHARS,
2149
- call: call9
2801
+ call: call10
2150
2802
  };
2151
2803
 
2152
2804
  // src/tools/index.ts
2153
2805
  var ALL_TOOLS = [
2154
2806
  readTool,
2155
2807
  editTool,
2808
+ multiEditTool,
2156
2809
  writeTool,
2157
2810
  globTool,
2158
2811
  grepTool,
@@ -2962,7 +3615,7 @@ function MessageRow({ message }) {
2962
3615
 
2963
3616
  // src/ui/StatusLine.tsx
2964
3617
  import { Box as Box4, Text as Text4 } from "ink";
2965
- import { homedir as homedir4 } from "os";
3618
+ import { homedir as homedir5 } from "os";
2966
3619
  import { sep } from "path";
2967
3620
 
2968
3621
  // src/llm/client.ts
@@ -3350,13 +4003,26 @@ function useTokenUsage(messages, provider) {
3350
4003
  }
3351
4004
 
3352
4005
  // src/ui/StatusLine.tsx
3353
- import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
3354
- function StatusLine({ provider, history }) {
4006
+ import { Fragment, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
4007
+ function StatusLine({ provider, history, pluginLoop }) {
3355
4008
  const usage = useTokenUsage(history, provider);
3356
4009
  const ratio = usage.tokens / usage.threshold;
3357
4010
  const color = ratio >= 1 ? "red" : ratio >= 0.7 ? "yellow" : "green";
3358
4011
  const cwdDisplay = shortenPath(getWorkingDir());
3359
4012
  return /* @__PURE__ */ jsxs4(Box4, { children: [
4013
+ pluginLoop && /* @__PURE__ */ jsxs4(Fragment, { children: [
4014
+ /* @__PURE__ */ jsxs4(Text4, { color: "magenta", children: [
4015
+ "\u{1F504} ",
4016
+ pluginLoop.pluginName,
4017
+ " "
4018
+ ] }),
4019
+ /* @__PURE__ */ jsxs4(Text4, { color: "magenta", children: [
4020
+ pluginLoop.current,
4021
+ "/",
4022
+ pluginLoop.max
4023
+ ] }),
4024
+ /* @__PURE__ */ jsx4(Text4, { color: "gray", children: " \xB7 " })
4025
+ ] }),
3360
4026
  /* @__PURE__ */ jsx4(Text4, { color: "cyan", children: "cwd " }),
3361
4027
  /* @__PURE__ */ jsx4(Text4, { color: "gray", children: cwdDisplay }),
3362
4028
  /* @__PURE__ */ jsx4(Text4, { color: "gray", children: " \xB7 " }),
@@ -3382,7 +4048,7 @@ function fmt(n) {
3382
4048
  return `${(n / 1e6).toFixed(2)}M`;
3383
4049
  }
3384
4050
  function shortenPath(abs) {
3385
- const home = homedir4();
4051
+ const home = homedir5();
3386
4052
  let p = abs;
3387
4053
  if (home && (p === home || p.startsWith(home + sep))) {
3388
4054
  p = "~" + p.slice(home.length);
@@ -3547,6 +4213,688 @@ async function reactiveCompactIfApplicable(messages, provider, error, state = de
3547
4213
  };
3548
4214
  }
3549
4215
 
4216
+ // src/plugins/commandRouter.ts
4217
+ import { readFile as readFile9, readdir as readdir3 } from "fs/promises";
4218
+ import { join as join6 } from "path";
4219
+ var PLUGINS_DIR = join6(findPackageRoot(import.meta.url), "plugins");
4220
+ var pluginCache = /* @__PURE__ */ new Map();
4221
+ var discoveryDone = false;
4222
+ function stripQuotes2(s) {
4223
+ const trimmed = s.trim();
4224
+ if (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'")) {
4225
+ return trimmed.slice(1, -1);
4226
+ }
4227
+ return trimmed;
4228
+ }
4229
+ function parseMarkdownFrontmatter(content) {
4230
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
4231
+ if (!match) return {};
4232
+ const frontmatter = {};
4233
+ for (const line of match[1].split("\n")) {
4234
+ const colon = line.indexOf(":");
4235
+ if (colon < 0) continue;
4236
+ const key = line.slice(0, colon).trim();
4237
+ const value = line.slice(colon + 1).trim();
4238
+ if (key) frontmatter[key] = stripQuotes2(value);
4239
+ }
4240
+ return frontmatter;
4241
+ }
4242
+ async function loadPlugin(pluginDirPath) {
4243
+ const dirName = pluginDirPath.split("/").pop() ?? pluginDirPath;
4244
+ const manifestPath = join6(pluginDirPath, ".claude-plugin", "plugin.json");
4245
+ let manifestName = dirName;
4246
+ let manifestVersion;
4247
+ let manifestDesc;
4248
+ try {
4249
+ const raw = await readFile9(manifestPath, "utf8");
4250
+ const parsed = JSON.parse(raw);
4251
+ manifestName = parsed.name ?? dirName;
4252
+ manifestVersion = parsed.version;
4253
+ manifestDesc = parsed.description;
4254
+ } catch {
4255
+ }
4256
+ const commandsDir = join6(pluginDirPath, "commands");
4257
+ const commands = [];
4258
+ try {
4259
+ const entries = await readdir3(commandsDir, { withFileTypes: true });
4260
+ for (const entry of entries) {
4261
+ if (!entry.name.endsWith(".md")) continue;
4262
+ const cmdPath = join6(commandsDir, entry.name);
4263
+ try {
4264
+ const content = await readFile9(cmdPath, "utf8");
4265
+ const fm = parseMarkdownFrontmatter(content);
4266
+ const sep2 = content.indexOf("\n---", 4);
4267
+ const body = sep2 >= 0 ? content.slice(sep2 + 4).trim() : content.trim();
4268
+ commands.push({
4269
+ name: entry.name.replace(/\.md$/, ""),
4270
+ description: fm.description ?? "(no description)",
4271
+ argumentHint: fm["argument-hint"],
4272
+ pluginName: manifestName,
4273
+ pluginRoot: pluginDirPath,
4274
+ promptBody: body
4275
+ });
4276
+ } catch {
4277
+ }
4278
+ }
4279
+ } catch {
4280
+ }
4281
+ const hooksJsonPath = join6(pluginDirPath, "hooks", "hooks.json");
4282
+ let hasStopHook = false;
4283
+ try {
4284
+ const hooksRaw = await readFile9(hooksJsonPath, "utf8");
4285
+ const hooksParsed = JSON.parse(hooksRaw);
4286
+ const stopHooks = hooksParsed?.hooks?.Stop;
4287
+ if (Array.isArray(stopHooks) && stopHooks.length > 0) {
4288
+ hasStopHook = true;
4289
+ }
4290
+ } catch {
4291
+ }
4292
+ if (commands.length === 0 && !hasStopHook) return null;
4293
+ return {
4294
+ name: manifestName,
4295
+ version: manifestVersion,
4296
+ description: manifestDesc,
4297
+ root: pluginDirPath,
4298
+ commands,
4299
+ hasStopHook
4300
+ };
4301
+ }
4302
+ async function discoverPlugins() {
4303
+ if (discoveryDone && pluginCache.size > 0) {
4304
+ return Array.from(pluginCache.values());
4305
+ }
4306
+ pluginCache.clear();
4307
+ discoveryDone = true;
4308
+ try {
4309
+ const entries = await readdir3(PLUGINS_DIR, { withFileTypes: true });
4310
+ for (const entry of entries) {
4311
+ if (!entry.isDirectory()) continue;
4312
+ if (entry.name.startsWith(".")) continue;
4313
+ const plugin = await loadPlugin(join6(PLUGINS_DIR, entry.name));
4314
+ if (plugin) {
4315
+ pluginCache.set(plugin.name, plugin);
4316
+ }
4317
+ }
4318
+ } catch {
4319
+ }
4320
+ return Array.from(pluginCache.values());
4321
+ }
4322
+ function resolveCommand(input) {
4323
+ const trimmed = input.trimStart();
4324
+ if (!trimmed.startsWith("/")) return null;
4325
+ const spaceIdx = trimmed.indexOf(" ", 1);
4326
+ const cmdName = spaceIdx >= 0 ? trimmed.slice(1, spaceIdx) : trimmed.slice(1);
4327
+ const args = spaceIdx >= 0 ? trimmed.slice(spaceIdx + 1) : "";
4328
+ for (const plugin of pluginCache.values()) {
4329
+ for (const cmd of plugin.commands) {
4330
+ if (cmd.name === cmdName) {
4331
+ return { cmd, arguments: args };
4332
+ }
4333
+ }
4334
+ }
4335
+ return null;
4336
+ }
4337
+ var COMMAND_DEFAULTS = {
4338
+ "ralph-loop": ["--max-iterations", "50"]
4339
+ };
4340
+ function applyDefaultArgs(cmdName, args) {
4341
+ const defaults = COMMAND_DEFAULTS[cmdName];
4342
+ if (!defaults) return args;
4343
+ const existing = new Set(args.trim().split(/\s+/).filter(Boolean));
4344
+ let result = args;
4345
+ for (let i = 0; i < defaults.length; i += 2) {
4346
+ const flag = defaults[i];
4347
+ if (existing.has(flag)) continue;
4348
+ result = result.trim() ? `${result} ${flag} ${defaults[i + 1]}` : `${flag} ${defaults[i + 1]}`;
4349
+ }
4350
+ return result;
4351
+ }
4352
+ function buildCommandInput(resolved) {
4353
+ const { cmd, arguments: rawArgs } = resolved;
4354
+ const args = applyDefaultArgs(cmd.name, rawArgs);
4355
+ let input = cmd.promptBody;
4356
+ input = input.replaceAll("${CLAUDE_PLUGIN_ROOT}", cmd.pluginRoot);
4357
+ input = input.replaceAll("$ARGUMENTS", args);
4358
+ input = input.replaceAll("${ARGUMENTS}", args);
4359
+ if (args.trim()) {
4360
+ input += `
4361
+
4362
+ \u7528\u6237\u53C2\u6570: ${args.trim()}`;
4363
+ }
4364
+ return input;
4365
+ }
4366
+ function getActiveStopHookPlugins() {
4367
+ const result = [];
4368
+ for (const plugin of pluginCache.values()) {
4369
+ if (plugin.hasStopHook) {
4370
+ result.push(plugin.root);
4371
+ }
4372
+ }
4373
+ return result;
4374
+ }
4375
+
4376
+ // src/plugins/stopHook.ts
4377
+ import { readFile as readFile10 } from "fs/promises";
4378
+ import { join as join7 } from "path";
4379
+ import { spawn as spawn4 } from "child_process";
4380
+ async function loadStopHookConfig(pluginRoot) {
4381
+ const hooksJsonPath = join7(pluginRoot, "hooks", "hooks.json");
4382
+ try {
4383
+ const raw = await readFile10(hooksJsonPath, "utf8");
4384
+ const parsed = JSON.parse(raw);
4385
+ const stopEntries = parsed?.hooks?.Stop;
4386
+ if (!Array.isArray(stopEntries)) return [];
4387
+ const commands = [];
4388
+ for (const entry of stopEntries) {
4389
+ const hooks = entry.hooks;
4390
+ if (Array.isArray(hooks)) {
4391
+ for (const h of hooks) {
4392
+ if (h.type === "command" && h.command) {
4393
+ commands.push(h);
4394
+ }
4395
+ }
4396
+ }
4397
+ }
4398
+ return commands;
4399
+ } catch {
4400
+ return [];
4401
+ }
4402
+ }
4403
+ async function executeStopHooks(pluginRoots, transcriptText) {
4404
+ if (process.platform === "win32") {
4405
+ return { decision: "pass" };
4406
+ }
4407
+ for (const pluginRoot of pluginRoots) {
4408
+ const hookConfigs = await loadStopHookConfig(pluginRoot);
4409
+ for (const hookConfig of hookConfigs) {
4410
+ const result = await runSingleStopHook(hookConfig, pluginRoot, transcriptText);
4411
+ if (result.decision === "block") {
4412
+ return result;
4413
+ }
4414
+ }
4415
+ }
4416
+ return { decision: "pass" };
4417
+ }
4418
+ function runSingleStopHook(hookConfig, pluginRoot, transcriptText) {
4419
+ const resolvedCommand = hookConfig.command.replaceAll("${CLAUDE_PLUGIN_ROOT}", pluginRoot);
4420
+ return new Promise((resolve9) => {
4421
+ const child = spawn4("bash", [resolvedCommand], {
4422
+ env: {
4423
+ ...process.env,
4424
+ CLAUDE_PLUGIN_ROOT: pluginRoot
4425
+ }
4426
+ });
4427
+ let stdout = "";
4428
+ child.stdout.on("data", (data) => {
4429
+ stdout += data.toString();
4430
+ });
4431
+ child.on("error", () => {
4432
+ resolve9({ decision: "pass" });
4433
+ });
4434
+ child.on("close", (code) => {
4435
+ if (code !== 0) {
4436
+ resolve9({ decision: "pass" });
4437
+ return;
4438
+ }
4439
+ const trimmed = stdout.trim();
4440
+ if (!trimmed) {
4441
+ resolve9({ decision: "pass" });
4442
+ return;
4443
+ }
4444
+ try {
4445
+ const parsed = JSON.parse(trimmed);
4446
+ if (parsed.decision === "block") {
4447
+ resolve9({
4448
+ decision: "block",
4449
+ reason: typeof parsed.reason === "string" ? parsed.reason : void 0,
4450
+ systemMessage: typeof parsed.systemMessage === "string" ? parsed.systemMessage : void 0
4451
+ });
4452
+ return;
4453
+ }
4454
+ resolve9({ decision: "pass" });
4455
+ } catch {
4456
+ resolve9({ decision: "pass" });
4457
+ }
4458
+ });
4459
+ child.stdin.write(transcriptText);
4460
+ child.stdin.end();
4461
+ });
4462
+ }
4463
+
4464
+ // src/plugins/verificationGate.ts
4465
+ import { existsSync as existsSync7, readFileSync } from "fs";
4466
+ import { spawn as spawn5 } from "child_process";
4467
+ function parseVerifyArg(arg) {
4468
+ const colonIdx = arg.indexOf(":");
4469
+ if (colonIdx < 0) return null;
4470
+ const type = arg.slice(0, colonIdx).trim().toLowerCase();
4471
+ const value = arg.slice(colonIdx + 1).trim();
4472
+ switch (type) {
4473
+ case "shell":
4474
+ return { type: "shell", command: value, timeout: 3e4 };
4475
+ case "file_exists":
4476
+ return { type: "file_exists", file: value };
4477
+ case "file_contains": {
4478
+ const sep2 = value.indexOf(":");
4479
+ if (sep2 < 0) return null;
4480
+ return {
4481
+ type: "file_contains",
4482
+ file: value.slice(0, sep2),
4483
+ pattern: value.slice(sep2 + 1)
4484
+ };
4485
+ }
4486
+ case "test_count": {
4487
+ const count = parseInt(value, 10);
4488
+ if (isNaN(count) || count < 0) return null;
4489
+ return { type: "test_count", minCount: count };
4490
+ }
4491
+ default:
4492
+ return null;
4493
+ }
4494
+ }
4495
+ function parseVerifyArgs(args) {
4496
+ const checks = [];
4497
+ const regex = /--verify\s+("[^"]*"|\S+)/gi;
4498
+ let match;
4499
+ while ((match = regex.exec(args)) !== null) {
4500
+ const raw = match[1].replace(/^"|"$/g, "");
4501
+ const check = parseVerifyArg(raw);
4502
+ if (check) checks.push(check);
4503
+ }
4504
+ return checks;
4505
+ }
4506
+ function runShell(command, timeout) {
4507
+ return new Promise((resolve9) => {
4508
+ const isWin = process.platform === "win32";
4509
+ const child = isWin ? spawn5("cmd", ["/c", command], { timeout, env: process.env }) : spawn5("bash", ["-c", command], { timeout, env: process.env });
4510
+ let stdout = "";
4511
+ let stderr = "";
4512
+ child.stdout.on("data", (d) => {
4513
+ stdout += d.toString();
4514
+ });
4515
+ child.stderr.on("data", (d) => {
4516
+ stderr += d.toString();
4517
+ });
4518
+ child.on("error", () => {
4519
+ resolve9({ exitCode: null, stdout, stderr, errored: true });
4520
+ });
4521
+ child.on("close", (code) => {
4522
+ resolve9({ exitCode: code, stdout, stderr, errored: false });
4523
+ });
4524
+ });
4525
+ }
4526
+ async function verifyShell(command, timeout) {
4527
+ const r = await runShell(command, timeout);
4528
+ if (r.errored) {
4529
+ return {
4530
+ check: { type: "shell", command },
4531
+ passed: false,
4532
+ output: `\u6267\u884C\u5931\u8D25`
4533
+ };
4534
+ }
4535
+ return {
4536
+ check: { type: "shell", command },
4537
+ passed: r.exitCode === 0,
4538
+ output: r.stdout.trim() || r.stderr.trim() || `exit code ${r.exitCode}`
4539
+ };
4540
+ }
4541
+ function verifyFileExists(file) {
4542
+ const exists = existsSync7(file);
4543
+ return {
4544
+ check: { type: "file_exists", file },
4545
+ passed: exists,
4546
+ output: exists ? "\u6587\u4EF6\u5B58\u5728" : `\u6587\u4EF6\u4E0D\u5B58\u5728: ${file}`
4547
+ };
4548
+ }
4549
+ function verifyFileContains(file, pattern) {
4550
+ try {
4551
+ const content = readFileSync(file, "utf8");
4552
+ const regex = new RegExp(pattern);
4553
+ const matched = regex.test(content);
4554
+ return {
4555
+ check: { type: "file_contains", file, pattern },
4556
+ passed: matched,
4557
+ output: matched ? `\u6587\u4EF6\u5305\u542B "${pattern}"` : `\u6587\u4EF6\u4E0D\u5305\u542B "${pattern}"`
4558
+ };
4559
+ } catch {
4560
+ return {
4561
+ check: { type: "file_contains", file, pattern },
4562
+ passed: false,
4563
+ output: `\u65E0\u6CD5\u8BFB\u53D6\u6587\u4EF6: ${file}`
4564
+ };
4565
+ }
4566
+ }
4567
+ async function verifyTestCount(minCount) {
4568
+ const r = await runShell(`bun test`, 6e4);
4569
+ const combined = `${r.stdout}
4570
+ ${r.stderr}`;
4571
+ if (r.errored) {
4572
+ return {
4573
+ check: { type: "test_count", minCount },
4574
+ passed: false,
4575
+ output: "\u65E0\u6CD5\u6267\u884C bun test"
4576
+ };
4577
+ }
4578
+ const matches = [...combined.matchAll(/(\d+)\s+pass/gi)];
4579
+ const count = matches.length > 0 ? parseInt(matches[matches.length - 1][1], 10) : 0;
4580
+ const passed = count >= minCount;
4581
+ return {
4582
+ check: { type: "test_count", minCount },
4583
+ passed,
4584
+ output: `${count} pass (\u9700\u8981 >=${minCount})`
4585
+ };
4586
+ }
4587
+ async function runVerification(checks) {
4588
+ if (checks.length === 0) {
4589
+ return { passed: true, details: [], summary: "\u65E0\u9A8C\u8BC1\u9879" };
4590
+ }
4591
+ const results = [];
4592
+ for (const check of checks) {
4593
+ let result;
4594
+ switch (check.type) {
4595
+ case "shell":
4596
+ result = await verifyShell(check.command ?? "", check.timeout ?? 3e4);
4597
+ break;
4598
+ case "file_exists":
4599
+ result = verifyFileExists(check.file ?? "");
4600
+ break;
4601
+ case "file_contains":
4602
+ result = verifyFileContains(check.file ?? "", check.pattern ?? "");
4603
+ break;
4604
+ case "test_count":
4605
+ result = await verifyTestCount(check.minCount ?? 0);
4606
+ break;
4607
+ default:
4608
+ result = {
4609
+ check,
4610
+ passed: false,
4611
+ output: `\u672A\u77E5\u9A8C\u8BC1\u7C7B\u578B: ${check.type}`
4612
+ };
4613
+ }
4614
+ results.push(result);
4615
+ }
4616
+ const allPassed = results.every((r) => r.passed);
4617
+ const failedNames = results.filter((r) => !r.passed).map((r) => formatCheckName(r.check));
4618
+ let summary;
4619
+ if (allPassed) {
4620
+ summary = `\u2705 \u5168\u90E8\u901A\u8FC7 (${results.length}/${results.length})`;
4621
+ } else {
4622
+ summary = `\u274C \u9A8C\u8BC1\u672A\u901A\u8FC7: ${failedNames.join(", ")}`;
4623
+ }
4624
+ return { passed: allPassed, details: results, summary };
4625
+ }
4626
+ function formatCheckName(check) {
4627
+ switch (check.type) {
4628
+ case "shell":
4629
+ return `shell(${(check.command ?? "").slice(0, 40)})`;
4630
+ case "file_exists":
4631
+ return `exists(${check.file})`;
4632
+ case "file_contains":
4633
+ return `contains(${check.file}:${check.pattern})`;
4634
+ case "test_count":
4635
+ return `tests(>=${check.minCount})`;
4636
+ default:
4637
+ return check.type;
4638
+ }
4639
+ }
4640
+
4641
+ // src/plugins/goalState.ts
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";
4644
+ import { join as join8 } from "path";
4645
+ var Phase = /* @__PURE__ */ ((Phase2) => {
4646
+ Phase2["PLAN"] = "plan";
4647
+ Phase2["BUILD"] = "build";
4648
+ Phase2["VERIFY"] = "verify";
4649
+ Phase2["HEAL"] = "heal";
4650
+ Phase2["DONE"] = "done";
4651
+ return Phase2;
4652
+ })(Phase || {});
4653
+ var VALID_PHASES = new Set(Object.values(Phase));
4654
+ var PHASE_TRANSITIONS = {
4655
+ ["plan" /* PLAN */]: {
4656
+ plan_complete: "build" /* BUILD */,
4657
+ stuck: "plan" /* PLAN */
4658
+ },
4659
+ ["build" /* BUILD */]: {
4660
+ task_complete: "verify" /* VERIFY */,
4661
+ need_replan: "plan" /* PLAN */,
4662
+ tests_failing: "heal" /* HEAL */
4663
+ },
4664
+ ["verify" /* VERIFY */]: {
4665
+ all_pass: "build" /* BUILD */,
4666
+ failures: "heal" /* HEAL */,
4667
+ goal_complete: "done" /* DONE */
4668
+ },
4669
+ ["heal" /* HEAL */]: {
4670
+ fixed: "verify" /* VERIFY */,
4671
+ cannot_fix_locally: "plan" /* PLAN */
4672
+ },
4673
+ ["done" /* DONE */]: {}
4674
+ };
4675
+ function isLegalTransition(from, to) {
4676
+ if (from === to) return true;
4677
+ const allowed = PHASE_TRANSITIONS[from];
4678
+ if (!allowed) return false;
4679
+ return Object.values(allowed).includes(to);
4680
+ }
4681
+ var LEARNINGS_TAIL_LINES = 20;
4682
+ var GoalState = class {
4683
+ dir;
4684
+ constructor(workspaceDir, sessionTag) {
4685
+ const suffix = sessionTag ? `-${sessionTag}` : "";
4686
+ this.dir = join8(workspaceDir, `.minimal-agent${suffix}`);
4687
+ }
4688
+ async reset() {
4689
+ const files = ["goal.md", "completion.md", "phase.md", "progress.md", "learnings.md", "decisions.md"];
4690
+ for (const f of files) {
4691
+ try {
4692
+ await unlink2(join8(this.dir, f));
4693
+ } catch {
4694
+ }
4695
+ }
4696
+ }
4697
+ async init(goal, criteria) {
4698
+ await mkdir6(this.dir, { recursive: true });
4699
+ const files = {
4700
+ goal: `# \u4E0D\u53EF\u53D8\u76EE\u6807 (IMMUTABLE GOAL)
4701
+
4702
+ ${goal}
4703
+ `,
4704
+ completion: JSON.stringify(criteria, null, 2),
4705
+ phase: "plan" /* PLAN */,
4706
+ progress: "",
4707
+ learnings: "",
4708
+ decisions: ""
4709
+ };
4710
+ for (const [name, content] of Object.entries(files)) {
4711
+ const path2 = join8(this.dir, `${name}.md`);
4712
+ if (!existsSync8(path2)) {
4713
+ await writeFile6(path2, content);
4714
+ }
4715
+ }
4716
+ }
4717
+ get goal() {
4718
+ try {
4719
+ return readFileSync2(join8(this.dir, "goal.md"), "utf8").trim();
4720
+ } catch {
4721
+ return "";
4722
+ }
4723
+ }
4724
+ get completionCriteria() {
4725
+ try {
4726
+ const raw = readFileSync2(join8(this.dir, "completion.md"), "utf8").trim();
4727
+ return JSON.parse(raw);
4728
+ } catch {
4729
+ return [];
4730
+ }
4731
+ }
4732
+ get currentPhase() {
4733
+ try {
4734
+ const raw = readFileSync2(join8(this.dir, "phase.md"), "utf8").trim();
4735
+ if (VALID_PHASES.has(raw)) {
4736
+ return raw;
4737
+ }
4738
+ } catch {
4739
+ }
4740
+ return "plan" /* PLAN */;
4741
+ }
4742
+ /**
4743
+ * 切换阶段。`reason` 是给人看的日志文本,不参与校验。
4744
+ * 校验只看 from→to 是否在 PHASE_TRANSITIONS 表里有路径(任意 event 通向 to 即合法)。
4745
+ * 想绕开 FSM 用 forceSetPhase。
4746
+ */
4747
+ async setPhase(phase, reason) {
4748
+ if (!VALID_PHASES.has(phase)) {
4749
+ throw new Error(`Invalid phase: ${phase}`);
4750
+ }
4751
+ const current = this.currentPhase;
4752
+ if (!isLegalTransition(current, phase)) {
4753
+ throw new Error(
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`
4755
+ );
4756
+ }
4757
+ await writeFile6(join8(this.dir, "phase.md"), phase);
4758
+ await this.appendProgress(`PHASE \u2192 ${phase}: ${reason}`);
4759
+ }
4760
+ async forceSetPhase(phase, reason) {
4761
+ if (!VALID_PHASES.has(phase)) {
4762
+ throw new Error(`Invalid phase: ${phase}`);
4763
+ }
4764
+ await writeFile6(join8(this.dir, "phase.md"), phase);
4765
+ await this.appendProgress(`PHASE \u2192 ${phase}: ${reason}`);
4766
+ }
4767
+ async appendProgress(line) {
4768
+ const timestamp = (/* @__PURE__ */ new Date()).toLocaleTimeString("en-US", { hour12: false });
4769
+ await appendFile(join8(this.dir, "progress.md"), `[${timestamp}] ${line}
4770
+ `);
4771
+ }
4772
+ tailProgress(n) {
4773
+ try {
4774
+ const content = readFileSync2(join8(this.dir, "progress.md"), "utf8");
4775
+ const lines = content.trim().split("\n").filter(Boolean);
4776
+ return lines.slice(-n).join("\n");
4777
+ } catch {
4778
+ return "";
4779
+ }
4780
+ }
4781
+ async appendLearning(lesson) {
4782
+ await appendFile(join8(this.dir, "learnings.md"), `- ${lesson}
4783
+ `);
4784
+ }
4785
+ get learnings() {
4786
+ try {
4787
+ const raw = readFileSync2(join8(this.dir, "learnings.md"), "utf8").trim();
4788
+ if (!raw) return "";
4789
+ const lines = raw.split("\n").filter(Boolean);
4790
+ return lines.slice(-LEARNINGS_TAIL_LINES).join("\n");
4791
+ } catch {
4792
+ return "";
4793
+ }
4794
+ }
4795
+ async recordDecision(ctx, options, chosen, reasoning) {
4796
+ const entry = {
4797
+ iteration: ctx.iteration,
4798
+ phase: ctx.phase,
4799
+ contextSummary: ctx.summary,
4800
+ options,
4801
+ chosen,
4802
+ reasoning
4803
+ };
4804
+ const line = JSON.stringify(entry);
4805
+ await appendFile(join8(this.dir, "decisions.md"), `${line}
4806
+ `);
4807
+ }
4808
+ findSimilarDecisions(ctx, k = 3) {
4809
+ try {
4810
+ const content = readFileSync2(join8(this.dir, "decisions.md"), "utf8");
4811
+ const lines = content.trim().split("\n").filter(Boolean);
4812
+ const entries = [];
4813
+ for (const line of lines) {
4814
+ try {
4815
+ entries.push(JSON.parse(line));
4816
+ } catch {
4817
+ }
4818
+ }
4819
+ const scored = entries.map((entry) => ({
4820
+ entry,
4821
+ score: this._similarity(entry.contextSummary, ctx.summary)
4822
+ })).sort((a, b) => b.score - a.score);
4823
+ return scored.slice(0, k).filter((s) => s.score > 0.3).map((s) => s.entry);
4824
+ } catch {
4825
+ return [];
4826
+ }
4827
+ }
4828
+ summarizeDecisions(maxEntries = 5) {
4829
+ try {
4830
+ const content = readFileSync2(join8(this.dir, "decisions.md"), "utf8");
4831
+ const lines = content.trim().split("\n").filter(Boolean);
4832
+ const entries = [];
4833
+ for (const line of lines.slice(-maxEntries * 2)) {
4834
+ try {
4835
+ entries.push(JSON.parse(line));
4836
+ } catch {
4837
+ }
4838
+ }
4839
+ return entries.slice(-maxEntries).map(
4840
+ (e) => `\u8FED\u4EE3 ${e.iteration}\uFF08${e.phase}\uFF09: \u5728 [${e.options.join(", ")}] \u4E2D\u9009\u62E9\u4E86 **${e.chosen}**\uFF0C\u539F\u56E0\uFF1A${e.reasoning.slice(0, 80)}`
4841
+ ).join("\n");
4842
+ } catch {
4843
+ return "(\u65E0\u51B3\u7B56\u8BB0\u5F55)";
4844
+ }
4845
+ }
4846
+ composeContext(iteration) {
4847
+ const sections = [];
4848
+ sections.push(`# \u4E0D\u53EF\u53D8\u76EE\u6807 (IMMUTABLE GOAL)
4849
+ ${this.goal}
4850
+ \u26A0\uFE0F \u6CE8\u610F\uFF1A\u4F60\u4E0D\u80FD\u4FEE\u6539\u6216\u6269\u5927\u4E0A\u8FF0\u76EE\u6807\u3002\u5982\u679C\u4F60\u8BA4\u4E3A\u76EE\u6807\u672C\u8EAB\u6709\u95EE\u9898\uFF0C\u8BF7\u8F93\u51FA <PROMISE>NEED_GOAL_REVISION</PROMISE> \u5E76\u505C\u6B62\u3002`);
4851
+ sections.push(`# \u5F53\u524D\u9636\u6BB5: ${this.currentPhase.toUpperCase()}`);
4852
+ const lrn = this.learnings;
4853
+ if (lrn) {
4854
+ sections.push(`# \u5173\u952E\u6559\u8BAD\uFF08\u5FC5\u987B\u9075\u5B88\uFF0C\u907F\u514D\u91CD\u590D\u8E29\u5751\uFF09
4855
+ ${lrn}`);
4856
+ }
4857
+ const decisions = this.summarizeDecisions(3);
4858
+ if (decisions !== "(\u65E0\u51B3\u7B56\u8BB0\u5F55)") {
4859
+ sections.push(`# \u5386\u53F2\u5173\u952E\u51B3\u7B56\uFF08\u8BF7\u53C2\u8003\uFF0C\u907F\u514D\u91CD\u590D\u8BD5\u9519\uFF09
4860
+ ${decisions}`);
4861
+ }
4862
+ const recentProgress = this.tailProgress(10);
4863
+ if (recentProgress) {
4864
+ sections.push(`# \u6700\u8FD1\u8FDB\u5EA6
4865
+ ${recentProgress}`);
4866
+ }
4867
+ sections.push(`---
4868
+
4869
+ # \u672C\u8F6E\u4EFB\u52A1 (\u8FED\u4EE3 ${iteration})`);
4870
+ return sections.join("\n\n---\n\n");
4871
+ }
4872
+ async cleanup() {
4873
+ const files = ["goal.md", "completion.md", "phase.md", "progress.md", "learnings.md", "decisions.md"];
4874
+ for (const f of files) {
4875
+ try {
4876
+ await unlink2(join8(this.dir, f));
4877
+ } catch {
4878
+ }
4879
+ }
4880
+ try {
4881
+ await rmdir2(this.dir);
4882
+ } catch {
4883
+ }
4884
+ }
4885
+ _similarity(a, b) {
4886
+ if (!a || !b) return 0;
4887
+ const wordsA = new Set(a.toLowerCase().split(/\s+/));
4888
+ const wordsB = new Set(b.toLowerCase().split(/\s+/));
4889
+ let intersection = 0;
4890
+ for (const word of wordsA) {
4891
+ if (wordsB.has(word)) intersection++;
4892
+ }
4893
+ const union = (/* @__PURE__ */ new Set([...wordsA, ...wordsB])).size;
4894
+ return union > 0 ? intersection / union : 0;
4895
+ }
4896
+ };
4897
+
3550
4898
  // src/context/microCompactLite.ts
3551
4899
  import { createHash as createHash2 } from "crypto";
3552
4900
  var MAX_REPEAT_COUNT = 3;
@@ -3782,6 +5130,179 @@ function previewArgs(rawJson) {
3782
5130
  return oneLine.slice(0, 60) + "...";
3783
5131
  }
3784
5132
 
5133
+ // src/plugins/pluginRunner.ts
5134
+ var DEFAULT_MAX_ITERATIONS = 50;
5135
+ var SAFETY_CEILING = 200;
5136
+ function extractMaxIterations(args) {
5137
+ const match = args.match(/--max-iterations\s+(\d+)/i);
5138
+ return match ? parseInt(match[1], 10) : void 0;
5139
+ }
5140
+ async function* runWithPlugins(userInput, options) {
5141
+ const { provider, history, signal } = options;
5142
+ const isSlashCommand = userInput.trimStart().startsWith("/");
5143
+ let currentInput = userInput;
5144
+ let matchedCmd = null;
5145
+ if (isSlashCommand) {
5146
+ await discoverPlugins();
5147
+ matchedCmd = resolveCommand(userInput);
5148
+ if (matchedCmd) {
5149
+ currentInput = buildCommandInput(matchedCmd);
5150
+ }
5151
+ }
5152
+ const activeHookPlugins = matchedCmd ? getActiveStopHookPlugins() : [];
5153
+ const enterLoop = matchedCmd !== null && activeHookPlugins.length > 0;
5154
+ if (!enterLoop) {
5155
+ yield* runQuery(currentInput, {
5156
+ provider,
5157
+ history,
5158
+ signal,
5159
+ maxTurns: options.maxTurns,
5160
+ sessionState: options.sessionState
5161
+ });
5162
+ return;
5163
+ }
5164
+ const cmd = matchedCmd.cmd;
5165
+ const maxIter = Math.min(
5166
+ extractMaxIterations(currentInput) ?? DEFAULT_MAX_ITERATIONS,
5167
+ SAFETY_CEILING
5168
+ );
5169
+ yield {
5170
+ type: "plugin_start",
5171
+ pluginName: cmd.pluginName,
5172
+ description: cmd.description
5173
+ };
5174
+ const checks = parseVerifyArgs(matchedCmd.arguments);
5175
+ const goalState = new GoalState(getWorkingDir(), cmd.pluginName);
5176
+ await goalState.reset();
5177
+ await goalState.init(currentInput, checks);
5178
+ await goalState.appendProgress(`=== Loop \u542F\u52A8 === \u76EE\u6807: ${currentInput.slice(0, 120)}...`);
5179
+ const baseHistory = history.slice();
5180
+ let iterationCount = 0;
5181
+ let consecutiveFailures = 0;
5182
+ let finalAssistantMsg = null;
5183
+ try {
5184
+ do {
5185
+ iterationCount++;
5186
+ if (iterationCount > maxIter) {
5187
+ await goalState.forceSetPhase("done" /* DONE */, `\u8FBE\u5230\u8FED\u4EE3\u4E0A\u9650 ${maxIter}`);
5188
+ await goalState.appendLearning(`[\u8FED\u4EE3\u4E0A\u9650] \u5FAA\u73AF\u5728 ${iterationCount - 1} \u8F6E\u540E\u5F3A\u5236\u7EC8\u6B62\uFF0C\u53EF\u80FD\u76EE\u6807\u8FC7\u5927\u6216\u9677\u5165\u6B7B\u5FAA\u73AF`);
5189
+ yield { type: "error", error: `Loop \u5DF2\u8FBE\u8FED\u4EE3\u4E0A\u9650 ${maxIter}\uFF0C\u81EA\u52A8\u505C\u6B62` };
5190
+ return;
5191
+ }
5192
+ if (signal?.aborted) {
5193
+ yield { type: "interrupted" };
5194
+ return;
5195
+ }
5196
+ yield {
5197
+ type: "plugin_iteration",
5198
+ pluginName: cmd.pluginName,
5199
+ current: iterationCount,
5200
+ max: maxIter
5201
+ };
5202
+ history.length = 0;
5203
+ history.push(...baseHistory);
5204
+ const freshContext = goalState.composeContext(iterationCount);
5205
+ const enhancedInput = `${freshContext}
5206
+
5207
+ ${currentInput}`;
5208
+ yield* runQuery(enhancedInput, {
5209
+ provider,
5210
+ history,
5211
+ signal,
5212
+ maxTurns: options.maxTurns,
5213
+ sessionState: options.sessionState
5214
+ });
5215
+ if (signal?.aborted) {
5216
+ yield { type: "interrupted" };
5217
+ return;
5218
+ }
5219
+ const lastAssistantIdx = (() => {
5220
+ for (let i = history.length - 1; i >= 0; i--) {
5221
+ if (history[i].role === "assistant") return i;
5222
+ }
5223
+ return -1;
5224
+ })();
5225
+ finalAssistantMsg = lastAssistantIdx >= 0 ? history[lastAssistantIdx] : null;
5226
+ const lastAssistantText = finalAssistantMsg ? typeof finalAssistantMsg.content === "string" ? finalAssistantMsg.content : JSON.stringify(finalAssistantMsg.content) : "";
5227
+ const hasCompleteSentinel = /<promise>(?:COMPLETE|DONE|GOAL_COMPLETE)<\/promise>/i.test(lastAssistantText);
5228
+ const hasNeedReplan = /<PROMISE>NEED_REPLAN<\/PROMISE>/i.test(lastAssistantText);
5229
+ if (hasCompleteSentinel) {
5230
+ await goalState.forceSetPhase("verify" /* VERIFY */, "\u68C0\u6D4B\u5230\u5B8C\u6210\u54E8\u5175\uFF0C\u8FDB\u5165\u9A8C\u8BC1");
5231
+ await goalState.appendProgress(`\u8FED\u4EE3 ${iterationCount}: \u68C0\u6D4B\u5230\u5B8C\u6210\u54E8\u5175\uFF0C\u8FD0\u884C\u9A8C\u8BC1\u95E8...`);
5232
+ if (checks.length > 0) {
5233
+ const vResult = await runVerification(checks);
5234
+ if (!vResult.passed) {
5235
+ consecutiveFailures++;
5236
+ await goalState.appendLearning(
5237
+ `[\u8FED\u4EE3 ${iterationCount}] \u58F0\u79F0\u5B8C\u6210\u4F46\u9A8C\u8BC1\u672A\u901A\u8FC7: ${vResult.summary}`
5238
+ );
5239
+ yield {
5240
+ type: "error",
5241
+ error: `\u26A0\uFE0F \u9A8C\u8BC1\u672A\u901A\u8FC7: ${vResult.summary}\u3002\u7EE7\u7EED\u5C1D\u8BD5...`
5242
+ };
5243
+ if (consecutiveFailures >= 3) {
5244
+ await goalState.forceSetPhase(
5245
+ "heal" /* HEAL */,
5246
+ `\u8FDE\u7EED ${consecutiveFailures} \u6B21\u9A8C\u8BC1\u5931\u8D25`
5247
+ );
5248
+ } else {
5249
+ await goalState.setPhase("build" /* BUILD */, "\u9A8C\u8BC1\u672A\u901A\u8FC7\uFF0C\u8FD4\u56DE\u6784\u5EFA");
5250
+ }
5251
+ continue;
5252
+ }
5253
+ await goalState.appendProgress(`\u2705 \u9A8C\u8BC1\u901A\u8FC7: ${vResult.summary}`);
5254
+ }
5255
+ await goalState.setPhase("done" /* DONE */, "goal complete & verified");
5256
+ yield {
5257
+ type: "plugin_iteration",
5258
+ pluginName: cmd.pluginName,
5259
+ current: iterationCount,
5260
+ max: maxIter
5261
+ };
5262
+ return;
5263
+ }
5264
+ if (hasNeedReplan) {
5265
+ await goalState.forceSetPhase("plan" /* PLAN */, "agent \u8BF7\u6C42\u91CD\u65B0\u89C4\u5212");
5266
+ await goalState.appendLearning("[NEED_REPLAN] Agent \u8BA4\u4E3A\u5F53\u524D\u65B9\u6848\u4E0D\u53EF\u884C\uFF0C\u9700\u8981\u91CD\u65B0\u89C4\u5212");
5267
+ await goalState.appendProgress("Agent \u8BF7\u6C42 NEED_REPLAN\uFF0C\u56DE PLAN \u9636\u6BB5");
5268
+ consecutiveFailures = 0;
5269
+ continue;
5270
+ }
5271
+ if (goalState.currentPhase === "plan" /* PLAN */ && iterationCount >= 2) {
5272
+ await goalState.setPhase("build" /* BUILD */, "\u89C4\u5212\u9636\u6BB5\u5DF2\u5B8C\u6210\uFF0C\u8FDB\u5165\u6784\u5EFA");
5273
+ }
5274
+ const hookTranscript = lastAssistantText;
5275
+ const hookResult = await executeStopHooks(activeHookPlugins, hookTranscript);
5276
+ if (hookResult.decision === "block" && hookResult.reason) {
5277
+ currentInput = hookResult.reason;
5278
+ consecutiveFailures = 0;
5279
+ await goalState.recordDecision(
5280
+ { iteration: iterationCount, phase: goalState.currentPhase, summary: "stop-hook \u53CD\u9988" },
5281
+ ["\u7EE7\u7EED\u5FAA\u73AF", "\u7EC8\u6B62"],
5282
+ "\u7EE7\u7EED\u5FAA\u73AF",
5283
+ hookResult.reason.slice(0, 200)
5284
+ );
5285
+ if (hookResult.systemMessage) {
5286
+ baseHistory.push({
5287
+ role: "user",
5288
+ content: `[Plugin Stop Hook] ${hookResult.systemMessage}`
5289
+ });
5290
+ }
5291
+ await goalState.appendProgress(`\u8FED\u4EE3 ${iterationCount}: Stop hook block\uFF0C\u6CE8\u5165\u53CD\u9988\u7EE7\u7EED`);
5292
+ } else {
5293
+ await goalState.appendProgress(`\u8FED\u4EE3 ${iterationCount}: \u65E0\u54E8\u5175 / hook pass\uFF0C\u7EE7\u7EED\u4E0B\u4E00\u8F6E`);
5294
+ }
5295
+ } while (true);
5296
+ } finally {
5297
+ history.length = 0;
5298
+ history.push(...baseHistory);
5299
+ if (finalAssistantMsg) {
5300
+ history.push(finalAssistantMsg);
5301
+ }
5302
+ await goalState.cleanup();
5303
+ }
5304
+ }
5305
+
3785
5306
  // src/ui/hooks/useChat.ts
3786
5307
  function useChat(args) {
3787
5308
  const historyRef = useRef3(args.initialHistory.slice());
@@ -3793,6 +5314,7 @@ function useChat(args) {
3793
5314
  const [error, setError] = useState5(null);
3794
5315
  const [interrupted, setInterrupted] = useState5(false);
3795
5316
  const [compacting, setCompacting] = useState5(false);
5317
+ const [pluginLoop, setPluginLoop] = useState5(null);
3796
5318
  const abortRef = useRef3(null);
3797
5319
  useEffect(() => {
3798
5320
  return () => {
@@ -3808,10 +5330,11 @@ function useChat(args) {
3808
5330
  setError(null);
3809
5331
  setInterrupted(false);
3810
5332
  setStreamingText("");
5333
+ setPluginLoop(null);
3811
5334
  const ac = new AbortController();
3812
5335
  abortRef.current = ac;
3813
5336
  try {
3814
- for await (const ev of runQuery(trimmed, {
5337
+ for await (const ev of runWithPlugins(trimmed, {
3815
5338
  provider: args.provider,
3816
5339
  history: historyRef.current,
3817
5340
  signal: ac.signal
@@ -3822,6 +5345,7 @@ function useChat(args) {
3822
5345
  setCompacting,
3823
5346
  setError,
3824
5347
  setInterrupted,
5348
+ setPluginLoop,
3825
5349
  bump
3826
5350
  });
3827
5351
  }
@@ -3832,6 +5356,7 @@ function useChat(args) {
3832
5356
  setStreamingText("");
3833
5357
  setToolStatus(null);
3834
5358
  setCompacting(false);
5359
+ setPluginLoop(null);
3835
5360
  abortRef.current = null;
3836
5361
  args.onPersist?.(historyRef.current);
3837
5362
  }
@@ -3854,6 +5379,7 @@ function useChat(args) {
3854
5379
  setCompacting(false);
3855
5380
  bump();
3856
5381
  await clearContext();
5382
+ clearFileState();
3857
5383
  }, [bump, isLoading]);
3858
5384
  const compactNow = useCallback5(async () => {
3859
5385
  if (isLoading) return;
@@ -3887,6 +5413,7 @@ function useChat(args) {
3887
5413
  error,
3888
5414
  interrupted,
3889
5415
  compacting,
5416
+ pluginLoop,
3890
5417
  submit,
3891
5418
  abort,
3892
5419
  clearHistory,
@@ -3934,6 +5461,16 @@ function handleEvent(ev, setters) {
3934
5461
  setters.setError(ev.error);
3935
5462
  setters.bump();
3936
5463
  break;
5464
+ case "plugin_start":
5465
+ break;
5466
+ case "plugin_iteration":
5467
+ setters.setPluginLoop({
5468
+ pluginName: ev.pluginName,
5469
+ current: ev.current,
5470
+ max: ev.max ?? 0
5471
+ });
5472
+ setters.bump();
5473
+ break;
3937
5474
  }
3938
5475
  }
3939
5476
 
@@ -3982,7 +5519,7 @@ function App({ provider, initialHistory }) {
3982
5519
  onCompact: chat2.compactNow
3983
5520
  }
3984
5521
  ),
3985
- /* @__PURE__ */ jsx6(StatusLine, { provider, history: chat2.history })
5522
+ /* @__PURE__ */ jsx6(StatusLine, { provider, history: chat2.history, pluginLoop: chat2.pluginLoop })
3986
5523
  ] });
3987
5524
  }
3988
5525
 
@@ -4041,8 +5578,28 @@ function truncateForDisplay(content, max = TOOL_OUTPUT_PREVIEW_MAX) {
4041
5578
  return content.slice(0, max) + "...";
4042
5579
  }
4043
5580
  function extractPromptArgs(args) {
4044
- const FLAGS = /* @__PURE__ */ new Set(["-p", "--print", "--verbose", "-v", "-h", "--help"]);
4045
- return args.filter((a) => !FLAGS.has(a));
5581
+ const FLAG_BOOLEAN = /* @__PURE__ */ new Set([
5582
+ "-p",
5583
+ "--print",
5584
+ "--verbose",
5585
+ "-v",
5586
+ "-h",
5587
+ "--help",
5588
+ "-V",
5589
+ "--version"
5590
+ ]);
5591
+ const FLAG_WITH_VALUE = /* @__PURE__ */ new Set(["-d", "--cwd"]);
5592
+ const result = [];
5593
+ for (let i = 0; i < args.length; i++) {
5594
+ const a = args[i];
5595
+ if (FLAG_BOOLEAN.has(a)) continue;
5596
+ if (FLAG_WITH_VALUE.has(a)) {
5597
+ i++;
5598
+ continue;
5599
+ }
5600
+ result.push(a);
5601
+ }
5602
+ return result;
4046
5603
  }
4047
5604
  async function runPrintMode(provider, args, initialHistory, options) {
4048
5605
  process.stdout.on("error", handleEPIPE(process.stdout));
@@ -4073,7 +5630,7 @@ async function runPrintMode(provider, args, initialHistory, options) {
4073
5630
  const history = initialHistory;
4074
5631
  const output = { buffer: "" };
4075
5632
  try {
4076
- for await (const event of runQuery(prompt, {
5633
+ for await (const event of runWithPlugins(prompt, {
4077
5634
  provider,
4078
5635
  history,
4079
5636
  signal: abortController.signal
@@ -4142,13 +5699,13 @@ function handleEvent2(event, output, verbose) {
4142
5699
  }
4143
5700
  }
4144
5701
  function readFromStdin() {
4145
- return new Promise((resolve10) => {
5702
+ return new Promise((resolve9) => {
4146
5703
  let data = "";
4147
5704
  let settled = false;
4148
5705
  const timer = setTimeout(() => {
4149
5706
  if (!settled) {
4150
5707
  settled = true;
4151
- resolve10("");
5708
+ resolve9("");
4152
5709
  }
4153
5710
  }, STDIN_TIMEOUT_MS);
4154
5711
  process.stdin.setEncoding("utf8");
@@ -4159,7 +5716,7 @@ function readFromStdin() {
4159
5716
  if (!settled) {
4160
5717
  clearTimeout(timer);
4161
5718
  settled = true;
4162
- resolve10(data.trim());
5719
+ resolve9(data.trim());
4163
5720
  }
4164
5721
  }
4165
5722
  process.stdin.on("data", onData);
@@ -4172,9 +5729,17 @@ import { jsx as jsx8 } from "react/jsx-runtime";
4172
5729
  var require2 = createRequire(import.meta.url);
4173
5730
  var pkg = require2("../package.json");
4174
5731
  async function main() {
5732
+ const args = process.argv.slice(2);
5733
+ const dirArg = extractCwdArg(args);
5734
+ if (dirArg) {
5735
+ const abs = resolve8(dirArg);
5736
+ if (!existsSync9(abs)) {
5737
+ mkdirSync(abs, { recursive: true });
5738
+ }
5739
+ process.chdir(abs);
5740
+ }
4175
5741
  initWorkingDir();
4176
5742
  await migrateLegacyContext(getWorkingDir());
4177
- const args = process.argv.slice(2);
4178
5743
  if (args.includes("-h") || args.includes("--help")) {
4179
5744
  printHelp();
4180
5745
  return;
@@ -4185,15 +5750,15 @@ async function main() {
4185
5750
  }
4186
5751
  const isPrintMode = args.includes("-p") || args.includes("--print");
4187
5752
  if (isPrintMode) {
4188
- let provider;
4189
- try {
4190
- provider = await loadProvider();
4191
- } catch (e) {
5753
+ const provider = await loadProviderLayered();
5754
+ if (!provider) {
4192
5755
  process.stderr.write(
4193
5756
  `
4194
- ${e.message}
5757
+ \u672A\u627E\u5230 provider \u914D\u7F6E\u3002
4195
5758
 
4196
- \u63D0\u793A\uFF1A-p \u6A21\u5F0F\u4E0D\u652F\u6301\u4EA4\u4E92\u914D\u7F6E\u3002\u8BF7\u5148\u76F4\u63A5\u8FD0\u884C \`minimal-agent\` \u5B8C\u6210\u9996\u6B21\u914D\u7F6E\u5411\u5BFC\uFF0C\u6216\u5728 .env \u4E2D\u624B\u52A8\u586B\u597D BASE_URL / API_KEY / MODEL\u3002
5759
+ \u8BF7\u4E8C\u9009\u4E00\uFF1A
5760
+ 1. \u8BBE\u7F6E\u73AF\u5883\u53D8\u91CF MINIMAL_AGENT_BASE_URL / MINIMAL_AGENT_API_KEY / MINIMAL_AGENT_MODEL
5761
+ 2. \u5148\u76F4\u63A5\u8FD0\u884C \`minimal-agent\`\uFF08TUI\uFF09\u5B8C\u6210\u9996\u6B21\u914D\u7F6E\u5411\u5BFC\uFF1B\u5411\u5BFC\u4F1A\u5199\u51FA ~/.minimal-agent/config.json\uFF0C\u4E4B\u540E -p \u6A21\u5F0F\u81EA\u52A8\u590D\u7528
4197
5762
 
4198
5763
  `
4199
5764
  );
@@ -4227,6 +5792,8 @@ minimal-agent - \u8F7B\u91CF\u7EA7 AI \u7F16\u7A0B\u52A9\u624B
4227
5792
  \u9009\u9879:
4228
5793
  -p, --print \u975E\u4EA4\u4E92\u6A21\u5F0F\uFF0C\u76F4\u63A5\u6267\u884C\u5355\u6B21\u95EE\u7B54
4229
5794
  -v, --verbose \u663E\u793A\u8BE6\u7EC6\u8F93\u51FA\uFF08\u5DE5\u5177\u8C03\u7528\u3001\u538B\u7F29\u4FE1\u606F\uFF09
5795
+ -d, --cwd <dir> \u6307\u5B9A\u5DE5\u4F5C\u76EE\u5F55\uFF08\u4E0D\u5B58\u5728\u81EA\u52A8\u521B\u5EFA\uFF09\uFF1B\u542F\u52A8\u65F6 chdir \u5230\u8FD9\u91CC\uFF0C
5796
+ \u4E0A\u4E0B\u6587\u6587\u4EF6\u3001\u5DE5\u5177\u76F8\u5BF9\u8DEF\u5F84\u3001.env \u52A0\u8F7D\u90FD\u4EE5\u6B64\u4E3A\u57FA\u51C6
4230
5797
  -h, --help \u663E\u793A\u5E2E\u52A9\u4FE1\u606F
4231
5798
 
4232
5799
  \u4F1A\u8BDD\u8BB0\u5FC6:
@@ -4238,6 +5805,7 @@ minimal-agent - \u8F7B\u91CF\u7EA7 AI \u7F16\u7A0B\u52A9\u624B
4238
5805
  minimal-agent -p "\u5E2E\u6211\u5199\u4E00\u4E2A hello world"
4239
5806
  echo "\u89E3\u91CA\u4EE3\u7801" | minimal-agent -p
4240
5807
  minimal-agent -p --verbose "\u8FD0\u884C\u6D4B\u8BD5\u5E76\u62A5\u544A\u7ED3\u679C"
5808
+ minimal-agent -p "\u5904\u7406\u8D44\u6599" -d /tmp/job-123 # \u5DE5\u4F5C\u76EE\u5F55\u9694\u79BB
4241
5809
  `);
4242
5810
  }
4243
5811
  main().catch((e) => {