minimal-agent 0.1.4 → 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/README.md +22 -1
- package/dist/main.js +1285 -138
- package/package.json +1 -1
- package/skills/docx/scripts/office/helpers/__pycache__/__init__.cpython-314.pyc +0 -0
- package/skills/docx/scripts/office/helpers/__pycache__/merge_runs.cpython-314.pyc +0 -0
- package/skills/docx/scripts/office/helpers/__pycache__/simplify_redlines.cpython-314.pyc +0 -0
- package/skills/docx/scripts/office/validators/__pycache__/__init__.cpython-314.pyc +0 -0
- package/skills/docx/scripts/office/validators/__pycache__/base.cpython-314.pyc +0 -0
- package/skills/docx/scripts/office/validators/__pycache__/docx.cpython-314.pyc +0 -0
- package/skills/docx/scripts/office/validators/__pycache__/pptx.cpython-314.pyc +0 -0
- package/skills/docx/scripts/office/validators/__pycache__/redlining.cpython-314.pyc +0 -0
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
|
|
201
|
+
import { join as join4 } from "path";
|
|
202
202
|
var FILENAME = "minimal-agent.md";
|
|
203
203
|
async function loadProjectInstructions(cwd) {
|
|
204
|
-
const filePath =
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
735
|
-
if (!
|
|
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
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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 {
|
|
1194
|
-
import {
|
|
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
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
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 (
|
|
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
|
-
|
|
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 ${
|
|
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
|
|
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
|
|
2110
|
-
|
|
2111
|
-
|
|
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
|
-
|
|
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 ${
|
|
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
|
|
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 =
|
|
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);
|
|
@@ -3480,9 +3712,12 @@ function findHeadCutpoint(messages, proposedCut) {
|
|
|
3480
3712
|
}
|
|
3481
3713
|
|
|
3482
3714
|
// src/context/reactiveCompact.ts
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3715
|
+
function createReactiveCompactState() {
|
|
3716
|
+
return { attempted: false };
|
|
3717
|
+
}
|
|
3718
|
+
var defaultState = createReactiveCompactState();
|
|
3719
|
+
function resetReactiveCompactState(state = defaultState) {
|
|
3720
|
+
state.attempted = false;
|
|
3486
3721
|
}
|
|
3487
3722
|
function isPromptTooLongError(error) {
|
|
3488
3723
|
const msg = errorMessage(error).toLowerCase();
|
|
@@ -3499,18 +3734,18 @@ function errorMessage(error) {
|
|
|
3499
3734
|
}
|
|
3500
3735
|
return String(error ?? "");
|
|
3501
3736
|
}
|
|
3502
|
-
async function reactiveCompactIfApplicable(messages, provider, error) {
|
|
3737
|
+
async function reactiveCompactIfApplicable(messages, provider, error, state = defaultState) {
|
|
3503
3738
|
if (!isPromptTooLongError(error)) {
|
|
3504
3739
|
return { recovered: false, messages, reason: "not a prompt-too-long error" };
|
|
3505
3740
|
}
|
|
3506
|
-
if (
|
|
3741
|
+
if (state.attempted) {
|
|
3507
3742
|
return {
|
|
3508
3743
|
recovered: false,
|
|
3509
3744
|
messages,
|
|
3510
3745
|
reason: "already attempted this session \u2014 use /new or /compact manually"
|
|
3511
3746
|
};
|
|
3512
3747
|
}
|
|
3513
|
-
|
|
3748
|
+
state.attempted = true;
|
|
3514
3749
|
try {
|
|
3515
3750
|
const r = await forceCompact(messages, provider);
|
|
3516
3751
|
return {
|
|
@@ -3544,6 +3779,688 @@ async function reactiveCompactIfApplicable(messages, provider, error) {
|
|
|
3544
3779
|
};
|
|
3545
3780
|
}
|
|
3546
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
|
+
|
|
3547
4464
|
// src/context/microCompactLite.ts
|
|
3548
4465
|
import { createHash as createHash2 } from "crypto";
|
|
3549
4466
|
var MAX_REPEAT_COUNT = 3;
|
|
@@ -3552,10 +4469,12 @@ var HEAD_KEEP_CHARS = 2e3;
|
|
|
3552
4469
|
var TAIL_KEEP_CHARS = 1e3;
|
|
3553
4470
|
var MAX_KEEP_ROUNDS = 10;
|
|
3554
4471
|
var SHORT_CONTENT_THRESHOLD = 200;
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
4472
|
+
function createMicroCompactState() {
|
|
4473
|
+
return { turn: 0, cache: /* @__PURE__ */ new Map() };
|
|
4474
|
+
}
|
|
4475
|
+
var defaultState2 = createMicroCompactState();
|
|
4476
|
+
function incrementTurn(state = defaultState2) {
|
|
4477
|
+
state.turn++;
|
|
3559
4478
|
}
|
|
3560
4479
|
var COMPRESSIBLE_TOOLS = /* @__PURE__ */ new Set([
|
|
3561
4480
|
"Grep",
|
|
@@ -3563,7 +4482,7 @@ var COMPRESSIBLE_TOOLS = /* @__PURE__ */ new Set([
|
|
|
3563
4482
|
"WebFetch",
|
|
3564
4483
|
"Glob"
|
|
3565
4484
|
]);
|
|
3566
|
-
function microCompact(toolName, content) {
|
|
4485
|
+
function microCompact(toolName, content, state = defaultState2) {
|
|
3567
4486
|
if (!content) return content;
|
|
3568
4487
|
if (content.startsWith("Error:") || content.startsWith("\u9519\u8BEF")) {
|
|
3569
4488
|
return content;
|
|
@@ -3572,7 +4491,7 @@ function microCompact(toolName, content) {
|
|
|
3572
4491
|
return content;
|
|
3573
4492
|
}
|
|
3574
4493
|
const hash = sha1(content);
|
|
3575
|
-
const existing =
|
|
4494
|
+
const existing = state.cache.get(hash);
|
|
3576
4495
|
if (existing) {
|
|
3577
4496
|
existing.count++;
|
|
3578
4497
|
if (existing.count > MAX_REPEAT_COUNT) {
|
|
@@ -3580,17 +4499,17 @@ function microCompact(toolName, content) {
|
|
|
3580
4499
|
}
|
|
3581
4500
|
return content;
|
|
3582
4501
|
}
|
|
3583
|
-
|
|
4502
|
+
state.cache.set(hash, { count: 1, firstToolName: toolName, firstSeenTurn: state.turn });
|
|
3584
4503
|
if (content.length > MAX_RESULT_SIZE && COMPRESSIBLE_TOOLS.has(toolName)) {
|
|
3585
4504
|
return truncateContent(content);
|
|
3586
4505
|
}
|
|
3587
4506
|
return content;
|
|
3588
4507
|
}
|
|
3589
|
-
function expireOldEntries() {
|
|
4508
|
+
function expireOldEntries(state = defaultState2) {
|
|
3590
4509
|
let expired = 0;
|
|
3591
|
-
for (const [hash, entry] of
|
|
3592
|
-
if (
|
|
3593
|
-
|
|
4510
|
+
for (const [hash, entry] of state.cache) {
|
|
4511
|
+
if (state.turn - entry.firstSeenTurn > MAX_KEEP_ROUNDS) {
|
|
4512
|
+
state.cache.delete(hash);
|
|
3594
4513
|
expired++;
|
|
3595
4514
|
}
|
|
3596
4515
|
}
|
|
@@ -3610,15 +4529,15 @@ function truncateContent(content) {
|
|
|
3610
4529
|
|
|
3611
4530
|
// src/loop.ts
|
|
3612
4531
|
async function* runQuery(userInput, options) {
|
|
3613
|
-
const { provider, history, signal } = options;
|
|
4532
|
+
const { provider, history, signal, sessionState } = options;
|
|
3614
4533
|
const maxTurns = options.maxTurns ?? 50;
|
|
3615
4534
|
history.push({ role: "user", content: userInput });
|
|
3616
4535
|
let reactiveAttempted = false;
|
|
3617
4536
|
let turn = 0;
|
|
3618
4537
|
while (turn < maxTurns) {
|
|
3619
4538
|
turn++;
|
|
3620
|
-
incrementTurn();
|
|
3621
|
-
expireOldEntries();
|
|
4539
|
+
incrementTurn(sessionState?.microCompact);
|
|
4540
|
+
expireOldEntries(sessionState?.microCompact);
|
|
3622
4541
|
if (signal?.aborted) {
|
|
3623
4542
|
history.push({
|
|
3624
4543
|
role: "user",
|
|
@@ -3680,7 +4599,12 @@ async function* runQuery(userInput, options) {
|
|
|
3680
4599
|
if (isPromptTooLongError(e) && !reactiveAttempted) {
|
|
3681
4600
|
reactiveAttempted = true;
|
|
3682
4601
|
yield { type: "compact_start" };
|
|
3683
|
-
const result = await reactiveCompactIfApplicable(
|
|
4602
|
+
const result = await reactiveCompactIfApplicable(
|
|
4603
|
+
history,
|
|
4604
|
+
provider,
|
|
4605
|
+
e,
|
|
4606
|
+
sessionState?.reactive
|
|
4607
|
+
);
|
|
3684
4608
|
if (result.recovered) {
|
|
3685
4609
|
history.length = 0;
|
|
3686
4610
|
history.push(...result.messages);
|
|
@@ -3728,7 +4652,11 @@ async function* runQuery(userInput, options) {
|
|
|
3728
4652
|
};
|
|
3729
4653
|
const result = await executeTool(tc.function.name, tc.function.arguments, signal);
|
|
3730
4654
|
const rawContent = result.ok ? result.content : `Error: ${result.error}`;
|
|
3731
|
-
const content = microCompact(
|
|
4655
|
+
const content = microCompact(
|
|
4656
|
+
tc.function.name,
|
|
4657
|
+
rawContent,
|
|
4658
|
+
sessionState?.microCompact
|
|
4659
|
+
);
|
|
3732
4660
|
history.push({
|
|
3733
4661
|
role: "tool",
|
|
3734
4662
|
content,
|
|
@@ -3768,6 +4696,179 @@ function previewArgs(rawJson) {
|
|
|
3768
4696
|
return oneLine.slice(0, 60) + "...";
|
|
3769
4697
|
}
|
|
3770
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
|
+
|
|
3771
4872
|
// src/ui/hooks/useChat.ts
|
|
3772
4873
|
function useChat(args) {
|
|
3773
4874
|
const historyRef = useRef3(args.initialHistory.slice());
|
|
@@ -3779,6 +4880,7 @@ function useChat(args) {
|
|
|
3779
4880
|
const [error, setError] = useState5(null);
|
|
3780
4881
|
const [interrupted, setInterrupted] = useState5(false);
|
|
3781
4882
|
const [compacting, setCompacting] = useState5(false);
|
|
4883
|
+
const [pluginLoop, setPluginLoop] = useState5(null);
|
|
3782
4884
|
const abortRef = useRef3(null);
|
|
3783
4885
|
useEffect(() => {
|
|
3784
4886
|
return () => {
|
|
@@ -3794,10 +4896,11 @@ function useChat(args) {
|
|
|
3794
4896
|
setError(null);
|
|
3795
4897
|
setInterrupted(false);
|
|
3796
4898
|
setStreamingText("");
|
|
4899
|
+
setPluginLoop(null);
|
|
3797
4900
|
const ac = new AbortController();
|
|
3798
4901
|
abortRef.current = ac;
|
|
3799
4902
|
try {
|
|
3800
|
-
for await (const ev of
|
|
4903
|
+
for await (const ev of runWithPlugins(trimmed, {
|
|
3801
4904
|
provider: args.provider,
|
|
3802
4905
|
history: historyRef.current,
|
|
3803
4906
|
signal: ac.signal
|
|
@@ -3808,6 +4911,7 @@ function useChat(args) {
|
|
|
3808
4911
|
setCompacting,
|
|
3809
4912
|
setError,
|
|
3810
4913
|
setInterrupted,
|
|
4914
|
+
setPluginLoop,
|
|
3811
4915
|
bump
|
|
3812
4916
|
});
|
|
3813
4917
|
}
|
|
@@ -3818,6 +4922,7 @@ function useChat(args) {
|
|
|
3818
4922
|
setStreamingText("");
|
|
3819
4923
|
setToolStatus(null);
|
|
3820
4924
|
setCompacting(false);
|
|
4925
|
+
setPluginLoop(null);
|
|
3821
4926
|
abortRef.current = null;
|
|
3822
4927
|
args.onPersist?.(historyRef.current);
|
|
3823
4928
|
}
|
|
@@ -3873,6 +4978,7 @@ function useChat(args) {
|
|
|
3873
4978
|
error,
|
|
3874
4979
|
interrupted,
|
|
3875
4980
|
compacting,
|
|
4981
|
+
pluginLoop,
|
|
3876
4982
|
submit,
|
|
3877
4983
|
abort,
|
|
3878
4984
|
clearHistory,
|
|
@@ -3920,6 +5026,16 @@ function handleEvent(ev, setters) {
|
|
|
3920
5026
|
setters.setError(ev.error);
|
|
3921
5027
|
setters.bump();
|
|
3922
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;
|
|
3923
5039
|
}
|
|
3924
5040
|
}
|
|
3925
5041
|
|
|
@@ -3968,7 +5084,7 @@ function App({ provider, initialHistory }) {
|
|
|
3968
5084
|
onCompact: chat2.compactNow
|
|
3969
5085
|
}
|
|
3970
5086
|
),
|
|
3971
|
-
/* @__PURE__ */ jsx6(StatusLine, { provider, history: chat2.history })
|
|
5087
|
+
/* @__PURE__ */ jsx6(StatusLine, { provider, history: chat2.history, pluginLoop: chat2.pluginLoop })
|
|
3972
5088
|
] });
|
|
3973
5089
|
}
|
|
3974
5090
|
|
|
@@ -4027,8 +5143,28 @@ function truncateForDisplay(content, max = TOOL_OUTPUT_PREVIEW_MAX) {
|
|
|
4027
5143
|
return content.slice(0, max) + "...";
|
|
4028
5144
|
}
|
|
4029
5145
|
function extractPromptArgs(args) {
|
|
4030
|
-
const
|
|
4031
|
-
|
|
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;
|
|
4032
5168
|
}
|
|
4033
5169
|
async function runPrintMode(provider, args, initialHistory, options) {
|
|
4034
5170
|
process.stdout.on("error", handleEPIPE(process.stdout));
|
|
@@ -4059,7 +5195,7 @@ async function runPrintMode(provider, args, initialHistory, options) {
|
|
|
4059
5195
|
const history = initialHistory;
|
|
4060
5196
|
const output = { buffer: "" };
|
|
4061
5197
|
try {
|
|
4062
|
-
for await (const event of
|
|
5198
|
+
for await (const event of runWithPlugins(prompt, {
|
|
4063
5199
|
provider,
|
|
4064
5200
|
history,
|
|
4065
5201
|
signal: abortController.signal
|
|
@@ -4128,13 +5264,13 @@ function handleEvent2(event, output, verbose) {
|
|
|
4128
5264
|
}
|
|
4129
5265
|
}
|
|
4130
5266
|
function readFromStdin() {
|
|
4131
|
-
return new Promise((
|
|
5267
|
+
return new Promise((resolve9) => {
|
|
4132
5268
|
let data = "";
|
|
4133
5269
|
let settled = false;
|
|
4134
5270
|
const timer = setTimeout(() => {
|
|
4135
5271
|
if (!settled) {
|
|
4136
5272
|
settled = true;
|
|
4137
|
-
|
|
5273
|
+
resolve9("");
|
|
4138
5274
|
}
|
|
4139
5275
|
}, STDIN_TIMEOUT_MS);
|
|
4140
5276
|
process.stdin.setEncoding("utf8");
|
|
@@ -4145,7 +5281,7 @@ function readFromStdin() {
|
|
|
4145
5281
|
if (!settled) {
|
|
4146
5282
|
clearTimeout(timer);
|
|
4147
5283
|
settled = true;
|
|
4148
|
-
|
|
5284
|
+
resolve9(data.trim());
|
|
4149
5285
|
}
|
|
4150
5286
|
}
|
|
4151
5287
|
process.stdin.on("data", onData);
|
|
@@ -4158,9 +5294,17 @@ import { jsx as jsx8 } from "react/jsx-runtime";
|
|
|
4158
5294
|
var require2 = createRequire(import.meta.url);
|
|
4159
5295
|
var pkg = require2("../package.json");
|
|
4160
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
|
+
}
|
|
4161
5306
|
initWorkingDir();
|
|
4162
5307
|
await migrateLegacyContext(getWorkingDir());
|
|
4163
|
-
const args = process.argv.slice(2);
|
|
4164
5308
|
if (args.includes("-h") || args.includes("--help")) {
|
|
4165
5309
|
printHelp();
|
|
4166
5310
|
return;
|
|
@@ -4171,15 +5315,15 @@ async function main() {
|
|
|
4171
5315
|
}
|
|
4172
5316
|
const isPrintMode = args.includes("-p") || args.includes("--print");
|
|
4173
5317
|
if (isPrintMode) {
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
provider = await loadProvider();
|
|
4177
|
-
} catch (e) {
|
|
5318
|
+
const provider = await loadProviderLayered();
|
|
5319
|
+
if (!provider) {
|
|
4178
5320
|
process.stderr.write(
|
|
4179
5321
|
`
|
|
4180
|
-
|
|
5322
|
+
\u672A\u627E\u5230 provider \u914D\u7F6E\u3002
|
|
4181
5323
|
|
|
4182
|
-
\
|
|
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
|
|
4183
5327
|
|
|
4184
5328
|
`
|
|
4185
5329
|
);
|
|
@@ -4213,6 +5357,8 @@ minimal-agent - \u8F7B\u91CF\u7EA7 AI \u7F16\u7A0B\u52A9\u624B
|
|
|
4213
5357
|
\u9009\u9879:
|
|
4214
5358
|
-p, --print \u975E\u4EA4\u4E92\u6A21\u5F0F\uFF0C\u76F4\u63A5\u6267\u884C\u5355\u6B21\u95EE\u7B54
|
|
4215
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
|
|
4216
5362
|
-h, --help \u663E\u793A\u5E2E\u52A9\u4FE1\u606F
|
|
4217
5363
|
|
|
4218
5364
|
\u4F1A\u8BDD\u8BB0\u5FC6:
|
|
@@ -4224,6 +5370,7 @@ minimal-agent - \u8F7B\u91CF\u7EA7 AI \u7F16\u7A0B\u52A9\u624B
|
|
|
4224
5370
|
minimal-agent -p "\u5E2E\u6211\u5199\u4E00\u4E2A hello world"
|
|
4225
5371
|
echo "\u89E3\u91CA\u4EE3\u7801" | minimal-agent -p
|
|
4226
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
|
|
4227
5374
|
`);
|
|
4228
5375
|
}
|
|
4229
5376
|
main().catch((e) => {
|