minimal-agent 0.1.5 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -1
- package/dist/main.js +1792 -224
- package/package.json +2 -2
- 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 existsSync9, mkdirSync } from "fs";
|
|
5
6
|
import { createRequire } from "module";
|
|
7
|
+
import { resolve as resolve8 } from "path";
|
|
8
|
+
|
|
9
|
+
// src/bootstrap/cwdArg.ts
|
|
10
|
+
function extractCwdArg(argv) {
|
|
11
|
+
for (let i = 0; i < argv.length; i++) {
|
|
12
|
+
if (argv[i] === "-d" || argv[i] === "--cwd") {
|
|
13
|
+
return argv[i + 1] ?? null;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
6
18
|
|
|
7
19
|
// src/bootstrap/workingDir.ts
|
|
8
20
|
import { resolve } from "path";
|
|
@@ -57,42 +69,6 @@ async function saveConfig(cfg) {
|
|
|
57
69
|
|
|
58
70
|
// src/config.ts
|
|
59
71
|
var DEFAULT_CONTEXT_WINDOW = 128e3;
|
|
60
|
-
async function loadProvider() {
|
|
61
|
-
const baseURL = process.env.MINIMAL_AGENT_BASE_URL;
|
|
62
|
-
const apiKey = process.env.MINIMAL_AGENT_API_KEY;
|
|
63
|
-
const model = process.env.MINIMAL_AGENT_MODEL;
|
|
64
|
-
if (!baseURL || !apiKey || !model) {
|
|
65
|
-
const missing = [];
|
|
66
|
-
if (!baseURL) missing.push("MINIMAL_AGENT_BASE_URL");
|
|
67
|
-
if (!apiKey) missing.push("MINIMAL_AGENT_API_KEY");
|
|
68
|
-
if (!model) missing.push("MINIMAL_AGENT_MODEL");
|
|
69
|
-
throw new Error(
|
|
70
|
-
`\u7F3A\u5C11\u5FC5\u9700\u7684\u73AF\u5883\u53D8\u91CF\uFF1A${missing.join(", ")}
|
|
71
|
-
|
|
72
|
-
\u8BF7\u5728 .env \u4E2D\u914D\u7F6E\uFF1A
|
|
73
|
-
MINIMAL_AGENT_BASE_URL=https://api.example.com/v1
|
|
74
|
-
MINIMAL_AGENT_API_KEY=your-api-key
|
|
75
|
-
MINIMAL_AGENT_MODEL=your-model
|
|
76
|
-
|
|
77
|
-
\u53C2\u8003 .env.example`
|
|
78
|
-
);
|
|
79
|
-
}
|
|
80
|
-
const contextWindowRaw = process.env.MINIMAL_AGENT_CONTEXT_WINDOW;
|
|
81
|
-
let contextWindow = DEFAULT_CONTEXT_WINDOW;
|
|
82
|
-
if (contextWindowRaw) {
|
|
83
|
-
const n = parseInt(contextWindowRaw, 10);
|
|
84
|
-
if (!Number.isNaN(n) && n > 0) {
|
|
85
|
-
contextWindow = n;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
return {
|
|
89
|
-
name: process.env.MINIMAL_AGENT_PROVIDER ?? "env",
|
|
90
|
-
baseURL,
|
|
91
|
-
apiKey,
|
|
92
|
-
model,
|
|
93
|
-
contextWindow
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
72
|
async function loadProviderLayered() {
|
|
97
73
|
const envBaseURL = process.env.MINIMAL_AGENT_BASE_URL;
|
|
98
74
|
const envApiKey = process.env.MINIMAL_AGENT_API_KEY;
|
|
@@ -124,8 +100,8 @@ async function loadProviderLayered() {
|
|
|
124
100
|
}
|
|
125
101
|
|
|
126
102
|
// src/context/persistContext.ts
|
|
127
|
-
import { mkdir as mkdir3, readFile as readFile2, unlink, writeFile as writeFile2 } from "fs/promises";
|
|
128
|
-
import { dirname as dirname3 } from "path";
|
|
103
|
+
import { mkdir as mkdir3, readFile as readFile2, readdir, rmdir, unlink, writeFile as writeFile2 } from "fs/promises";
|
|
104
|
+
import { dirname as dirname3, join as join3 } from "path";
|
|
129
105
|
|
|
130
106
|
// src/context/sessionPath.ts
|
|
131
107
|
import { createHash } from "crypto";
|
|
@@ -191,6 +167,30 @@ async function clearContext(file) {
|
|
|
191
167
|
await unlink(target);
|
|
192
168
|
} catch {
|
|
193
169
|
}
|
|
170
|
+
const cwd = getWorkingDir();
|
|
171
|
+
let topEntries;
|
|
172
|
+
try {
|
|
173
|
+
topEntries = await readdir(cwd);
|
|
174
|
+
} catch {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const stateDirs = topEntries.filter(
|
|
178
|
+
(name) => name === ".minimal-agent" || name.startsWith(".minimal-agent-")
|
|
179
|
+
);
|
|
180
|
+
for (const name of stateDirs) {
|
|
181
|
+
const dir = join3(cwd, name);
|
|
182
|
+
try {
|
|
183
|
+
const entries = await readdir(dir);
|
|
184
|
+
for (const entry of entries) {
|
|
185
|
+
try {
|
|
186
|
+
await unlink(join3(dir, entry));
|
|
187
|
+
} catch {
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
await rmdir(dir);
|
|
191
|
+
} catch {
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
194
|
}
|
|
195
195
|
|
|
196
196
|
// src/prompts/system.ts
|
|
@@ -198,10 +198,10 @@ import { homedir as homedir3 } from "os";
|
|
|
198
198
|
|
|
199
199
|
// src/prompts/projectInstructions.ts
|
|
200
200
|
import { readFile as readFile3 } from "fs/promises";
|
|
201
|
-
import { join as
|
|
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);
|
|
@@ -462,6 +462,141 @@ function toToolParameters(schema) {
|
|
|
462
462
|
return rest;
|
|
463
463
|
}
|
|
464
464
|
|
|
465
|
+
// src/tools/bash/semantics.ts
|
|
466
|
+
var DEFAULT_SEMANTIC = (exitCode) => ({
|
|
467
|
+
isError: exitCode !== 0,
|
|
468
|
+
message: exitCode !== 0 ? `Command failed with exit code ${exitCode}` : void 0
|
|
469
|
+
});
|
|
470
|
+
var COMMAND_SEMANTICS = /* @__PURE__ */ new Map([
|
|
471
|
+
// grep: 0=找到匹配, 1=无匹配(非错误), 2+=真错误
|
|
472
|
+
["grep", (exitCode) => ({
|
|
473
|
+
isError: exitCode >= 2,
|
|
474
|
+
message: exitCode === 1 ? "No matches found" : void 0
|
|
475
|
+
})],
|
|
476
|
+
// ripgrep 与 grep 同义
|
|
477
|
+
["rg", (exitCode) => ({
|
|
478
|
+
isError: exitCode >= 2,
|
|
479
|
+
message: exitCode === 1 ? "No matches found" : void 0
|
|
480
|
+
})],
|
|
481
|
+
// find: 1=部分目录不可达(仍有结果,非致命), 2+=错误
|
|
482
|
+
["find", (exitCode) => ({
|
|
483
|
+
isError: exitCode >= 2,
|
|
484
|
+
message: exitCode === 1 ? "Some directories were inaccessible" : void 0
|
|
485
|
+
})],
|
|
486
|
+
// diff: 0=相同, 1=有差异(非错误), 2+=错误
|
|
487
|
+
["diff", (exitCode) => ({
|
|
488
|
+
isError: exitCode >= 2,
|
|
489
|
+
message: exitCode === 1 ? "Files differ" : void 0
|
|
490
|
+
})],
|
|
491
|
+
// test: 0=真, 1=假(非错误), 2+=错误
|
|
492
|
+
["test", (exitCode) => ({
|
|
493
|
+
isError: exitCode >= 2,
|
|
494
|
+
message: exitCode === 1 ? "Condition is false" : void 0
|
|
495
|
+
})],
|
|
496
|
+
// [ 是 test 的别名
|
|
497
|
+
["[", (exitCode) => ({
|
|
498
|
+
isError: exitCode >= 2,
|
|
499
|
+
message: exitCode === 1 ? "Condition is false" : void 0
|
|
500
|
+
})]
|
|
501
|
+
]);
|
|
502
|
+
function extractPrimaryCommand(command) {
|
|
503
|
+
let cmd = command.trim();
|
|
504
|
+
const wrapMatch = cmd.match(/^(?:bash|sh|zsh|dash)\s+-c\s+(['"])(.+)\1\s*$/s);
|
|
505
|
+
if (wrapMatch) cmd = wrapMatch[2].trim();
|
|
506
|
+
const segments = cmd.split(/\s*(?:\|\||&&|;|\|)\s*/).filter((s) => s.length > 0);
|
|
507
|
+
let last = segments[segments.length - 1] ?? cmd;
|
|
508
|
+
last = last.trim();
|
|
509
|
+
const tokens = last.split(/\s+/);
|
|
510
|
+
let i = 0;
|
|
511
|
+
if (tokens[i] === "env") i++;
|
|
512
|
+
while (i < tokens.length && /^[A-Za-z_][A-Za-z0-9_]*=/.test(tokens[i])) i++;
|
|
513
|
+
return tokens[i] ?? "";
|
|
514
|
+
}
|
|
515
|
+
function interpretCommandResult(command, exitCode, stdout, stderr) {
|
|
516
|
+
const base = extractPrimaryCommand(command);
|
|
517
|
+
const fn = COMMAND_SEMANTICS.get(base) ?? DEFAULT_SEMANTIC;
|
|
518
|
+
return fn(exitCode, stdout, stderr);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// src/tools/bash/warnings.ts
|
|
522
|
+
var DESTRUCTIVE_PATTERNS = [
|
|
523
|
+
// Git —— 数据丢失 / 难回退
|
|
524
|
+
{
|
|
525
|
+
pattern: /\bgit\s+reset\s+--hard\b/,
|
|
526
|
+
warning: "Note: may discard uncommitted changes"
|
|
527
|
+
},
|
|
528
|
+
{
|
|
529
|
+
pattern: /\bgit\s+push\b[^;&|\n]*[ \t](--force|--force-with-lease|-f)\b/,
|
|
530
|
+
warning: "Note: may overwrite remote history"
|
|
531
|
+
},
|
|
532
|
+
{
|
|
533
|
+
pattern: /\bgit\s+clean\b(?![^;&|\n]*(?:-[a-zA-Z]*n|--dry-run))[^;&|\n]*-[a-zA-Z]*f/,
|
|
534
|
+
warning: "Note: may permanently delete untracked files"
|
|
535
|
+
},
|
|
536
|
+
{
|
|
537
|
+
pattern: /\bgit\s+checkout\s+(--\s+)?\.[ \t]*($|[;&|\n])/,
|
|
538
|
+
warning: "Note: may discard all working tree changes"
|
|
539
|
+
},
|
|
540
|
+
{
|
|
541
|
+
pattern: /\bgit\s+restore\s+(--\s+)?\.[ \t]*($|[;&|\n])/,
|
|
542
|
+
warning: "Note: may discard all working tree changes"
|
|
543
|
+
},
|
|
544
|
+
{
|
|
545
|
+
pattern: /\bgit\s+stash[ \t]+(drop|clear)\b/,
|
|
546
|
+
warning: "Note: may permanently remove stashed changes"
|
|
547
|
+
},
|
|
548
|
+
{
|
|
549
|
+
pattern: /\bgit\s+branch\s+(-D[ \t]|--delete\s+--force|--force\s+--delete)\b/,
|
|
550
|
+
warning: "Note: may force-delete a branch"
|
|
551
|
+
},
|
|
552
|
+
// Git —— 安全绕过
|
|
553
|
+
{
|
|
554
|
+
pattern: /\bgit\s+(commit|push|merge)\b[^;&|\n]*--no-verify\b/,
|
|
555
|
+
warning: "Note: may skip safety hooks"
|
|
556
|
+
},
|
|
557
|
+
{
|
|
558
|
+
pattern: /\bgit\s+commit\b[^;&|\n]*--amend\b/,
|
|
559
|
+
warning: "Note: may rewrite the last commit"
|
|
560
|
+
},
|
|
561
|
+
// 文件删除(rm -rf / 之类的致命形式由 bash.ts 黑名单处理;这里只做"未到致命"的提醒)
|
|
562
|
+
{
|
|
563
|
+
pattern: /(^|[;&|\n]\s*)rm\s+-[a-zA-Z]*[rR][a-zA-Z]*f|(^|[;&|\n]\s*)rm\s+-[a-zA-Z]*f[a-zA-Z]*[rR]/,
|
|
564
|
+
warning: "Note: may recursively force-remove files"
|
|
565
|
+
},
|
|
566
|
+
{
|
|
567
|
+
pattern: /(^|[;&|\n]\s*)rm\s+-[a-zA-Z]*[rR]/,
|
|
568
|
+
warning: "Note: may recursively remove files"
|
|
569
|
+
},
|
|
570
|
+
{
|
|
571
|
+
pattern: /(^|[;&|\n]\s*)rm\s+-[a-zA-Z]*f/,
|
|
572
|
+
warning: "Note: may force-remove files"
|
|
573
|
+
},
|
|
574
|
+
// 数据库
|
|
575
|
+
{
|
|
576
|
+
pattern: /\b(DROP|TRUNCATE)\s+(TABLE|DATABASE|SCHEMA)\b/i,
|
|
577
|
+
warning: "Note: may drop or truncate database objects"
|
|
578
|
+
},
|
|
579
|
+
{
|
|
580
|
+
pattern: /\bDELETE\s+FROM\s+\w+[ \t]*(;|"|'|\n|$)/i,
|
|
581
|
+
warning: "Note: may delete all rows from a database table"
|
|
582
|
+
},
|
|
583
|
+
// 基础设施
|
|
584
|
+
{
|
|
585
|
+
pattern: /\bkubectl\s+delete\b/,
|
|
586
|
+
warning: "Note: may delete Kubernetes resources"
|
|
587
|
+
},
|
|
588
|
+
{
|
|
589
|
+
pattern: /\bterraform\s+destroy\b/,
|
|
590
|
+
warning: "Note: may destroy Terraform infrastructure"
|
|
591
|
+
}
|
|
592
|
+
];
|
|
593
|
+
function scanDestructiveCommand(command) {
|
|
594
|
+
for (const { pattern, warning } of DESTRUCTIVE_PATTERNS) {
|
|
595
|
+
if (pattern.test(command)) return warning;
|
|
596
|
+
}
|
|
597
|
+
return null;
|
|
598
|
+
}
|
|
599
|
+
|
|
465
600
|
// src/tools/bash/bash.ts
|
|
466
601
|
var DEFAULT_TIMEOUT_MS = 12e4;
|
|
467
602
|
var MAX_TIMEOUT_MS = 6e5;
|
|
@@ -599,6 +734,7 @@ async function call(input, signal) {
|
|
|
599
734
|
\u547D\u4EE4\uFF1A${command}`
|
|
600
735
|
};
|
|
601
736
|
}
|
|
737
|
+
const destructiveWarning = scanDestructiveCommand(command);
|
|
602
738
|
let stdout = "";
|
|
603
739
|
let stderr = "";
|
|
604
740
|
let exitCode = null;
|
|
@@ -669,16 +805,33 @@ ${stderr.replace(/\s+$/, "")}
|
|
|
669
805
|
}
|
|
670
806
|
parts.push(`
|
|
671
807
|
[exit code: ${exitCode === null ? "killed" : exitCode}]`);
|
|
672
|
-
let
|
|
673
|
-
if (
|
|
674
|
-
|
|
808
|
+
let combinedOutput = parts.join("\n");
|
|
809
|
+
if (combinedOutput.length > DEFAULT_MAX_RESULT_SIZE_CHARS) {
|
|
810
|
+
combinedOutput = combinedOutput.slice(0, DEFAULT_MAX_RESULT_SIZE_CHARS) + `
|
|
675
811
|
|
|
676
812
|
... (\u8F93\u51FA\u8D85\u8FC7 ${DEFAULT_MAX_RESULT_SIZE_CHARS} \u5B57\u7B26\uFF0C\u5DF2\u622A\u65AD)`;
|
|
677
813
|
}
|
|
678
|
-
if (timedOut ||
|
|
679
|
-
return { ok: false, error:
|
|
814
|
+
if (timedOut || killedBySignal) {
|
|
815
|
+
return { ok: false, error: combinedOutput };
|
|
680
816
|
}
|
|
681
|
-
|
|
817
|
+
const semantic = interpretCommandResult(
|
|
818
|
+
command,
|
|
819
|
+
exitCode ?? 0,
|
|
820
|
+
stdout,
|
|
821
|
+
stderr
|
|
822
|
+
);
|
|
823
|
+
const finalContent = destructiveWarning ? `\u26A0\uFE0F \u8B66\u544A: ${destructiveWarning}
|
|
824
|
+
|
|
825
|
+
${combinedOutput}` : combinedOutput;
|
|
826
|
+
if (semantic.isError) {
|
|
827
|
+
return {
|
|
828
|
+
ok: false,
|
|
829
|
+
error: `\u547D\u4EE4\u5931\u8D25 (exit ${exitCode}): ${stderr || stdout || semantic.message || ""}`.trim() + `
|
|
830
|
+
|
|
831
|
+
${finalContent}`
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
return { ok: true, content: finalContent };
|
|
682
835
|
}
|
|
683
836
|
var bashTool = {
|
|
684
837
|
name: "Bash",
|
|
@@ -693,10 +846,294 @@ var bashTool = {
|
|
|
693
846
|
};
|
|
694
847
|
|
|
695
848
|
// src/tools/edit/edit.ts
|
|
696
|
-
import { readFile as
|
|
697
|
-
import { existsSync as
|
|
698
|
-
import { dirname as dirname5
|
|
849
|
+
import { readFile as readFile6, writeFile as writeFile3, mkdir as mkdir4 } from "fs/promises";
|
|
850
|
+
import { existsSync as existsSync3 } from "fs";
|
|
851
|
+
import { dirname as dirname5 } from "path";
|
|
699
852
|
import { z as z2 } from "zod";
|
|
853
|
+
|
|
854
|
+
// src/tools/shared/fileUtils.ts
|
|
855
|
+
import { open, readFile as readFile5 } from "fs/promises";
|
|
856
|
+
import { homedir as homedir4 } from "os";
|
|
857
|
+
import { extname, resolve as resolve4 } from "path";
|
|
858
|
+
var BLOCKED_DEVICE_PATHS = /* @__PURE__ */ new Set([
|
|
859
|
+
"/dev/zero",
|
|
860
|
+
"/dev/random",
|
|
861
|
+
"/dev/urandom",
|
|
862
|
+
"/dev/full",
|
|
863
|
+
"/dev/stdin",
|
|
864
|
+
"/dev/tty",
|
|
865
|
+
"/dev/console",
|
|
866
|
+
"/dev/stdout",
|
|
867
|
+
"/dev/stderr",
|
|
868
|
+
"/dev/fd/0",
|
|
869
|
+
"/dev/fd/1",
|
|
870
|
+
"/dev/fd/2"
|
|
871
|
+
]);
|
|
872
|
+
var WINDOWS_BLOCKED_NAMES = /* @__PURE__ */ new Set(["NUL", "CON", "PRN", "AUX", "COM1", "COM2", "LPT1"]);
|
|
873
|
+
function isBlockedDevicePath(filePath) {
|
|
874
|
+
const slashed = filePath.replaceAll("\\", "/");
|
|
875
|
+
if (BLOCKED_DEVICE_PATHS.has(slashed)) return true;
|
|
876
|
+
if (slashed.startsWith("/proc/") && (slashed.endsWith("/fd/0") || slashed.endsWith("/fd/1") || slashed.endsWith("/fd/2"))) {
|
|
877
|
+
return true;
|
|
878
|
+
}
|
|
879
|
+
const baseName = slashed.split("/").pop() ?? "";
|
|
880
|
+
if (WINDOWS_BLOCKED_NAMES.has(baseName.toUpperCase())) {
|
|
881
|
+
return true;
|
|
882
|
+
}
|
|
883
|
+
return false;
|
|
884
|
+
}
|
|
885
|
+
function validateAndResolvePath(rawPath, workingDir) {
|
|
886
|
+
if (rawPath.includes("\0")) {
|
|
887
|
+
return { ok: false, error: "\u8DEF\u5F84\u5305\u542B\u975E\u6CD5\u5B57\u7B26\uFF08null byte\uFF09" };
|
|
888
|
+
}
|
|
889
|
+
if (isBlockedDevicePath(rawPath)) {
|
|
890
|
+
return { ok: false, error: `\u4E0D\u5141\u8BB8\u8BFB\u53D6\u8BBE\u5907\u6587\u4EF6\uFF1A${rawPath}\u3002\u8BE5\u8DEF\u5F84\u53EF\u80FD\u4EA7\u751F\u65E0\u9650\u8F93\u51FA\u6216\u963B\u585E\u8FDB\u7A0B\u3002` };
|
|
891
|
+
}
|
|
892
|
+
const expanded = expandPath(rawPath);
|
|
893
|
+
const resolved = resolve4(workingDir, expanded);
|
|
894
|
+
if (isBlockedDevicePath(resolved)) {
|
|
895
|
+
return { ok: false, error: `\u4E0D\u5141\u8BB8\u8BFB\u53D6\u8BBE\u5907\u6587\u4EF6\uFF1A${resolved}\u3002\u8BE5\u8DEF\u5F84\u53EF\u80FD\u4EA7\u751F\u65E0\u9650\u8F93\u51FA\u6216\u963B\u585E\u8FDB\u7A0B\u3002` };
|
|
896
|
+
}
|
|
897
|
+
if (process.platform === "win32" && /^\\\\/.test(resolved)) {
|
|
898
|
+
return { ok: false, error: "\u4E0D\u652F\u6301 UNC \u8DEF\u5F84\uFF08\\\\server\\share \u683C\u5F0F\uFF09\uFF0C\u8BF7\u4F7F\u7528\u672C\u5730\u8DEF\u5F84" };
|
|
899
|
+
}
|
|
900
|
+
return { ok: true, resolvedPath: resolved };
|
|
901
|
+
}
|
|
902
|
+
function expandPath(p) {
|
|
903
|
+
if (p.startsWith("~/") || p === "~") {
|
|
904
|
+
return resolve4(homedir4(), p.slice(2));
|
|
905
|
+
}
|
|
906
|
+
return p;
|
|
907
|
+
}
|
|
908
|
+
function detectLineEndingsForString(content) {
|
|
909
|
+
let crlfCount = 0;
|
|
910
|
+
let lfCount = 0;
|
|
911
|
+
for (let i = 0; i < content.length; i++) {
|
|
912
|
+
if (content[i] === "\n") {
|
|
913
|
+
if (i > 0 && content[i - 1] === "\r") {
|
|
914
|
+
crlfCount++;
|
|
915
|
+
} else {
|
|
916
|
+
lfCount++;
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
return crlfCount > lfCount ? "CRLF" : "LF";
|
|
921
|
+
}
|
|
922
|
+
async function detectFileLineEndings(filePath) {
|
|
923
|
+
try {
|
|
924
|
+
const handle = await readFile5(filePath, { encoding: "utf8" });
|
|
925
|
+
const head = handle.slice(0, 4096);
|
|
926
|
+
return detectLineEndingsForString(head);
|
|
927
|
+
} catch {
|
|
928
|
+
return "LF";
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
function applyLineEnding(content, ending) {
|
|
932
|
+
if (ending === "CRLF") {
|
|
933
|
+
return content.replaceAll("\r\n", "\n").split("\n").join("\r\n");
|
|
934
|
+
}
|
|
935
|
+
return content;
|
|
936
|
+
}
|
|
937
|
+
var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
938
|
+
".png",
|
|
939
|
+
".jpg",
|
|
940
|
+
".jpeg",
|
|
941
|
+
".gif",
|
|
942
|
+
".webp",
|
|
943
|
+
".bmp",
|
|
944
|
+
".ico",
|
|
945
|
+
".tiff",
|
|
946
|
+
".tif",
|
|
947
|
+
".pdf",
|
|
948
|
+
".doc",
|
|
949
|
+
".docx",
|
|
950
|
+
".xls",
|
|
951
|
+
".xlsx",
|
|
952
|
+
".ppt",
|
|
953
|
+
".pptx",
|
|
954
|
+
".exe",
|
|
955
|
+
".dll",
|
|
956
|
+
".so",
|
|
957
|
+
".dylib",
|
|
958
|
+
".o",
|
|
959
|
+
".a",
|
|
960
|
+
".pyc",
|
|
961
|
+
".pyo",
|
|
962
|
+
".class",
|
|
963
|
+
".jar",
|
|
964
|
+
".zip",
|
|
965
|
+
".tar",
|
|
966
|
+
".gz",
|
|
967
|
+
".bz2",
|
|
968
|
+
".7z",
|
|
969
|
+
".rar",
|
|
970
|
+
".iso",
|
|
971
|
+
".mp3",
|
|
972
|
+
".mp4",
|
|
973
|
+
".mov",
|
|
974
|
+
".avi",
|
|
975
|
+
".mkv",
|
|
976
|
+
".wav",
|
|
977
|
+
".flac",
|
|
978
|
+
".ogg",
|
|
979
|
+
".ttf",
|
|
980
|
+
".otf",
|
|
981
|
+
".woff",
|
|
982
|
+
".woff2",
|
|
983
|
+
".sqlite",
|
|
984
|
+
".sqlite3",
|
|
985
|
+
".db",
|
|
986
|
+
".psd",
|
|
987
|
+
".ai",
|
|
988
|
+
".bin",
|
|
989
|
+
".wasm"
|
|
990
|
+
]);
|
|
991
|
+
function hasBinaryExtension(filePath) {
|
|
992
|
+
const ext = extname(filePath).toLowerCase();
|
|
993
|
+
return BINARY_EXTENSIONS.has(ext);
|
|
994
|
+
}
|
|
995
|
+
async function detectFileBomEncoding(filePath) {
|
|
996
|
+
let fh = null;
|
|
997
|
+
try {
|
|
998
|
+
fh = await open(filePath, "r");
|
|
999
|
+
const buf = Buffer.alloc(3);
|
|
1000
|
+
const { bytesRead } = await fh.read(buf, 0, 3, 0);
|
|
1001
|
+
if (bytesRead >= 3 && buf[0] === 239 && buf[1] === 187 && buf[2] === 191) {
|
|
1002
|
+
return "utf8-bom";
|
|
1003
|
+
}
|
|
1004
|
+
if (bytesRead >= 2 && buf[0] === 255 && buf[1] === 254) {
|
|
1005
|
+
return "utf16le";
|
|
1006
|
+
}
|
|
1007
|
+
return "utf8";
|
|
1008
|
+
} catch {
|
|
1009
|
+
return "utf8";
|
|
1010
|
+
} finally {
|
|
1011
|
+
if (fh) {
|
|
1012
|
+
try {
|
|
1013
|
+
await fh.close();
|
|
1014
|
+
} catch {
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
var LEFT_SINGLE_CURLY_QUOTE = "\u2018";
|
|
1020
|
+
var RIGHT_SINGLE_CURLY_QUOTE = "\u2019";
|
|
1021
|
+
var LEFT_DOUBLE_CURLY_QUOTE = "\u201C";
|
|
1022
|
+
var RIGHT_DOUBLE_CURLY_QUOTE = "\u201D";
|
|
1023
|
+
function normalizeQuotes(str) {
|
|
1024
|
+
return str.replaceAll(LEFT_SINGLE_CURLY_QUOTE, "'").replaceAll(RIGHT_SINGLE_CURLY_QUOTE, "'").replaceAll(LEFT_DOUBLE_CURLY_QUOTE, '"').replaceAll(RIGHT_DOUBLE_CURLY_QUOTE, '"');
|
|
1025
|
+
}
|
|
1026
|
+
function findActualString(fileContent, searchString) {
|
|
1027
|
+
if (fileContent.includes(searchString)) {
|
|
1028
|
+
return searchString;
|
|
1029
|
+
}
|
|
1030
|
+
const normalizedSearch = normalizeQuotes(searchString);
|
|
1031
|
+
const normalizedFile = normalizeQuotes(fileContent);
|
|
1032
|
+
const searchIndex = normalizedFile.indexOf(normalizedSearch);
|
|
1033
|
+
if (searchIndex !== -1) {
|
|
1034
|
+
return fileContent.substring(searchIndex, searchIndex + searchString.length);
|
|
1035
|
+
}
|
|
1036
|
+
return null;
|
|
1037
|
+
}
|
|
1038
|
+
function preserveQuoteStyle(oldString, actualOldString, newString) {
|
|
1039
|
+
if (oldString === actualOldString) {
|
|
1040
|
+
return newString;
|
|
1041
|
+
}
|
|
1042
|
+
const hasDoubleQuotes = actualOldString.includes(LEFT_DOUBLE_CURLY_QUOTE) || actualOldString.includes(RIGHT_DOUBLE_CURLY_QUOTE);
|
|
1043
|
+
const hasSingleQuotes = actualOldString.includes(LEFT_SINGLE_CURLY_QUOTE) || actualOldString.includes(RIGHT_SINGLE_CURLY_QUOTE);
|
|
1044
|
+
if (!hasDoubleQuotes && !hasSingleQuotes) {
|
|
1045
|
+
return newString;
|
|
1046
|
+
}
|
|
1047
|
+
let result = newString;
|
|
1048
|
+
if (hasDoubleQuotes) {
|
|
1049
|
+
result = applyCurlyDoubleQuotes(result);
|
|
1050
|
+
}
|
|
1051
|
+
if (hasSingleQuotes) {
|
|
1052
|
+
result = applyCurlySingleQuotes(result);
|
|
1053
|
+
}
|
|
1054
|
+
return result;
|
|
1055
|
+
}
|
|
1056
|
+
function isOpeningContext(chars, index) {
|
|
1057
|
+
if (index === 0) return true;
|
|
1058
|
+
const prev = chars[index - 1];
|
|
1059
|
+
return prev === " " || prev === " " || prev === "\n" || prev === "\r" || prev === "(" || prev === "[" || prev === "{" || prev === "\u2014" || prev === "\u2013";
|
|
1060
|
+
}
|
|
1061
|
+
function applyCurlyDoubleQuotes(str) {
|
|
1062
|
+
const chars = [...str];
|
|
1063
|
+
const result = [];
|
|
1064
|
+
for (let i = 0; i < chars.length; i++) {
|
|
1065
|
+
if (chars[i] === '"') {
|
|
1066
|
+
result.push(
|
|
1067
|
+
isOpeningContext(chars, i) ? LEFT_DOUBLE_CURLY_QUOTE : RIGHT_DOUBLE_CURLY_QUOTE
|
|
1068
|
+
);
|
|
1069
|
+
} else {
|
|
1070
|
+
result.push(chars[i]);
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
return result.join("");
|
|
1074
|
+
}
|
|
1075
|
+
function applyCurlySingleQuotes(str) {
|
|
1076
|
+
const chars = [...str];
|
|
1077
|
+
const result = [];
|
|
1078
|
+
for (let i = 0; i < chars.length; i++) {
|
|
1079
|
+
if (chars[i] === "'") {
|
|
1080
|
+
const prev = i > 0 ? chars[i - 1] : void 0;
|
|
1081
|
+
const next = i < chars.length - 1 ? chars[i + 1] : void 0;
|
|
1082
|
+
const prevIsLetter = prev !== void 0 && new RegExp("\\p{L}", "u").test(prev);
|
|
1083
|
+
const nextIsLetter = next !== void 0 && new RegExp("\\p{L}", "u").test(next);
|
|
1084
|
+
if (prevIsLetter && nextIsLetter) {
|
|
1085
|
+
result.push(RIGHT_SINGLE_CURLY_QUOTE);
|
|
1086
|
+
} else {
|
|
1087
|
+
result.push(
|
|
1088
|
+
isOpeningContext(chars, i) ? LEFT_SINGLE_CURLY_QUOTE : RIGHT_SINGLE_CURLY_QUOTE
|
|
1089
|
+
);
|
|
1090
|
+
}
|
|
1091
|
+
} else {
|
|
1092
|
+
result.push(chars[i]);
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
return result.join("");
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
// src/tools/shared/fileState.ts
|
|
1099
|
+
import { existsSync as existsSync2, statSync } from "fs";
|
|
1100
|
+
var fileState = /* @__PURE__ */ new Map();
|
|
1101
|
+
function recordRead(absPath) {
|
|
1102
|
+
try {
|
|
1103
|
+
const st = statSync(absPath);
|
|
1104
|
+
fileState.set(absPath, { timestamp: st.mtimeMs, size: st.size });
|
|
1105
|
+
} catch {
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
function assertFresh(absPath) {
|
|
1109
|
+
const entry = fileState.get(absPath);
|
|
1110
|
+
if (!entry) {
|
|
1111
|
+
if (existsSync2(absPath)) {
|
|
1112
|
+
return {
|
|
1113
|
+
ok: false,
|
|
1114
|
+
error: `\u6587\u4EF6 ${absPath} \u5DF2\u5B58\u5728\u4F46\u672A\u5728\u672C\u4F1A\u8BDD Read \u8FC7\u3002\u8BF7\u5148\u7528 Read \u5DE5\u5177\u8BFB\u53D6\uFF0C\u786E\u8BA4\u5185\u5BB9\u540E\u518D\u4FEE\u6539\u3002`
|
|
1115
|
+
};
|
|
1116
|
+
}
|
|
1117
|
+
return { ok: true };
|
|
1118
|
+
}
|
|
1119
|
+
try {
|
|
1120
|
+
const st = statSync(absPath);
|
|
1121
|
+
if (st.mtimeMs > entry.timestamp) {
|
|
1122
|
+
return {
|
|
1123
|
+
ok: false,
|
|
1124
|
+
error: `${absPath} \u5728 Read \u540E\u88AB\u5916\u90E8\u4FEE\u6539\uFF08mtime \u6F02\u79FB\uFF09\u3002\u8BF7\u91CD\u65B0\u7528 Read \u5DE5\u5177\u8BFB\u53D6\u6700\u65B0\u5185\u5BB9\u3002`
|
|
1125
|
+
};
|
|
1126
|
+
}
|
|
1127
|
+
} catch {
|
|
1128
|
+
return { ok: true };
|
|
1129
|
+
}
|
|
1130
|
+
return { ok: true };
|
|
1131
|
+
}
|
|
1132
|
+
function clearFileState() {
|
|
1133
|
+
fileState.clear();
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
// src/tools/edit/edit.ts
|
|
700
1137
|
var MAX_EDIT_FILE_SIZE_BYTES = 1024 * 1024 * 1024;
|
|
701
1138
|
var inputSchema2 = z2.object({
|
|
702
1139
|
file_path: z2.string().min(1).describe("\u8981\u7F16\u8F91\u7684\u6587\u4EF6\u8DEF\u5F84"),
|
|
@@ -718,25 +1155,20 @@ Usage:
|
|
|
718
1155
|
- The edit will FAIL if \`old_string\` is not unique in the file. Either provide a larger string with more surrounding context to make it unique or use \`replace_all\` to change every instance of \`old_string\`.
|
|
719
1156
|
- Use \`replace_all\` for replacing and renaming strings across the file. This parameter is useful if you want to rename a variable for instance.
|
|
720
1157
|
- Preserve exact indentation (tabs/spaces).`;
|
|
721
|
-
function validatePath(filePath) {
|
|
722
|
-
if (filePath.includes("\0")) {
|
|
723
|
-
return { ok: false, error: "\u8DEF\u5F84\u5305\u542B\u975E\u6CD5\u5B57\u7B26\uFF08null byte\uFF09" };
|
|
724
|
-
}
|
|
725
|
-
if (process.platform === "win32" && filePath.includes("\\\\")) {
|
|
726
|
-
return { ok: false, error: "\u4E0D\u652F\u6301 UNC \u8DEF\u5F84\uFF08\\\\server\\share \u683C\u5F0F\uFF09\uFF0C\u8BF7\u4F7F\u7528\u672C\u5730\u8DEF\u5F84" };
|
|
727
|
-
}
|
|
728
|
-
return { ok: true };
|
|
729
|
-
}
|
|
730
1158
|
async function call2(input) {
|
|
731
|
-
const filePath = resolve4(input.file_path);
|
|
732
1159
|
const { old_string, new_string } = input;
|
|
733
1160
|
const replaceAll = input.replace_all ?? false;
|
|
734
|
-
const
|
|
735
|
-
if (!
|
|
1161
|
+
const pathResult = validateAndResolvePath(input.file_path, getWorkingDir());
|
|
1162
|
+
if (!pathResult.ok) return pathResult;
|
|
1163
|
+
const filePath = pathResult.resolvedPath;
|
|
1164
|
+
const freshness = assertFresh(filePath);
|
|
1165
|
+
if (!freshness.ok) {
|
|
1166
|
+
return { ok: false, error: freshness.error };
|
|
1167
|
+
}
|
|
736
1168
|
if (old_string === new_string) {
|
|
737
1169
|
return { ok: false, error: "old_string \u4E0E new_string \u5B8C\u5168\u76F8\u540C\uFF0C\u6CA1\u6709\u53EF\u6539\u7684\u5185\u5BB9\u3002" };
|
|
738
1170
|
}
|
|
739
|
-
if (old_string === "" && !
|
|
1171
|
+
if (old_string === "" && !existsSync3(filePath)) {
|
|
740
1172
|
try {
|
|
741
1173
|
await mkdir4(dirname5(filePath), { recursive: true });
|
|
742
1174
|
await writeFile3(filePath, new_string, "utf8");
|
|
@@ -748,7 +1180,7 @@ async function call2(input) {
|
|
|
748
1180
|
return { ok: false, error: `\u521B\u5EFA\u6587\u4EF6\u5931\u8D25\uFF1A${e.message}` };
|
|
749
1181
|
}
|
|
750
1182
|
}
|
|
751
|
-
if (!
|
|
1183
|
+
if (!existsSync3(filePath)) {
|
|
752
1184
|
return {
|
|
753
1185
|
ok: false,
|
|
754
1186
|
error: `\u6587\u4EF6\u4E0D\u5B58\u5728\uFF1A${filePath}
|
|
@@ -757,10 +1189,11 @@ async function call2(input) {
|
|
|
757
1189
|
}
|
|
758
1190
|
let original;
|
|
759
1191
|
try {
|
|
760
|
-
original = await
|
|
1192
|
+
original = await readFile6(filePath, "utf8");
|
|
761
1193
|
} catch (e) {
|
|
762
1194
|
return { ok: false, error: `\u8BFB\u53D6\u5931\u8D25\uFF1A${e.message}` };
|
|
763
1195
|
}
|
|
1196
|
+
const originalLineEnding = detectLineEndingsForString(original);
|
|
764
1197
|
const fileSize = Buffer.byteLength(original, "utf8");
|
|
765
1198
|
if (fileSize > MAX_EDIT_FILE_SIZE_BYTES) {
|
|
766
1199
|
return {
|
|
@@ -774,9 +1207,16 @@ async function call2(input) {
|
|
|
774
1207
|
error: "old_string \u4E3A\u7A7A\u4F46\u6587\u4EF6\u5DF2\u5B58\u5728 \u2014\u2014 \u8FD9\u901A\u5E38\u662F\u9519\u8BEF\u7684\u3002\u8981\u66FF\u6362\u5168\u6587\u8BF7\u7528 Write \u5DE5\u5177\u3002"
|
|
775
1208
|
};
|
|
776
1209
|
}
|
|
777
|
-
|
|
1210
|
+
let searchTarget = old_string;
|
|
1211
|
+
let processedNewString = new_string;
|
|
1212
|
+
const actualOld = findActualString(original, old_string);
|
|
1213
|
+
if (actualOld !== null && actualOld !== old_string) {
|
|
1214
|
+
searchTarget = actualOld;
|
|
1215
|
+
processedNewString = preserveQuoteStyle(old_string, actualOld, new_string);
|
|
1216
|
+
}
|
|
1217
|
+
const occurrences = countOccurrences(original, searchTarget);
|
|
778
1218
|
if (occurrences === 0) {
|
|
779
|
-
const hint = findFuzzyMatchHint(original,
|
|
1219
|
+
const hint = findFuzzyMatchHint(original, searchTarget);
|
|
780
1220
|
const extraMsg = hint ? `
|
|
781
1221
|
|
|
782
1222
|
\u{1F4A1} \u63D0\u793A\uFF1A${hint}` : "";
|
|
@@ -792,9 +1232,10 @@ async function call2(input) {
|
|
|
792
1232
|
\u8BF7\u6269\u5927 old_string \u5305\u542B\u66F4\u591A\u4E0A\u4E0B\u6587\uFF0C\u6216\u663E\u5F0F\u4F20 replace_all=true\u3002`
|
|
793
1233
|
};
|
|
794
1234
|
}
|
|
795
|
-
const replaced = replaceAll ? splitReplaceAll(original,
|
|
1235
|
+
const replaced = replaceAll ? splitReplaceAll(original, searchTarget, processedNewString) : original.replace(searchTarget, processedNewString);
|
|
1236
|
+
const normalizedReplaced = applyLineEnding(replaced, originalLineEnding);
|
|
796
1237
|
try {
|
|
797
|
-
await writeFile3(filePath,
|
|
1238
|
+
await writeFile3(filePath, normalizedReplaced, "utf8");
|
|
798
1239
|
} catch (e) {
|
|
799
1240
|
return { ok: false, error: `\u5199\u5165\u5931\u8D25\uFF1A${e.message}` };
|
|
800
1241
|
}
|
|
@@ -892,22 +1333,146 @@ var editTool = {
|
|
|
892
1333
|
call: call2
|
|
893
1334
|
};
|
|
894
1335
|
|
|
1336
|
+
// src/tools/edit/multi-edit.ts
|
|
1337
|
+
import { readFile as readFile7, writeFile as writeFile4 } from "fs/promises";
|
|
1338
|
+
import { existsSync as existsSync4 } from "fs";
|
|
1339
|
+
import { z as z3 } from "zod";
|
|
1340
|
+
var editItemSchema = z3.object({
|
|
1341
|
+
old_string: z3.string().min(1).describe("\u8981\u66FF\u6362\u7684\u539F\u6587\u672C\uFF08\u4E0D\u5141\u8BB8\u4E3A\u7A7A \u2014\u2014 \u521B\u5EFA\u65B0\u6587\u4EF6\u8BF7\u7528 Edit \u5DE5\u5177\uFF09"),
|
|
1342
|
+
new_string: z3.string().describe("\u66FF\u6362\u4E3A\u7684\u65B0\u6587\u672C"),
|
|
1343
|
+
replace_all: z3.boolean().optional().describe("\u662F\u5426\u66FF\u6362\u6240\u6709\u51FA\u73B0\u4F4D\u7F6E\uFF08\u9ED8\u8BA4 false\uFF0C\u8981\u6C42 old_string \u5728\u5F53\u524D\u5185\u5BB9\u4E2D\u552F\u4E00\uFF09")
|
|
1344
|
+
});
|
|
1345
|
+
var inputSchema3 = z3.object({
|
|
1346
|
+
file_path: z3.string().min(1).describe("\u8981\u7F16\u8F91\u7684\u6587\u4EF6\u8DEF\u5F84"),
|
|
1347
|
+
edits: z3.array(editItemSchema).min(1).max(50).describe("\u6309\u987A\u5E8F\u5E94\u7528\u7684 edit \u5217\u8868\uFF081-50 \u6761\uFF09\uFF0C\u539F\u5B50\u5316\u6267\u884C\uFF1A\u5168\u90E8\u6210\u529F\u624D\u843D\u76D8")
|
|
1348
|
+
});
|
|
1349
|
+
var parameters3 = toToolParameters(inputSchema3);
|
|
1350
|
+
var description3 = `Performs multiple exact string replacements in a single file, applied atomically (all-or-nothing).
|
|
1351
|
+
|
|
1352
|
+
Usage:
|
|
1353
|
+
- You MUST use your \`Read\` tool to read the current content of the file BEFORE calling MultiEdit.
|
|
1354
|
+
- Provide a list of \`edits\`, each with \`old_string\`, \`new_string\`, and optional \`replace_all\`.
|
|
1355
|
+
- All edits are applied sequentially in memory; if ANY edit fails (string not found, ambiguous match, or dependency conflict), the file on disk is UNTOUCHED.
|
|
1356
|
+
- Order matters: later edits operate on the result of earlier edits. If a later edit's \`old_string\` is a substring of an earlier edit's \`new_string\`, MultiEdit refuses (reorder or merge instead).
|
|
1357
|
+
- Each \`old_string\` must be unique in the current content (after prior edits) unless \`replace_all=true\`.
|
|
1358
|
+
- Empty \`old_string\` is NOT allowed in MultiEdit. To create a new file, use the \`Edit\` tool with a single empty-old_string call.
|
|
1359
|
+
- Preserve exact indentation (tabs/spaces).`;
|
|
1360
|
+
function countOccurrences2(haystack, needle) {
|
|
1361
|
+
if (needle.length === 0) return 0;
|
|
1362
|
+
let count = 0;
|
|
1363
|
+
let pos = 0;
|
|
1364
|
+
while ((pos = haystack.indexOf(needle, pos)) !== -1) {
|
|
1365
|
+
count++;
|
|
1366
|
+
pos += needle.length;
|
|
1367
|
+
}
|
|
1368
|
+
return count;
|
|
1369
|
+
}
|
|
1370
|
+
function splitReplaceAll2(haystack, needle, replacement) {
|
|
1371
|
+
return haystack.split(needle).join(replacement);
|
|
1372
|
+
}
|
|
1373
|
+
async function call3(input) {
|
|
1374
|
+
const pathResult = validateAndResolvePath(input.file_path, getWorkingDir());
|
|
1375
|
+
if (!pathResult.ok) return pathResult;
|
|
1376
|
+
const filePath = pathResult.resolvedPath;
|
|
1377
|
+
const freshness = assertFresh(filePath);
|
|
1378
|
+
if (!freshness.ok) {
|
|
1379
|
+
return { ok: false, error: freshness.error };
|
|
1380
|
+
}
|
|
1381
|
+
if (!existsSync4(filePath)) {
|
|
1382
|
+
return {
|
|
1383
|
+
ok: false,
|
|
1384
|
+
error: `\u6587\u4EF6\u4E0D\u5B58\u5728\uFF1A${filePath}
|
|
1385
|
+
\uFF08MultiEdit \u4E0D\u652F\u6301\u521B\u5EFA\u65B0\u6587\u4EF6\uFF0C\u8BF7\u6539\u7528 Edit \u5DE5\u5177\uFF09`
|
|
1386
|
+
};
|
|
1387
|
+
}
|
|
1388
|
+
const edits = input.edits;
|
|
1389
|
+
for (let i = 0; i < edits.length; i++) {
|
|
1390
|
+
for (let j = 0; j < i; j++) {
|
|
1391
|
+
if (edits[j].new_string.includes(edits[i].old_string)) {
|
|
1392
|
+
return {
|
|
1393
|
+
ok: false,
|
|
1394
|
+
error: `edits[${i}].old_string \u662F edits[${j}].new_string \u7684\u5B50\u4E32\uFF0C\u4F1A\u5BFC\u81F4\u540E\u7EED edit \u547D\u4E2D\u524D\u5E8F\u4EA7\u7269\uFF0C\u8BF7\u91CD\u65B0\u6392\u5E8F\u6216\u5408\u5E76`
|
|
1395
|
+
};
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
let originalContent;
|
|
1400
|
+
try {
|
|
1401
|
+
originalContent = await readFile7(filePath, "utf8");
|
|
1402
|
+
} catch (e) {
|
|
1403
|
+
return { ok: false, error: `\u8BFB\u53D6\u5931\u8D25\uFF1A${e.message}` };
|
|
1404
|
+
}
|
|
1405
|
+
let currentContent = originalContent;
|
|
1406
|
+
for (let i = 0; i < edits.length; i++) {
|
|
1407
|
+
const edit = edits[i];
|
|
1408
|
+
const replaceAll = edit.replace_all ?? false;
|
|
1409
|
+
let searchTarget = edit.old_string;
|
|
1410
|
+
let processedNewString = edit.new_string;
|
|
1411
|
+
const actualOld = findActualString(currentContent, edit.old_string);
|
|
1412
|
+
if (actualOld === null) {
|
|
1413
|
+
return {
|
|
1414
|
+
ok: false,
|
|
1415
|
+
error: `edits[${i}] \u672A\u5339\u914D\u5230 old_string\uFF08\u5728\u5DF2\u5E94\u7528\u524D\u5E8F ${i} \u5904\u4FEE\u6539\u540E\u7684\u5185\u5BB9\u4E2D\u627E\u4E0D\u5230\uFF09\u3002\u8BF7\u5148\u7528 Read \u5DE5\u5177\u6838\u5BF9\u5F53\u524D\u5185\u5BB9\uFF08\u6CE8\u610F\u7A7A\u683C/\u7F29\u8FDB/\u6362\u884C\uFF09\uFF0C\u5E76\u68C0\u67E5 edit \u987A\u5E8F\u3002`
|
|
1416
|
+
};
|
|
1417
|
+
}
|
|
1418
|
+
if (actualOld !== edit.old_string) {
|
|
1419
|
+
searchTarget = actualOld;
|
|
1420
|
+
processedNewString = preserveQuoteStyle(edit.old_string, actualOld, edit.new_string);
|
|
1421
|
+
}
|
|
1422
|
+
const occurrences = countOccurrences2(currentContent, searchTarget);
|
|
1423
|
+
if (occurrences === 0) {
|
|
1424
|
+
return {
|
|
1425
|
+
ok: false,
|
|
1426
|
+
error: `edits[${i}] \u672A\u5339\u914D\u5230 old_string\uFF08\u5DF2\u5E94\u7528\u524D\u5E8F ${i} \u5904\u4FEE\u6539\u540E\u5185\u5BB9\u4E2D\u51FA\u73B0 0 \u6B21\uFF09\u3002`
|
|
1427
|
+
};
|
|
1428
|
+
}
|
|
1429
|
+
if (occurrences > 1 && !replaceAll) {
|
|
1430
|
+
return {
|
|
1431
|
+
ok: false,
|
|
1432
|
+
error: `edits[${i}].old_string \u5728\u5F53\u524D\u5185\u5BB9\u4E2D\u51FA\u73B0 ${occurrences} \u6B21\uFF0C\u4E0D\u552F\u4E00\u3002\u8BF7\u6269\u5927 old_string \u5305\u542B\u66F4\u591A\u4E0A\u4E0B\u6587\uFF0C\u6216\u663E\u5F0F\u4F20 replace_all=true\u3002`
|
|
1433
|
+
};
|
|
1434
|
+
}
|
|
1435
|
+
currentContent = replaceAll ? splitReplaceAll2(currentContent, searchTarget, processedNewString) : currentContent.replace(searchTarget, processedNewString);
|
|
1436
|
+
}
|
|
1437
|
+
const lineEnding = await detectFileLineEndings(filePath);
|
|
1438
|
+
const finalContent = applyLineEnding(currentContent, lineEnding);
|
|
1439
|
+
try {
|
|
1440
|
+
await writeFile4(filePath, finalContent, "utf8");
|
|
1441
|
+
} catch (e) {
|
|
1442
|
+
return { ok: false, error: `\u5199\u5165\u5931\u8D25\uFF1A${e.message}` };
|
|
1443
|
+
}
|
|
1444
|
+
return {
|
|
1445
|
+
ok: true,
|
|
1446
|
+
content: `\u5DF2\u5BF9 ${filePath} \u5E94\u7528 ${edits.length} \u5904\u4FEE\u6539`
|
|
1447
|
+
};
|
|
1448
|
+
}
|
|
1449
|
+
var multiEditTool = {
|
|
1450
|
+
name: "MultiEdit",
|
|
1451
|
+
description: description3,
|
|
1452
|
+
inputSchema: inputSchema3,
|
|
1453
|
+
parameters: parameters3,
|
|
1454
|
+
isReadOnly: false,
|
|
1455
|
+
isConcurrencySafe: false,
|
|
1456
|
+
maxResultSizeChars: DEFAULT_MAX_RESULT_SIZE_CHARS,
|
|
1457
|
+
call: call3
|
|
1458
|
+
};
|
|
1459
|
+
|
|
895
1460
|
// src/tools/glob/glob.ts
|
|
896
1461
|
import { stat as stat2 } from "fs/promises";
|
|
897
1462
|
import { isAbsolute, resolve as resolve5 } from "path";
|
|
898
1463
|
import fg from "fast-glob";
|
|
899
|
-
import { z as
|
|
900
|
-
var
|
|
901
|
-
pattern:
|
|
902
|
-
path:
|
|
1464
|
+
import { z as z4 } from "zod";
|
|
1465
|
+
var inputSchema4 = z4.object({
|
|
1466
|
+
pattern: z4.string().min(1).describe('glob \u6A21\u5F0F\uFF0C\u4F8B\u5982 "**/*.ts" \u6216 "src/components/**/*.tsx"'),
|
|
1467
|
+
path: z4.string().optional().describe('\u641C\u7D22\u7684\u6839\u76EE\u5F55\uFF08\u9ED8\u8BA4\u5F53\u524D\u5DE5\u4F5C\u76EE\u5F55\uFF09\uFF1B\u7701\u7565\u65F6\u4E0D\u8981\u4F20 "undefined" \u5B57\u7B26\u4E32')
|
|
903
1468
|
});
|
|
904
|
-
var
|
|
905
|
-
var
|
|
1469
|
+
var parameters4 = toToolParameters(inputSchema4);
|
|
1470
|
+
var description4 = `- Fast file pattern matching tool that works with any codebase size
|
|
906
1471
|
- Supports glob patterns like "**/*.js" or "src/**/*.ts"
|
|
907
1472
|
- Returns matching file paths sorted by modification time (oldest first)
|
|
908
1473
|
- Use this tool when you need to find files by name patterns
|
|
909
1474
|
- When you need to do an open ended search that may require multiple rounds, prefer the Grep tool for content search`;
|
|
910
|
-
async function
|
|
1475
|
+
async function call4(input) {
|
|
911
1476
|
const cwd = input.path ? resolve5(input.path) : getWorkingDir();
|
|
912
1477
|
const pattern = input.pattern.replace(/\\/g, "/");
|
|
913
1478
|
let matches;
|
|
@@ -954,23 +1519,23 @@ async function call3(input) {
|
|
|
954
1519
|
}
|
|
955
1520
|
var globTool = {
|
|
956
1521
|
name: "Glob",
|
|
957
|
-
description:
|
|
958
|
-
inputSchema:
|
|
959
|
-
parameters:
|
|
1522
|
+
description: description4,
|
|
1523
|
+
inputSchema: inputSchema4,
|
|
1524
|
+
parameters: parameters4,
|
|
960
1525
|
isReadOnly: true,
|
|
961
1526
|
isConcurrencySafe: true,
|
|
962
1527
|
maxResultSizeChars: DEFAULT_MAX_RESULT_SIZE_CHARS,
|
|
963
|
-
call:
|
|
1528
|
+
call: call4
|
|
964
1529
|
};
|
|
965
1530
|
|
|
966
1531
|
// src/tools/grep/grep.ts
|
|
967
1532
|
import { spawn as spawn3 } from "child_process";
|
|
968
1533
|
import { resolve as resolve7 } from "path";
|
|
969
|
-
import { z as
|
|
1534
|
+
import { z as z5 } from "zod";
|
|
970
1535
|
|
|
971
1536
|
// src/tools/grep/rgPath.ts
|
|
972
1537
|
import { spawn as spawn2 } from "child_process";
|
|
973
|
-
import { chmodSync, existsSync as
|
|
1538
|
+
import { chmodSync, existsSync as existsSync5 } from "fs";
|
|
974
1539
|
import { resolve as resolve6 } from "path";
|
|
975
1540
|
var cached;
|
|
976
1541
|
async function resolveRgPath() {
|
|
@@ -980,15 +1545,15 @@ async function resolveRgPath() {
|
|
|
980
1545
|
}
|
|
981
1546
|
async function detect() {
|
|
982
1547
|
const fromEnv = process.env.MINIMAL_AGENT_RIPGREP_PATH;
|
|
983
|
-
if (fromEnv &&
|
|
1548
|
+
if (fromEnv && existsSync5(fromEnv)) return fromEnv;
|
|
984
1549
|
const vendored = vendoredRgPath();
|
|
985
|
-
if (vendored &&
|
|
1550
|
+
if (vendored && existsSync5(vendored)) {
|
|
986
1551
|
ensureExecutable(vendored);
|
|
987
1552
|
return vendored;
|
|
988
1553
|
}
|
|
989
1554
|
if (await trySpawn("rg")) return "rg";
|
|
990
1555
|
for (const candidate of claudeCodeCandidates()) {
|
|
991
|
-
if (
|
|
1556
|
+
if (existsSync5(candidate)) {
|
|
992
1557
|
ensureExecutable(candidate);
|
|
993
1558
|
return candidate;
|
|
994
1559
|
}
|
|
@@ -1081,21 +1646,21 @@ function claudeCodeCandidates() {
|
|
|
1081
1646
|
}
|
|
1082
1647
|
|
|
1083
1648
|
// src/tools/grep/grep.ts
|
|
1084
|
-
var
|
|
1085
|
-
pattern:
|
|
1086
|
-
path:
|
|
1087
|
-
glob:
|
|
1088
|
-
type:
|
|
1089
|
-
output_mode:
|
|
1090
|
-
"-i":
|
|
1091
|
-
"-n":
|
|
1092
|
-
"-A":
|
|
1093
|
-
"-B":
|
|
1094
|
-
"-C":
|
|
1095
|
-
head_limit:
|
|
1649
|
+
var inputSchema5 = z5.object({
|
|
1650
|
+
pattern: z5.string().min(1).describe("\u6B63\u5219\u8868\u8FBE\u5F0F\uFF08ripgrep \u517C\u5BB9\u8BED\u6CD5\uFF09"),
|
|
1651
|
+
path: z5.string().optional().describe("\u641C\u7D22\u7684\u6839\u76EE\u5F55\u6216\u6587\u4EF6\uFF08\u9ED8\u8BA4\u5F53\u524D\u5DE5\u4F5C\u76EE\u5F55\uFF09"),
|
|
1652
|
+
glob: z5.string().optional().describe('\u6587\u4EF6\u540D glob \u8FC7\u6EE4\uFF0C\u5982 "*.ts"'),
|
|
1653
|
+
type: z5.string().optional().describe('rg \u7684\u6587\u4EF6\u7C7B\u578B\u5FEB\u6377\u540D\uFF0C\u5982 "py"\u3001"rust"\u3001"js"'),
|
|
1654
|
+
output_mode: z5.enum(["content", "files_with_matches", "count"]).optional().describe("\u8F93\u51FA\u6A21\u5F0F\uFF1Acontent=\u5339\u914D\u884C\uFF1Bfiles_with_matches=\u53EA\u5217\u6587\u4EF6\uFF1Bcount=\u6BCF\u6587\u4EF6\u8BA1\u6570"),
|
|
1655
|
+
"-i": z5.boolean().optional().describe("\u5FFD\u7565\u5927\u5C0F\u5199"),
|
|
1656
|
+
"-n": z5.boolean().optional().describe("\u663E\u793A\u884C\u53F7\uFF08\u4EC5 content \u6A21\u5F0F\uFF09"),
|
|
1657
|
+
"-A": z5.number().int().min(0).optional().describe("\u5339\u914D\u540E\u5C55\u793A\u51E0\u884C\u4E0A\u4E0B\u6587"),
|
|
1658
|
+
"-B": z5.number().int().min(0).optional().describe("\u5339\u914D\u524D\u5C55\u793A\u51E0\u884C\u4E0A\u4E0B\u6587"),
|
|
1659
|
+
"-C": z5.number().int().min(0).optional().describe("\u5339\u914D\u524D\u540E\u5404\u5C55\u793A\u51E0\u884C\uFF08\u8986\u76D6 -A/-B\uFF09"),
|
|
1660
|
+
head_limit: z5.number().int().positive().optional().describe("\u8F93\u51FA\u6700\u591A\u4FDD\u7559\u524D N \u884C\uFF08\u9632\u6B62\u7ED3\u679C\u8FC7\u5927\uFF09")
|
|
1096
1661
|
});
|
|
1097
|
-
var
|
|
1098
|
-
var
|
|
1662
|
+
var parameters5 = toToolParameters(inputSchema5);
|
|
1663
|
+
var description5 = `A powerful search tool built on ripgrep.
|
|
1099
1664
|
|
|
1100
1665
|
Usage:
|
|
1101
1666
|
- ALWAYS use Grep for content search tasks. Do NOT invoke \`grep\` or \`rg\` directly via Bash.
|
|
@@ -1103,7 +1668,7 @@ Usage:
|
|
|
1103
1668
|
- Filter files with glob parameter (e.g., "*.js", "**/*.tsx") or type parameter (e.g., "js", "py", "rust")
|
|
1104
1669
|
- Output modes: "content" shows matching lines, "files_with_matches" shows only file paths (default), "count" shows match counts
|
|
1105
1670
|
- Pattern syntax: Uses ripgrep (not classic grep)`;
|
|
1106
|
-
async function
|
|
1671
|
+
async function call5(input, signal) {
|
|
1107
1672
|
const args = [];
|
|
1108
1673
|
const mode = input.output_mode ?? "files_with_matches";
|
|
1109
1674
|
if (mode === "files_with_matches") args.push("-l");
|
|
@@ -1180,26 +1745,27 @@ async function call4(input, signal) {
|
|
|
1180
1745
|
}
|
|
1181
1746
|
var grepTool = {
|
|
1182
1747
|
name: "Grep",
|
|
1183
|
-
description:
|
|
1184
|
-
inputSchema:
|
|
1185
|
-
parameters:
|
|
1748
|
+
description: description5,
|
|
1749
|
+
inputSchema: inputSchema5,
|
|
1750
|
+
parameters: parameters5,
|
|
1186
1751
|
isReadOnly: true,
|
|
1187
1752
|
isConcurrencySafe: true,
|
|
1188
1753
|
maxResultSizeChars: DEFAULT_MAX_RESULT_SIZE_CHARS,
|
|
1189
|
-
call:
|
|
1754
|
+
call: call5
|
|
1190
1755
|
};
|
|
1191
1756
|
|
|
1192
1757
|
// src/tools/read/read.ts
|
|
1193
|
-
import {
|
|
1194
|
-
import {
|
|
1195
|
-
import {
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1758
|
+
import { createReadStream } from "fs";
|
|
1759
|
+
import { readFile as readFile8, stat as stat3 } from "fs/promises";
|
|
1760
|
+
import { createInterface } from "readline";
|
|
1761
|
+
import { z as z6 } from "zod";
|
|
1762
|
+
var inputSchema6 = z6.object({
|
|
1763
|
+
file_path: z6.string().min(1, "\u5FC5\u987B\u63D0\u4F9B file_path").describe("\u8981\u8BFB\u53D6\u7684\u6587\u4EF6\u8DEF\u5F84\uFF0C\u7EDD\u5BF9\u8DEF\u5F84\u4F18\u5148"),
|
|
1764
|
+
offset: z6.number().int().positive().optional().describe("\u8D77\u59CB\u884C\u53F7\uFF081-indexed\uFF09\uFF1B\u4E0D\u586B\u5219\u4ECE\u6587\u4EF6\u5F00\u5934\u8BFB"),
|
|
1765
|
+
limit: z6.number().int().positive().optional().describe(`\u6700\u591A\u8BFB\u591A\u5C11\u884C\uFF1B\u4E0D\u586B\u5219\u7528\u9ED8\u8BA4\u503C ${MAX_LINES_TO_READ}`)
|
|
1200
1766
|
});
|
|
1201
|
-
var
|
|
1202
|
-
var
|
|
1767
|
+
var parameters6 = toToolParameters(inputSchema6);
|
|
1768
|
+
var description6 = `Reads a file from the local filesystem. You can access any file directly by using this tool.
|
|
1203
1769
|
Assume this tool is able to read all files on the machine. If the User provides a path to a file assume that path is valid. It is okay to read a file that does not exist; an error will be returned.
|
|
1204
1770
|
|
|
1205
1771
|
Usage:
|
|
@@ -1209,10 +1775,24 @@ Usage:
|
|
|
1209
1775
|
- Results are returned using cat -n format, with line numbers starting at 1
|
|
1210
1776
|
- This tool can only read text files, not directories. To read a directory, use the Glob tool.
|
|
1211
1777
|
- If you read a file that exists but has empty contents you will receive a warning in place of file contents.`;
|
|
1212
|
-
|
|
1213
|
-
|
|
1778
|
+
var STREAM_THRESHOLD = 1024 * 1024;
|
|
1779
|
+
async function call6(input) {
|
|
1214
1780
|
const offset = input.offset ?? 1;
|
|
1215
1781
|
const limit = input.limit ?? MAX_LINES_TO_READ;
|
|
1782
|
+
const pathResult = validateAndResolvePath(input.file_path, getWorkingDir());
|
|
1783
|
+
if (!pathResult.ok) {
|
|
1784
|
+
return { ok: false, error: pathResult.error };
|
|
1785
|
+
}
|
|
1786
|
+
const filePath = pathResult.resolvedPath;
|
|
1787
|
+
if (isBlockedDevicePath(filePath)) {
|
|
1788
|
+
return { ok: false, error: `\u4E0D\u5141\u8BB8\u8BFB\u53D6\u8BBE\u5907\u6587\u4EF6\uFF1A${filePath}\u3002\u8BE5\u8DEF\u5F84\u53EF\u80FD\u4EA7\u751F\u65E0\u9650\u8F93\u51FA\u6216\u963B\u585E\u8FDB\u7A0B\u3002` };
|
|
1789
|
+
}
|
|
1790
|
+
if (hasBinaryExtension(filePath)) {
|
|
1791
|
+
return {
|
|
1792
|
+
ok: false,
|
|
1793
|
+
error: `\u4E0D\u652F\u6301\u8BFB\u53D6\u4E8C\u8FDB\u5236\u6587\u4EF6\uFF1A${filePath}\uFF08\u6269\u5C55\u540D\u547D\u4E2D\u4E8C\u8FDB\u5236\u9ED1\u540D\u5355\uFF09\u3002\u82E5\u8BE5\u6587\u4EF6\u5B9E\u9645\u4E3A\u6587\u672C\uFF0C\u53EF\u6539\u540E\u7F00\u6216\u7528 Bash cat \u65C1\u8DEF\u3002`
|
|
1794
|
+
};
|
|
1795
|
+
}
|
|
1216
1796
|
let st;
|
|
1217
1797
|
try {
|
|
1218
1798
|
st = await stat3(filePath);
|
|
@@ -1231,54 +1811,110 @@ async function call5(input) {
|
|
|
1231
1811
|
error: `\u6587\u4EF6\u8FC7\u5927\uFF08${st.size} \u5B57\u8282 > ${MAX_FILE_SIZE_BYTES}\uFF09\u3002\u8BF7\u7528 offset/limit \u5206\u6BB5\u8BFB\u3002`
|
|
1232
1812
|
};
|
|
1233
1813
|
}
|
|
1234
|
-
let
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1814
|
+
let numbered;
|
|
1815
|
+
let totalLines;
|
|
1816
|
+
if (st.size <= STREAM_THRESHOLD) {
|
|
1817
|
+
const result = await readSmallFile(filePath, offset, limit);
|
|
1818
|
+
numbered = result.numbered;
|
|
1819
|
+
totalLines = result.totalLines;
|
|
1820
|
+
if (result.isEmpty) {
|
|
1821
|
+
recordRead(filePath);
|
|
1822
|
+
return { ok: true, content: "<file is empty>" };
|
|
1823
|
+
}
|
|
1824
|
+
} else {
|
|
1825
|
+
const result = await readLargeFileStream(filePath, offset, limit);
|
|
1826
|
+
numbered = result.numbered;
|
|
1827
|
+
totalLines = result.totalLines;
|
|
1239
1828
|
}
|
|
1240
|
-
if (
|
|
1829
|
+
if (totalLines === 0 || !numbered) {
|
|
1830
|
+
recordRead(filePath);
|
|
1241
1831
|
return { ok: true, content: "<file is empty>" };
|
|
1242
1832
|
}
|
|
1243
|
-
const allLines = raw.split("\n");
|
|
1244
|
-
const totalLines = allLines.length;
|
|
1245
|
-
const startIdx = Math.max(0, offset - 1);
|
|
1246
|
-
const endIdx = Math.min(totalLines, startIdx + limit);
|
|
1247
|
-
const slice = allLines.slice(startIdx, endIdx);
|
|
1248
|
-
const numbered = slice.map((line, i) => `${(startIdx + i + 1).toString()} ${line}`).join("\n");
|
|
1249
1833
|
let content = numbered;
|
|
1250
|
-
|
|
1251
|
-
if (contentLength > DEFAULT_MAX_RESULT_SIZE_CHARS) {
|
|
1834
|
+
if (content.length > DEFAULT_MAX_RESULT_SIZE_CHARS) {
|
|
1252
1835
|
content = content.slice(0, DEFAULT_MAX_RESULT_SIZE_CHARS) + `
|
|
1253
1836
|
|
|
1254
1837
|
... (\u8F93\u51FA\u8D85\u8FC7 ${DEFAULT_MAX_RESULT_SIZE_CHARS} \u5B57\u7B26\uFF0C\u5DF2\u622A\u65AD)`;
|
|
1255
1838
|
}
|
|
1839
|
+
const startIdx = Math.max(0, offset - 1);
|
|
1840
|
+
const endIdx = Math.min(totalLines, startIdx + limit);
|
|
1256
1841
|
if (endIdx < totalLines) {
|
|
1257
1842
|
const nextOffset = endIdx + 1;
|
|
1843
|
+
const returnedLines = content.split("\n").filter((l) => l.trim()).length;
|
|
1258
1844
|
content += `
|
|
1259
1845
|
|
|
1260
|
-
... (\u672C\u6B21\u8FD4\u56DE ${
|
|
1846
|
+
... (\u672C\u6B21\u8FD4\u56DE ${returnedLines} \u884C / \u6587\u4EF6\u5171 ${totalLines} \u884C\uFF1B\u7528 offset=${nextOffset} \u7EE7\u7EED\u8BFB)`;
|
|
1261
1847
|
}
|
|
1262
1848
|
if (st.size > 100 * 1024 && offset === 1) {
|
|
1263
1849
|
content += `
|
|
1264
1850
|
|
|
1265
1851
|
\u26A0\uFE0F \u6CE8\u610F\uFF1A\u8FD9\u662F\u4E00\u4E2A\u5927\u6587\u4EF6\uFF08${(st.size / 1024).toFixed(1)} KB\uFF09\u3002\u5EFA\u8BAE\u7528 offset/limit \u5206\u6BB5\u8BFB\u53D6\uFF0C\u4F8B\u5982\u5148\u8BFB\u5173\u952E\u90E8\u5206\uFF08imports\u3001exports\u3001\u51FD\u6570\u7B7E\u540D\uFF09\u3002`;
|
|
1266
1852
|
}
|
|
1853
|
+
recordRead(filePath);
|
|
1267
1854
|
return { ok: true, content };
|
|
1268
1855
|
}
|
|
1856
|
+
async function readSmallFile(filePath, offset, limit) {
|
|
1857
|
+
const raw = await readFile8(filePath, "utf8");
|
|
1858
|
+
if (raw.length === 0) {
|
|
1859
|
+
return { numbered: "", totalLines: 0, isEmpty: true };
|
|
1860
|
+
}
|
|
1861
|
+
const allLines = raw.split("\n");
|
|
1862
|
+
const totalLines = allLines.length;
|
|
1863
|
+
const startIdx = Math.max(0, offset - 1);
|
|
1864
|
+
const endIdx = Math.min(totalLines, startIdx + limit);
|
|
1865
|
+
const slice = allLines.slice(startIdx, endIdx);
|
|
1866
|
+
const numbered = slice.map((line, i) => `${(startIdx + i + 1).toString()} ${line}`).join("\n");
|
|
1867
|
+
return { numbered, totalLines, isEmpty: false };
|
|
1868
|
+
}
|
|
1869
|
+
async function readLargeFileStream(filePath, offset, limit) {
|
|
1870
|
+
return new Promise((resolvePromise, reject) => {
|
|
1871
|
+
const lines = [];
|
|
1872
|
+
let currentLine = 0;
|
|
1873
|
+
const startIdx = Math.max(0, offset - 1);
|
|
1874
|
+
const endLine = startIdx + limit;
|
|
1875
|
+
const input = createReadStream(filePath, { encoding: "utf8" });
|
|
1876
|
+
const rl = createInterface({
|
|
1877
|
+
input,
|
|
1878
|
+
crlfDelay: Infinity
|
|
1879
|
+
});
|
|
1880
|
+
input.on("error", (err) => {
|
|
1881
|
+
reject(err);
|
|
1882
|
+
});
|
|
1883
|
+
rl.on("line", (line) => {
|
|
1884
|
+
currentLine++;
|
|
1885
|
+
if (currentLine > endLine) {
|
|
1886
|
+
rl.close();
|
|
1887
|
+
rl.removeAllListeners();
|
|
1888
|
+
return;
|
|
1889
|
+
}
|
|
1890
|
+
if (currentLine >= offset) {
|
|
1891
|
+
lines.push(`${currentLine} ${line}`);
|
|
1892
|
+
}
|
|
1893
|
+
});
|
|
1894
|
+
rl.on("close", () => {
|
|
1895
|
+
resolvePromise({
|
|
1896
|
+
numbered: lines.join("\n"),
|
|
1897
|
+
totalLines: currentLine
|
|
1898
|
+
});
|
|
1899
|
+
});
|
|
1900
|
+
rl.on("error", (err) => {
|
|
1901
|
+
reject(err);
|
|
1902
|
+
});
|
|
1903
|
+
});
|
|
1904
|
+
}
|
|
1269
1905
|
var readTool = {
|
|
1270
1906
|
name: "Read",
|
|
1271
|
-
description:
|
|
1272
|
-
inputSchema:
|
|
1273
|
-
parameters:
|
|
1907
|
+
description: description6,
|
|
1908
|
+
inputSchema: inputSchema6,
|
|
1909
|
+
parameters: parameters6,
|
|
1274
1910
|
isReadOnly: true,
|
|
1275
1911
|
isConcurrencySafe: true,
|
|
1276
1912
|
maxResultSizeChars: DEFAULT_MAX_RESULT_SIZE_CHARS,
|
|
1277
|
-
call:
|
|
1913
|
+
call: call6
|
|
1278
1914
|
};
|
|
1279
1915
|
|
|
1280
1916
|
// src/tools/webfetch/webfetch.ts
|
|
1281
|
-
import { z as
|
|
1917
|
+
import { z as z7 } from "zod";
|
|
1282
1918
|
|
|
1283
1919
|
// src/tools/webfetch/preapproved.ts
|
|
1284
1920
|
var PREAPPROVED_HOSTS = /* @__PURE__ */ new Set([
|
|
@@ -1563,12 +2199,12 @@ function cleanCache() {
|
|
|
1563
2199
|
}
|
|
1564
2200
|
}
|
|
1565
2201
|
}
|
|
1566
|
-
var
|
|
1567
|
-
url:
|
|
1568
|
-
prompt:
|
|
2202
|
+
var inputSchema7 = z7.object({
|
|
2203
|
+
url: z7.string().describe("\u8981\u83B7\u53D6\u5185\u5BB9\u7684 URL"),
|
|
2204
|
+
prompt: z7.string().describe("\u5BF9\u5185\u5BB9\u8FDB\u884C\u5904\u7406\u7684\u6307\u4EE4\uFF0C\u63CF\u8FF0\u4F60\u60F3\u4ECE\u9875\u9762\u63D0\u53D6\u4EC0\u4E48\u4FE1\u606F")
|
|
1569
2205
|
});
|
|
1570
|
-
var
|
|
1571
|
-
var
|
|
2206
|
+
var parameters7 = toToolParameters(inputSchema7);
|
|
2207
|
+
var description7 = `- Fetches content from a specified URL and processes it using an AI model.
|
|
1572
2208
|
- Takes a URL and a prompt as input.
|
|
1573
2209
|
- Fetches the URL content, converts HTML to markdown.
|
|
1574
2210
|
- Processes the content with the prompt (e.g., extract summary, find specific info).
|
|
@@ -1677,7 +2313,7 @@ async function htmlToMarkdown(html) {
|
|
|
1677
2313
|
const td = new TurndownService();
|
|
1678
2314
|
return td.turndown(html);
|
|
1679
2315
|
}
|
|
1680
|
-
async function
|
|
2316
|
+
async function call7(input, signal) {
|
|
1681
2317
|
const { url } = input;
|
|
1682
2318
|
const start = Date.now();
|
|
1683
2319
|
const cacheKey = url;
|
|
@@ -1775,17 +2411,17 @@ ${content}`;
|
|
|
1775
2411
|
}
|
|
1776
2412
|
var webfetchTool = {
|
|
1777
2413
|
name: "WebFetch",
|
|
1778
|
-
description:
|
|
1779
|
-
inputSchema:
|
|
1780
|
-
parameters:
|
|
2414
|
+
description: description7,
|
|
2415
|
+
inputSchema: inputSchema7,
|
|
2416
|
+
parameters: parameters7,
|
|
1781
2417
|
isReadOnly: true,
|
|
1782
2418
|
isConcurrencySafe: true,
|
|
1783
2419
|
maxResultSizeChars: DEFAULT_MAX_RESULT_SIZE_CHARS,
|
|
1784
|
-
call:
|
|
2420
|
+
call: call7
|
|
1785
2421
|
};
|
|
1786
2422
|
|
|
1787
2423
|
// src/tools/webbrowser/webbrowser.ts
|
|
1788
|
-
import { z as
|
|
2424
|
+
import { z as z8 } from "zod";
|
|
1789
2425
|
|
|
1790
2426
|
// src/tools/webbrowser/browser.ts
|
|
1791
2427
|
import os from "os";
|
|
@@ -1821,15 +2457,15 @@ function screenshotPath(prefix = "browser") {
|
|
|
1821
2457
|
}
|
|
1822
2458
|
|
|
1823
2459
|
// src/tools/webbrowser/webbrowser.ts
|
|
1824
|
-
var
|
|
1825
|
-
action:
|
|
1826
|
-
url:
|
|
1827
|
-
selector:
|
|
1828
|
-
value:
|
|
1829
|
-
timeout:
|
|
2460
|
+
var inputSchema8 = z8.object({
|
|
2461
|
+
action: z8.enum(["navigate", "screenshot", "getContent", "click", "fill", "submit"]).describe("Browser action to perform"),
|
|
2462
|
+
url: z8.string().url().optional().describe("URL to navigate to (required for navigate action)"),
|
|
2463
|
+
selector: z8.string().optional().describe("CSS selector for click/fill/submit actions"),
|
|
2464
|
+
value: z8.string().optional().describe("Value to fill in input fields"),
|
|
2465
|
+
timeout: z8.number().int().positive().optional().describe("Timeout in milliseconds (default: 30000)")
|
|
1830
2466
|
});
|
|
1831
|
-
var
|
|
1832
|
-
var
|
|
2467
|
+
var parameters8 = toToolParameters(inputSchema8);
|
|
2468
|
+
var description8 = `Control a headless web browser. Navigate to URLs, take screenshots, and interact with web pages.
|
|
1833
2469
|
|
|
1834
2470
|
When to use WebBrowser vs WebSearch:
|
|
1835
2471
|
- WebSearch: When you need to find information or discover URLs through search
|
|
@@ -1860,7 +2496,7 @@ Example actions:
|
|
|
1860
2496
|
- Take screenshot: { action: "screenshot" }
|
|
1861
2497
|
- Click element: { action: "click", selector: "#submit-btn" }
|
|
1862
2498
|
- Fill form field: { action: "fill", selector: "input[name='email']", value: "user@example.com" }`;
|
|
1863
|
-
async function
|
|
2499
|
+
async function call8(input, signal) {
|
|
1864
2500
|
const { action, url, selector, value, timeout = 3e4 } = input;
|
|
1865
2501
|
let page;
|
|
1866
2502
|
try {
|
|
@@ -1964,30 +2600,30 @@ Current URL: ${page.url()}`
|
|
|
1964
2600
|
}
|
|
1965
2601
|
var webbrowserTool = {
|
|
1966
2602
|
name: "WebBrowser",
|
|
1967
|
-
description:
|
|
1968
|
-
inputSchema:
|
|
1969
|
-
parameters:
|
|
2603
|
+
description: description8,
|
|
2604
|
+
inputSchema: inputSchema8,
|
|
2605
|
+
parameters: parameters8,
|
|
1970
2606
|
isReadOnly: false,
|
|
1971
2607
|
isConcurrencySafe: false,
|
|
1972
2608
|
maxResultSizeChars: DEFAULT_MAX_RESULT_SIZE_CHARS,
|
|
1973
|
-
call:
|
|
2609
|
+
call: call8
|
|
1974
2610
|
};
|
|
1975
2611
|
|
|
1976
2612
|
// src/tools/websearch/websearch.ts
|
|
1977
|
-
import { z as
|
|
1978
|
-
var
|
|
1979
|
-
query:
|
|
1980
|
-
max_results:
|
|
1981
|
-
search_depth:
|
|
1982
|
-
topic:
|
|
2613
|
+
import { z as z9 } from "zod";
|
|
2614
|
+
var inputSchema9 = z9.object({
|
|
2615
|
+
query: z9.string().min(1, "\u5FC5\u987B\u63D0\u4F9B\u641C\u7D22\u5173\u952E\u8BCD").max(400, "\u641C\u7D22\u5173\u952E\u8BCD\u592A\u957F\uFF08>400 \u5B57\uFF09").describe("\u641C\u7D22\u5173\u952E\u8BCD\uFF0C\u5EFA\u8BAE\u81EA\u7136\u8BED\u8A00\u63CF\u8FF0\u9700\u8981\u67E5\u7684\u4FE1\u606F"),
|
|
2616
|
+
max_results: z9.number().int().min(1).max(20).optional().describe("\u8FD4\u56DE\u7ED3\u679C\u6570\u91CF\uFF0C1-20\uFF0C\u9ED8\u8BA4 5"),
|
|
2617
|
+
search_depth: z9.enum(["basic", "advanced"]).optional().describe("basic \u5FEB\u4F46\u6D45\uFF1Badvanced \u6162\u4F46\u6DF1\uFF08\u542B answer \u6458\u8981\uFF09\uFF0C\u9ED8\u8BA4 basic"),
|
|
2618
|
+
topic: z9.enum(["general", "news"]).optional().describe("general=\u901A\u7528\u7F51\u9875\uFF1Bnews=\u504F\u65B0\u95FB\u6E90\uFF1B\u9ED8\u8BA4 general")
|
|
1983
2619
|
});
|
|
1984
|
-
var
|
|
1985
|
-
var
|
|
2620
|
+
var parameters9 = toToolParameters(inputSchema9);
|
|
2621
|
+
var description9 = `- Searches the public web via the Tavily Search API and returns structured results.
|
|
1986
2622
|
- Use this when you need up-to-date information that is not in your training data, or when the user asks for recent news / docs / API references.
|
|
1987
2623
|
- Returns the top N results, each with a title, URL, and content snippet. With \`search_depth: "advanced"\` Tavily also returns a synthesized answer at the top.
|
|
1988
2624
|
- Prefer specific natural-language queries over keyword soup (e.g. "how does Bun handle .env files in version 1.1").
|
|
1989
2625
|
- Requires the TAVILY_API_KEY environment variable to be set; if missing the tool returns a friendly error.`;
|
|
1990
|
-
async function
|
|
2626
|
+
async function call9(input, signal) {
|
|
1991
2627
|
const apiKey = process.env.TAVILY_API_KEY;
|
|
1992
2628
|
if (!apiKey) {
|
|
1993
2629
|
return {
|
|
@@ -2069,46 +2705,41 @@ ${(r.content ?? "").trim()}`
|
|
|
2069
2705
|
}
|
|
2070
2706
|
var webSearchTool = {
|
|
2071
2707
|
name: "WebSearch",
|
|
2072
|
-
description:
|
|
2073
|
-
inputSchema:
|
|
2074
|
-
parameters:
|
|
2708
|
+
description: description9,
|
|
2709
|
+
inputSchema: inputSchema9,
|
|
2710
|
+
parameters: parameters9,
|
|
2075
2711
|
isReadOnly: true,
|
|
2076
2712
|
isConcurrencySafe: true,
|
|
2077
2713
|
maxResultSizeChars: DEFAULT_MAX_RESULT_SIZE_CHARS,
|
|
2078
|
-
call:
|
|
2714
|
+
call: call9
|
|
2079
2715
|
};
|
|
2080
2716
|
|
|
2081
2717
|
// src/tools/write/write.ts
|
|
2082
|
-
import { existsSync as
|
|
2083
|
-
import { mkdir as mkdir5, stat as stat4, writeFile as
|
|
2084
|
-
import { dirname as dirname6
|
|
2085
|
-
import { z as
|
|
2718
|
+
import { existsSync as existsSync6 } from "fs";
|
|
2719
|
+
import { mkdir as mkdir5, stat as stat4, writeFile as writeFile5 } from "fs/promises";
|
|
2720
|
+
import { dirname as dirname6 } from "path";
|
|
2721
|
+
import { z as z10 } from "zod";
|
|
2086
2722
|
var MAX_WRITE_SIZE_BYTES = 1024 * 1024 * 1024;
|
|
2087
|
-
var
|
|
2088
|
-
file_path:
|
|
2089
|
-
content:
|
|
2723
|
+
var inputSchema10 = z10.object({
|
|
2724
|
+
file_path: z10.string().min(1).describe("\u8981\u5199\u5165\u7684\u6587\u4EF6\u8DEF\u5F84"),
|
|
2725
|
+
content: z10.string().describe("\u6587\u4EF6\u5B8C\u6574\u5185\u5BB9\uFF08\u4F1A\u8986\u76D6\u65E2\u6709\u5185\u5BB9\uFF09")
|
|
2090
2726
|
});
|
|
2091
|
-
var
|
|
2092
|
-
var
|
|
2727
|
+
var parameters10 = toToolParameters(inputSchema10);
|
|
2728
|
+
var description10 = `Writes a file to the local filesystem.
|
|
2093
2729
|
|
|
2094
2730
|
Usage:
|
|
2095
2731
|
- This tool will overwrite the existing file if there is one at the provided path.
|
|
2096
2732
|
- If the parent directory does not exist, it will be created recursively.
|
|
2097
2733
|
- ALWAYS prefer editing existing files in the codebase via the Edit tool. NEVER write new files unless explicitly required.
|
|
2098
2734
|
- NEVER create documentation files (*.md) or README files unless explicitly requested by the User.`;
|
|
2099
|
-
function
|
|
2100
|
-
|
|
2101
|
-
|
|
2735
|
+
async function call10(input) {
|
|
2736
|
+
const pathResult = validateAndResolvePath(input.file_path, getWorkingDir());
|
|
2737
|
+
if (!pathResult.ok) return pathResult;
|
|
2738
|
+
const filePath = pathResult.resolvedPath;
|
|
2739
|
+
const freshness = assertFresh(filePath);
|
|
2740
|
+
if (!freshness.ok) {
|
|
2741
|
+
return { ok: false, error: freshness.error };
|
|
2102
2742
|
}
|
|
2103
|
-
if (process.platform === "win32" && filePath.includes("\\\\")) {
|
|
2104
|
-
return { ok: false, error: "\u4E0D\u652F\u6301 UNC \u8DEF\u5F84\uFF08\\\\server\\share \u683C\u5F0F\uFF09\uFF0C\u8BF7\u4F7F\u7528\u672C\u5730\u8DEF\u5F84" };
|
|
2105
|
-
}
|
|
2106
|
-
return { ok: true };
|
|
2107
|
-
}
|
|
2108
|
-
async function call9(input) {
|
|
2109
|
-
const filePath = resolve9(input.file_path);
|
|
2110
|
-
const pathCheck = validatePath2(filePath);
|
|
2111
|
-
if (!pathCheck.ok) return pathCheck;
|
|
2112
2743
|
const contentSize = Buffer.byteLength(input.content, "utf8");
|
|
2113
2744
|
if (contentSize > MAX_WRITE_SIZE_BYTES) {
|
|
2114
2745
|
return {
|
|
@@ -2119,7 +2750,7 @@ async function call9(input) {
|
|
|
2119
2750
|
try {
|
|
2120
2751
|
await mkdir5(dirname6(filePath), { recursive: true });
|
|
2121
2752
|
let originalSize = 0;
|
|
2122
|
-
const fileExisted =
|
|
2753
|
+
const fileExisted = existsSync6(filePath);
|
|
2123
2754
|
if (fileExisted) {
|
|
2124
2755
|
try {
|
|
2125
2756
|
const st = await stat4(filePath);
|
|
@@ -2127,12 +2758,33 @@ async function call9(input) {
|
|
|
2127
2758
|
} catch {
|
|
2128
2759
|
}
|
|
2129
2760
|
}
|
|
2130
|
-
|
|
2761
|
+
let contentToWrite = input.content;
|
|
2762
|
+
let bomEncoding = "utf8";
|
|
2763
|
+
if (fileExisted) {
|
|
2764
|
+
const detected = await detectFileBomEncoding(filePath);
|
|
2765
|
+
if (detected === "utf16le") {
|
|
2766
|
+
return {
|
|
2767
|
+
ok: false,
|
|
2768
|
+
error: "\u6682\u4E0D\u652F\u6301\u6539\u5199 UTF-16 LE \u6587\u4EF6\uFF08BOM=FF FE\uFF09\u3002\u8BF7\u6539\u7528 UTF-8 \u7F16\u7801\u3002"
|
|
2769
|
+
};
|
|
2770
|
+
}
|
|
2771
|
+
bomEncoding = detected;
|
|
2772
|
+
const lineEnding = await detectFileLineEndings(filePath);
|
|
2773
|
+
contentToWrite = applyLineEnding(input.content, lineEnding);
|
|
2774
|
+
}
|
|
2775
|
+
if (bomEncoding === "utf8-bom") {
|
|
2776
|
+
const bomBytes = Buffer.from([239, 187, 191]);
|
|
2777
|
+
const bodyBytes = Buffer.from(contentToWrite, "utf8");
|
|
2778
|
+
await writeFile5(filePath, Buffer.concat([bomBytes, bodyBytes]));
|
|
2779
|
+
} else {
|
|
2780
|
+
await writeFile5(filePath, contentToWrite, "utf8");
|
|
2781
|
+
}
|
|
2131
2782
|
const action = fileExisted ? "\u5DF2\u8986\u76D6" : "\u5DF2\u521B\u5EFA\u65B0\u6587\u4EF6";
|
|
2132
|
-
const sizeInfo = fileExisted ? `\uFF08\u539F\u6587\u4EF6 ${originalSize} \u5B57\u7B26 \u2192 \u65B0\u5185\u5BB9 ${
|
|
2783
|
+
const sizeInfo = fileExisted ? `\uFF08\u539F\u6587\u4EF6 ${originalSize} \u5B57\u7B26 \u2192 \u65B0\u5185\u5BB9 ${contentToWrite.length} \u5B57\u7B26\uFF09` : `\uFF08${contentToWrite.length} \u5B57\u7B26\uFF09`;
|
|
2784
|
+
const bomInfo = bomEncoding === "utf8-bom" ? "\uFF0C\u5DF2\u4FDD\u7559 UTF-8 BOM" : "";
|
|
2133
2785
|
return {
|
|
2134
2786
|
ok: true,
|
|
2135
|
-
content: `${action} ${filePath}${sizeInfo}`
|
|
2787
|
+
content: `${action} ${filePath}${sizeInfo}${bomInfo}`
|
|
2136
2788
|
};
|
|
2137
2789
|
} catch (e) {
|
|
2138
2790
|
return { ok: false, error: `\u5199\u5165\u5931\u8D25\uFF1A${e.message}` };
|
|
@@ -2140,19 +2792,20 @@ async function call9(input) {
|
|
|
2140
2792
|
}
|
|
2141
2793
|
var writeTool = {
|
|
2142
2794
|
name: "Write",
|
|
2143
|
-
description:
|
|
2144
|
-
inputSchema:
|
|
2145
|
-
parameters:
|
|
2795
|
+
description: description10,
|
|
2796
|
+
inputSchema: inputSchema10,
|
|
2797
|
+
parameters: parameters10,
|
|
2146
2798
|
isReadOnly: false,
|
|
2147
2799
|
isConcurrencySafe: false,
|
|
2148
2800
|
maxResultSizeChars: DEFAULT_MAX_RESULT_SIZE_CHARS,
|
|
2149
|
-
call:
|
|
2801
|
+
call: call10
|
|
2150
2802
|
};
|
|
2151
2803
|
|
|
2152
2804
|
// src/tools/index.ts
|
|
2153
2805
|
var ALL_TOOLS = [
|
|
2154
2806
|
readTool,
|
|
2155
2807
|
editTool,
|
|
2808
|
+
multiEditTool,
|
|
2156
2809
|
writeTool,
|
|
2157
2810
|
globTool,
|
|
2158
2811
|
grepTool,
|
|
@@ -2962,7 +3615,7 @@ function MessageRow({ message }) {
|
|
|
2962
3615
|
|
|
2963
3616
|
// src/ui/StatusLine.tsx
|
|
2964
3617
|
import { Box as Box4, Text as Text4 } from "ink";
|
|
2965
|
-
import { homedir as
|
|
3618
|
+
import { homedir as homedir5 } from "os";
|
|
2966
3619
|
import { sep } from "path";
|
|
2967
3620
|
|
|
2968
3621
|
// src/llm/client.ts
|
|
@@ -3350,13 +4003,26 @@ function useTokenUsage(messages, provider) {
|
|
|
3350
4003
|
}
|
|
3351
4004
|
|
|
3352
4005
|
// src/ui/StatusLine.tsx
|
|
3353
|
-
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
3354
|
-
function StatusLine({ provider, history }) {
|
|
4006
|
+
import { Fragment, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
4007
|
+
function StatusLine({ provider, history, pluginLoop }) {
|
|
3355
4008
|
const usage = useTokenUsage(history, provider);
|
|
3356
4009
|
const ratio = usage.tokens / usage.threshold;
|
|
3357
4010
|
const color = ratio >= 1 ? "red" : ratio >= 0.7 ? "yellow" : "green";
|
|
3358
4011
|
const cwdDisplay = shortenPath(getWorkingDir());
|
|
3359
4012
|
return /* @__PURE__ */ jsxs4(Box4, { children: [
|
|
4013
|
+
pluginLoop && /* @__PURE__ */ jsxs4(Fragment, { children: [
|
|
4014
|
+
/* @__PURE__ */ jsxs4(Text4, { color: "magenta", children: [
|
|
4015
|
+
"\u{1F504} ",
|
|
4016
|
+
pluginLoop.pluginName,
|
|
4017
|
+
" "
|
|
4018
|
+
] }),
|
|
4019
|
+
/* @__PURE__ */ jsxs4(Text4, { color: "magenta", children: [
|
|
4020
|
+
pluginLoop.current,
|
|
4021
|
+
"/",
|
|
4022
|
+
pluginLoop.max
|
|
4023
|
+
] }),
|
|
4024
|
+
/* @__PURE__ */ jsx4(Text4, { color: "gray", children: " \xB7 " })
|
|
4025
|
+
] }),
|
|
3360
4026
|
/* @__PURE__ */ jsx4(Text4, { color: "cyan", children: "cwd " }),
|
|
3361
4027
|
/* @__PURE__ */ jsx4(Text4, { color: "gray", children: cwdDisplay }),
|
|
3362
4028
|
/* @__PURE__ */ jsx4(Text4, { color: "gray", children: " \xB7 " }),
|
|
@@ -3382,7 +4048,7 @@ function fmt(n) {
|
|
|
3382
4048
|
return `${(n / 1e6).toFixed(2)}M`;
|
|
3383
4049
|
}
|
|
3384
4050
|
function shortenPath(abs) {
|
|
3385
|
-
const home =
|
|
4051
|
+
const home = homedir5();
|
|
3386
4052
|
let p = abs;
|
|
3387
4053
|
if (home && (p === home || p.startsWith(home + sep))) {
|
|
3388
4054
|
p = "~" + p.slice(home.length);
|
|
@@ -3547,6 +4213,688 @@ async function reactiveCompactIfApplicable(messages, provider, error, state = de
|
|
|
3547
4213
|
};
|
|
3548
4214
|
}
|
|
3549
4215
|
|
|
4216
|
+
// src/plugins/commandRouter.ts
|
|
4217
|
+
import { readFile as readFile9, readdir as readdir3 } from "fs/promises";
|
|
4218
|
+
import { join as join6 } from "path";
|
|
4219
|
+
var PLUGINS_DIR = join6(findPackageRoot(import.meta.url), "plugins");
|
|
4220
|
+
var pluginCache = /* @__PURE__ */ new Map();
|
|
4221
|
+
var discoveryDone = false;
|
|
4222
|
+
function stripQuotes2(s) {
|
|
4223
|
+
const trimmed = s.trim();
|
|
4224
|
+
if (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'")) {
|
|
4225
|
+
return trimmed.slice(1, -1);
|
|
4226
|
+
}
|
|
4227
|
+
return trimmed;
|
|
4228
|
+
}
|
|
4229
|
+
function parseMarkdownFrontmatter(content) {
|
|
4230
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
4231
|
+
if (!match) return {};
|
|
4232
|
+
const frontmatter = {};
|
|
4233
|
+
for (const line of match[1].split("\n")) {
|
|
4234
|
+
const colon = line.indexOf(":");
|
|
4235
|
+
if (colon < 0) continue;
|
|
4236
|
+
const key = line.slice(0, colon).trim();
|
|
4237
|
+
const value = line.slice(colon + 1).trim();
|
|
4238
|
+
if (key) frontmatter[key] = stripQuotes2(value);
|
|
4239
|
+
}
|
|
4240
|
+
return frontmatter;
|
|
4241
|
+
}
|
|
4242
|
+
async function loadPlugin(pluginDirPath) {
|
|
4243
|
+
const dirName = pluginDirPath.split("/").pop() ?? pluginDirPath;
|
|
4244
|
+
const manifestPath = join6(pluginDirPath, ".claude-plugin", "plugin.json");
|
|
4245
|
+
let manifestName = dirName;
|
|
4246
|
+
let manifestVersion;
|
|
4247
|
+
let manifestDesc;
|
|
4248
|
+
try {
|
|
4249
|
+
const raw = await readFile9(manifestPath, "utf8");
|
|
4250
|
+
const parsed = JSON.parse(raw);
|
|
4251
|
+
manifestName = parsed.name ?? dirName;
|
|
4252
|
+
manifestVersion = parsed.version;
|
|
4253
|
+
manifestDesc = parsed.description;
|
|
4254
|
+
} catch {
|
|
4255
|
+
}
|
|
4256
|
+
const commandsDir = join6(pluginDirPath, "commands");
|
|
4257
|
+
const commands = [];
|
|
4258
|
+
try {
|
|
4259
|
+
const entries = await readdir3(commandsDir, { withFileTypes: true });
|
|
4260
|
+
for (const entry of entries) {
|
|
4261
|
+
if (!entry.name.endsWith(".md")) continue;
|
|
4262
|
+
const cmdPath = join6(commandsDir, entry.name);
|
|
4263
|
+
try {
|
|
4264
|
+
const content = await readFile9(cmdPath, "utf8");
|
|
4265
|
+
const fm = parseMarkdownFrontmatter(content);
|
|
4266
|
+
const sep2 = content.indexOf("\n---", 4);
|
|
4267
|
+
const body = sep2 >= 0 ? content.slice(sep2 + 4).trim() : content.trim();
|
|
4268
|
+
commands.push({
|
|
4269
|
+
name: entry.name.replace(/\.md$/, ""),
|
|
4270
|
+
description: fm.description ?? "(no description)",
|
|
4271
|
+
argumentHint: fm["argument-hint"],
|
|
4272
|
+
pluginName: manifestName,
|
|
4273
|
+
pluginRoot: pluginDirPath,
|
|
4274
|
+
promptBody: body
|
|
4275
|
+
});
|
|
4276
|
+
} catch {
|
|
4277
|
+
}
|
|
4278
|
+
}
|
|
4279
|
+
} catch {
|
|
4280
|
+
}
|
|
4281
|
+
const hooksJsonPath = join6(pluginDirPath, "hooks", "hooks.json");
|
|
4282
|
+
let hasStopHook = false;
|
|
4283
|
+
try {
|
|
4284
|
+
const hooksRaw = await readFile9(hooksJsonPath, "utf8");
|
|
4285
|
+
const hooksParsed = JSON.parse(hooksRaw);
|
|
4286
|
+
const stopHooks = hooksParsed?.hooks?.Stop;
|
|
4287
|
+
if (Array.isArray(stopHooks) && stopHooks.length > 0) {
|
|
4288
|
+
hasStopHook = true;
|
|
4289
|
+
}
|
|
4290
|
+
} catch {
|
|
4291
|
+
}
|
|
4292
|
+
if (commands.length === 0 && !hasStopHook) return null;
|
|
4293
|
+
return {
|
|
4294
|
+
name: manifestName,
|
|
4295
|
+
version: manifestVersion,
|
|
4296
|
+
description: manifestDesc,
|
|
4297
|
+
root: pluginDirPath,
|
|
4298
|
+
commands,
|
|
4299
|
+
hasStopHook
|
|
4300
|
+
};
|
|
4301
|
+
}
|
|
4302
|
+
async function discoverPlugins() {
|
|
4303
|
+
if (discoveryDone && pluginCache.size > 0) {
|
|
4304
|
+
return Array.from(pluginCache.values());
|
|
4305
|
+
}
|
|
4306
|
+
pluginCache.clear();
|
|
4307
|
+
discoveryDone = true;
|
|
4308
|
+
try {
|
|
4309
|
+
const entries = await readdir3(PLUGINS_DIR, { withFileTypes: true });
|
|
4310
|
+
for (const entry of entries) {
|
|
4311
|
+
if (!entry.isDirectory()) continue;
|
|
4312
|
+
if (entry.name.startsWith(".")) continue;
|
|
4313
|
+
const plugin = await loadPlugin(join6(PLUGINS_DIR, entry.name));
|
|
4314
|
+
if (plugin) {
|
|
4315
|
+
pluginCache.set(plugin.name, plugin);
|
|
4316
|
+
}
|
|
4317
|
+
}
|
|
4318
|
+
} catch {
|
|
4319
|
+
}
|
|
4320
|
+
return Array.from(pluginCache.values());
|
|
4321
|
+
}
|
|
4322
|
+
function resolveCommand(input) {
|
|
4323
|
+
const trimmed = input.trimStart();
|
|
4324
|
+
if (!trimmed.startsWith("/")) return null;
|
|
4325
|
+
const spaceIdx = trimmed.indexOf(" ", 1);
|
|
4326
|
+
const cmdName = spaceIdx >= 0 ? trimmed.slice(1, spaceIdx) : trimmed.slice(1);
|
|
4327
|
+
const args = spaceIdx >= 0 ? trimmed.slice(spaceIdx + 1) : "";
|
|
4328
|
+
for (const plugin of pluginCache.values()) {
|
|
4329
|
+
for (const cmd of plugin.commands) {
|
|
4330
|
+
if (cmd.name === cmdName) {
|
|
4331
|
+
return { cmd, arguments: args };
|
|
4332
|
+
}
|
|
4333
|
+
}
|
|
4334
|
+
}
|
|
4335
|
+
return null;
|
|
4336
|
+
}
|
|
4337
|
+
var COMMAND_DEFAULTS = {
|
|
4338
|
+
"ralph-loop": ["--max-iterations", "50"]
|
|
4339
|
+
};
|
|
4340
|
+
function applyDefaultArgs(cmdName, args) {
|
|
4341
|
+
const defaults = COMMAND_DEFAULTS[cmdName];
|
|
4342
|
+
if (!defaults) return args;
|
|
4343
|
+
const existing = new Set(args.trim().split(/\s+/).filter(Boolean));
|
|
4344
|
+
let result = args;
|
|
4345
|
+
for (let i = 0; i < defaults.length; i += 2) {
|
|
4346
|
+
const flag = defaults[i];
|
|
4347
|
+
if (existing.has(flag)) continue;
|
|
4348
|
+
result = result.trim() ? `${result} ${flag} ${defaults[i + 1]}` : `${flag} ${defaults[i + 1]}`;
|
|
4349
|
+
}
|
|
4350
|
+
return result;
|
|
4351
|
+
}
|
|
4352
|
+
function buildCommandInput(resolved) {
|
|
4353
|
+
const { cmd, arguments: rawArgs } = resolved;
|
|
4354
|
+
const args = applyDefaultArgs(cmd.name, rawArgs);
|
|
4355
|
+
let input = cmd.promptBody;
|
|
4356
|
+
input = input.replaceAll("${CLAUDE_PLUGIN_ROOT}", cmd.pluginRoot);
|
|
4357
|
+
input = input.replaceAll("$ARGUMENTS", args);
|
|
4358
|
+
input = input.replaceAll("${ARGUMENTS}", args);
|
|
4359
|
+
if (args.trim()) {
|
|
4360
|
+
input += `
|
|
4361
|
+
|
|
4362
|
+
\u7528\u6237\u53C2\u6570: ${args.trim()}`;
|
|
4363
|
+
}
|
|
4364
|
+
return input;
|
|
4365
|
+
}
|
|
4366
|
+
function getActiveStopHookPlugins() {
|
|
4367
|
+
const result = [];
|
|
4368
|
+
for (const plugin of pluginCache.values()) {
|
|
4369
|
+
if (plugin.hasStopHook) {
|
|
4370
|
+
result.push(plugin.root);
|
|
4371
|
+
}
|
|
4372
|
+
}
|
|
4373
|
+
return result;
|
|
4374
|
+
}
|
|
4375
|
+
|
|
4376
|
+
// src/plugins/stopHook.ts
|
|
4377
|
+
import { readFile as readFile10 } from "fs/promises";
|
|
4378
|
+
import { join as join7 } from "path";
|
|
4379
|
+
import { spawn as spawn4 } from "child_process";
|
|
4380
|
+
async function loadStopHookConfig(pluginRoot) {
|
|
4381
|
+
const hooksJsonPath = join7(pluginRoot, "hooks", "hooks.json");
|
|
4382
|
+
try {
|
|
4383
|
+
const raw = await readFile10(hooksJsonPath, "utf8");
|
|
4384
|
+
const parsed = JSON.parse(raw);
|
|
4385
|
+
const stopEntries = parsed?.hooks?.Stop;
|
|
4386
|
+
if (!Array.isArray(stopEntries)) return [];
|
|
4387
|
+
const commands = [];
|
|
4388
|
+
for (const entry of stopEntries) {
|
|
4389
|
+
const hooks = entry.hooks;
|
|
4390
|
+
if (Array.isArray(hooks)) {
|
|
4391
|
+
for (const h of hooks) {
|
|
4392
|
+
if (h.type === "command" && h.command) {
|
|
4393
|
+
commands.push(h);
|
|
4394
|
+
}
|
|
4395
|
+
}
|
|
4396
|
+
}
|
|
4397
|
+
}
|
|
4398
|
+
return commands;
|
|
4399
|
+
} catch {
|
|
4400
|
+
return [];
|
|
4401
|
+
}
|
|
4402
|
+
}
|
|
4403
|
+
async function executeStopHooks(pluginRoots, transcriptText) {
|
|
4404
|
+
if (process.platform === "win32") {
|
|
4405
|
+
return { decision: "pass" };
|
|
4406
|
+
}
|
|
4407
|
+
for (const pluginRoot of pluginRoots) {
|
|
4408
|
+
const hookConfigs = await loadStopHookConfig(pluginRoot);
|
|
4409
|
+
for (const hookConfig of hookConfigs) {
|
|
4410
|
+
const result = await runSingleStopHook(hookConfig, pluginRoot, transcriptText);
|
|
4411
|
+
if (result.decision === "block") {
|
|
4412
|
+
return result;
|
|
4413
|
+
}
|
|
4414
|
+
}
|
|
4415
|
+
}
|
|
4416
|
+
return { decision: "pass" };
|
|
4417
|
+
}
|
|
4418
|
+
function runSingleStopHook(hookConfig, pluginRoot, transcriptText) {
|
|
4419
|
+
const resolvedCommand = hookConfig.command.replaceAll("${CLAUDE_PLUGIN_ROOT}", pluginRoot);
|
|
4420
|
+
return new Promise((resolve9) => {
|
|
4421
|
+
const child = spawn4("bash", [resolvedCommand], {
|
|
4422
|
+
env: {
|
|
4423
|
+
...process.env,
|
|
4424
|
+
CLAUDE_PLUGIN_ROOT: pluginRoot
|
|
4425
|
+
}
|
|
4426
|
+
});
|
|
4427
|
+
let stdout = "";
|
|
4428
|
+
child.stdout.on("data", (data) => {
|
|
4429
|
+
stdout += data.toString();
|
|
4430
|
+
});
|
|
4431
|
+
child.on("error", () => {
|
|
4432
|
+
resolve9({ decision: "pass" });
|
|
4433
|
+
});
|
|
4434
|
+
child.on("close", (code) => {
|
|
4435
|
+
if (code !== 0) {
|
|
4436
|
+
resolve9({ decision: "pass" });
|
|
4437
|
+
return;
|
|
4438
|
+
}
|
|
4439
|
+
const trimmed = stdout.trim();
|
|
4440
|
+
if (!trimmed) {
|
|
4441
|
+
resolve9({ decision: "pass" });
|
|
4442
|
+
return;
|
|
4443
|
+
}
|
|
4444
|
+
try {
|
|
4445
|
+
const parsed = JSON.parse(trimmed);
|
|
4446
|
+
if (parsed.decision === "block") {
|
|
4447
|
+
resolve9({
|
|
4448
|
+
decision: "block",
|
|
4449
|
+
reason: typeof parsed.reason === "string" ? parsed.reason : void 0,
|
|
4450
|
+
systemMessage: typeof parsed.systemMessage === "string" ? parsed.systemMessage : void 0
|
|
4451
|
+
});
|
|
4452
|
+
return;
|
|
4453
|
+
}
|
|
4454
|
+
resolve9({ decision: "pass" });
|
|
4455
|
+
} catch {
|
|
4456
|
+
resolve9({ decision: "pass" });
|
|
4457
|
+
}
|
|
4458
|
+
});
|
|
4459
|
+
child.stdin.write(transcriptText);
|
|
4460
|
+
child.stdin.end();
|
|
4461
|
+
});
|
|
4462
|
+
}
|
|
4463
|
+
|
|
4464
|
+
// src/plugins/verificationGate.ts
|
|
4465
|
+
import { existsSync as existsSync7, readFileSync } from "fs";
|
|
4466
|
+
import { spawn as spawn5 } from "child_process";
|
|
4467
|
+
function parseVerifyArg(arg) {
|
|
4468
|
+
const colonIdx = arg.indexOf(":");
|
|
4469
|
+
if (colonIdx < 0) return null;
|
|
4470
|
+
const type = arg.slice(0, colonIdx).trim().toLowerCase();
|
|
4471
|
+
const value = arg.slice(colonIdx + 1).trim();
|
|
4472
|
+
switch (type) {
|
|
4473
|
+
case "shell":
|
|
4474
|
+
return { type: "shell", command: value, timeout: 3e4 };
|
|
4475
|
+
case "file_exists":
|
|
4476
|
+
return { type: "file_exists", file: value };
|
|
4477
|
+
case "file_contains": {
|
|
4478
|
+
const sep2 = value.indexOf(":");
|
|
4479
|
+
if (sep2 < 0) return null;
|
|
4480
|
+
return {
|
|
4481
|
+
type: "file_contains",
|
|
4482
|
+
file: value.slice(0, sep2),
|
|
4483
|
+
pattern: value.slice(sep2 + 1)
|
|
4484
|
+
};
|
|
4485
|
+
}
|
|
4486
|
+
case "test_count": {
|
|
4487
|
+
const count = parseInt(value, 10);
|
|
4488
|
+
if (isNaN(count) || count < 0) return null;
|
|
4489
|
+
return { type: "test_count", minCount: count };
|
|
4490
|
+
}
|
|
4491
|
+
default:
|
|
4492
|
+
return null;
|
|
4493
|
+
}
|
|
4494
|
+
}
|
|
4495
|
+
function parseVerifyArgs(args) {
|
|
4496
|
+
const checks = [];
|
|
4497
|
+
const regex = /--verify\s+("[^"]*"|\S+)/gi;
|
|
4498
|
+
let match;
|
|
4499
|
+
while ((match = regex.exec(args)) !== null) {
|
|
4500
|
+
const raw = match[1].replace(/^"|"$/g, "");
|
|
4501
|
+
const check = parseVerifyArg(raw);
|
|
4502
|
+
if (check) checks.push(check);
|
|
4503
|
+
}
|
|
4504
|
+
return checks;
|
|
4505
|
+
}
|
|
4506
|
+
function runShell(command, timeout) {
|
|
4507
|
+
return new Promise((resolve9) => {
|
|
4508
|
+
const isWin = process.platform === "win32";
|
|
4509
|
+
const child = isWin ? spawn5("cmd", ["/c", command], { timeout, env: process.env }) : spawn5("bash", ["-c", command], { timeout, env: process.env });
|
|
4510
|
+
let stdout = "";
|
|
4511
|
+
let stderr = "";
|
|
4512
|
+
child.stdout.on("data", (d) => {
|
|
4513
|
+
stdout += d.toString();
|
|
4514
|
+
});
|
|
4515
|
+
child.stderr.on("data", (d) => {
|
|
4516
|
+
stderr += d.toString();
|
|
4517
|
+
});
|
|
4518
|
+
child.on("error", () => {
|
|
4519
|
+
resolve9({ exitCode: null, stdout, stderr, errored: true });
|
|
4520
|
+
});
|
|
4521
|
+
child.on("close", (code) => {
|
|
4522
|
+
resolve9({ exitCode: code, stdout, stderr, errored: false });
|
|
4523
|
+
});
|
|
4524
|
+
});
|
|
4525
|
+
}
|
|
4526
|
+
async function verifyShell(command, timeout) {
|
|
4527
|
+
const r = await runShell(command, timeout);
|
|
4528
|
+
if (r.errored) {
|
|
4529
|
+
return {
|
|
4530
|
+
check: { type: "shell", command },
|
|
4531
|
+
passed: false,
|
|
4532
|
+
output: `\u6267\u884C\u5931\u8D25`
|
|
4533
|
+
};
|
|
4534
|
+
}
|
|
4535
|
+
return {
|
|
4536
|
+
check: { type: "shell", command },
|
|
4537
|
+
passed: r.exitCode === 0,
|
|
4538
|
+
output: r.stdout.trim() || r.stderr.trim() || `exit code ${r.exitCode}`
|
|
4539
|
+
};
|
|
4540
|
+
}
|
|
4541
|
+
function verifyFileExists(file) {
|
|
4542
|
+
const exists = existsSync7(file);
|
|
4543
|
+
return {
|
|
4544
|
+
check: { type: "file_exists", file },
|
|
4545
|
+
passed: exists,
|
|
4546
|
+
output: exists ? "\u6587\u4EF6\u5B58\u5728" : `\u6587\u4EF6\u4E0D\u5B58\u5728: ${file}`
|
|
4547
|
+
};
|
|
4548
|
+
}
|
|
4549
|
+
function verifyFileContains(file, pattern) {
|
|
4550
|
+
try {
|
|
4551
|
+
const content = readFileSync(file, "utf8");
|
|
4552
|
+
const regex = new RegExp(pattern);
|
|
4553
|
+
const matched = regex.test(content);
|
|
4554
|
+
return {
|
|
4555
|
+
check: { type: "file_contains", file, pattern },
|
|
4556
|
+
passed: matched,
|
|
4557
|
+
output: matched ? `\u6587\u4EF6\u5305\u542B "${pattern}"` : `\u6587\u4EF6\u4E0D\u5305\u542B "${pattern}"`
|
|
4558
|
+
};
|
|
4559
|
+
} catch {
|
|
4560
|
+
return {
|
|
4561
|
+
check: { type: "file_contains", file, pattern },
|
|
4562
|
+
passed: false,
|
|
4563
|
+
output: `\u65E0\u6CD5\u8BFB\u53D6\u6587\u4EF6: ${file}`
|
|
4564
|
+
};
|
|
4565
|
+
}
|
|
4566
|
+
}
|
|
4567
|
+
async function verifyTestCount(minCount) {
|
|
4568
|
+
const r = await runShell(`bun test`, 6e4);
|
|
4569
|
+
const combined = `${r.stdout}
|
|
4570
|
+
${r.stderr}`;
|
|
4571
|
+
if (r.errored) {
|
|
4572
|
+
return {
|
|
4573
|
+
check: { type: "test_count", minCount },
|
|
4574
|
+
passed: false,
|
|
4575
|
+
output: "\u65E0\u6CD5\u6267\u884C bun test"
|
|
4576
|
+
};
|
|
4577
|
+
}
|
|
4578
|
+
const matches = [...combined.matchAll(/(\d+)\s+pass/gi)];
|
|
4579
|
+
const count = matches.length > 0 ? parseInt(matches[matches.length - 1][1], 10) : 0;
|
|
4580
|
+
const passed = count >= minCount;
|
|
4581
|
+
return {
|
|
4582
|
+
check: { type: "test_count", minCount },
|
|
4583
|
+
passed,
|
|
4584
|
+
output: `${count} pass (\u9700\u8981 >=${minCount})`
|
|
4585
|
+
};
|
|
4586
|
+
}
|
|
4587
|
+
async function runVerification(checks) {
|
|
4588
|
+
if (checks.length === 0) {
|
|
4589
|
+
return { passed: true, details: [], summary: "\u65E0\u9A8C\u8BC1\u9879" };
|
|
4590
|
+
}
|
|
4591
|
+
const results = [];
|
|
4592
|
+
for (const check of checks) {
|
|
4593
|
+
let result;
|
|
4594
|
+
switch (check.type) {
|
|
4595
|
+
case "shell":
|
|
4596
|
+
result = await verifyShell(check.command ?? "", check.timeout ?? 3e4);
|
|
4597
|
+
break;
|
|
4598
|
+
case "file_exists":
|
|
4599
|
+
result = verifyFileExists(check.file ?? "");
|
|
4600
|
+
break;
|
|
4601
|
+
case "file_contains":
|
|
4602
|
+
result = verifyFileContains(check.file ?? "", check.pattern ?? "");
|
|
4603
|
+
break;
|
|
4604
|
+
case "test_count":
|
|
4605
|
+
result = await verifyTestCount(check.minCount ?? 0);
|
|
4606
|
+
break;
|
|
4607
|
+
default:
|
|
4608
|
+
result = {
|
|
4609
|
+
check,
|
|
4610
|
+
passed: false,
|
|
4611
|
+
output: `\u672A\u77E5\u9A8C\u8BC1\u7C7B\u578B: ${check.type}`
|
|
4612
|
+
};
|
|
4613
|
+
}
|
|
4614
|
+
results.push(result);
|
|
4615
|
+
}
|
|
4616
|
+
const allPassed = results.every((r) => r.passed);
|
|
4617
|
+
const failedNames = results.filter((r) => !r.passed).map((r) => formatCheckName(r.check));
|
|
4618
|
+
let summary;
|
|
4619
|
+
if (allPassed) {
|
|
4620
|
+
summary = `\u2705 \u5168\u90E8\u901A\u8FC7 (${results.length}/${results.length})`;
|
|
4621
|
+
} else {
|
|
4622
|
+
summary = `\u274C \u9A8C\u8BC1\u672A\u901A\u8FC7: ${failedNames.join(", ")}`;
|
|
4623
|
+
}
|
|
4624
|
+
return { passed: allPassed, details: results, summary };
|
|
4625
|
+
}
|
|
4626
|
+
function formatCheckName(check) {
|
|
4627
|
+
switch (check.type) {
|
|
4628
|
+
case "shell":
|
|
4629
|
+
return `shell(${(check.command ?? "").slice(0, 40)})`;
|
|
4630
|
+
case "file_exists":
|
|
4631
|
+
return `exists(${check.file})`;
|
|
4632
|
+
case "file_contains":
|
|
4633
|
+
return `contains(${check.file}:${check.pattern})`;
|
|
4634
|
+
case "test_count":
|
|
4635
|
+
return `tests(>=${check.minCount})`;
|
|
4636
|
+
default:
|
|
4637
|
+
return check.type;
|
|
4638
|
+
}
|
|
4639
|
+
}
|
|
4640
|
+
|
|
4641
|
+
// src/plugins/goalState.ts
|
|
4642
|
+
import { mkdir as mkdir6, appendFile, writeFile as writeFile6, unlink as unlink2, rmdir as rmdir2 } from "fs/promises";
|
|
4643
|
+
import { existsSync as existsSync8, readFileSync as readFileSync2 } from "fs";
|
|
4644
|
+
import { join as join8 } from "path";
|
|
4645
|
+
var Phase = /* @__PURE__ */ ((Phase2) => {
|
|
4646
|
+
Phase2["PLAN"] = "plan";
|
|
4647
|
+
Phase2["BUILD"] = "build";
|
|
4648
|
+
Phase2["VERIFY"] = "verify";
|
|
4649
|
+
Phase2["HEAL"] = "heal";
|
|
4650
|
+
Phase2["DONE"] = "done";
|
|
4651
|
+
return Phase2;
|
|
4652
|
+
})(Phase || {});
|
|
4653
|
+
var VALID_PHASES = new Set(Object.values(Phase));
|
|
4654
|
+
var PHASE_TRANSITIONS = {
|
|
4655
|
+
["plan" /* PLAN */]: {
|
|
4656
|
+
plan_complete: "build" /* BUILD */,
|
|
4657
|
+
stuck: "plan" /* PLAN */
|
|
4658
|
+
},
|
|
4659
|
+
["build" /* BUILD */]: {
|
|
4660
|
+
task_complete: "verify" /* VERIFY */,
|
|
4661
|
+
need_replan: "plan" /* PLAN */,
|
|
4662
|
+
tests_failing: "heal" /* HEAL */
|
|
4663
|
+
},
|
|
4664
|
+
["verify" /* VERIFY */]: {
|
|
4665
|
+
all_pass: "build" /* BUILD */,
|
|
4666
|
+
failures: "heal" /* HEAL */,
|
|
4667
|
+
goal_complete: "done" /* DONE */
|
|
4668
|
+
},
|
|
4669
|
+
["heal" /* HEAL */]: {
|
|
4670
|
+
fixed: "verify" /* VERIFY */,
|
|
4671
|
+
cannot_fix_locally: "plan" /* PLAN */
|
|
4672
|
+
},
|
|
4673
|
+
["done" /* DONE */]: {}
|
|
4674
|
+
};
|
|
4675
|
+
function isLegalTransition(from, to) {
|
|
4676
|
+
if (from === to) return true;
|
|
4677
|
+
const allowed = PHASE_TRANSITIONS[from];
|
|
4678
|
+
if (!allowed) return false;
|
|
4679
|
+
return Object.values(allowed).includes(to);
|
|
4680
|
+
}
|
|
4681
|
+
var LEARNINGS_TAIL_LINES = 20;
|
|
4682
|
+
var GoalState = class {
|
|
4683
|
+
dir;
|
|
4684
|
+
constructor(workspaceDir, sessionTag) {
|
|
4685
|
+
const suffix = sessionTag ? `-${sessionTag}` : "";
|
|
4686
|
+
this.dir = join8(workspaceDir, `.minimal-agent${suffix}`);
|
|
4687
|
+
}
|
|
4688
|
+
async reset() {
|
|
4689
|
+
const files = ["goal.md", "completion.md", "phase.md", "progress.md", "learnings.md", "decisions.md"];
|
|
4690
|
+
for (const f of files) {
|
|
4691
|
+
try {
|
|
4692
|
+
await unlink2(join8(this.dir, f));
|
|
4693
|
+
} catch {
|
|
4694
|
+
}
|
|
4695
|
+
}
|
|
4696
|
+
}
|
|
4697
|
+
async init(goal, criteria) {
|
|
4698
|
+
await mkdir6(this.dir, { recursive: true });
|
|
4699
|
+
const files = {
|
|
4700
|
+
goal: `# \u4E0D\u53EF\u53D8\u76EE\u6807 (IMMUTABLE GOAL)
|
|
4701
|
+
|
|
4702
|
+
${goal}
|
|
4703
|
+
`,
|
|
4704
|
+
completion: JSON.stringify(criteria, null, 2),
|
|
4705
|
+
phase: "plan" /* PLAN */,
|
|
4706
|
+
progress: "",
|
|
4707
|
+
learnings: "",
|
|
4708
|
+
decisions: ""
|
|
4709
|
+
};
|
|
4710
|
+
for (const [name, content] of Object.entries(files)) {
|
|
4711
|
+
const path2 = join8(this.dir, `${name}.md`);
|
|
4712
|
+
if (!existsSync8(path2)) {
|
|
4713
|
+
await writeFile6(path2, content);
|
|
4714
|
+
}
|
|
4715
|
+
}
|
|
4716
|
+
}
|
|
4717
|
+
get goal() {
|
|
4718
|
+
try {
|
|
4719
|
+
return readFileSync2(join8(this.dir, "goal.md"), "utf8").trim();
|
|
4720
|
+
} catch {
|
|
4721
|
+
return "";
|
|
4722
|
+
}
|
|
4723
|
+
}
|
|
4724
|
+
get completionCriteria() {
|
|
4725
|
+
try {
|
|
4726
|
+
const raw = readFileSync2(join8(this.dir, "completion.md"), "utf8").trim();
|
|
4727
|
+
return JSON.parse(raw);
|
|
4728
|
+
} catch {
|
|
4729
|
+
return [];
|
|
4730
|
+
}
|
|
4731
|
+
}
|
|
4732
|
+
get currentPhase() {
|
|
4733
|
+
try {
|
|
4734
|
+
const raw = readFileSync2(join8(this.dir, "phase.md"), "utf8").trim();
|
|
4735
|
+
if (VALID_PHASES.has(raw)) {
|
|
4736
|
+
return raw;
|
|
4737
|
+
}
|
|
4738
|
+
} catch {
|
|
4739
|
+
}
|
|
4740
|
+
return "plan" /* PLAN */;
|
|
4741
|
+
}
|
|
4742
|
+
/**
|
|
4743
|
+
* 切换阶段。`reason` 是给人看的日志文本,不参与校验。
|
|
4744
|
+
* 校验只看 from→to 是否在 PHASE_TRANSITIONS 表里有路径(任意 event 通向 to 即合法)。
|
|
4745
|
+
* 想绕开 FSM 用 forceSetPhase。
|
|
4746
|
+
*/
|
|
4747
|
+
async setPhase(phase, reason) {
|
|
4748
|
+
if (!VALID_PHASES.has(phase)) {
|
|
4749
|
+
throw new Error(`Invalid phase: ${phase}`);
|
|
4750
|
+
}
|
|
4751
|
+
const current = this.currentPhase;
|
|
4752
|
+
if (!isLegalTransition(current, phase)) {
|
|
4753
|
+
throw new Error(
|
|
4754
|
+
`\u975E\u6CD5\u9636\u6BB5\u5207\u6362: ${current} \u2192 ${phase}\u3002\u8BE5\u76EE\u6807\u9636\u6BB5\u4E0D\u5728 PHASE_TRANSITIONS[${current}] \u7684\u53EF\u8FBE\u96C6\u5408\u5185\uFF0C\u9700\u8981\u8D70 forceSetPhase\u3002`
|
|
4755
|
+
);
|
|
4756
|
+
}
|
|
4757
|
+
await writeFile6(join8(this.dir, "phase.md"), phase);
|
|
4758
|
+
await this.appendProgress(`PHASE \u2192 ${phase}: ${reason}`);
|
|
4759
|
+
}
|
|
4760
|
+
async forceSetPhase(phase, reason) {
|
|
4761
|
+
if (!VALID_PHASES.has(phase)) {
|
|
4762
|
+
throw new Error(`Invalid phase: ${phase}`);
|
|
4763
|
+
}
|
|
4764
|
+
await writeFile6(join8(this.dir, "phase.md"), phase);
|
|
4765
|
+
await this.appendProgress(`PHASE \u2192 ${phase}: ${reason}`);
|
|
4766
|
+
}
|
|
4767
|
+
async appendProgress(line) {
|
|
4768
|
+
const timestamp = (/* @__PURE__ */ new Date()).toLocaleTimeString("en-US", { hour12: false });
|
|
4769
|
+
await appendFile(join8(this.dir, "progress.md"), `[${timestamp}] ${line}
|
|
4770
|
+
`);
|
|
4771
|
+
}
|
|
4772
|
+
tailProgress(n) {
|
|
4773
|
+
try {
|
|
4774
|
+
const content = readFileSync2(join8(this.dir, "progress.md"), "utf8");
|
|
4775
|
+
const lines = content.trim().split("\n").filter(Boolean);
|
|
4776
|
+
return lines.slice(-n).join("\n");
|
|
4777
|
+
} catch {
|
|
4778
|
+
return "";
|
|
4779
|
+
}
|
|
4780
|
+
}
|
|
4781
|
+
async appendLearning(lesson) {
|
|
4782
|
+
await appendFile(join8(this.dir, "learnings.md"), `- ${lesson}
|
|
4783
|
+
`);
|
|
4784
|
+
}
|
|
4785
|
+
get learnings() {
|
|
4786
|
+
try {
|
|
4787
|
+
const raw = readFileSync2(join8(this.dir, "learnings.md"), "utf8").trim();
|
|
4788
|
+
if (!raw) return "";
|
|
4789
|
+
const lines = raw.split("\n").filter(Boolean);
|
|
4790
|
+
return lines.slice(-LEARNINGS_TAIL_LINES).join("\n");
|
|
4791
|
+
} catch {
|
|
4792
|
+
return "";
|
|
4793
|
+
}
|
|
4794
|
+
}
|
|
4795
|
+
async recordDecision(ctx, options, chosen, reasoning) {
|
|
4796
|
+
const entry = {
|
|
4797
|
+
iteration: ctx.iteration,
|
|
4798
|
+
phase: ctx.phase,
|
|
4799
|
+
contextSummary: ctx.summary,
|
|
4800
|
+
options,
|
|
4801
|
+
chosen,
|
|
4802
|
+
reasoning
|
|
4803
|
+
};
|
|
4804
|
+
const line = JSON.stringify(entry);
|
|
4805
|
+
await appendFile(join8(this.dir, "decisions.md"), `${line}
|
|
4806
|
+
`);
|
|
4807
|
+
}
|
|
4808
|
+
findSimilarDecisions(ctx, k = 3) {
|
|
4809
|
+
try {
|
|
4810
|
+
const content = readFileSync2(join8(this.dir, "decisions.md"), "utf8");
|
|
4811
|
+
const lines = content.trim().split("\n").filter(Boolean);
|
|
4812
|
+
const entries = [];
|
|
4813
|
+
for (const line of lines) {
|
|
4814
|
+
try {
|
|
4815
|
+
entries.push(JSON.parse(line));
|
|
4816
|
+
} catch {
|
|
4817
|
+
}
|
|
4818
|
+
}
|
|
4819
|
+
const scored = entries.map((entry) => ({
|
|
4820
|
+
entry,
|
|
4821
|
+
score: this._similarity(entry.contextSummary, ctx.summary)
|
|
4822
|
+
})).sort((a, b) => b.score - a.score);
|
|
4823
|
+
return scored.slice(0, k).filter((s) => s.score > 0.3).map((s) => s.entry);
|
|
4824
|
+
} catch {
|
|
4825
|
+
return [];
|
|
4826
|
+
}
|
|
4827
|
+
}
|
|
4828
|
+
summarizeDecisions(maxEntries = 5) {
|
|
4829
|
+
try {
|
|
4830
|
+
const content = readFileSync2(join8(this.dir, "decisions.md"), "utf8");
|
|
4831
|
+
const lines = content.trim().split("\n").filter(Boolean);
|
|
4832
|
+
const entries = [];
|
|
4833
|
+
for (const line of lines.slice(-maxEntries * 2)) {
|
|
4834
|
+
try {
|
|
4835
|
+
entries.push(JSON.parse(line));
|
|
4836
|
+
} catch {
|
|
4837
|
+
}
|
|
4838
|
+
}
|
|
4839
|
+
return entries.slice(-maxEntries).map(
|
|
4840
|
+
(e) => `\u8FED\u4EE3 ${e.iteration}\uFF08${e.phase}\uFF09: \u5728 [${e.options.join(", ")}] \u4E2D\u9009\u62E9\u4E86 **${e.chosen}**\uFF0C\u539F\u56E0\uFF1A${e.reasoning.slice(0, 80)}`
|
|
4841
|
+
).join("\n");
|
|
4842
|
+
} catch {
|
|
4843
|
+
return "(\u65E0\u51B3\u7B56\u8BB0\u5F55)";
|
|
4844
|
+
}
|
|
4845
|
+
}
|
|
4846
|
+
composeContext(iteration) {
|
|
4847
|
+
const sections = [];
|
|
4848
|
+
sections.push(`# \u4E0D\u53EF\u53D8\u76EE\u6807 (IMMUTABLE GOAL)
|
|
4849
|
+
${this.goal}
|
|
4850
|
+
\u26A0\uFE0F \u6CE8\u610F\uFF1A\u4F60\u4E0D\u80FD\u4FEE\u6539\u6216\u6269\u5927\u4E0A\u8FF0\u76EE\u6807\u3002\u5982\u679C\u4F60\u8BA4\u4E3A\u76EE\u6807\u672C\u8EAB\u6709\u95EE\u9898\uFF0C\u8BF7\u8F93\u51FA <PROMISE>NEED_GOAL_REVISION</PROMISE> \u5E76\u505C\u6B62\u3002`);
|
|
4851
|
+
sections.push(`# \u5F53\u524D\u9636\u6BB5: ${this.currentPhase.toUpperCase()}`);
|
|
4852
|
+
const lrn = this.learnings;
|
|
4853
|
+
if (lrn) {
|
|
4854
|
+
sections.push(`# \u5173\u952E\u6559\u8BAD\uFF08\u5FC5\u987B\u9075\u5B88\uFF0C\u907F\u514D\u91CD\u590D\u8E29\u5751\uFF09
|
|
4855
|
+
${lrn}`);
|
|
4856
|
+
}
|
|
4857
|
+
const decisions = this.summarizeDecisions(3);
|
|
4858
|
+
if (decisions !== "(\u65E0\u51B3\u7B56\u8BB0\u5F55)") {
|
|
4859
|
+
sections.push(`# \u5386\u53F2\u5173\u952E\u51B3\u7B56\uFF08\u8BF7\u53C2\u8003\uFF0C\u907F\u514D\u91CD\u590D\u8BD5\u9519\uFF09
|
|
4860
|
+
${decisions}`);
|
|
4861
|
+
}
|
|
4862
|
+
const recentProgress = this.tailProgress(10);
|
|
4863
|
+
if (recentProgress) {
|
|
4864
|
+
sections.push(`# \u6700\u8FD1\u8FDB\u5EA6
|
|
4865
|
+
${recentProgress}`);
|
|
4866
|
+
}
|
|
4867
|
+
sections.push(`---
|
|
4868
|
+
|
|
4869
|
+
# \u672C\u8F6E\u4EFB\u52A1 (\u8FED\u4EE3 ${iteration})`);
|
|
4870
|
+
return sections.join("\n\n---\n\n");
|
|
4871
|
+
}
|
|
4872
|
+
async cleanup() {
|
|
4873
|
+
const files = ["goal.md", "completion.md", "phase.md", "progress.md", "learnings.md", "decisions.md"];
|
|
4874
|
+
for (const f of files) {
|
|
4875
|
+
try {
|
|
4876
|
+
await unlink2(join8(this.dir, f));
|
|
4877
|
+
} catch {
|
|
4878
|
+
}
|
|
4879
|
+
}
|
|
4880
|
+
try {
|
|
4881
|
+
await rmdir2(this.dir);
|
|
4882
|
+
} catch {
|
|
4883
|
+
}
|
|
4884
|
+
}
|
|
4885
|
+
_similarity(a, b) {
|
|
4886
|
+
if (!a || !b) return 0;
|
|
4887
|
+
const wordsA = new Set(a.toLowerCase().split(/\s+/));
|
|
4888
|
+
const wordsB = new Set(b.toLowerCase().split(/\s+/));
|
|
4889
|
+
let intersection = 0;
|
|
4890
|
+
for (const word of wordsA) {
|
|
4891
|
+
if (wordsB.has(word)) intersection++;
|
|
4892
|
+
}
|
|
4893
|
+
const union = (/* @__PURE__ */ new Set([...wordsA, ...wordsB])).size;
|
|
4894
|
+
return union > 0 ? intersection / union : 0;
|
|
4895
|
+
}
|
|
4896
|
+
};
|
|
4897
|
+
|
|
3550
4898
|
// src/context/microCompactLite.ts
|
|
3551
4899
|
import { createHash as createHash2 } from "crypto";
|
|
3552
4900
|
var MAX_REPEAT_COUNT = 3;
|
|
@@ -3782,6 +5130,179 @@ function previewArgs(rawJson) {
|
|
|
3782
5130
|
return oneLine.slice(0, 60) + "...";
|
|
3783
5131
|
}
|
|
3784
5132
|
|
|
5133
|
+
// src/plugins/pluginRunner.ts
|
|
5134
|
+
var DEFAULT_MAX_ITERATIONS = 50;
|
|
5135
|
+
var SAFETY_CEILING = 200;
|
|
5136
|
+
function extractMaxIterations(args) {
|
|
5137
|
+
const match = args.match(/--max-iterations\s+(\d+)/i);
|
|
5138
|
+
return match ? parseInt(match[1], 10) : void 0;
|
|
5139
|
+
}
|
|
5140
|
+
async function* runWithPlugins(userInput, options) {
|
|
5141
|
+
const { provider, history, signal } = options;
|
|
5142
|
+
const isSlashCommand = userInput.trimStart().startsWith("/");
|
|
5143
|
+
let currentInput = userInput;
|
|
5144
|
+
let matchedCmd = null;
|
|
5145
|
+
if (isSlashCommand) {
|
|
5146
|
+
await discoverPlugins();
|
|
5147
|
+
matchedCmd = resolveCommand(userInput);
|
|
5148
|
+
if (matchedCmd) {
|
|
5149
|
+
currentInput = buildCommandInput(matchedCmd);
|
|
5150
|
+
}
|
|
5151
|
+
}
|
|
5152
|
+
const activeHookPlugins = matchedCmd ? getActiveStopHookPlugins() : [];
|
|
5153
|
+
const enterLoop = matchedCmd !== null && activeHookPlugins.length > 0;
|
|
5154
|
+
if (!enterLoop) {
|
|
5155
|
+
yield* runQuery(currentInput, {
|
|
5156
|
+
provider,
|
|
5157
|
+
history,
|
|
5158
|
+
signal,
|
|
5159
|
+
maxTurns: options.maxTurns,
|
|
5160
|
+
sessionState: options.sessionState
|
|
5161
|
+
});
|
|
5162
|
+
return;
|
|
5163
|
+
}
|
|
5164
|
+
const cmd = matchedCmd.cmd;
|
|
5165
|
+
const maxIter = Math.min(
|
|
5166
|
+
extractMaxIterations(currentInput) ?? DEFAULT_MAX_ITERATIONS,
|
|
5167
|
+
SAFETY_CEILING
|
|
5168
|
+
);
|
|
5169
|
+
yield {
|
|
5170
|
+
type: "plugin_start",
|
|
5171
|
+
pluginName: cmd.pluginName,
|
|
5172
|
+
description: cmd.description
|
|
5173
|
+
};
|
|
5174
|
+
const checks = parseVerifyArgs(matchedCmd.arguments);
|
|
5175
|
+
const goalState = new GoalState(getWorkingDir(), cmd.pluginName);
|
|
5176
|
+
await goalState.reset();
|
|
5177
|
+
await goalState.init(currentInput, checks);
|
|
5178
|
+
await goalState.appendProgress(`=== Loop \u542F\u52A8 === \u76EE\u6807: ${currentInput.slice(0, 120)}...`);
|
|
5179
|
+
const baseHistory = history.slice();
|
|
5180
|
+
let iterationCount = 0;
|
|
5181
|
+
let consecutiveFailures = 0;
|
|
5182
|
+
let finalAssistantMsg = null;
|
|
5183
|
+
try {
|
|
5184
|
+
do {
|
|
5185
|
+
iterationCount++;
|
|
5186
|
+
if (iterationCount > maxIter) {
|
|
5187
|
+
await goalState.forceSetPhase("done" /* DONE */, `\u8FBE\u5230\u8FED\u4EE3\u4E0A\u9650 ${maxIter}`);
|
|
5188
|
+
await goalState.appendLearning(`[\u8FED\u4EE3\u4E0A\u9650] \u5FAA\u73AF\u5728 ${iterationCount - 1} \u8F6E\u540E\u5F3A\u5236\u7EC8\u6B62\uFF0C\u53EF\u80FD\u76EE\u6807\u8FC7\u5927\u6216\u9677\u5165\u6B7B\u5FAA\u73AF`);
|
|
5189
|
+
yield { type: "error", error: `Loop \u5DF2\u8FBE\u8FED\u4EE3\u4E0A\u9650 ${maxIter}\uFF0C\u81EA\u52A8\u505C\u6B62` };
|
|
5190
|
+
return;
|
|
5191
|
+
}
|
|
5192
|
+
if (signal?.aborted) {
|
|
5193
|
+
yield { type: "interrupted" };
|
|
5194
|
+
return;
|
|
5195
|
+
}
|
|
5196
|
+
yield {
|
|
5197
|
+
type: "plugin_iteration",
|
|
5198
|
+
pluginName: cmd.pluginName,
|
|
5199
|
+
current: iterationCount,
|
|
5200
|
+
max: maxIter
|
|
5201
|
+
};
|
|
5202
|
+
history.length = 0;
|
|
5203
|
+
history.push(...baseHistory);
|
|
5204
|
+
const freshContext = goalState.composeContext(iterationCount);
|
|
5205
|
+
const enhancedInput = `${freshContext}
|
|
5206
|
+
|
|
5207
|
+
${currentInput}`;
|
|
5208
|
+
yield* runQuery(enhancedInput, {
|
|
5209
|
+
provider,
|
|
5210
|
+
history,
|
|
5211
|
+
signal,
|
|
5212
|
+
maxTurns: options.maxTurns,
|
|
5213
|
+
sessionState: options.sessionState
|
|
5214
|
+
});
|
|
5215
|
+
if (signal?.aborted) {
|
|
5216
|
+
yield { type: "interrupted" };
|
|
5217
|
+
return;
|
|
5218
|
+
}
|
|
5219
|
+
const lastAssistantIdx = (() => {
|
|
5220
|
+
for (let i = history.length - 1; i >= 0; i--) {
|
|
5221
|
+
if (history[i].role === "assistant") return i;
|
|
5222
|
+
}
|
|
5223
|
+
return -1;
|
|
5224
|
+
})();
|
|
5225
|
+
finalAssistantMsg = lastAssistantIdx >= 0 ? history[lastAssistantIdx] : null;
|
|
5226
|
+
const lastAssistantText = finalAssistantMsg ? typeof finalAssistantMsg.content === "string" ? finalAssistantMsg.content : JSON.stringify(finalAssistantMsg.content) : "";
|
|
5227
|
+
const hasCompleteSentinel = /<promise>(?:COMPLETE|DONE|GOAL_COMPLETE)<\/promise>/i.test(lastAssistantText);
|
|
5228
|
+
const hasNeedReplan = /<PROMISE>NEED_REPLAN<\/PROMISE>/i.test(lastAssistantText);
|
|
5229
|
+
if (hasCompleteSentinel) {
|
|
5230
|
+
await goalState.forceSetPhase("verify" /* VERIFY */, "\u68C0\u6D4B\u5230\u5B8C\u6210\u54E8\u5175\uFF0C\u8FDB\u5165\u9A8C\u8BC1");
|
|
5231
|
+
await goalState.appendProgress(`\u8FED\u4EE3 ${iterationCount}: \u68C0\u6D4B\u5230\u5B8C\u6210\u54E8\u5175\uFF0C\u8FD0\u884C\u9A8C\u8BC1\u95E8...`);
|
|
5232
|
+
if (checks.length > 0) {
|
|
5233
|
+
const vResult = await runVerification(checks);
|
|
5234
|
+
if (!vResult.passed) {
|
|
5235
|
+
consecutiveFailures++;
|
|
5236
|
+
await goalState.appendLearning(
|
|
5237
|
+
`[\u8FED\u4EE3 ${iterationCount}] \u58F0\u79F0\u5B8C\u6210\u4F46\u9A8C\u8BC1\u672A\u901A\u8FC7: ${vResult.summary}`
|
|
5238
|
+
);
|
|
5239
|
+
yield {
|
|
5240
|
+
type: "error",
|
|
5241
|
+
error: `\u26A0\uFE0F \u9A8C\u8BC1\u672A\u901A\u8FC7: ${vResult.summary}\u3002\u7EE7\u7EED\u5C1D\u8BD5...`
|
|
5242
|
+
};
|
|
5243
|
+
if (consecutiveFailures >= 3) {
|
|
5244
|
+
await goalState.forceSetPhase(
|
|
5245
|
+
"heal" /* HEAL */,
|
|
5246
|
+
`\u8FDE\u7EED ${consecutiveFailures} \u6B21\u9A8C\u8BC1\u5931\u8D25`
|
|
5247
|
+
);
|
|
5248
|
+
} else {
|
|
5249
|
+
await goalState.setPhase("build" /* BUILD */, "\u9A8C\u8BC1\u672A\u901A\u8FC7\uFF0C\u8FD4\u56DE\u6784\u5EFA");
|
|
5250
|
+
}
|
|
5251
|
+
continue;
|
|
5252
|
+
}
|
|
5253
|
+
await goalState.appendProgress(`\u2705 \u9A8C\u8BC1\u901A\u8FC7: ${vResult.summary}`);
|
|
5254
|
+
}
|
|
5255
|
+
await goalState.setPhase("done" /* DONE */, "goal complete & verified");
|
|
5256
|
+
yield {
|
|
5257
|
+
type: "plugin_iteration",
|
|
5258
|
+
pluginName: cmd.pluginName,
|
|
5259
|
+
current: iterationCount,
|
|
5260
|
+
max: maxIter
|
|
5261
|
+
};
|
|
5262
|
+
return;
|
|
5263
|
+
}
|
|
5264
|
+
if (hasNeedReplan) {
|
|
5265
|
+
await goalState.forceSetPhase("plan" /* PLAN */, "agent \u8BF7\u6C42\u91CD\u65B0\u89C4\u5212");
|
|
5266
|
+
await goalState.appendLearning("[NEED_REPLAN] Agent \u8BA4\u4E3A\u5F53\u524D\u65B9\u6848\u4E0D\u53EF\u884C\uFF0C\u9700\u8981\u91CD\u65B0\u89C4\u5212");
|
|
5267
|
+
await goalState.appendProgress("Agent \u8BF7\u6C42 NEED_REPLAN\uFF0C\u56DE PLAN \u9636\u6BB5");
|
|
5268
|
+
consecutiveFailures = 0;
|
|
5269
|
+
continue;
|
|
5270
|
+
}
|
|
5271
|
+
if (goalState.currentPhase === "plan" /* PLAN */ && iterationCount >= 2) {
|
|
5272
|
+
await goalState.setPhase("build" /* BUILD */, "\u89C4\u5212\u9636\u6BB5\u5DF2\u5B8C\u6210\uFF0C\u8FDB\u5165\u6784\u5EFA");
|
|
5273
|
+
}
|
|
5274
|
+
const hookTranscript = lastAssistantText;
|
|
5275
|
+
const hookResult = await executeStopHooks(activeHookPlugins, hookTranscript);
|
|
5276
|
+
if (hookResult.decision === "block" && hookResult.reason) {
|
|
5277
|
+
currentInput = hookResult.reason;
|
|
5278
|
+
consecutiveFailures = 0;
|
|
5279
|
+
await goalState.recordDecision(
|
|
5280
|
+
{ iteration: iterationCount, phase: goalState.currentPhase, summary: "stop-hook \u53CD\u9988" },
|
|
5281
|
+
["\u7EE7\u7EED\u5FAA\u73AF", "\u7EC8\u6B62"],
|
|
5282
|
+
"\u7EE7\u7EED\u5FAA\u73AF",
|
|
5283
|
+
hookResult.reason.slice(0, 200)
|
|
5284
|
+
);
|
|
5285
|
+
if (hookResult.systemMessage) {
|
|
5286
|
+
baseHistory.push({
|
|
5287
|
+
role: "user",
|
|
5288
|
+
content: `[Plugin Stop Hook] ${hookResult.systemMessage}`
|
|
5289
|
+
});
|
|
5290
|
+
}
|
|
5291
|
+
await goalState.appendProgress(`\u8FED\u4EE3 ${iterationCount}: Stop hook block\uFF0C\u6CE8\u5165\u53CD\u9988\u7EE7\u7EED`);
|
|
5292
|
+
} else {
|
|
5293
|
+
await goalState.appendProgress(`\u8FED\u4EE3 ${iterationCount}: \u65E0\u54E8\u5175 / hook pass\uFF0C\u7EE7\u7EED\u4E0B\u4E00\u8F6E`);
|
|
5294
|
+
}
|
|
5295
|
+
} while (true);
|
|
5296
|
+
} finally {
|
|
5297
|
+
history.length = 0;
|
|
5298
|
+
history.push(...baseHistory);
|
|
5299
|
+
if (finalAssistantMsg) {
|
|
5300
|
+
history.push(finalAssistantMsg);
|
|
5301
|
+
}
|
|
5302
|
+
await goalState.cleanup();
|
|
5303
|
+
}
|
|
5304
|
+
}
|
|
5305
|
+
|
|
3785
5306
|
// src/ui/hooks/useChat.ts
|
|
3786
5307
|
function useChat(args) {
|
|
3787
5308
|
const historyRef = useRef3(args.initialHistory.slice());
|
|
@@ -3793,6 +5314,7 @@ function useChat(args) {
|
|
|
3793
5314
|
const [error, setError] = useState5(null);
|
|
3794
5315
|
const [interrupted, setInterrupted] = useState5(false);
|
|
3795
5316
|
const [compacting, setCompacting] = useState5(false);
|
|
5317
|
+
const [pluginLoop, setPluginLoop] = useState5(null);
|
|
3796
5318
|
const abortRef = useRef3(null);
|
|
3797
5319
|
useEffect(() => {
|
|
3798
5320
|
return () => {
|
|
@@ -3808,10 +5330,11 @@ function useChat(args) {
|
|
|
3808
5330
|
setError(null);
|
|
3809
5331
|
setInterrupted(false);
|
|
3810
5332
|
setStreamingText("");
|
|
5333
|
+
setPluginLoop(null);
|
|
3811
5334
|
const ac = new AbortController();
|
|
3812
5335
|
abortRef.current = ac;
|
|
3813
5336
|
try {
|
|
3814
|
-
for await (const ev of
|
|
5337
|
+
for await (const ev of runWithPlugins(trimmed, {
|
|
3815
5338
|
provider: args.provider,
|
|
3816
5339
|
history: historyRef.current,
|
|
3817
5340
|
signal: ac.signal
|
|
@@ -3822,6 +5345,7 @@ function useChat(args) {
|
|
|
3822
5345
|
setCompacting,
|
|
3823
5346
|
setError,
|
|
3824
5347
|
setInterrupted,
|
|
5348
|
+
setPluginLoop,
|
|
3825
5349
|
bump
|
|
3826
5350
|
});
|
|
3827
5351
|
}
|
|
@@ -3832,6 +5356,7 @@ function useChat(args) {
|
|
|
3832
5356
|
setStreamingText("");
|
|
3833
5357
|
setToolStatus(null);
|
|
3834
5358
|
setCompacting(false);
|
|
5359
|
+
setPluginLoop(null);
|
|
3835
5360
|
abortRef.current = null;
|
|
3836
5361
|
args.onPersist?.(historyRef.current);
|
|
3837
5362
|
}
|
|
@@ -3854,6 +5379,7 @@ function useChat(args) {
|
|
|
3854
5379
|
setCompacting(false);
|
|
3855
5380
|
bump();
|
|
3856
5381
|
await clearContext();
|
|
5382
|
+
clearFileState();
|
|
3857
5383
|
}, [bump, isLoading]);
|
|
3858
5384
|
const compactNow = useCallback5(async () => {
|
|
3859
5385
|
if (isLoading) return;
|
|
@@ -3887,6 +5413,7 @@ function useChat(args) {
|
|
|
3887
5413
|
error,
|
|
3888
5414
|
interrupted,
|
|
3889
5415
|
compacting,
|
|
5416
|
+
pluginLoop,
|
|
3890
5417
|
submit,
|
|
3891
5418
|
abort,
|
|
3892
5419
|
clearHistory,
|
|
@@ -3934,6 +5461,16 @@ function handleEvent(ev, setters) {
|
|
|
3934
5461
|
setters.setError(ev.error);
|
|
3935
5462
|
setters.bump();
|
|
3936
5463
|
break;
|
|
5464
|
+
case "plugin_start":
|
|
5465
|
+
break;
|
|
5466
|
+
case "plugin_iteration":
|
|
5467
|
+
setters.setPluginLoop({
|
|
5468
|
+
pluginName: ev.pluginName,
|
|
5469
|
+
current: ev.current,
|
|
5470
|
+
max: ev.max ?? 0
|
|
5471
|
+
});
|
|
5472
|
+
setters.bump();
|
|
5473
|
+
break;
|
|
3937
5474
|
}
|
|
3938
5475
|
}
|
|
3939
5476
|
|
|
@@ -3982,7 +5519,7 @@ function App({ provider, initialHistory }) {
|
|
|
3982
5519
|
onCompact: chat2.compactNow
|
|
3983
5520
|
}
|
|
3984
5521
|
),
|
|
3985
|
-
/* @__PURE__ */ jsx6(StatusLine, { provider, history: chat2.history })
|
|
5522
|
+
/* @__PURE__ */ jsx6(StatusLine, { provider, history: chat2.history, pluginLoop: chat2.pluginLoop })
|
|
3986
5523
|
] });
|
|
3987
5524
|
}
|
|
3988
5525
|
|
|
@@ -4041,8 +5578,28 @@ function truncateForDisplay(content, max = TOOL_OUTPUT_PREVIEW_MAX) {
|
|
|
4041
5578
|
return content.slice(0, max) + "...";
|
|
4042
5579
|
}
|
|
4043
5580
|
function extractPromptArgs(args) {
|
|
4044
|
-
const
|
|
4045
|
-
|
|
5581
|
+
const FLAG_BOOLEAN = /* @__PURE__ */ new Set([
|
|
5582
|
+
"-p",
|
|
5583
|
+
"--print",
|
|
5584
|
+
"--verbose",
|
|
5585
|
+
"-v",
|
|
5586
|
+
"-h",
|
|
5587
|
+
"--help",
|
|
5588
|
+
"-V",
|
|
5589
|
+
"--version"
|
|
5590
|
+
]);
|
|
5591
|
+
const FLAG_WITH_VALUE = /* @__PURE__ */ new Set(["-d", "--cwd"]);
|
|
5592
|
+
const result = [];
|
|
5593
|
+
for (let i = 0; i < args.length; i++) {
|
|
5594
|
+
const a = args[i];
|
|
5595
|
+
if (FLAG_BOOLEAN.has(a)) continue;
|
|
5596
|
+
if (FLAG_WITH_VALUE.has(a)) {
|
|
5597
|
+
i++;
|
|
5598
|
+
continue;
|
|
5599
|
+
}
|
|
5600
|
+
result.push(a);
|
|
5601
|
+
}
|
|
5602
|
+
return result;
|
|
4046
5603
|
}
|
|
4047
5604
|
async function runPrintMode(provider, args, initialHistory, options) {
|
|
4048
5605
|
process.stdout.on("error", handleEPIPE(process.stdout));
|
|
@@ -4073,7 +5630,7 @@ async function runPrintMode(provider, args, initialHistory, options) {
|
|
|
4073
5630
|
const history = initialHistory;
|
|
4074
5631
|
const output = { buffer: "" };
|
|
4075
5632
|
try {
|
|
4076
|
-
for await (const event of
|
|
5633
|
+
for await (const event of runWithPlugins(prompt, {
|
|
4077
5634
|
provider,
|
|
4078
5635
|
history,
|
|
4079
5636
|
signal: abortController.signal
|
|
@@ -4142,13 +5699,13 @@ function handleEvent2(event, output, verbose) {
|
|
|
4142
5699
|
}
|
|
4143
5700
|
}
|
|
4144
5701
|
function readFromStdin() {
|
|
4145
|
-
return new Promise((
|
|
5702
|
+
return new Promise((resolve9) => {
|
|
4146
5703
|
let data = "";
|
|
4147
5704
|
let settled = false;
|
|
4148
5705
|
const timer = setTimeout(() => {
|
|
4149
5706
|
if (!settled) {
|
|
4150
5707
|
settled = true;
|
|
4151
|
-
|
|
5708
|
+
resolve9("");
|
|
4152
5709
|
}
|
|
4153
5710
|
}, STDIN_TIMEOUT_MS);
|
|
4154
5711
|
process.stdin.setEncoding("utf8");
|
|
@@ -4159,7 +5716,7 @@ function readFromStdin() {
|
|
|
4159
5716
|
if (!settled) {
|
|
4160
5717
|
clearTimeout(timer);
|
|
4161
5718
|
settled = true;
|
|
4162
|
-
|
|
5719
|
+
resolve9(data.trim());
|
|
4163
5720
|
}
|
|
4164
5721
|
}
|
|
4165
5722
|
process.stdin.on("data", onData);
|
|
@@ -4172,9 +5729,17 @@ import { jsx as jsx8 } from "react/jsx-runtime";
|
|
|
4172
5729
|
var require2 = createRequire(import.meta.url);
|
|
4173
5730
|
var pkg = require2("../package.json");
|
|
4174
5731
|
async function main() {
|
|
5732
|
+
const args = process.argv.slice(2);
|
|
5733
|
+
const dirArg = extractCwdArg(args);
|
|
5734
|
+
if (dirArg) {
|
|
5735
|
+
const abs = resolve8(dirArg);
|
|
5736
|
+
if (!existsSync9(abs)) {
|
|
5737
|
+
mkdirSync(abs, { recursive: true });
|
|
5738
|
+
}
|
|
5739
|
+
process.chdir(abs);
|
|
5740
|
+
}
|
|
4175
5741
|
initWorkingDir();
|
|
4176
5742
|
await migrateLegacyContext(getWorkingDir());
|
|
4177
|
-
const args = process.argv.slice(2);
|
|
4178
5743
|
if (args.includes("-h") || args.includes("--help")) {
|
|
4179
5744
|
printHelp();
|
|
4180
5745
|
return;
|
|
@@ -4185,15 +5750,15 @@ async function main() {
|
|
|
4185
5750
|
}
|
|
4186
5751
|
const isPrintMode = args.includes("-p") || args.includes("--print");
|
|
4187
5752
|
if (isPrintMode) {
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
provider = await loadProvider();
|
|
4191
|
-
} catch (e) {
|
|
5753
|
+
const provider = await loadProviderLayered();
|
|
5754
|
+
if (!provider) {
|
|
4192
5755
|
process.stderr.write(
|
|
4193
5756
|
`
|
|
4194
|
-
|
|
5757
|
+
\u672A\u627E\u5230 provider \u914D\u7F6E\u3002
|
|
4195
5758
|
|
|
4196
|
-
\
|
|
5759
|
+
\u8BF7\u4E8C\u9009\u4E00\uFF1A
|
|
5760
|
+
1. \u8BBE\u7F6E\u73AF\u5883\u53D8\u91CF MINIMAL_AGENT_BASE_URL / MINIMAL_AGENT_API_KEY / MINIMAL_AGENT_MODEL
|
|
5761
|
+
2. \u5148\u76F4\u63A5\u8FD0\u884C \`minimal-agent\`\uFF08TUI\uFF09\u5B8C\u6210\u9996\u6B21\u914D\u7F6E\u5411\u5BFC\uFF1B\u5411\u5BFC\u4F1A\u5199\u51FA ~/.minimal-agent/config.json\uFF0C\u4E4B\u540E -p \u6A21\u5F0F\u81EA\u52A8\u590D\u7528
|
|
4197
5762
|
|
|
4198
5763
|
`
|
|
4199
5764
|
);
|
|
@@ -4227,6 +5792,8 @@ minimal-agent - \u8F7B\u91CF\u7EA7 AI \u7F16\u7A0B\u52A9\u624B
|
|
|
4227
5792
|
\u9009\u9879:
|
|
4228
5793
|
-p, --print \u975E\u4EA4\u4E92\u6A21\u5F0F\uFF0C\u76F4\u63A5\u6267\u884C\u5355\u6B21\u95EE\u7B54
|
|
4229
5794
|
-v, --verbose \u663E\u793A\u8BE6\u7EC6\u8F93\u51FA\uFF08\u5DE5\u5177\u8C03\u7528\u3001\u538B\u7F29\u4FE1\u606F\uFF09
|
|
5795
|
+
-d, --cwd <dir> \u6307\u5B9A\u5DE5\u4F5C\u76EE\u5F55\uFF08\u4E0D\u5B58\u5728\u81EA\u52A8\u521B\u5EFA\uFF09\uFF1B\u542F\u52A8\u65F6 chdir \u5230\u8FD9\u91CC\uFF0C
|
|
5796
|
+
\u4E0A\u4E0B\u6587\u6587\u4EF6\u3001\u5DE5\u5177\u76F8\u5BF9\u8DEF\u5F84\u3001.env \u52A0\u8F7D\u90FD\u4EE5\u6B64\u4E3A\u57FA\u51C6
|
|
4230
5797
|
-h, --help \u663E\u793A\u5E2E\u52A9\u4FE1\u606F
|
|
4231
5798
|
|
|
4232
5799
|
\u4F1A\u8BDD\u8BB0\u5FC6:
|
|
@@ -4238,6 +5805,7 @@ minimal-agent - \u8F7B\u91CF\u7EA7 AI \u7F16\u7A0B\u52A9\u624B
|
|
|
4238
5805
|
minimal-agent -p "\u5E2E\u6211\u5199\u4E00\u4E2A hello world"
|
|
4239
5806
|
echo "\u89E3\u91CA\u4EE3\u7801" | minimal-agent -p
|
|
4240
5807
|
minimal-agent -p --verbose "\u8FD0\u884C\u6D4B\u8BD5\u5E76\u62A5\u544A\u7ED3\u679C"
|
|
5808
|
+
minimal-agent -p "\u5904\u7406\u8D44\u6599" -d /tmp/job-123 # \u5DE5\u4F5C\u76EE\u5F55\u9694\u79BB
|
|
4241
5809
|
`);
|
|
4242
5810
|
}
|
|
4243
5811
|
main().catch((e) => {
|