minimal-agent 0.1.5 → 0.1.6

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 existsSync7, 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);
@@ -693,10 +693,171 @@ var bashTool = {
693
693
  };
694
694
 
695
695
  // src/tools/edit/edit.ts
696
- import { readFile as readFile5, writeFile as writeFile3, mkdir as mkdir4 } from "fs/promises";
696
+ import { readFile as readFile6, writeFile as writeFile3, mkdir as mkdir4 } from "fs/promises";
697
697
  import { existsSync as existsSync2 } from "fs";
698
- import { dirname as dirname5, resolve as resolve4 } from "path";
698
+ import { dirname as dirname5 } from "path";
699
699
  import { z as z2 } from "zod";
700
+
701
+ // src/tools/shared/fileUtils.ts
702
+ import { readFile as readFile5 } from "fs/promises";
703
+ import { homedir as homedir4 } from "os";
704
+ import { resolve as resolve4, normalize } from "path";
705
+ var BLOCKED_DEVICE_PATHS = /* @__PURE__ */ new Set([
706
+ "/dev/zero",
707
+ "/dev/random",
708
+ "/dev/urandom",
709
+ "/dev/full",
710
+ "/dev/stdin",
711
+ "/dev/tty",
712
+ "/dev/console",
713
+ "/dev/stdout",
714
+ "/dev/stderr",
715
+ "/dev/fd/0",
716
+ "/dev/fd/1",
717
+ "/dev/fd/2"
718
+ ]);
719
+ var WINDOWS_BLOCKED_NAMES = /* @__PURE__ */ new Set(["NUL", "CON", "PRN", "AUX", "COM1", "COM2", "LPT1"]);
720
+ function isBlockedDevicePath(filePath) {
721
+ const normalized = normalize(filePath);
722
+ if (BLOCKED_DEVICE_PATHS.has(normalized)) return true;
723
+ if (normalized.startsWith("/proc/") && (normalized.endsWith("/fd/0") || normalized.endsWith("/fd/1") || normalized.endsWith("/fd/2"))) {
724
+ return true;
725
+ }
726
+ const baseName = normalized.split(/[/\\]/).pop() ?? "";
727
+ if (WINDOWS_BLOCKED_NAMES.has(baseName.toUpperCase())) {
728
+ return true;
729
+ }
730
+ return false;
731
+ }
732
+ function validateAndResolvePath(rawPath, workingDir) {
733
+ if (rawPath.includes("\0")) {
734
+ return { ok: false, error: "\u8DEF\u5F84\u5305\u542B\u975E\u6CD5\u5B57\u7B26\uFF08null byte\uFF09" };
735
+ }
736
+ const expanded = expandPath(rawPath);
737
+ const resolved = resolve4(workingDir, expanded);
738
+ if (isBlockedDevicePath(resolved)) {
739
+ 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` };
740
+ }
741
+ if (process.platform === "win32" && /^\\\\/.test(resolved)) {
742
+ return { ok: false, error: "\u4E0D\u652F\u6301 UNC \u8DEF\u5F84\uFF08\\\\server\\share \u683C\u5F0F\uFF09\uFF0C\u8BF7\u4F7F\u7528\u672C\u5730\u8DEF\u5F84" };
743
+ }
744
+ return { ok: true, resolvedPath: resolved };
745
+ }
746
+ function expandPath(p) {
747
+ if (p.startsWith("~/") || p === "~") {
748
+ return resolve4(homedir4(), p.slice(2));
749
+ }
750
+ return p;
751
+ }
752
+ function detectLineEndingsForString(content) {
753
+ let crlfCount = 0;
754
+ let lfCount = 0;
755
+ for (let i = 0; i < content.length; i++) {
756
+ if (content[i] === "\n") {
757
+ if (i > 0 && content[i - 1] === "\r") {
758
+ crlfCount++;
759
+ } else {
760
+ lfCount++;
761
+ }
762
+ }
763
+ }
764
+ return crlfCount > lfCount ? "CRLF" : "LF";
765
+ }
766
+ async function detectFileLineEndings(filePath) {
767
+ try {
768
+ const handle = await readFile5(filePath, { encoding: "utf8" });
769
+ const head = handle.slice(0, 4096);
770
+ return detectLineEndingsForString(head);
771
+ } catch {
772
+ return "LF";
773
+ }
774
+ }
775
+ function applyLineEnding(content, ending) {
776
+ if (ending === "CRLF") {
777
+ return content.replaceAll("\r\n", "\n").split("\n").join("\r\n");
778
+ }
779
+ return content;
780
+ }
781
+ var LEFT_SINGLE_CURLY_QUOTE = "\u2018";
782
+ var RIGHT_SINGLE_CURLY_QUOTE = "\u2019";
783
+ var LEFT_DOUBLE_CURLY_QUOTE = "\u201C";
784
+ var RIGHT_DOUBLE_CURLY_QUOTE = "\u201D";
785
+ function normalizeQuotes(str) {
786
+ return str.replaceAll(LEFT_SINGLE_CURLY_QUOTE, "'").replaceAll(RIGHT_SINGLE_CURLY_QUOTE, "'").replaceAll(LEFT_DOUBLE_CURLY_QUOTE, '"').replaceAll(RIGHT_DOUBLE_CURLY_QUOTE, '"');
787
+ }
788
+ function findActualString(fileContent, searchString) {
789
+ if (fileContent.includes(searchString)) {
790
+ return searchString;
791
+ }
792
+ const normalizedSearch = normalizeQuotes(searchString);
793
+ const normalizedFile = normalizeQuotes(fileContent);
794
+ const searchIndex = normalizedFile.indexOf(normalizedSearch);
795
+ if (searchIndex !== -1) {
796
+ return fileContent.substring(searchIndex, searchIndex + searchString.length);
797
+ }
798
+ return null;
799
+ }
800
+ function preserveQuoteStyle(oldString, actualOldString, newString) {
801
+ if (oldString === actualOldString) {
802
+ return newString;
803
+ }
804
+ const hasDoubleQuotes = actualOldString.includes(LEFT_DOUBLE_CURLY_QUOTE) || actualOldString.includes(RIGHT_DOUBLE_CURLY_QUOTE);
805
+ const hasSingleQuotes = actualOldString.includes(LEFT_SINGLE_CURLY_QUOTE) || actualOldString.includes(RIGHT_SINGLE_CURLY_QUOTE);
806
+ if (!hasDoubleQuotes && !hasSingleQuotes) {
807
+ return newString;
808
+ }
809
+ let result = newString;
810
+ if (hasDoubleQuotes) {
811
+ result = applyCurlyDoubleQuotes(result);
812
+ }
813
+ if (hasSingleQuotes) {
814
+ result = applyCurlySingleQuotes(result);
815
+ }
816
+ return result;
817
+ }
818
+ function isOpeningContext(chars, index) {
819
+ if (index === 0) return true;
820
+ const prev = chars[index - 1];
821
+ return prev === " " || prev === " " || prev === "\n" || prev === "\r" || prev === "(" || prev === "[" || prev === "{" || prev === "\u2014" || prev === "\u2013";
822
+ }
823
+ function applyCurlyDoubleQuotes(str) {
824
+ const chars = [...str];
825
+ const result = [];
826
+ for (let i = 0; i < chars.length; i++) {
827
+ if (chars[i] === '"') {
828
+ result.push(
829
+ isOpeningContext(chars, i) ? LEFT_DOUBLE_CURLY_QUOTE : RIGHT_DOUBLE_CURLY_QUOTE
830
+ );
831
+ } else {
832
+ result.push(chars[i]);
833
+ }
834
+ }
835
+ return result.join("");
836
+ }
837
+ function applyCurlySingleQuotes(str) {
838
+ const chars = [...str];
839
+ const result = [];
840
+ for (let i = 0; i < chars.length; i++) {
841
+ if (chars[i] === "'") {
842
+ const prev = i > 0 ? chars[i - 1] : void 0;
843
+ const next = i < chars.length - 1 ? chars[i + 1] : void 0;
844
+ const prevIsLetter = prev !== void 0 && new RegExp("\\p{L}", "u").test(prev);
845
+ const nextIsLetter = next !== void 0 && new RegExp("\\p{L}", "u").test(next);
846
+ if (prevIsLetter && nextIsLetter) {
847
+ result.push(RIGHT_SINGLE_CURLY_QUOTE);
848
+ } else {
849
+ result.push(
850
+ isOpeningContext(chars, i) ? LEFT_SINGLE_CURLY_QUOTE : RIGHT_SINGLE_CURLY_QUOTE
851
+ );
852
+ }
853
+ } else {
854
+ result.push(chars[i]);
855
+ }
856
+ }
857
+ return result.join("");
858
+ }
859
+
860
+ // src/tools/edit/edit.ts
700
861
  var MAX_EDIT_FILE_SIZE_BYTES = 1024 * 1024 * 1024;
701
862
  var inputSchema2 = z2.object({
702
863
  file_path: z2.string().min(1).describe("\u8981\u7F16\u8F91\u7684\u6587\u4EF6\u8DEF\u5F84"),
@@ -718,21 +879,12 @@ Usage:
718
879
  - 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
880
  - 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
881
  - 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
882
  async function call2(input) {
731
- const filePath = resolve4(input.file_path);
732
883
  const { old_string, new_string } = input;
733
884
  const replaceAll = input.replace_all ?? false;
734
- const pathCheck = validatePath(filePath);
735
- if (!pathCheck.ok) return pathCheck;
885
+ const pathResult = validateAndResolvePath(input.file_path, getWorkingDir());
886
+ if (!pathResult.ok) return pathResult;
887
+ const filePath = pathResult.resolvedPath;
736
888
  if (old_string === new_string) {
737
889
  return { ok: false, error: "old_string \u4E0E new_string \u5B8C\u5168\u76F8\u540C\uFF0C\u6CA1\u6709\u53EF\u6539\u7684\u5185\u5BB9\u3002" };
738
890
  }
@@ -757,10 +909,11 @@ async function call2(input) {
757
909
  }
758
910
  let original;
759
911
  try {
760
- original = await readFile5(filePath, "utf8");
912
+ original = await readFile6(filePath, "utf8");
761
913
  } catch (e) {
762
914
  return { ok: false, error: `\u8BFB\u53D6\u5931\u8D25\uFF1A${e.message}` };
763
915
  }
916
+ const originalLineEnding = detectLineEndingsForString(original);
764
917
  const fileSize = Buffer.byteLength(original, "utf8");
765
918
  if (fileSize > MAX_EDIT_FILE_SIZE_BYTES) {
766
919
  return {
@@ -774,9 +927,16 @@ async function call2(input) {
774
927
  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
928
  };
776
929
  }
777
- const occurrences = countOccurrences(original, old_string);
930
+ let searchTarget = old_string;
931
+ let processedNewString = new_string;
932
+ const actualOld = findActualString(original, old_string);
933
+ if (actualOld !== null && actualOld !== old_string) {
934
+ searchTarget = actualOld;
935
+ processedNewString = preserveQuoteStyle(old_string, actualOld, new_string);
936
+ }
937
+ const occurrences = countOccurrences(original, searchTarget);
778
938
  if (occurrences === 0) {
779
- const hint = findFuzzyMatchHint(original, old_string);
939
+ const hint = findFuzzyMatchHint(original, searchTarget);
780
940
  const extraMsg = hint ? `
781
941
 
782
942
  \u{1F4A1} \u63D0\u793A\uFF1A${hint}` : "";
@@ -792,9 +952,10 @@ async function call2(input) {
792
952
  \u8BF7\u6269\u5927 old_string \u5305\u542B\u66F4\u591A\u4E0A\u4E0B\u6587\uFF0C\u6216\u663E\u5F0F\u4F20 replace_all=true\u3002`
793
953
  };
794
954
  }
795
- const replaced = replaceAll ? splitReplaceAll(original, old_string, new_string) : original.replace(old_string, new_string);
955
+ const replaced = replaceAll ? splitReplaceAll(original, searchTarget, processedNewString) : original.replace(searchTarget, processedNewString);
956
+ const normalizedReplaced = applyLineEnding(replaced, originalLineEnding);
796
957
  try {
797
- await writeFile3(filePath, replaced, "utf8");
958
+ await writeFile3(filePath, normalizedReplaced, "utf8");
798
959
  } catch (e) {
799
960
  return { ok: false, error: `\u5199\u5165\u5931\u8D25\uFF1A${e.message}` };
800
961
  }
@@ -1190,8 +1351,9 @@ var grepTool = {
1190
1351
  };
1191
1352
 
1192
1353
  // src/tools/read/read.ts
1193
- import { readFile as readFile6, stat as stat3 } from "fs/promises";
1194
- import { resolve as resolve8 } from "path";
1354
+ import { createReadStream } from "fs";
1355
+ import { readFile as readFile7, stat as stat3 } from "fs/promises";
1356
+ import { createInterface } from "readline";
1195
1357
  import { z as z5 } from "zod";
1196
1358
  var inputSchema5 = z5.object({
1197
1359
  file_path: z5.string().min(1, "\u5FC5\u987B\u63D0\u4F9B file_path").describe("\u8981\u8BFB\u53D6\u7684\u6587\u4EF6\u8DEF\u5F84\uFF0C\u7EDD\u5BF9\u8DEF\u5F84\u4F18\u5148"),
@@ -1209,10 +1371,18 @@ Usage:
1209
1371
  - Results are returned using cat -n format, with line numbers starting at 1
1210
1372
  - This tool can only read text files, not directories. To read a directory, use the Glob tool.
1211
1373
  - If you read a file that exists but has empty contents you will receive a warning in place of file contents.`;
1374
+ var STREAM_THRESHOLD = 1024 * 1024;
1212
1375
  async function call5(input) {
1213
- const filePath = resolve8(input.file_path);
1214
1376
  const offset = input.offset ?? 1;
1215
1377
  const limit = input.limit ?? MAX_LINES_TO_READ;
1378
+ const pathResult = validateAndResolvePath(input.file_path, getWorkingDir());
1379
+ if (!pathResult.ok) {
1380
+ return { ok: false, error: pathResult.error };
1381
+ }
1382
+ const filePath = pathResult.resolvedPath;
1383
+ if (isBlockedDevicePath(filePath)) {
1384
+ return { ok: false, error: `\u4E0D\u5141\u8BB8\u8BFB\u53D6\u8BBE\u5907\u6587\u4EF6\uFF1A${filePath}\u3002\u8BE5\u8DEF\u5F84\u53EF\u80FD\u4EA7\u751F\u65E0\u9650\u8F93\u51FA\u6216\u963B\u585E\u8FDB\u7A0B\u3002` };
1385
+ }
1216
1386
  let st;
1217
1387
  try {
1218
1388
  st = await stat3(filePath);
@@ -1231,33 +1401,37 @@ async function call5(input) {
1231
1401
  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
1402
  };
1233
1403
  }
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}` };
1404
+ let numbered;
1405
+ let totalLines;
1406
+ if (st.size <= STREAM_THRESHOLD) {
1407
+ const result = await readSmallFile(filePath, offset, limit);
1408
+ numbered = result.numbered;
1409
+ totalLines = result.totalLines;
1410
+ if (result.isEmpty) {
1411
+ return { ok: true, content: "<file is empty>" };
1412
+ }
1413
+ } else {
1414
+ const result = await readLargeFileStream(filePath, offset, limit);
1415
+ numbered = result.numbered;
1416
+ totalLines = result.totalLines;
1239
1417
  }
1240
- if (raw.length === 0) {
1418
+ if (totalLines === 0 || !numbered) {
1241
1419
  return { ok: true, content: "<file is empty>" };
1242
1420
  }
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
1421
  let content = numbered;
1250
- const contentLength = content.length;
1251
- if (contentLength > DEFAULT_MAX_RESULT_SIZE_CHARS) {
1422
+ if (content.length > DEFAULT_MAX_RESULT_SIZE_CHARS) {
1252
1423
  content = content.slice(0, DEFAULT_MAX_RESULT_SIZE_CHARS) + `
1253
1424
 
1254
1425
  ... (\u8F93\u51FA\u8D85\u8FC7 ${DEFAULT_MAX_RESULT_SIZE_CHARS} \u5B57\u7B26\uFF0C\u5DF2\u622A\u65AD)`;
1255
1426
  }
1427
+ const startIdx = Math.max(0, offset - 1);
1428
+ const endIdx = Math.min(totalLines, startIdx + limit);
1256
1429
  if (endIdx < totalLines) {
1257
1430
  const nextOffset = endIdx + 1;
1431
+ const returnedLines = content.split("\n").filter((l) => l.trim()).length;
1258
1432
  content += `
1259
1433
 
1260
- ... (\u672C\u6B21\u8FD4\u56DE ${slice.length} \u884C / \u6587\u4EF6\u5171 ${totalLines} \u884C\uFF1B\u7528 offset=${nextOffset} \u7EE7\u7EED\u8BFB)`;
1434
+ ... (\u672C\u6B21\u8FD4\u56DE ${returnedLines} \u884C / \u6587\u4EF6\u5171 ${totalLines} \u884C\uFF1B\u7528 offset=${nextOffset} \u7EE7\u7EED\u8BFB)`;
1261
1435
  }
1262
1436
  if (st.size > 100 * 1024 && offset === 1) {
1263
1437
  content += `
@@ -1266,6 +1440,55 @@ async function call5(input) {
1266
1440
  }
1267
1441
  return { ok: true, content };
1268
1442
  }
1443
+ async function readSmallFile(filePath, offset, limit) {
1444
+ const raw = await readFile7(filePath, "utf8");
1445
+ if (raw.length === 0) {
1446
+ return { numbered: "", totalLines: 0, isEmpty: true };
1447
+ }
1448
+ const allLines = raw.split("\n");
1449
+ const totalLines = allLines.length;
1450
+ const startIdx = Math.max(0, offset - 1);
1451
+ const endIdx = Math.min(totalLines, startIdx + limit);
1452
+ const slice = allLines.slice(startIdx, endIdx);
1453
+ const numbered = slice.map((line, i) => `${(startIdx + i + 1).toString()} ${line}`).join("\n");
1454
+ return { numbered, totalLines, isEmpty: false };
1455
+ }
1456
+ async function readLargeFileStream(filePath, offset, limit) {
1457
+ return new Promise((resolvePromise, reject) => {
1458
+ const lines = [];
1459
+ let currentLine = 0;
1460
+ const startIdx = Math.max(0, offset - 1);
1461
+ const endLine = startIdx + limit;
1462
+ const input = createReadStream(filePath, { encoding: "utf8" });
1463
+ const rl = createInterface({
1464
+ input,
1465
+ crlfDelay: Infinity
1466
+ });
1467
+ input.on("error", (err) => {
1468
+ reject(err);
1469
+ });
1470
+ rl.on("line", (line) => {
1471
+ currentLine++;
1472
+ if (currentLine > endLine) {
1473
+ rl.close();
1474
+ rl.removeAllListeners();
1475
+ return;
1476
+ }
1477
+ if (currentLine >= offset) {
1478
+ lines.push(`${currentLine} ${line}`);
1479
+ }
1480
+ });
1481
+ rl.on("close", () => {
1482
+ resolvePromise({
1483
+ numbered: lines.join("\n"),
1484
+ totalLines: currentLine
1485
+ });
1486
+ });
1487
+ rl.on("error", (err) => {
1488
+ reject(err);
1489
+ });
1490
+ });
1491
+ }
1269
1492
  var readTool = {
1270
1493
  name: "Read",
1271
1494
  description: description5,
@@ -2081,7 +2304,7 @@ var webSearchTool = {
2081
2304
  // src/tools/write/write.ts
2082
2305
  import { existsSync as existsSync4 } from "fs";
2083
2306
  import { mkdir as mkdir5, stat as stat4, writeFile as writeFile4 } from "fs/promises";
2084
- import { dirname as dirname6, resolve as resolve9 } from "path";
2307
+ import { dirname as dirname6 } from "path";
2085
2308
  import { z as z9 } from "zod";
2086
2309
  var MAX_WRITE_SIZE_BYTES = 1024 * 1024 * 1024;
2087
2310
  var inputSchema9 = z9.object({
@@ -2096,19 +2319,10 @@ Usage:
2096
2319
  - If the parent directory does not exist, it will be created recursively.
2097
2320
  - ALWAYS prefer editing existing files in the codebase via the Edit tool. NEVER write new files unless explicitly required.
2098
2321
  - 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" };
2102
- }
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
2322
  async function call9(input) {
2109
- const filePath = resolve9(input.file_path);
2110
- const pathCheck = validatePath2(filePath);
2111
- if (!pathCheck.ok) return pathCheck;
2323
+ const pathResult = validateAndResolvePath(input.file_path, getWorkingDir());
2324
+ if (!pathResult.ok) return pathResult;
2325
+ const filePath = pathResult.resolvedPath;
2112
2326
  const contentSize = Buffer.byteLength(input.content, "utf8");
2113
2327
  if (contentSize > MAX_WRITE_SIZE_BYTES) {
2114
2328
  return {
@@ -2127,9 +2341,14 @@ async function call9(input) {
2127
2341
  } catch {
2128
2342
  }
2129
2343
  }
2130
- await writeFile4(filePath, input.content, "utf8");
2344
+ let contentToWrite = input.content;
2345
+ if (fileExisted) {
2346
+ const lineEnding = await detectFileLineEndings(filePath);
2347
+ contentToWrite = applyLineEnding(input.content, lineEnding);
2348
+ }
2349
+ await writeFile4(filePath, contentToWrite, "utf8");
2131
2350
  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`;
2351
+ 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`;
2133
2352
  return {
2134
2353
  ok: true,
2135
2354
  content: `${action} ${filePath}${sizeInfo}`
@@ -2962,7 +3181,7 @@ function MessageRow({ message }) {
2962
3181
 
2963
3182
  // src/ui/StatusLine.tsx
2964
3183
  import { Box as Box4, Text as Text4 } from "ink";
2965
- import { homedir as homedir4 } from "os";
3184
+ import { homedir as homedir5 } from "os";
2966
3185
  import { sep } from "path";
2967
3186
 
2968
3187
  // src/llm/client.ts
@@ -3350,13 +3569,26 @@ function useTokenUsage(messages, provider) {
3350
3569
  }
3351
3570
 
3352
3571
  // src/ui/StatusLine.tsx
3353
- import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
3354
- function StatusLine({ provider, history }) {
3572
+ import { Fragment, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
3573
+ function StatusLine({ provider, history, pluginLoop }) {
3355
3574
  const usage = useTokenUsage(history, provider);
3356
3575
  const ratio = usage.tokens / usage.threshold;
3357
3576
  const color = ratio >= 1 ? "red" : ratio >= 0.7 ? "yellow" : "green";
3358
3577
  const cwdDisplay = shortenPath(getWorkingDir());
3359
3578
  return /* @__PURE__ */ jsxs4(Box4, { children: [
3579
+ pluginLoop && /* @__PURE__ */ jsxs4(Fragment, { children: [
3580
+ /* @__PURE__ */ jsxs4(Text4, { color: "magenta", children: [
3581
+ "\u{1F504} ",
3582
+ pluginLoop.pluginName,
3583
+ " "
3584
+ ] }),
3585
+ /* @__PURE__ */ jsxs4(Text4, { color: "magenta", children: [
3586
+ pluginLoop.current,
3587
+ "/",
3588
+ pluginLoop.max
3589
+ ] }),
3590
+ /* @__PURE__ */ jsx4(Text4, { color: "gray", children: " \xB7 " })
3591
+ ] }),
3360
3592
  /* @__PURE__ */ jsx4(Text4, { color: "cyan", children: "cwd " }),
3361
3593
  /* @__PURE__ */ jsx4(Text4, { color: "gray", children: cwdDisplay }),
3362
3594
  /* @__PURE__ */ jsx4(Text4, { color: "gray", children: " \xB7 " }),
@@ -3382,7 +3614,7 @@ function fmt(n) {
3382
3614
  return `${(n / 1e6).toFixed(2)}M`;
3383
3615
  }
3384
3616
  function shortenPath(abs) {
3385
- const home = homedir4();
3617
+ const home = homedir5();
3386
3618
  let p = abs;
3387
3619
  if (home && (p === home || p.startsWith(home + sep))) {
3388
3620
  p = "~" + p.slice(home.length);
@@ -3547,6 +3779,688 @@ async function reactiveCompactIfApplicable(messages, provider, error, state = de
3547
3779
  };
3548
3780
  }
3549
3781
 
3782
+ // src/plugins/commandRouter.ts
3783
+ import { readFile as readFile8, readdir as readdir3 } from "fs/promises";
3784
+ import { join as join6 } from "path";
3785
+ var PLUGINS_DIR = join6(findPackageRoot(import.meta.url), "plugins");
3786
+ var pluginCache = /* @__PURE__ */ new Map();
3787
+ var discoveryDone = false;
3788
+ function stripQuotes2(s) {
3789
+ const trimmed = s.trim();
3790
+ if (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'")) {
3791
+ return trimmed.slice(1, -1);
3792
+ }
3793
+ return trimmed;
3794
+ }
3795
+ function parseMarkdownFrontmatter(content) {
3796
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
3797
+ if (!match) return {};
3798
+ const frontmatter = {};
3799
+ for (const line of match[1].split("\n")) {
3800
+ const colon = line.indexOf(":");
3801
+ if (colon < 0) continue;
3802
+ const key = line.slice(0, colon).trim();
3803
+ const value = line.slice(colon + 1).trim();
3804
+ if (key) frontmatter[key] = stripQuotes2(value);
3805
+ }
3806
+ return frontmatter;
3807
+ }
3808
+ async function loadPlugin(pluginDirPath) {
3809
+ const dirName = pluginDirPath.split("/").pop() ?? pluginDirPath;
3810
+ const manifestPath = join6(pluginDirPath, ".claude-plugin", "plugin.json");
3811
+ let manifestName = dirName;
3812
+ let manifestVersion;
3813
+ let manifestDesc;
3814
+ try {
3815
+ const raw = await readFile8(manifestPath, "utf8");
3816
+ const parsed = JSON.parse(raw);
3817
+ manifestName = parsed.name ?? dirName;
3818
+ manifestVersion = parsed.version;
3819
+ manifestDesc = parsed.description;
3820
+ } catch {
3821
+ }
3822
+ const commandsDir = join6(pluginDirPath, "commands");
3823
+ const commands = [];
3824
+ try {
3825
+ const entries = await readdir3(commandsDir, { withFileTypes: true });
3826
+ for (const entry of entries) {
3827
+ if (!entry.name.endsWith(".md")) continue;
3828
+ const cmdPath = join6(commandsDir, entry.name);
3829
+ try {
3830
+ const content = await readFile8(cmdPath, "utf8");
3831
+ const fm = parseMarkdownFrontmatter(content);
3832
+ const sep2 = content.indexOf("\n---", 4);
3833
+ const body = sep2 >= 0 ? content.slice(sep2 + 4).trim() : content.trim();
3834
+ commands.push({
3835
+ name: entry.name.replace(/\.md$/, ""),
3836
+ description: fm.description ?? "(no description)",
3837
+ argumentHint: fm["argument-hint"],
3838
+ pluginName: manifestName,
3839
+ pluginRoot: pluginDirPath,
3840
+ promptBody: body
3841
+ });
3842
+ } catch {
3843
+ }
3844
+ }
3845
+ } catch {
3846
+ }
3847
+ const hooksJsonPath = join6(pluginDirPath, "hooks", "hooks.json");
3848
+ let hasStopHook = false;
3849
+ try {
3850
+ const hooksRaw = await readFile8(hooksJsonPath, "utf8");
3851
+ const hooksParsed = JSON.parse(hooksRaw);
3852
+ const stopHooks = hooksParsed?.hooks?.Stop;
3853
+ if (Array.isArray(stopHooks) && stopHooks.length > 0) {
3854
+ hasStopHook = true;
3855
+ }
3856
+ } catch {
3857
+ }
3858
+ if (commands.length === 0 && !hasStopHook) return null;
3859
+ return {
3860
+ name: manifestName,
3861
+ version: manifestVersion,
3862
+ description: manifestDesc,
3863
+ root: pluginDirPath,
3864
+ commands,
3865
+ hasStopHook
3866
+ };
3867
+ }
3868
+ async function discoverPlugins() {
3869
+ if (discoveryDone && pluginCache.size > 0) {
3870
+ return Array.from(pluginCache.values());
3871
+ }
3872
+ pluginCache.clear();
3873
+ discoveryDone = true;
3874
+ try {
3875
+ const entries = await readdir3(PLUGINS_DIR, { withFileTypes: true });
3876
+ for (const entry of entries) {
3877
+ if (!entry.isDirectory()) continue;
3878
+ if (entry.name.startsWith(".")) continue;
3879
+ const plugin = await loadPlugin(join6(PLUGINS_DIR, entry.name));
3880
+ if (plugin) {
3881
+ pluginCache.set(plugin.name, plugin);
3882
+ }
3883
+ }
3884
+ } catch {
3885
+ }
3886
+ return Array.from(pluginCache.values());
3887
+ }
3888
+ function resolveCommand(input) {
3889
+ const trimmed = input.trimStart();
3890
+ if (!trimmed.startsWith("/")) return null;
3891
+ const spaceIdx = trimmed.indexOf(" ", 1);
3892
+ const cmdName = spaceIdx >= 0 ? trimmed.slice(1, spaceIdx) : trimmed.slice(1);
3893
+ const args = spaceIdx >= 0 ? trimmed.slice(spaceIdx + 1) : "";
3894
+ for (const plugin of pluginCache.values()) {
3895
+ for (const cmd of plugin.commands) {
3896
+ if (cmd.name === cmdName) {
3897
+ return { cmd, arguments: args };
3898
+ }
3899
+ }
3900
+ }
3901
+ return null;
3902
+ }
3903
+ var COMMAND_DEFAULTS = {
3904
+ "ralph-loop": ["--max-iterations", "50"]
3905
+ };
3906
+ function applyDefaultArgs(cmdName, args) {
3907
+ const defaults = COMMAND_DEFAULTS[cmdName];
3908
+ if (!defaults) return args;
3909
+ const existing = new Set(args.trim().split(/\s+/).filter(Boolean));
3910
+ let result = args;
3911
+ for (let i = 0; i < defaults.length; i += 2) {
3912
+ const flag = defaults[i];
3913
+ if (existing.has(flag)) continue;
3914
+ result = result.trim() ? `${result} ${flag} ${defaults[i + 1]}` : `${flag} ${defaults[i + 1]}`;
3915
+ }
3916
+ return result;
3917
+ }
3918
+ function buildCommandInput(resolved) {
3919
+ const { cmd, arguments: rawArgs } = resolved;
3920
+ const args = applyDefaultArgs(cmd.name, rawArgs);
3921
+ let input = cmd.promptBody;
3922
+ input = input.replaceAll("${CLAUDE_PLUGIN_ROOT}", cmd.pluginRoot);
3923
+ input = input.replaceAll("$ARGUMENTS", args);
3924
+ input = input.replaceAll("${ARGUMENTS}", args);
3925
+ if (args.trim()) {
3926
+ input += `
3927
+
3928
+ \u7528\u6237\u53C2\u6570: ${args.trim()}`;
3929
+ }
3930
+ return input;
3931
+ }
3932
+ function getActiveStopHookPlugins() {
3933
+ const result = [];
3934
+ for (const plugin of pluginCache.values()) {
3935
+ if (plugin.hasStopHook) {
3936
+ result.push(plugin.root);
3937
+ }
3938
+ }
3939
+ return result;
3940
+ }
3941
+
3942
+ // src/plugins/stopHook.ts
3943
+ import { readFile as readFile9 } from "fs/promises";
3944
+ import { join as join7 } from "path";
3945
+ import { spawn as spawn4 } from "child_process";
3946
+ async function loadStopHookConfig(pluginRoot) {
3947
+ const hooksJsonPath = join7(pluginRoot, "hooks", "hooks.json");
3948
+ try {
3949
+ const raw = await readFile9(hooksJsonPath, "utf8");
3950
+ const parsed = JSON.parse(raw);
3951
+ const stopEntries = parsed?.hooks?.Stop;
3952
+ if (!Array.isArray(stopEntries)) return [];
3953
+ const commands = [];
3954
+ for (const entry of stopEntries) {
3955
+ const hooks = entry.hooks;
3956
+ if (Array.isArray(hooks)) {
3957
+ for (const h of hooks) {
3958
+ if (h.type === "command" && h.command) {
3959
+ commands.push(h);
3960
+ }
3961
+ }
3962
+ }
3963
+ }
3964
+ return commands;
3965
+ } catch {
3966
+ return [];
3967
+ }
3968
+ }
3969
+ async function executeStopHooks(pluginRoots, transcriptText) {
3970
+ if (process.platform === "win32") {
3971
+ return { decision: "pass" };
3972
+ }
3973
+ for (const pluginRoot of pluginRoots) {
3974
+ const hookConfigs = await loadStopHookConfig(pluginRoot);
3975
+ for (const hookConfig of hookConfigs) {
3976
+ const result = await runSingleStopHook(hookConfig, pluginRoot, transcriptText);
3977
+ if (result.decision === "block") {
3978
+ return result;
3979
+ }
3980
+ }
3981
+ }
3982
+ return { decision: "pass" };
3983
+ }
3984
+ function runSingleStopHook(hookConfig, pluginRoot, transcriptText) {
3985
+ const resolvedCommand = hookConfig.command.replaceAll("${CLAUDE_PLUGIN_ROOT}", pluginRoot);
3986
+ return new Promise((resolve9) => {
3987
+ const child = spawn4("bash", [resolvedCommand], {
3988
+ env: {
3989
+ ...process.env,
3990
+ CLAUDE_PLUGIN_ROOT: pluginRoot
3991
+ }
3992
+ });
3993
+ let stdout = "";
3994
+ child.stdout.on("data", (data) => {
3995
+ stdout += data.toString();
3996
+ });
3997
+ child.on("error", () => {
3998
+ resolve9({ decision: "pass" });
3999
+ });
4000
+ child.on("close", (code) => {
4001
+ if (code !== 0) {
4002
+ resolve9({ decision: "pass" });
4003
+ return;
4004
+ }
4005
+ const trimmed = stdout.trim();
4006
+ if (!trimmed) {
4007
+ resolve9({ decision: "pass" });
4008
+ return;
4009
+ }
4010
+ try {
4011
+ const parsed = JSON.parse(trimmed);
4012
+ if (parsed.decision === "block") {
4013
+ resolve9({
4014
+ decision: "block",
4015
+ reason: typeof parsed.reason === "string" ? parsed.reason : void 0,
4016
+ systemMessage: typeof parsed.systemMessage === "string" ? parsed.systemMessage : void 0
4017
+ });
4018
+ return;
4019
+ }
4020
+ resolve9({ decision: "pass" });
4021
+ } catch {
4022
+ resolve9({ decision: "pass" });
4023
+ }
4024
+ });
4025
+ child.stdin.write(transcriptText);
4026
+ child.stdin.end();
4027
+ });
4028
+ }
4029
+
4030
+ // src/plugins/verificationGate.ts
4031
+ import { existsSync as existsSync5, readFileSync } from "fs";
4032
+ import { spawn as spawn5 } from "child_process";
4033
+ function parseVerifyArg(arg) {
4034
+ const colonIdx = arg.indexOf(":");
4035
+ if (colonIdx < 0) return null;
4036
+ const type = arg.slice(0, colonIdx).trim().toLowerCase();
4037
+ const value = arg.slice(colonIdx + 1).trim();
4038
+ switch (type) {
4039
+ case "shell":
4040
+ return { type: "shell", command: value, timeout: 3e4 };
4041
+ case "file_exists":
4042
+ return { type: "file_exists", file: value };
4043
+ case "file_contains": {
4044
+ const sep2 = value.indexOf(":");
4045
+ if (sep2 < 0) return null;
4046
+ return {
4047
+ type: "file_contains",
4048
+ file: value.slice(0, sep2),
4049
+ pattern: value.slice(sep2 + 1)
4050
+ };
4051
+ }
4052
+ case "test_count": {
4053
+ const count = parseInt(value, 10);
4054
+ if (isNaN(count) || count < 0) return null;
4055
+ return { type: "test_count", minCount: count };
4056
+ }
4057
+ default:
4058
+ return null;
4059
+ }
4060
+ }
4061
+ function parseVerifyArgs(args) {
4062
+ const checks = [];
4063
+ const regex = /--verify\s+("[^"]*"|\S+)/gi;
4064
+ let match;
4065
+ while ((match = regex.exec(args)) !== null) {
4066
+ const raw = match[1].replace(/^"|"$/g, "");
4067
+ const check = parseVerifyArg(raw);
4068
+ if (check) checks.push(check);
4069
+ }
4070
+ return checks;
4071
+ }
4072
+ function runShell(command, timeout) {
4073
+ return new Promise((resolve9) => {
4074
+ const isWin = process.platform === "win32";
4075
+ const child = isWin ? spawn5("cmd", ["/c", command], { timeout, env: process.env }) : spawn5("bash", ["-c", command], { timeout, env: process.env });
4076
+ let stdout = "";
4077
+ let stderr = "";
4078
+ child.stdout.on("data", (d) => {
4079
+ stdout += d.toString();
4080
+ });
4081
+ child.stderr.on("data", (d) => {
4082
+ stderr += d.toString();
4083
+ });
4084
+ child.on("error", () => {
4085
+ resolve9({ exitCode: null, stdout, stderr, errored: true });
4086
+ });
4087
+ child.on("close", (code) => {
4088
+ resolve9({ exitCode: code, stdout, stderr, errored: false });
4089
+ });
4090
+ });
4091
+ }
4092
+ async function verifyShell(command, timeout) {
4093
+ const r = await runShell(command, timeout);
4094
+ if (r.errored) {
4095
+ return {
4096
+ check: { type: "shell", command },
4097
+ passed: false,
4098
+ output: `\u6267\u884C\u5931\u8D25`
4099
+ };
4100
+ }
4101
+ return {
4102
+ check: { type: "shell", command },
4103
+ passed: r.exitCode === 0,
4104
+ output: r.stdout.trim() || r.stderr.trim() || `exit code ${r.exitCode}`
4105
+ };
4106
+ }
4107
+ function verifyFileExists(file) {
4108
+ const exists = existsSync5(file);
4109
+ return {
4110
+ check: { type: "file_exists", file },
4111
+ passed: exists,
4112
+ output: exists ? "\u6587\u4EF6\u5B58\u5728" : `\u6587\u4EF6\u4E0D\u5B58\u5728: ${file}`
4113
+ };
4114
+ }
4115
+ function verifyFileContains(file, pattern) {
4116
+ try {
4117
+ const content = readFileSync(file, "utf8");
4118
+ const regex = new RegExp(pattern);
4119
+ const matched = regex.test(content);
4120
+ return {
4121
+ check: { type: "file_contains", file, pattern },
4122
+ passed: matched,
4123
+ output: matched ? `\u6587\u4EF6\u5305\u542B "${pattern}"` : `\u6587\u4EF6\u4E0D\u5305\u542B "${pattern}"`
4124
+ };
4125
+ } catch {
4126
+ return {
4127
+ check: { type: "file_contains", file, pattern },
4128
+ passed: false,
4129
+ output: `\u65E0\u6CD5\u8BFB\u53D6\u6587\u4EF6: ${file}`
4130
+ };
4131
+ }
4132
+ }
4133
+ async function verifyTestCount(minCount) {
4134
+ const r = await runShell(`bun test`, 6e4);
4135
+ const combined = `${r.stdout}
4136
+ ${r.stderr}`;
4137
+ if (r.errored) {
4138
+ return {
4139
+ check: { type: "test_count", minCount },
4140
+ passed: false,
4141
+ output: "\u65E0\u6CD5\u6267\u884C bun test"
4142
+ };
4143
+ }
4144
+ const matches = [...combined.matchAll(/(\d+)\s+pass/gi)];
4145
+ const count = matches.length > 0 ? parseInt(matches[matches.length - 1][1], 10) : 0;
4146
+ const passed = count >= minCount;
4147
+ return {
4148
+ check: { type: "test_count", minCount },
4149
+ passed,
4150
+ output: `${count} pass (\u9700\u8981 >=${minCount})`
4151
+ };
4152
+ }
4153
+ async function runVerification(checks) {
4154
+ if (checks.length === 0) {
4155
+ return { passed: true, details: [], summary: "\u65E0\u9A8C\u8BC1\u9879" };
4156
+ }
4157
+ const results = [];
4158
+ for (const check of checks) {
4159
+ let result;
4160
+ switch (check.type) {
4161
+ case "shell":
4162
+ result = await verifyShell(check.command ?? "", check.timeout ?? 3e4);
4163
+ break;
4164
+ case "file_exists":
4165
+ result = verifyFileExists(check.file ?? "");
4166
+ break;
4167
+ case "file_contains":
4168
+ result = verifyFileContains(check.file ?? "", check.pattern ?? "");
4169
+ break;
4170
+ case "test_count":
4171
+ result = await verifyTestCount(check.minCount ?? 0);
4172
+ break;
4173
+ default:
4174
+ result = {
4175
+ check,
4176
+ passed: false,
4177
+ output: `\u672A\u77E5\u9A8C\u8BC1\u7C7B\u578B: ${check.type}`
4178
+ };
4179
+ }
4180
+ results.push(result);
4181
+ }
4182
+ const allPassed = results.every((r) => r.passed);
4183
+ const failedNames = results.filter((r) => !r.passed).map((r) => formatCheckName(r.check));
4184
+ let summary;
4185
+ if (allPassed) {
4186
+ summary = `\u2705 \u5168\u90E8\u901A\u8FC7 (${results.length}/${results.length})`;
4187
+ } else {
4188
+ summary = `\u274C \u9A8C\u8BC1\u672A\u901A\u8FC7: ${failedNames.join(", ")}`;
4189
+ }
4190
+ return { passed: allPassed, details: results, summary };
4191
+ }
4192
+ function formatCheckName(check) {
4193
+ switch (check.type) {
4194
+ case "shell":
4195
+ return `shell(${(check.command ?? "").slice(0, 40)})`;
4196
+ case "file_exists":
4197
+ return `exists(${check.file})`;
4198
+ case "file_contains":
4199
+ return `contains(${check.file}:${check.pattern})`;
4200
+ case "test_count":
4201
+ return `tests(>=${check.minCount})`;
4202
+ default:
4203
+ return check.type;
4204
+ }
4205
+ }
4206
+
4207
+ // src/plugins/goalState.ts
4208
+ import { mkdir as mkdir6, appendFile, writeFile as writeFile5, unlink as unlink2, rmdir as rmdir2 } from "fs/promises";
4209
+ import { existsSync as existsSync6, readFileSync as readFileSync2 } from "fs";
4210
+ import { join as join8 } from "path";
4211
+ var Phase = /* @__PURE__ */ ((Phase2) => {
4212
+ Phase2["PLAN"] = "plan";
4213
+ Phase2["BUILD"] = "build";
4214
+ Phase2["VERIFY"] = "verify";
4215
+ Phase2["HEAL"] = "heal";
4216
+ Phase2["DONE"] = "done";
4217
+ return Phase2;
4218
+ })(Phase || {});
4219
+ var VALID_PHASES = new Set(Object.values(Phase));
4220
+ var PHASE_TRANSITIONS = {
4221
+ ["plan" /* PLAN */]: {
4222
+ plan_complete: "build" /* BUILD */,
4223
+ stuck: "plan" /* PLAN */
4224
+ },
4225
+ ["build" /* BUILD */]: {
4226
+ task_complete: "verify" /* VERIFY */,
4227
+ need_replan: "plan" /* PLAN */,
4228
+ tests_failing: "heal" /* HEAL */
4229
+ },
4230
+ ["verify" /* VERIFY */]: {
4231
+ all_pass: "build" /* BUILD */,
4232
+ failures: "heal" /* HEAL */,
4233
+ goal_complete: "done" /* DONE */
4234
+ },
4235
+ ["heal" /* HEAL */]: {
4236
+ fixed: "verify" /* VERIFY */,
4237
+ cannot_fix_locally: "plan" /* PLAN */
4238
+ },
4239
+ ["done" /* DONE */]: {}
4240
+ };
4241
+ function isLegalTransition(from, to) {
4242
+ if (from === to) return true;
4243
+ const allowed = PHASE_TRANSITIONS[from];
4244
+ if (!allowed) return false;
4245
+ return Object.values(allowed).includes(to);
4246
+ }
4247
+ var LEARNINGS_TAIL_LINES = 20;
4248
+ var GoalState = class {
4249
+ dir;
4250
+ constructor(workspaceDir, sessionTag) {
4251
+ const suffix = sessionTag ? `-${sessionTag}` : "";
4252
+ this.dir = join8(workspaceDir, `.minimal-agent${suffix}`);
4253
+ }
4254
+ async reset() {
4255
+ const files = ["goal.md", "completion.md", "phase.md", "progress.md", "learnings.md", "decisions.md"];
4256
+ for (const f of files) {
4257
+ try {
4258
+ await unlink2(join8(this.dir, f));
4259
+ } catch {
4260
+ }
4261
+ }
4262
+ }
4263
+ async init(goal, criteria) {
4264
+ await mkdir6(this.dir, { recursive: true });
4265
+ const files = {
4266
+ goal: `# \u4E0D\u53EF\u53D8\u76EE\u6807 (IMMUTABLE GOAL)
4267
+
4268
+ ${goal}
4269
+ `,
4270
+ completion: JSON.stringify(criteria, null, 2),
4271
+ phase: "plan" /* PLAN */,
4272
+ progress: "",
4273
+ learnings: "",
4274
+ decisions: ""
4275
+ };
4276
+ for (const [name, content] of Object.entries(files)) {
4277
+ const path2 = join8(this.dir, `${name}.md`);
4278
+ if (!existsSync6(path2)) {
4279
+ await writeFile5(path2, content);
4280
+ }
4281
+ }
4282
+ }
4283
+ get goal() {
4284
+ try {
4285
+ return readFileSync2(join8(this.dir, "goal.md"), "utf8").trim();
4286
+ } catch {
4287
+ return "";
4288
+ }
4289
+ }
4290
+ get completionCriteria() {
4291
+ try {
4292
+ const raw = readFileSync2(join8(this.dir, "completion.md"), "utf8").trim();
4293
+ return JSON.parse(raw);
4294
+ } catch {
4295
+ return [];
4296
+ }
4297
+ }
4298
+ get currentPhase() {
4299
+ try {
4300
+ const raw = readFileSync2(join8(this.dir, "phase.md"), "utf8").trim();
4301
+ if (VALID_PHASES.has(raw)) {
4302
+ return raw;
4303
+ }
4304
+ } catch {
4305
+ }
4306
+ return "plan" /* PLAN */;
4307
+ }
4308
+ /**
4309
+ * 切换阶段。`reason` 是给人看的日志文本,不参与校验。
4310
+ * 校验只看 from→to 是否在 PHASE_TRANSITIONS 表里有路径(任意 event 通向 to 即合法)。
4311
+ * 想绕开 FSM 用 forceSetPhase。
4312
+ */
4313
+ async setPhase(phase, reason) {
4314
+ if (!VALID_PHASES.has(phase)) {
4315
+ throw new Error(`Invalid phase: ${phase}`);
4316
+ }
4317
+ const current = this.currentPhase;
4318
+ if (!isLegalTransition(current, phase)) {
4319
+ throw new Error(
4320
+ `\u975E\u6CD5\u9636\u6BB5\u5207\u6362: ${current} \u2192 ${phase}\u3002\u8BE5\u76EE\u6807\u9636\u6BB5\u4E0D\u5728 PHASE_TRANSITIONS[${current}] \u7684\u53EF\u8FBE\u96C6\u5408\u5185\uFF0C\u9700\u8981\u8D70 forceSetPhase\u3002`
4321
+ );
4322
+ }
4323
+ await writeFile5(join8(this.dir, "phase.md"), phase);
4324
+ await this.appendProgress(`PHASE \u2192 ${phase}: ${reason}`);
4325
+ }
4326
+ async forceSetPhase(phase, reason) {
4327
+ if (!VALID_PHASES.has(phase)) {
4328
+ throw new Error(`Invalid phase: ${phase}`);
4329
+ }
4330
+ await writeFile5(join8(this.dir, "phase.md"), phase);
4331
+ await this.appendProgress(`PHASE \u2192 ${phase}: ${reason}`);
4332
+ }
4333
+ async appendProgress(line) {
4334
+ const timestamp = (/* @__PURE__ */ new Date()).toLocaleTimeString("en-US", { hour12: false });
4335
+ await appendFile(join8(this.dir, "progress.md"), `[${timestamp}] ${line}
4336
+ `);
4337
+ }
4338
+ tailProgress(n) {
4339
+ try {
4340
+ const content = readFileSync2(join8(this.dir, "progress.md"), "utf8");
4341
+ const lines = content.trim().split("\n").filter(Boolean);
4342
+ return lines.slice(-n).join("\n");
4343
+ } catch {
4344
+ return "";
4345
+ }
4346
+ }
4347
+ async appendLearning(lesson) {
4348
+ await appendFile(join8(this.dir, "learnings.md"), `- ${lesson}
4349
+ `);
4350
+ }
4351
+ get learnings() {
4352
+ try {
4353
+ const raw = readFileSync2(join8(this.dir, "learnings.md"), "utf8").trim();
4354
+ if (!raw) return "";
4355
+ const lines = raw.split("\n").filter(Boolean);
4356
+ return lines.slice(-LEARNINGS_TAIL_LINES).join("\n");
4357
+ } catch {
4358
+ return "";
4359
+ }
4360
+ }
4361
+ async recordDecision(ctx, options, chosen, reasoning) {
4362
+ const entry = {
4363
+ iteration: ctx.iteration,
4364
+ phase: ctx.phase,
4365
+ contextSummary: ctx.summary,
4366
+ options,
4367
+ chosen,
4368
+ reasoning
4369
+ };
4370
+ const line = JSON.stringify(entry);
4371
+ await appendFile(join8(this.dir, "decisions.md"), `${line}
4372
+ `);
4373
+ }
4374
+ findSimilarDecisions(ctx, k = 3) {
4375
+ try {
4376
+ const content = readFileSync2(join8(this.dir, "decisions.md"), "utf8");
4377
+ const lines = content.trim().split("\n").filter(Boolean);
4378
+ const entries = [];
4379
+ for (const line of lines) {
4380
+ try {
4381
+ entries.push(JSON.parse(line));
4382
+ } catch {
4383
+ }
4384
+ }
4385
+ const scored = entries.map((entry) => ({
4386
+ entry,
4387
+ score: this._similarity(entry.contextSummary, ctx.summary)
4388
+ })).sort((a, b) => b.score - a.score);
4389
+ return scored.slice(0, k).filter((s) => s.score > 0.3).map((s) => s.entry);
4390
+ } catch {
4391
+ return [];
4392
+ }
4393
+ }
4394
+ summarizeDecisions(maxEntries = 5) {
4395
+ try {
4396
+ const content = readFileSync2(join8(this.dir, "decisions.md"), "utf8");
4397
+ const lines = content.trim().split("\n").filter(Boolean);
4398
+ const entries = [];
4399
+ for (const line of lines.slice(-maxEntries * 2)) {
4400
+ try {
4401
+ entries.push(JSON.parse(line));
4402
+ } catch {
4403
+ }
4404
+ }
4405
+ return entries.slice(-maxEntries).map(
4406
+ (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)}`
4407
+ ).join("\n");
4408
+ } catch {
4409
+ return "(\u65E0\u51B3\u7B56\u8BB0\u5F55)";
4410
+ }
4411
+ }
4412
+ composeContext(iteration) {
4413
+ const sections = [];
4414
+ sections.push(`# \u4E0D\u53EF\u53D8\u76EE\u6807 (IMMUTABLE GOAL)
4415
+ ${this.goal}
4416
+ \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`);
4417
+ sections.push(`# \u5F53\u524D\u9636\u6BB5: ${this.currentPhase.toUpperCase()}`);
4418
+ const lrn = this.learnings;
4419
+ if (lrn) {
4420
+ sections.push(`# \u5173\u952E\u6559\u8BAD\uFF08\u5FC5\u987B\u9075\u5B88\uFF0C\u907F\u514D\u91CD\u590D\u8E29\u5751\uFF09
4421
+ ${lrn}`);
4422
+ }
4423
+ const decisions = this.summarizeDecisions(3);
4424
+ if (decisions !== "(\u65E0\u51B3\u7B56\u8BB0\u5F55)") {
4425
+ sections.push(`# \u5386\u53F2\u5173\u952E\u51B3\u7B56\uFF08\u8BF7\u53C2\u8003\uFF0C\u907F\u514D\u91CD\u590D\u8BD5\u9519\uFF09
4426
+ ${decisions}`);
4427
+ }
4428
+ const recentProgress = this.tailProgress(10);
4429
+ if (recentProgress) {
4430
+ sections.push(`# \u6700\u8FD1\u8FDB\u5EA6
4431
+ ${recentProgress}`);
4432
+ }
4433
+ sections.push(`---
4434
+
4435
+ # \u672C\u8F6E\u4EFB\u52A1 (\u8FED\u4EE3 ${iteration})`);
4436
+ return sections.join("\n\n---\n\n");
4437
+ }
4438
+ async cleanup() {
4439
+ const files = ["goal.md", "completion.md", "phase.md", "progress.md", "learnings.md", "decisions.md"];
4440
+ for (const f of files) {
4441
+ try {
4442
+ await unlink2(join8(this.dir, f));
4443
+ } catch {
4444
+ }
4445
+ }
4446
+ try {
4447
+ await rmdir2(this.dir);
4448
+ } catch {
4449
+ }
4450
+ }
4451
+ _similarity(a, b) {
4452
+ if (!a || !b) return 0;
4453
+ const wordsA = new Set(a.toLowerCase().split(/\s+/));
4454
+ const wordsB = new Set(b.toLowerCase().split(/\s+/));
4455
+ let intersection = 0;
4456
+ for (const word of wordsA) {
4457
+ if (wordsB.has(word)) intersection++;
4458
+ }
4459
+ const union = (/* @__PURE__ */ new Set([...wordsA, ...wordsB])).size;
4460
+ return union > 0 ? intersection / union : 0;
4461
+ }
4462
+ };
4463
+
3550
4464
  // src/context/microCompactLite.ts
3551
4465
  import { createHash as createHash2 } from "crypto";
3552
4466
  var MAX_REPEAT_COUNT = 3;
@@ -3782,6 +4696,179 @@ function previewArgs(rawJson) {
3782
4696
  return oneLine.slice(0, 60) + "...";
3783
4697
  }
3784
4698
 
4699
+ // src/plugins/pluginRunner.ts
4700
+ var DEFAULT_MAX_ITERATIONS = 50;
4701
+ var SAFETY_CEILING = 200;
4702
+ function extractMaxIterations(args) {
4703
+ const match = args.match(/--max-iterations\s+(\d+)/i);
4704
+ return match ? parseInt(match[1], 10) : void 0;
4705
+ }
4706
+ async function* runWithPlugins(userInput, options) {
4707
+ const { provider, history, signal } = options;
4708
+ const isSlashCommand = userInput.trimStart().startsWith("/");
4709
+ let currentInput = userInput;
4710
+ let matchedCmd = null;
4711
+ if (isSlashCommand) {
4712
+ await discoverPlugins();
4713
+ matchedCmd = resolveCommand(userInput);
4714
+ if (matchedCmd) {
4715
+ currentInput = buildCommandInput(matchedCmd);
4716
+ }
4717
+ }
4718
+ const activeHookPlugins = matchedCmd ? getActiveStopHookPlugins() : [];
4719
+ const enterLoop = matchedCmd !== null && activeHookPlugins.length > 0;
4720
+ if (!enterLoop) {
4721
+ yield* runQuery(currentInput, {
4722
+ provider,
4723
+ history,
4724
+ signal,
4725
+ maxTurns: options.maxTurns,
4726
+ sessionState: options.sessionState
4727
+ });
4728
+ return;
4729
+ }
4730
+ const cmd = matchedCmd.cmd;
4731
+ const maxIter = Math.min(
4732
+ extractMaxIterations(currentInput) ?? DEFAULT_MAX_ITERATIONS,
4733
+ SAFETY_CEILING
4734
+ );
4735
+ yield {
4736
+ type: "plugin_start",
4737
+ pluginName: cmd.pluginName,
4738
+ description: cmd.description
4739
+ };
4740
+ const checks = parseVerifyArgs(matchedCmd.arguments);
4741
+ const goalState = new GoalState(getWorkingDir(), cmd.pluginName);
4742
+ await goalState.reset();
4743
+ await goalState.init(currentInput, checks);
4744
+ await goalState.appendProgress(`=== Loop \u542F\u52A8 === \u76EE\u6807: ${currentInput.slice(0, 120)}...`);
4745
+ const baseHistory = history.slice();
4746
+ let iterationCount = 0;
4747
+ let consecutiveFailures = 0;
4748
+ let finalAssistantMsg = null;
4749
+ try {
4750
+ do {
4751
+ iterationCount++;
4752
+ if (iterationCount > maxIter) {
4753
+ await goalState.forceSetPhase("done" /* DONE */, `\u8FBE\u5230\u8FED\u4EE3\u4E0A\u9650 ${maxIter}`);
4754
+ 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`);
4755
+ yield { type: "error", error: `Loop \u5DF2\u8FBE\u8FED\u4EE3\u4E0A\u9650 ${maxIter}\uFF0C\u81EA\u52A8\u505C\u6B62` };
4756
+ return;
4757
+ }
4758
+ if (signal?.aborted) {
4759
+ yield { type: "interrupted" };
4760
+ return;
4761
+ }
4762
+ yield {
4763
+ type: "plugin_iteration",
4764
+ pluginName: cmd.pluginName,
4765
+ current: iterationCount,
4766
+ max: maxIter
4767
+ };
4768
+ history.length = 0;
4769
+ history.push(...baseHistory);
4770
+ const freshContext = goalState.composeContext(iterationCount);
4771
+ const enhancedInput = `${freshContext}
4772
+
4773
+ ${currentInput}`;
4774
+ yield* runQuery(enhancedInput, {
4775
+ provider,
4776
+ history,
4777
+ signal,
4778
+ maxTurns: options.maxTurns,
4779
+ sessionState: options.sessionState
4780
+ });
4781
+ if (signal?.aborted) {
4782
+ yield { type: "interrupted" };
4783
+ return;
4784
+ }
4785
+ const lastAssistantIdx = (() => {
4786
+ for (let i = history.length - 1; i >= 0; i--) {
4787
+ if (history[i].role === "assistant") return i;
4788
+ }
4789
+ return -1;
4790
+ })();
4791
+ finalAssistantMsg = lastAssistantIdx >= 0 ? history[lastAssistantIdx] : null;
4792
+ const lastAssistantText = finalAssistantMsg ? typeof finalAssistantMsg.content === "string" ? finalAssistantMsg.content : JSON.stringify(finalAssistantMsg.content) : "";
4793
+ const hasCompleteSentinel = /<promise>(?:COMPLETE|DONE|GOAL_COMPLETE)<\/promise>/i.test(lastAssistantText);
4794
+ const hasNeedReplan = /<PROMISE>NEED_REPLAN<\/PROMISE>/i.test(lastAssistantText);
4795
+ if (hasCompleteSentinel) {
4796
+ await goalState.forceSetPhase("verify" /* VERIFY */, "\u68C0\u6D4B\u5230\u5B8C\u6210\u54E8\u5175\uFF0C\u8FDB\u5165\u9A8C\u8BC1");
4797
+ await goalState.appendProgress(`\u8FED\u4EE3 ${iterationCount}: \u68C0\u6D4B\u5230\u5B8C\u6210\u54E8\u5175\uFF0C\u8FD0\u884C\u9A8C\u8BC1\u95E8...`);
4798
+ if (checks.length > 0) {
4799
+ const vResult = await runVerification(checks);
4800
+ if (!vResult.passed) {
4801
+ consecutiveFailures++;
4802
+ await goalState.appendLearning(
4803
+ `[\u8FED\u4EE3 ${iterationCount}] \u58F0\u79F0\u5B8C\u6210\u4F46\u9A8C\u8BC1\u672A\u901A\u8FC7: ${vResult.summary}`
4804
+ );
4805
+ yield {
4806
+ type: "error",
4807
+ error: `\u26A0\uFE0F \u9A8C\u8BC1\u672A\u901A\u8FC7: ${vResult.summary}\u3002\u7EE7\u7EED\u5C1D\u8BD5...`
4808
+ };
4809
+ if (consecutiveFailures >= 3) {
4810
+ await goalState.forceSetPhase(
4811
+ "heal" /* HEAL */,
4812
+ `\u8FDE\u7EED ${consecutiveFailures} \u6B21\u9A8C\u8BC1\u5931\u8D25`
4813
+ );
4814
+ } else {
4815
+ await goalState.setPhase("build" /* BUILD */, "\u9A8C\u8BC1\u672A\u901A\u8FC7\uFF0C\u8FD4\u56DE\u6784\u5EFA");
4816
+ }
4817
+ continue;
4818
+ }
4819
+ await goalState.appendProgress(`\u2705 \u9A8C\u8BC1\u901A\u8FC7: ${vResult.summary}`);
4820
+ }
4821
+ await goalState.setPhase("done" /* DONE */, "goal complete & verified");
4822
+ yield {
4823
+ type: "plugin_iteration",
4824
+ pluginName: cmd.pluginName,
4825
+ current: iterationCount,
4826
+ max: maxIter
4827
+ };
4828
+ return;
4829
+ }
4830
+ if (hasNeedReplan) {
4831
+ await goalState.forceSetPhase("plan" /* PLAN */, "agent \u8BF7\u6C42\u91CD\u65B0\u89C4\u5212");
4832
+ await goalState.appendLearning("[NEED_REPLAN] Agent \u8BA4\u4E3A\u5F53\u524D\u65B9\u6848\u4E0D\u53EF\u884C\uFF0C\u9700\u8981\u91CD\u65B0\u89C4\u5212");
4833
+ await goalState.appendProgress("Agent \u8BF7\u6C42 NEED_REPLAN\uFF0C\u56DE PLAN \u9636\u6BB5");
4834
+ consecutiveFailures = 0;
4835
+ continue;
4836
+ }
4837
+ if (goalState.currentPhase === "plan" /* PLAN */ && iterationCount >= 2) {
4838
+ await goalState.setPhase("build" /* BUILD */, "\u89C4\u5212\u9636\u6BB5\u5DF2\u5B8C\u6210\uFF0C\u8FDB\u5165\u6784\u5EFA");
4839
+ }
4840
+ const hookTranscript = lastAssistantText;
4841
+ const hookResult = await executeStopHooks(activeHookPlugins, hookTranscript);
4842
+ if (hookResult.decision === "block" && hookResult.reason) {
4843
+ currentInput = hookResult.reason;
4844
+ consecutiveFailures = 0;
4845
+ await goalState.recordDecision(
4846
+ { iteration: iterationCount, phase: goalState.currentPhase, summary: "stop-hook \u53CD\u9988" },
4847
+ ["\u7EE7\u7EED\u5FAA\u73AF", "\u7EC8\u6B62"],
4848
+ "\u7EE7\u7EED\u5FAA\u73AF",
4849
+ hookResult.reason.slice(0, 200)
4850
+ );
4851
+ if (hookResult.systemMessage) {
4852
+ baseHistory.push({
4853
+ role: "user",
4854
+ content: `[Plugin Stop Hook] ${hookResult.systemMessage}`
4855
+ });
4856
+ }
4857
+ await goalState.appendProgress(`\u8FED\u4EE3 ${iterationCount}: Stop hook block\uFF0C\u6CE8\u5165\u53CD\u9988\u7EE7\u7EED`);
4858
+ } else {
4859
+ await goalState.appendProgress(`\u8FED\u4EE3 ${iterationCount}: \u65E0\u54E8\u5175 / hook pass\uFF0C\u7EE7\u7EED\u4E0B\u4E00\u8F6E`);
4860
+ }
4861
+ } while (true);
4862
+ } finally {
4863
+ history.length = 0;
4864
+ history.push(...baseHistory);
4865
+ if (finalAssistantMsg) {
4866
+ history.push(finalAssistantMsg);
4867
+ }
4868
+ await goalState.cleanup();
4869
+ }
4870
+ }
4871
+
3785
4872
  // src/ui/hooks/useChat.ts
3786
4873
  function useChat(args) {
3787
4874
  const historyRef = useRef3(args.initialHistory.slice());
@@ -3793,6 +4880,7 @@ function useChat(args) {
3793
4880
  const [error, setError] = useState5(null);
3794
4881
  const [interrupted, setInterrupted] = useState5(false);
3795
4882
  const [compacting, setCompacting] = useState5(false);
4883
+ const [pluginLoop, setPluginLoop] = useState5(null);
3796
4884
  const abortRef = useRef3(null);
3797
4885
  useEffect(() => {
3798
4886
  return () => {
@@ -3808,10 +4896,11 @@ function useChat(args) {
3808
4896
  setError(null);
3809
4897
  setInterrupted(false);
3810
4898
  setStreamingText("");
4899
+ setPluginLoop(null);
3811
4900
  const ac = new AbortController();
3812
4901
  abortRef.current = ac;
3813
4902
  try {
3814
- for await (const ev of runQuery(trimmed, {
4903
+ for await (const ev of runWithPlugins(trimmed, {
3815
4904
  provider: args.provider,
3816
4905
  history: historyRef.current,
3817
4906
  signal: ac.signal
@@ -3822,6 +4911,7 @@ function useChat(args) {
3822
4911
  setCompacting,
3823
4912
  setError,
3824
4913
  setInterrupted,
4914
+ setPluginLoop,
3825
4915
  bump
3826
4916
  });
3827
4917
  }
@@ -3832,6 +4922,7 @@ function useChat(args) {
3832
4922
  setStreamingText("");
3833
4923
  setToolStatus(null);
3834
4924
  setCompacting(false);
4925
+ setPluginLoop(null);
3835
4926
  abortRef.current = null;
3836
4927
  args.onPersist?.(historyRef.current);
3837
4928
  }
@@ -3887,6 +4978,7 @@ function useChat(args) {
3887
4978
  error,
3888
4979
  interrupted,
3889
4980
  compacting,
4981
+ pluginLoop,
3890
4982
  submit,
3891
4983
  abort,
3892
4984
  clearHistory,
@@ -3934,6 +5026,16 @@ function handleEvent(ev, setters) {
3934
5026
  setters.setError(ev.error);
3935
5027
  setters.bump();
3936
5028
  break;
5029
+ case "plugin_start":
5030
+ break;
5031
+ case "plugin_iteration":
5032
+ setters.setPluginLoop({
5033
+ pluginName: ev.pluginName,
5034
+ current: ev.current,
5035
+ max: ev.max ?? 0
5036
+ });
5037
+ setters.bump();
5038
+ break;
3937
5039
  }
3938
5040
  }
3939
5041
 
@@ -3982,7 +5084,7 @@ function App({ provider, initialHistory }) {
3982
5084
  onCompact: chat2.compactNow
3983
5085
  }
3984
5086
  ),
3985
- /* @__PURE__ */ jsx6(StatusLine, { provider, history: chat2.history })
5087
+ /* @__PURE__ */ jsx6(StatusLine, { provider, history: chat2.history, pluginLoop: chat2.pluginLoop })
3986
5088
  ] });
3987
5089
  }
3988
5090
 
@@ -4041,8 +5143,28 @@ function truncateForDisplay(content, max = TOOL_OUTPUT_PREVIEW_MAX) {
4041
5143
  return content.slice(0, max) + "...";
4042
5144
  }
4043
5145
  function extractPromptArgs(args) {
4044
- const FLAGS = /* @__PURE__ */ new Set(["-p", "--print", "--verbose", "-v", "-h", "--help"]);
4045
- return args.filter((a) => !FLAGS.has(a));
5146
+ const FLAG_BOOLEAN = /* @__PURE__ */ new Set([
5147
+ "-p",
5148
+ "--print",
5149
+ "--verbose",
5150
+ "-v",
5151
+ "-h",
5152
+ "--help",
5153
+ "-V",
5154
+ "--version"
5155
+ ]);
5156
+ const FLAG_WITH_VALUE = /* @__PURE__ */ new Set(["-d", "--cwd"]);
5157
+ const result = [];
5158
+ for (let i = 0; i < args.length; i++) {
5159
+ const a = args[i];
5160
+ if (FLAG_BOOLEAN.has(a)) continue;
5161
+ if (FLAG_WITH_VALUE.has(a)) {
5162
+ i++;
5163
+ continue;
5164
+ }
5165
+ result.push(a);
5166
+ }
5167
+ return result;
4046
5168
  }
4047
5169
  async function runPrintMode(provider, args, initialHistory, options) {
4048
5170
  process.stdout.on("error", handleEPIPE(process.stdout));
@@ -4073,7 +5195,7 @@ async function runPrintMode(provider, args, initialHistory, options) {
4073
5195
  const history = initialHistory;
4074
5196
  const output = { buffer: "" };
4075
5197
  try {
4076
- for await (const event of runQuery(prompt, {
5198
+ for await (const event of runWithPlugins(prompt, {
4077
5199
  provider,
4078
5200
  history,
4079
5201
  signal: abortController.signal
@@ -4142,13 +5264,13 @@ function handleEvent2(event, output, verbose) {
4142
5264
  }
4143
5265
  }
4144
5266
  function readFromStdin() {
4145
- return new Promise((resolve10) => {
5267
+ return new Promise((resolve9) => {
4146
5268
  let data = "";
4147
5269
  let settled = false;
4148
5270
  const timer = setTimeout(() => {
4149
5271
  if (!settled) {
4150
5272
  settled = true;
4151
- resolve10("");
5273
+ resolve9("");
4152
5274
  }
4153
5275
  }, STDIN_TIMEOUT_MS);
4154
5276
  process.stdin.setEncoding("utf8");
@@ -4159,7 +5281,7 @@ function readFromStdin() {
4159
5281
  if (!settled) {
4160
5282
  clearTimeout(timer);
4161
5283
  settled = true;
4162
- resolve10(data.trim());
5284
+ resolve9(data.trim());
4163
5285
  }
4164
5286
  }
4165
5287
  process.stdin.on("data", onData);
@@ -4172,9 +5294,17 @@ import { jsx as jsx8 } from "react/jsx-runtime";
4172
5294
  var require2 = createRequire(import.meta.url);
4173
5295
  var pkg = require2("../package.json");
4174
5296
  async function main() {
5297
+ const args = process.argv.slice(2);
5298
+ const dirArg = extractCwdArg(args);
5299
+ if (dirArg) {
5300
+ const abs = resolve8(dirArg);
5301
+ if (!existsSync7(abs)) {
5302
+ mkdirSync(abs, { recursive: true });
5303
+ }
5304
+ process.chdir(abs);
5305
+ }
4175
5306
  initWorkingDir();
4176
5307
  await migrateLegacyContext(getWorkingDir());
4177
- const args = process.argv.slice(2);
4178
5308
  if (args.includes("-h") || args.includes("--help")) {
4179
5309
  printHelp();
4180
5310
  return;
@@ -4185,15 +5315,15 @@ async function main() {
4185
5315
  }
4186
5316
  const isPrintMode = args.includes("-p") || args.includes("--print");
4187
5317
  if (isPrintMode) {
4188
- let provider;
4189
- try {
4190
- provider = await loadProvider();
4191
- } catch (e) {
5318
+ const provider = await loadProviderLayered();
5319
+ if (!provider) {
4192
5320
  process.stderr.write(
4193
5321
  `
4194
- ${e.message}
5322
+ \u672A\u627E\u5230 provider \u914D\u7F6E\u3002
4195
5323
 
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
5324
+ \u8BF7\u4E8C\u9009\u4E00\uFF1A
5325
+ 1. \u8BBE\u7F6E\u73AF\u5883\u53D8\u91CF MINIMAL_AGENT_BASE_URL / MINIMAL_AGENT_API_KEY / MINIMAL_AGENT_MODEL
5326
+ 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
5327
 
4198
5328
  `
4199
5329
  );
@@ -4227,6 +5357,8 @@ minimal-agent - \u8F7B\u91CF\u7EA7 AI \u7F16\u7A0B\u52A9\u624B
4227
5357
  \u9009\u9879:
4228
5358
  -p, --print \u975E\u4EA4\u4E92\u6A21\u5F0F\uFF0C\u76F4\u63A5\u6267\u884C\u5355\u6B21\u95EE\u7B54
4229
5359
  -v, --verbose \u663E\u793A\u8BE6\u7EC6\u8F93\u51FA\uFF08\u5DE5\u5177\u8C03\u7528\u3001\u538B\u7F29\u4FE1\u606F\uFF09
5360
+ -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
5361
+ \u4E0A\u4E0B\u6587\u6587\u4EF6\u3001\u5DE5\u5177\u76F8\u5BF9\u8DEF\u5F84\u3001.env \u52A0\u8F7D\u90FD\u4EE5\u6B64\u4E3A\u57FA\u51C6
4230
5362
  -h, --help \u663E\u793A\u5E2E\u52A9\u4FE1\u606F
4231
5363
 
4232
5364
  \u4F1A\u8BDD\u8BB0\u5FC6:
@@ -4238,6 +5370,7 @@ minimal-agent - \u8F7B\u91CF\u7EA7 AI \u7F16\u7A0B\u52A9\u624B
4238
5370
  minimal-agent -p "\u5E2E\u6211\u5199\u4E00\u4E2A hello world"
4239
5371
  echo "\u89E3\u91CA\u4EE3\u7801" | minimal-agent -p
4240
5372
  minimal-agent -p --verbose "\u8FD0\u884C\u6D4B\u8BD5\u5E76\u62A5\u544A\u7ED3\u679C"
5373
+ minimal-agent -p "\u5904\u7406\u8D44\u6599" -d /tmp/job-123 # \u5DE5\u4F5C\u76EE\u5F55\u9694\u79BB
4241
5374
  `);
4242
5375
  }
4243
5376
  main().catch((e) => {