minimal-agent 0.1.6 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/main.js +587 -136
- package/package.json +2 -2
package/dist/main.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/main.tsx
|
|
4
4
|
import { render } from "ink";
|
|
5
|
-
import { existsSync as
|
|
5
|
+
import { existsSync as existsSync9, mkdirSync } from "fs";
|
|
6
6
|
import { createRequire } from "module";
|
|
7
7
|
import { resolve as resolve8 } from "path";
|
|
8
8
|
|
|
@@ -360,6 +360,8 @@ ${toolList}
|
|
|
360
360
|
"\u6211\u8BB0\u5F97"\u3001"\u6211\u4E4B\u524D\u770B\u8FC7"\u3001"\u5E94\u8BE5\u5DEE\u4E0D\u591A"\u90FD\u4E0D\u7B97\u8BFB\u8FC7\u2014\u2014\u5FC5\u987B\u5728\u672C\u8F6E\u4EFB\u52A1\u4E2D\u5B9E\u9645\u8C03\u7528 Read\u3002
|
|
361
361
|
\u5C24\u5176\u5F53\u4F60\u8981\u53C2\u8003\u67D0\u4E2A\u6587\u4EF6\u7684\u5199\u6CD5\u6765\u5B9E\u73B0\u7C7B\u4F3C\u529F\u80FD\u65F6\uFF0C\u5FC5\u987B\u5148\u91CD\u8BFB\u8BE5\u6587\u4EF6\u7684\u5173\u952E\u90E8\u5206\uFF08\u51FD\u6570\u5B9E\u73B0\u3001\u5224\u65AD\u903B\u8F91\u3001\u6570\u636E\u6D41\uFF09\uFF0C\u7406\u89E3\u6E05\u695A\u540E\u518D\u52A8\u624B\u3002
|
|
362
362
|
- Edit \u5DE5\u5177\u7684 old_string \u5FC5\u987B\u5728\u6587\u4EF6\u4E2D\u552F\u4E00\uFF1B\u4E0D\u552F\u4E00\u65F6\u8BF7\u6269\u5927\u4E0A\u4E0B\u6587\u6216\u663E\u5F0F replace_all=true\u3002
|
|
363
|
+
- \u540C\u4E00\u6587\u4EF6\u9700\u8981\u4FEE\u6539\u591A\u5904\uFF083 \u5904\u53CA\u4EE5\u4E0A\uFF09\u65F6\u4F18\u5148\u7528 MultiEdit\uFF1A\u6240\u6709 edit \u6309\u987A\u5E8F\u5728\u5185\u5B58\u4E2D\u5E94\u7528\uFF0C**\u5168\u90E8\u6210\u529F\u624D\u843D\u76D8**\uFF1B\u4EFB\u4E00\u5931\u8D25\u78C1\u76D8\u4E0D\u52A8\uFF0C\u907F\u514D\u4E2D\u95F4\u72B6\u6001\u6C61\u67D3\u3002\u5355\u70B9\u4FEE\u6539\u7EE7\u7EED\u7528 Edit\u3002
|
|
364
|
+
- Write \u8986\u76D6\u65E2\u6709\u6587\u4EF6\u524D\u540C\u6837\u5FC5\u987B\u5148 Read\uFF08\u4E0E Edit \u5BF9\u79F0\uFF0C\u672A\u5148 Read \u4F1A\u88AB\u62D2\u7EDD\u5E76\u63D0\u793A"\u8BF7\u5148 Read"\uFF09\uFF1B\u5199\u65B0\u6587\u4EF6\u65E0\u6B64\u8981\u6C42\u3002
|
|
363
365
|
- \u521B\u5EFA\u65B0\u6587\u4EF6\u7528 Write\uFF0C\u6216 Edit \u65F6 old_string \u4F20\u7A7A\u5B57\u7B26\u4E32\u3002
|
|
364
366
|
- \u627E\u6587\u4EF6\u7528 Glob\uFF08"**/*.ts"\uFF09\uFF0C\u627E\u6587\u4EF6\u5185\u5BB9\u7528 Grep\uFF08\u57FA\u4E8E ripgrep\uFF09\u3002
|
|
365
367
|
- \u5F53\u7528\u6237\u95EE\u5230\u8BAD\u7EC3\u622A\u6B62\u540E\u624D\u51FA\u73B0\u7684\u4FE1\u606F\uFF08\u6700\u65B0\u7248\u672C\u53F7\u3001\u8FD1\u671F\u65B0\u95FB\u3001\u7B2C\u4E09\u65B9 API \u6587\u6863\uFF09\u65F6\u7528 WebSearch\uFF1B\u4F18\u5148\u7CBE\u786E\u7684\u81EA\u7136\u8BED\u8A00\u67E5\u8BE2\u3002
|
|
@@ -369,6 +371,8 @@ ${toolList}
|
|
|
369
371
|
Bash \u6709\u5B89\u5168\u9ED1\u540D\u5355\uFF08rm -rf /\u3001mkfs\u3001shutdown \u7B49\u5371\u9669\u547D\u4EE4\u4F1A\u88AB\u62E6\u622A\uFF09\uFF0C\u4F46\u4ECD\u9700\u8C28\u614E\uFF1A
|
|
370
372
|
\u5148\u786E\u8BA4\u547D\u4EE4\u65E0\u5BB3\u518D\u6267\u884C\uFF0C\u907F\u514D\u4E0D\u53EF\u9006\u64CD\u4F5C\uFF08\u5982 git push --force\u3001git reset --hard\uFF09\u3002
|
|
371
373
|
\u957F\u65F6\u95F4\u8FD0\u884C\u7684\u547D\u4EE4\uFF08npm install\u3001bun test\uFF09\u6CE8\u610F\u8D85\u65F6\u8BBE\u7F6E\uFF1B\u9700\u8981\u4EA4\u4E92\u8F93\u5165\u7684\u547D\u4EE4\u4E0D\u8981\u7528 Bash\uFF08\u7528 Write \u5199\u811A\u672C\u4EE3\u66FF\uFF09\u3002
|
|
374
|
+
Bash \u5DF2\u8BC6\u522B"\u4FE1\u606F\u6027\u9000\u51FA\u7801"\uFF1Agrep/rg/find/diff/test \u7684 exit=1\uFF08\u65E0\u5339\u914D / \u90E8\u5206\u4E0D\u53EF\u8BBF\u95EE / \u6709\u5DEE\u5F02 / \u6761\u4EF6\u5047\uFF09\u4F1A\u81EA\u52A8\u5224\u4E3A\u6210\u529F\uFF0C\u4E0D\u8981\u56E0 1 \u800C\u91CD\u8BD5\u3002
|
|
375
|
+
\u7834\u574F\u6027\u547D\u4EE4\uFF08git reset --hard / git push -f / rm -rf \u7B49\uFF09\u4F1A\u88AB Bash \u4E3B\u52A8\u5728\u8F93\u51FA\u5934\u90E8\u52A0 \u26A0\uFE0F \u8B66\u544A\uFF08\u4E0D\u62E6\u622A\uFF09\uFF0C\u770B\u5230\u65F6\u5E94\u5411\u7528\u6237\u786E\u8BA4\u610F\u56FE\u3002
|
|
372
376
|
- \u5F53\u9700\u8981\u83B7\u53D6\u7F51\u9875\u9759\u6001\u6587\u672C\u5185\u5BB9\uFF08\u6293\u53D6\u6587\u6863\u3001\u8BFB\u53D6\u6587\u7AE0\uFF09\u65F6\u7528 WebFetch\u3002
|
|
373
377
|
WebBrowser \u4F9D\u8D56\u53EF\u9009\u5305\uFF08playwright-core + chromium\uFF09\u2014\u2014 **\u9ED8\u8BA4\u5047\u5B9A\u672A\u5B89\u88C5**\uFF0C
|
|
374
378
|
\u4EC5\u5728 WebFetch \u660E\u786E\u65E0\u6CD5\u6EE1\u8DB3\uFF08\u5982\u9700\u8981 JS \u6E32\u67D3\u540E\u7684\u5185\u5BB9\u3001\u70B9\u51FB\u6309\u94AE\u3001\u586B\u8868\u5355\u3001\u622A\u56FE\uFF09\u65F6\u518D\u5C1D\u8BD5\u3002
|
|
@@ -462,6 +466,141 @@ function toToolParameters(schema) {
|
|
|
462
466
|
return rest;
|
|
463
467
|
}
|
|
464
468
|
|
|
469
|
+
// src/tools/bash/semantics.ts
|
|
470
|
+
var DEFAULT_SEMANTIC = (exitCode) => ({
|
|
471
|
+
isError: exitCode !== 0,
|
|
472
|
+
message: exitCode !== 0 ? `Command failed with exit code ${exitCode}` : void 0
|
|
473
|
+
});
|
|
474
|
+
var COMMAND_SEMANTICS = /* @__PURE__ */ new Map([
|
|
475
|
+
// grep: 0=找到匹配, 1=无匹配(非错误), 2+=真错误
|
|
476
|
+
["grep", (exitCode) => ({
|
|
477
|
+
isError: exitCode >= 2,
|
|
478
|
+
message: exitCode === 1 ? "No matches found" : void 0
|
|
479
|
+
})],
|
|
480
|
+
// ripgrep 与 grep 同义
|
|
481
|
+
["rg", (exitCode) => ({
|
|
482
|
+
isError: exitCode >= 2,
|
|
483
|
+
message: exitCode === 1 ? "No matches found" : void 0
|
|
484
|
+
})],
|
|
485
|
+
// find: 1=部分目录不可达(仍有结果,非致命), 2+=错误
|
|
486
|
+
["find", (exitCode) => ({
|
|
487
|
+
isError: exitCode >= 2,
|
|
488
|
+
message: exitCode === 1 ? "Some directories were inaccessible" : void 0
|
|
489
|
+
})],
|
|
490
|
+
// diff: 0=相同, 1=有差异(非错误), 2+=错误
|
|
491
|
+
["diff", (exitCode) => ({
|
|
492
|
+
isError: exitCode >= 2,
|
|
493
|
+
message: exitCode === 1 ? "Files differ" : void 0
|
|
494
|
+
})],
|
|
495
|
+
// test: 0=真, 1=假(非错误), 2+=错误
|
|
496
|
+
["test", (exitCode) => ({
|
|
497
|
+
isError: exitCode >= 2,
|
|
498
|
+
message: exitCode === 1 ? "Condition is false" : void 0
|
|
499
|
+
})],
|
|
500
|
+
// [ 是 test 的别名
|
|
501
|
+
["[", (exitCode) => ({
|
|
502
|
+
isError: exitCode >= 2,
|
|
503
|
+
message: exitCode === 1 ? "Condition is false" : void 0
|
|
504
|
+
})]
|
|
505
|
+
]);
|
|
506
|
+
function extractPrimaryCommand(command) {
|
|
507
|
+
let cmd = command.trim();
|
|
508
|
+
const wrapMatch = cmd.match(/^(?:bash|sh|zsh|dash)\s+-c\s+(['"])(.+)\1\s*$/s);
|
|
509
|
+
if (wrapMatch) cmd = wrapMatch[2].trim();
|
|
510
|
+
const segments = cmd.split(/\s*(?:\|\||&&|;|\|)\s*/).filter((s) => s.length > 0);
|
|
511
|
+
let last = segments[segments.length - 1] ?? cmd;
|
|
512
|
+
last = last.trim();
|
|
513
|
+
const tokens = last.split(/\s+/);
|
|
514
|
+
let i = 0;
|
|
515
|
+
if (tokens[i] === "env") i++;
|
|
516
|
+
while (i < tokens.length && /^[A-Za-z_][A-Za-z0-9_]*=/.test(tokens[i])) i++;
|
|
517
|
+
return tokens[i] ?? "";
|
|
518
|
+
}
|
|
519
|
+
function interpretCommandResult(command, exitCode, stdout, stderr) {
|
|
520
|
+
const base = extractPrimaryCommand(command);
|
|
521
|
+
const fn = COMMAND_SEMANTICS.get(base) ?? DEFAULT_SEMANTIC;
|
|
522
|
+
return fn(exitCode, stdout, stderr);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// src/tools/bash/warnings.ts
|
|
526
|
+
var DESTRUCTIVE_PATTERNS = [
|
|
527
|
+
// Git —— 数据丢失 / 难回退
|
|
528
|
+
{
|
|
529
|
+
pattern: /\bgit\s+reset\s+--hard\b/,
|
|
530
|
+
warning: "Note: may discard uncommitted changes"
|
|
531
|
+
},
|
|
532
|
+
{
|
|
533
|
+
pattern: /\bgit\s+push\b[^;&|\n]*[ \t](--force|--force-with-lease|-f)\b/,
|
|
534
|
+
warning: "Note: may overwrite remote history"
|
|
535
|
+
},
|
|
536
|
+
{
|
|
537
|
+
pattern: /\bgit\s+clean\b(?![^;&|\n]*(?:-[a-zA-Z]*n|--dry-run))[^;&|\n]*-[a-zA-Z]*f/,
|
|
538
|
+
warning: "Note: may permanently delete untracked files"
|
|
539
|
+
},
|
|
540
|
+
{
|
|
541
|
+
pattern: /\bgit\s+checkout\s+(--\s+)?\.[ \t]*($|[;&|\n])/,
|
|
542
|
+
warning: "Note: may discard all working tree changes"
|
|
543
|
+
},
|
|
544
|
+
{
|
|
545
|
+
pattern: /\bgit\s+restore\s+(--\s+)?\.[ \t]*($|[;&|\n])/,
|
|
546
|
+
warning: "Note: may discard all working tree changes"
|
|
547
|
+
},
|
|
548
|
+
{
|
|
549
|
+
pattern: /\bgit\s+stash[ \t]+(drop|clear)\b/,
|
|
550
|
+
warning: "Note: may permanently remove stashed changes"
|
|
551
|
+
},
|
|
552
|
+
{
|
|
553
|
+
pattern: /\bgit\s+branch\s+(-D[ \t]|--delete\s+--force|--force\s+--delete)\b/,
|
|
554
|
+
warning: "Note: may force-delete a branch"
|
|
555
|
+
},
|
|
556
|
+
// Git —— 安全绕过
|
|
557
|
+
{
|
|
558
|
+
pattern: /\bgit\s+(commit|push|merge)\b[^;&|\n]*--no-verify\b/,
|
|
559
|
+
warning: "Note: may skip safety hooks"
|
|
560
|
+
},
|
|
561
|
+
{
|
|
562
|
+
pattern: /\bgit\s+commit\b[^;&|\n]*--amend\b/,
|
|
563
|
+
warning: "Note: may rewrite the last commit"
|
|
564
|
+
},
|
|
565
|
+
// 文件删除(rm -rf / 之类的致命形式由 bash.ts 黑名单处理;这里只做"未到致命"的提醒)
|
|
566
|
+
{
|
|
567
|
+
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]/,
|
|
568
|
+
warning: "Note: may recursively force-remove files"
|
|
569
|
+
},
|
|
570
|
+
{
|
|
571
|
+
pattern: /(^|[;&|\n]\s*)rm\s+-[a-zA-Z]*[rR]/,
|
|
572
|
+
warning: "Note: may recursively remove files"
|
|
573
|
+
},
|
|
574
|
+
{
|
|
575
|
+
pattern: /(^|[;&|\n]\s*)rm\s+-[a-zA-Z]*f/,
|
|
576
|
+
warning: "Note: may force-remove files"
|
|
577
|
+
},
|
|
578
|
+
// 数据库
|
|
579
|
+
{
|
|
580
|
+
pattern: /\b(DROP|TRUNCATE)\s+(TABLE|DATABASE|SCHEMA)\b/i,
|
|
581
|
+
warning: "Note: may drop or truncate database objects"
|
|
582
|
+
},
|
|
583
|
+
{
|
|
584
|
+
pattern: /\bDELETE\s+FROM\s+\w+[ \t]*(;|"|'|\n|$)/i,
|
|
585
|
+
warning: "Note: may delete all rows from a database table"
|
|
586
|
+
},
|
|
587
|
+
// 基础设施
|
|
588
|
+
{
|
|
589
|
+
pattern: /\bkubectl\s+delete\b/,
|
|
590
|
+
warning: "Note: may delete Kubernetes resources"
|
|
591
|
+
},
|
|
592
|
+
{
|
|
593
|
+
pattern: /\bterraform\s+destroy\b/,
|
|
594
|
+
warning: "Note: may destroy Terraform infrastructure"
|
|
595
|
+
}
|
|
596
|
+
];
|
|
597
|
+
function scanDestructiveCommand(command) {
|
|
598
|
+
for (const { pattern, warning } of DESTRUCTIVE_PATTERNS) {
|
|
599
|
+
if (pattern.test(command)) return warning;
|
|
600
|
+
}
|
|
601
|
+
return null;
|
|
602
|
+
}
|
|
603
|
+
|
|
465
604
|
// src/tools/bash/bash.ts
|
|
466
605
|
var DEFAULT_TIMEOUT_MS = 12e4;
|
|
467
606
|
var MAX_TIMEOUT_MS = 6e5;
|
|
@@ -578,6 +717,14 @@ While the Bash tool can do similar things, it's better to use the built-in tools
|
|
|
578
717
|
- Never skip hooks (--no-verify) or bypass signing (--no-gpg-sign, -c commit.gpgsign=false) unless the user has explicitly asked for it. If a hook fails, investigate and fix the underlying issue.
|
|
579
718
|
- Avoid unnecessary \`sleep\` commands; do not retry failing commands in a sleep loop \u2014 diagnose the root cause.
|
|
580
719
|
|
|
720
|
+
# Exit code semantics
|
|
721
|
+
Some commands return non-zero exit codes for informational (non-error) reasons. Bash recognizes these and reports them as success (ok=true) \u2014 do NOT retry just because exit code is 1:
|
|
722
|
+
- \`grep\` / \`rg\` exit 1 \u2192 no match found (not an error)
|
|
723
|
+
- \`find\` exit 1 \u2192 some directories inaccessible (non-fatal, partial results still returned)
|
|
724
|
+
- \`diff\` / \`cmp\` exit 1 \u2192 files differ (informational, not an error)
|
|
725
|
+
- \`test\` / \`[\` exit 1 \u2192 condition is false (the answer to a question, not a failure)
|
|
726
|
+
Only exit codes \u2265 2 from these commands indicate a real failure. For all other commands, non-zero exit codes are treated as failures normally.
|
|
727
|
+
|
|
581
728
|
# Safety
|
|
582
729
|
The following command patterns are blocked at the tool level and will fail before execution (no need to try them):
|
|
583
730
|
- \`rm -rf /\` and variants targeting root, $HOME, ~, or system directories (/etc, /usr, /bin, /Windows, /Users, /home, ...)
|
|
@@ -587,7 +734,9 @@ The following command patterns are blocked at the tool level and will fail befor
|
|
|
587
734
|
- Pipe-to-shell from network: \`curl ... | sh\`, \`wget ... | bash\`, etc.
|
|
588
735
|
- \`chmod 777 /\`, Windows full-disk \`del /s\` / \`rmdir /s\`, \`diskpart\`
|
|
589
736
|
|
|
590
|
-
If you have a legitimate use case that requires one of the above patterns, ask the user to run the command themselves in their terminal \u2014 do not try to bypass the check
|
|
737
|
+
If you have a legitimate use case that requires one of the above patterns, ask the user to run the command themselves in their terminal \u2014 do not try to bypass the check.
|
|
738
|
+
|
|
739
|
+
Separately, Bash scans for common destructive-but-recoverable patterns (\`git reset --hard\`, \`git push -f\` / \`--force\` / \`--force-with-lease\`, \`git checkout .\`, \`git restore .\`, \`git clean -f\`, \`git stash drop/clear\`, \`git branch -D\`, \`git commit --amend\` / \`--no-verify\`, \`rm -rf <path>\`, \`DROP TABLE\`, \`TRUNCATE\`, \`DELETE FROM\`, \`kubectl delete\`, \`terraform destroy\`, etc.) and prepends a \`\u26A0\uFE0F \u8B66\u544A:\` line to the output. These commands are NOT blocked \u2014 the warning is informational. Treat it as a signal to double-check intent and surface the warning to the user when relevant.`;
|
|
591
740
|
async function call(input, signal) {
|
|
592
741
|
const command = input.command;
|
|
593
742
|
const timeoutMs = Math.min(input.timeout ?? DEFAULT_TIMEOUT_MS, MAX_TIMEOUT_MS);
|
|
@@ -599,6 +748,7 @@ async function call(input, signal) {
|
|
|
599
748
|
\u547D\u4EE4\uFF1A${command}`
|
|
600
749
|
};
|
|
601
750
|
}
|
|
751
|
+
const destructiveWarning = scanDestructiveCommand(command);
|
|
602
752
|
let stdout = "";
|
|
603
753
|
let stderr = "";
|
|
604
754
|
let exitCode = null;
|
|
@@ -669,16 +819,33 @@ ${stderr.replace(/\s+$/, "")}
|
|
|
669
819
|
}
|
|
670
820
|
parts.push(`
|
|
671
821
|
[exit code: ${exitCode === null ? "killed" : exitCode}]`);
|
|
672
|
-
let
|
|
673
|
-
if (
|
|
674
|
-
|
|
822
|
+
let combinedOutput = parts.join("\n");
|
|
823
|
+
if (combinedOutput.length > DEFAULT_MAX_RESULT_SIZE_CHARS) {
|
|
824
|
+
combinedOutput = combinedOutput.slice(0, DEFAULT_MAX_RESULT_SIZE_CHARS) + `
|
|
675
825
|
|
|
676
826
|
... (\u8F93\u51FA\u8D85\u8FC7 ${DEFAULT_MAX_RESULT_SIZE_CHARS} \u5B57\u7B26\uFF0C\u5DF2\u622A\u65AD)`;
|
|
677
827
|
}
|
|
678
|
-
if (timedOut ||
|
|
679
|
-
return { ok: false, error:
|
|
828
|
+
if (timedOut || killedBySignal) {
|
|
829
|
+
return { ok: false, error: combinedOutput };
|
|
680
830
|
}
|
|
681
|
-
|
|
831
|
+
const semantic = interpretCommandResult(
|
|
832
|
+
command,
|
|
833
|
+
exitCode ?? 0,
|
|
834
|
+
stdout,
|
|
835
|
+
stderr
|
|
836
|
+
);
|
|
837
|
+
const finalContent = destructiveWarning ? `\u26A0\uFE0F \u8B66\u544A: ${destructiveWarning}
|
|
838
|
+
|
|
839
|
+
${combinedOutput}` : combinedOutput;
|
|
840
|
+
if (semantic.isError) {
|
|
841
|
+
return {
|
|
842
|
+
ok: false,
|
|
843
|
+
error: `\u547D\u4EE4\u5931\u8D25 (exit ${exitCode}): ${stderr || stdout || semantic.message || ""}`.trim() + `
|
|
844
|
+
|
|
845
|
+
${finalContent}`
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
return { ok: true, content: finalContent };
|
|
682
849
|
}
|
|
683
850
|
var bashTool = {
|
|
684
851
|
name: "Bash",
|
|
@@ -694,14 +861,14 @@ var bashTool = {
|
|
|
694
861
|
|
|
695
862
|
// src/tools/edit/edit.ts
|
|
696
863
|
import { readFile as readFile6, writeFile as writeFile3, mkdir as mkdir4 } from "fs/promises";
|
|
697
|
-
import { existsSync as
|
|
864
|
+
import { existsSync as existsSync3 } from "fs";
|
|
698
865
|
import { dirname as dirname5 } from "path";
|
|
699
866
|
import { z as z2 } from "zod";
|
|
700
867
|
|
|
701
868
|
// src/tools/shared/fileUtils.ts
|
|
702
|
-
import { readFile as readFile5 } from "fs/promises";
|
|
869
|
+
import { open, readFile as readFile5 } from "fs/promises";
|
|
703
870
|
import { homedir as homedir4 } from "os";
|
|
704
|
-
import { resolve as resolve4
|
|
871
|
+
import { extname, resolve as resolve4 } from "path";
|
|
705
872
|
var BLOCKED_DEVICE_PATHS = /* @__PURE__ */ new Set([
|
|
706
873
|
"/dev/zero",
|
|
707
874
|
"/dev/random",
|
|
@@ -718,12 +885,12 @@ var BLOCKED_DEVICE_PATHS = /* @__PURE__ */ new Set([
|
|
|
718
885
|
]);
|
|
719
886
|
var WINDOWS_BLOCKED_NAMES = /* @__PURE__ */ new Set(["NUL", "CON", "PRN", "AUX", "COM1", "COM2", "LPT1"]);
|
|
720
887
|
function isBlockedDevicePath(filePath) {
|
|
721
|
-
const
|
|
722
|
-
if (BLOCKED_DEVICE_PATHS.has(
|
|
723
|
-
if (
|
|
888
|
+
const slashed = filePath.replaceAll("\\", "/");
|
|
889
|
+
if (BLOCKED_DEVICE_PATHS.has(slashed)) return true;
|
|
890
|
+
if (slashed.startsWith("/proc/") && (slashed.endsWith("/fd/0") || slashed.endsWith("/fd/1") || slashed.endsWith("/fd/2"))) {
|
|
724
891
|
return true;
|
|
725
892
|
}
|
|
726
|
-
const baseName =
|
|
893
|
+
const baseName = slashed.split("/").pop() ?? "";
|
|
727
894
|
if (WINDOWS_BLOCKED_NAMES.has(baseName.toUpperCase())) {
|
|
728
895
|
return true;
|
|
729
896
|
}
|
|
@@ -733,6 +900,9 @@ function validateAndResolvePath(rawPath, workingDir) {
|
|
|
733
900
|
if (rawPath.includes("\0")) {
|
|
734
901
|
return { ok: false, error: "\u8DEF\u5F84\u5305\u542B\u975E\u6CD5\u5B57\u7B26\uFF08null byte\uFF09" };
|
|
735
902
|
}
|
|
903
|
+
if (isBlockedDevicePath(rawPath)) {
|
|
904
|
+
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` };
|
|
905
|
+
}
|
|
736
906
|
const expanded = expandPath(rawPath);
|
|
737
907
|
const resolved = resolve4(workingDir, expanded);
|
|
738
908
|
if (isBlockedDevicePath(resolved)) {
|
|
@@ -778,6 +948,88 @@ function applyLineEnding(content, ending) {
|
|
|
778
948
|
}
|
|
779
949
|
return content;
|
|
780
950
|
}
|
|
951
|
+
var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
952
|
+
".png",
|
|
953
|
+
".jpg",
|
|
954
|
+
".jpeg",
|
|
955
|
+
".gif",
|
|
956
|
+
".webp",
|
|
957
|
+
".bmp",
|
|
958
|
+
".ico",
|
|
959
|
+
".tiff",
|
|
960
|
+
".tif",
|
|
961
|
+
".pdf",
|
|
962
|
+
".doc",
|
|
963
|
+
".docx",
|
|
964
|
+
".xls",
|
|
965
|
+
".xlsx",
|
|
966
|
+
".ppt",
|
|
967
|
+
".pptx",
|
|
968
|
+
".exe",
|
|
969
|
+
".dll",
|
|
970
|
+
".so",
|
|
971
|
+
".dylib",
|
|
972
|
+
".o",
|
|
973
|
+
".a",
|
|
974
|
+
".pyc",
|
|
975
|
+
".pyo",
|
|
976
|
+
".class",
|
|
977
|
+
".jar",
|
|
978
|
+
".zip",
|
|
979
|
+
".tar",
|
|
980
|
+
".gz",
|
|
981
|
+
".bz2",
|
|
982
|
+
".7z",
|
|
983
|
+
".rar",
|
|
984
|
+
".iso",
|
|
985
|
+
".mp3",
|
|
986
|
+
".mp4",
|
|
987
|
+
".mov",
|
|
988
|
+
".avi",
|
|
989
|
+
".mkv",
|
|
990
|
+
".wav",
|
|
991
|
+
".flac",
|
|
992
|
+
".ogg",
|
|
993
|
+
".ttf",
|
|
994
|
+
".otf",
|
|
995
|
+
".woff",
|
|
996
|
+
".woff2",
|
|
997
|
+
".sqlite",
|
|
998
|
+
".sqlite3",
|
|
999
|
+
".db",
|
|
1000
|
+
".psd",
|
|
1001
|
+
".ai",
|
|
1002
|
+
".bin",
|
|
1003
|
+
".wasm"
|
|
1004
|
+
]);
|
|
1005
|
+
function hasBinaryExtension(filePath) {
|
|
1006
|
+
const ext = extname(filePath).toLowerCase();
|
|
1007
|
+
return BINARY_EXTENSIONS.has(ext);
|
|
1008
|
+
}
|
|
1009
|
+
async function detectFileBomEncoding(filePath) {
|
|
1010
|
+
let fh = null;
|
|
1011
|
+
try {
|
|
1012
|
+
fh = await open(filePath, "r");
|
|
1013
|
+
const buf = Buffer.alloc(3);
|
|
1014
|
+
const { bytesRead } = await fh.read(buf, 0, 3, 0);
|
|
1015
|
+
if (bytesRead >= 3 && buf[0] === 239 && buf[1] === 187 && buf[2] === 191) {
|
|
1016
|
+
return "utf8-bom";
|
|
1017
|
+
}
|
|
1018
|
+
if (bytesRead >= 2 && buf[0] === 255 && buf[1] === 254) {
|
|
1019
|
+
return "utf16le";
|
|
1020
|
+
}
|
|
1021
|
+
return "utf8";
|
|
1022
|
+
} catch {
|
|
1023
|
+
return "utf8";
|
|
1024
|
+
} finally {
|
|
1025
|
+
if (fh) {
|
|
1026
|
+
try {
|
|
1027
|
+
await fh.close();
|
|
1028
|
+
} catch {
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
781
1033
|
var LEFT_SINGLE_CURLY_QUOTE = "\u2018";
|
|
782
1034
|
var RIGHT_SINGLE_CURLY_QUOTE = "\u2019";
|
|
783
1035
|
var LEFT_DOUBLE_CURLY_QUOTE = "\u201C";
|
|
@@ -857,6 +1109,44 @@ function applyCurlySingleQuotes(str) {
|
|
|
857
1109
|
return result.join("");
|
|
858
1110
|
}
|
|
859
1111
|
|
|
1112
|
+
// src/tools/shared/fileState.ts
|
|
1113
|
+
import { existsSync as existsSync2, statSync } from "fs";
|
|
1114
|
+
var fileState = /* @__PURE__ */ new Map();
|
|
1115
|
+
function recordRead(absPath) {
|
|
1116
|
+
try {
|
|
1117
|
+
const st = statSync(absPath);
|
|
1118
|
+
fileState.set(absPath, { timestamp: st.mtimeMs, size: st.size });
|
|
1119
|
+
} catch {
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
function assertFresh(absPath) {
|
|
1123
|
+
const entry = fileState.get(absPath);
|
|
1124
|
+
if (!entry) {
|
|
1125
|
+
if (existsSync2(absPath)) {
|
|
1126
|
+
return {
|
|
1127
|
+
ok: false,
|
|
1128
|
+
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`
|
|
1129
|
+
};
|
|
1130
|
+
}
|
|
1131
|
+
return { ok: true };
|
|
1132
|
+
}
|
|
1133
|
+
try {
|
|
1134
|
+
const st = statSync(absPath);
|
|
1135
|
+
if (st.mtimeMs > entry.timestamp) {
|
|
1136
|
+
return {
|
|
1137
|
+
ok: false,
|
|
1138
|
+
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`
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
1141
|
+
} catch {
|
|
1142
|
+
return { ok: true };
|
|
1143
|
+
}
|
|
1144
|
+
return { ok: true };
|
|
1145
|
+
}
|
|
1146
|
+
function clearFileState() {
|
|
1147
|
+
fileState.clear();
|
|
1148
|
+
}
|
|
1149
|
+
|
|
860
1150
|
// src/tools/edit/edit.ts
|
|
861
1151
|
var MAX_EDIT_FILE_SIZE_BYTES = 1024 * 1024 * 1024;
|
|
862
1152
|
var inputSchema2 = z2.object({
|
|
@@ -885,10 +1175,14 @@ async function call2(input) {
|
|
|
885
1175
|
const pathResult = validateAndResolvePath(input.file_path, getWorkingDir());
|
|
886
1176
|
if (!pathResult.ok) return pathResult;
|
|
887
1177
|
const filePath = pathResult.resolvedPath;
|
|
1178
|
+
const freshness = assertFresh(filePath);
|
|
1179
|
+
if (!freshness.ok) {
|
|
1180
|
+
return { ok: false, error: freshness.error };
|
|
1181
|
+
}
|
|
888
1182
|
if (old_string === new_string) {
|
|
889
1183
|
return { ok: false, error: "old_string \u4E0E new_string \u5B8C\u5168\u76F8\u540C\uFF0C\u6CA1\u6709\u53EF\u6539\u7684\u5185\u5BB9\u3002" };
|
|
890
1184
|
}
|
|
891
|
-
if (old_string === "" && !
|
|
1185
|
+
if (old_string === "" && !existsSync3(filePath)) {
|
|
892
1186
|
try {
|
|
893
1187
|
await mkdir4(dirname5(filePath), { recursive: true });
|
|
894
1188
|
await writeFile3(filePath, new_string, "utf8");
|
|
@@ -900,7 +1194,7 @@ async function call2(input) {
|
|
|
900
1194
|
return { ok: false, error: `\u521B\u5EFA\u6587\u4EF6\u5931\u8D25\uFF1A${e.message}` };
|
|
901
1195
|
}
|
|
902
1196
|
}
|
|
903
|
-
if (!
|
|
1197
|
+
if (!existsSync3(filePath)) {
|
|
904
1198
|
return {
|
|
905
1199
|
ok: false,
|
|
906
1200
|
error: `\u6587\u4EF6\u4E0D\u5B58\u5728\uFF1A${filePath}
|
|
@@ -1053,22 +1347,146 @@ var editTool = {
|
|
|
1053
1347
|
call: call2
|
|
1054
1348
|
};
|
|
1055
1349
|
|
|
1350
|
+
// src/tools/edit/multi-edit.ts
|
|
1351
|
+
import { readFile as readFile7, writeFile as writeFile4 } from "fs/promises";
|
|
1352
|
+
import { existsSync as existsSync4 } from "fs";
|
|
1353
|
+
import { z as z3 } from "zod";
|
|
1354
|
+
var editItemSchema = z3.object({
|
|
1355
|
+
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"),
|
|
1356
|
+
new_string: z3.string().describe("\u66FF\u6362\u4E3A\u7684\u65B0\u6587\u672C"),
|
|
1357
|
+
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")
|
|
1358
|
+
});
|
|
1359
|
+
var inputSchema3 = z3.object({
|
|
1360
|
+
file_path: z3.string().min(1).describe("\u8981\u7F16\u8F91\u7684\u6587\u4EF6\u8DEF\u5F84"),
|
|
1361
|
+
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")
|
|
1362
|
+
});
|
|
1363
|
+
var parameters3 = toToolParameters(inputSchema3);
|
|
1364
|
+
var description3 = `Performs multiple exact string replacements in a single file, applied atomically (all-or-nothing).
|
|
1365
|
+
|
|
1366
|
+
Usage:
|
|
1367
|
+
- You MUST use your \`Read\` tool to read the current content of the file BEFORE calling MultiEdit.
|
|
1368
|
+
- Provide a list of \`edits\`, each with \`old_string\`, \`new_string\`, and optional \`replace_all\`.
|
|
1369
|
+
- 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.
|
|
1370
|
+
- 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).
|
|
1371
|
+
- Each \`old_string\` must be unique in the current content (after prior edits) unless \`replace_all=true\`.
|
|
1372
|
+
- Empty \`old_string\` is NOT allowed in MultiEdit. To create a new file, use the \`Edit\` tool with a single empty-old_string call.
|
|
1373
|
+
- Preserve exact indentation (tabs/spaces).`;
|
|
1374
|
+
function countOccurrences2(haystack, needle) {
|
|
1375
|
+
if (needle.length === 0) return 0;
|
|
1376
|
+
let count = 0;
|
|
1377
|
+
let pos = 0;
|
|
1378
|
+
while ((pos = haystack.indexOf(needle, pos)) !== -1) {
|
|
1379
|
+
count++;
|
|
1380
|
+
pos += needle.length;
|
|
1381
|
+
}
|
|
1382
|
+
return count;
|
|
1383
|
+
}
|
|
1384
|
+
function splitReplaceAll2(haystack, needle, replacement) {
|
|
1385
|
+
return haystack.split(needle).join(replacement);
|
|
1386
|
+
}
|
|
1387
|
+
async function call3(input) {
|
|
1388
|
+
const pathResult = validateAndResolvePath(input.file_path, getWorkingDir());
|
|
1389
|
+
if (!pathResult.ok) return pathResult;
|
|
1390
|
+
const filePath = pathResult.resolvedPath;
|
|
1391
|
+
const freshness = assertFresh(filePath);
|
|
1392
|
+
if (!freshness.ok) {
|
|
1393
|
+
return { ok: false, error: freshness.error };
|
|
1394
|
+
}
|
|
1395
|
+
if (!existsSync4(filePath)) {
|
|
1396
|
+
return {
|
|
1397
|
+
ok: false,
|
|
1398
|
+
error: `\u6587\u4EF6\u4E0D\u5B58\u5728\uFF1A${filePath}
|
|
1399
|
+
\uFF08MultiEdit \u4E0D\u652F\u6301\u521B\u5EFA\u65B0\u6587\u4EF6\uFF0C\u8BF7\u6539\u7528 Edit \u5DE5\u5177\uFF09`
|
|
1400
|
+
};
|
|
1401
|
+
}
|
|
1402
|
+
const edits = input.edits;
|
|
1403
|
+
for (let i = 0; i < edits.length; i++) {
|
|
1404
|
+
for (let j = 0; j < i; j++) {
|
|
1405
|
+
if (edits[j].new_string.includes(edits[i].old_string)) {
|
|
1406
|
+
return {
|
|
1407
|
+
ok: false,
|
|
1408
|
+
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`
|
|
1409
|
+
};
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
let originalContent;
|
|
1414
|
+
try {
|
|
1415
|
+
originalContent = await readFile7(filePath, "utf8");
|
|
1416
|
+
} catch (e) {
|
|
1417
|
+
return { ok: false, error: `\u8BFB\u53D6\u5931\u8D25\uFF1A${e.message}` };
|
|
1418
|
+
}
|
|
1419
|
+
let currentContent = originalContent;
|
|
1420
|
+
for (let i = 0; i < edits.length; i++) {
|
|
1421
|
+
const edit = edits[i];
|
|
1422
|
+
const replaceAll = edit.replace_all ?? false;
|
|
1423
|
+
let searchTarget = edit.old_string;
|
|
1424
|
+
let processedNewString = edit.new_string;
|
|
1425
|
+
const actualOld = findActualString(currentContent, edit.old_string);
|
|
1426
|
+
if (actualOld === null) {
|
|
1427
|
+
return {
|
|
1428
|
+
ok: false,
|
|
1429
|
+
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`
|
|
1430
|
+
};
|
|
1431
|
+
}
|
|
1432
|
+
if (actualOld !== edit.old_string) {
|
|
1433
|
+
searchTarget = actualOld;
|
|
1434
|
+
processedNewString = preserveQuoteStyle(edit.old_string, actualOld, edit.new_string);
|
|
1435
|
+
}
|
|
1436
|
+
const occurrences = countOccurrences2(currentContent, searchTarget);
|
|
1437
|
+
if (occurrences === 0) {
|
|
1438
|
+
return {
|
|
1439
|
+
ok: false,
|
|
1440
|
+
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`
|
|
1441
|
+
};
|
|
1442
|
+
}
|
|
1443
|
+
if (occurrences > 1 && !replaceAll) {
|
|
1444
|
+
return {
|
|
1445
|
+
ok: false,
|
|
1446
|
+
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`
|
|
1447
|
+
};
|
|
1448
|
+
}
|
|
1449
|
+
currentContent = replaceAll ? splitReplaceAll2(currentContent, searchTarget, processedNewString) : currentContent.replace(searchTarget, processedNewString);
|
|
1450
|
+
}
|
|
1451
|
+
const lineEnding = await detectFileLineEndings(filePath);
|
|
1452
|
+
const finalContent = applyLineEnding(currentContent, lineEnding);
|
|
1453
|
+
try {
|
|
1454
|
+
await writeFile4(filePath, finalContent, "utf8");
|
|
1455
|
+
} catch (e) {
|
|
1456
|
+
return { ok: false, error: `\u5199\u5165\u5931\u8D25\uFF1A${e.message}` };
|
|
1457
|
+
}
|
|
1458
|
+
return {
|
|
1459
|
+
ok: true,
|
|
1460
|
+
content: `\u5DF2\u5BF9 ${filePath} \u5E94\u7528 ${edits.length} \u5904\u4FEE\u6539`
|
|
1461
|
+
};
|
|
1462
|
+
}
|
|
1463
|
+
var multiEditTool = {
|
|
1464
|
+
name: "MultiEdit",
|
|
1465
|
+
description: description3,
|
|
1466
|
+
inputSchema: inputSchema3,
|
|
1467
|
+
parameters: parameters3,
|
|
1468
|
+
isReadOnly: false,
|
|
1469
|
+
isConcurrencySafe: false,
|
|
1470
|
+
maxResultSizeChars: DEFAULT_MAX_RESULT_SIZE_CHARS,
|
|
1471
|
+
call: call3
|
|
1472
|
+
};
|
|
1473
|
+
|
|
1056
1474
|
// src/tools/glob/glob.ts
|
|
1057
1475
|
import { stat as stat2 } from "fs/promises";
|
|
1058
1476
|
import { isAbsolute, resolve as resolve5 } from "path";
|
|
1059
1477
|
import fg from "fast-glob";
|
|
1060
|
-
import { z as
|
|
1061
|
-
var
|
|
1062
|
-
pattern:
|
|
1063
|
-
path:
|
|
1478
|
+
import { z as z4 } from "zod";
|
|
1479
|
+
var inputSchema4 = z4.object({
|
|
1480
|
+
pattern: z4.string().min(1).describe('glob \u6A21\u5F0F\uFF0C\u4F8B\u5982 "**/*.ts" \u6216 "src/components/**/*.tsx"'),
|
|
1481
|
+
path: z4.string().optional().describe('\u641C\u7D22\u7684\u6839\u76EE\u5F55\uFF08\u9ED8\u8BA4\u5F53\u524D\u5DE5\u4F5C\u76EE\u5F55\uFF09\uFF1B\u7701\u7565\u65F6\u4E0D\u8981\u4F20 "undefined" \u5B57\u7B26\u4E32')
|
|
1064
1482
|
});
|
|
1065
|
-
var
|
|
1066
|
-
var
|
|
1483
|
+
var parameters4 = toToolParameters(inputSchema4);
|
|
1484
|
+
var description4 = `- Fast file pattern matching tool that works with any codebase size
|
|
1067
1485
|
- Supports glob patterns like "**/*.js" or "src/**/*.ts"
|
|
1068
1486
|
- Returns matching file paths sorted by modification time (oldest first)
|
|
1069
1487
|
- Use this tool when you need to find files by name patterns
|
|
1070
1488
|
- When you need to do an open ended search that may require multiple rounds, prefer the Grep tool for content search`;
|
|
1071
|
-
async function
|
|
1489
|
+
async function call4(input) {
|
|
1072
1490
|
const cwd = input.path ? resolve5(input.path) : getWorkingDir();
|
|
1073
1491
|
const pattern = input.pattern.replace(/\\/g, "/");
|
|
1074
1492
|
let matches;
|
|
@@ -1115,23 +1533,23 @@ async function call3(input) {
|
|
|
1115
1533
|
}
|
|
1116
1534
|
var globTool = {
|
|
1117
1535
|
name: "Glob",
|
|
1118
|
-
description:
|
|
1119
|
-
inputSchema:
|
|
1120
|
-
parameters:
|
|
1536
|
+
description: description4,
|
|
1537
|
+
inputSchema: inputSchema4,
|
|
1538
|
+
parameters: parameters4,
|
|
1121
1539
|
isReadOnly: true,
|
|
1122
1540
|
isConcurrencySafe: true,
|
|
1123
1541
|
maxResultSizeChars: DEFAULT_MAX_RESULT_SIZE_CHARS,
|
|
1124
|
-
call:
|
|
1542
|
+
call: call4
|
|
1125
1543
|
};
|
|
1126
1544
|
|
|
1127
1545
|
// src/tools/grep/grep.ts
|
|
1128
1546
|
import { spawn as spawn3 } from "child_process";
|
|
1129
1547
|
import { resolve as resolve7 } from "path";
|
|
1130
|
-
import { z as
|
|
1548
|
+
import { z as z5 } from "zod";
|
|
1131
1549
|
|
|
1132
1550
|
// src/tools/grep/rgPath.ts
|
|
1133
1551
|
import { spawn as spawn2 } from "child_process";
|
|
1134
|
-
import { chmodSync, existsSync as
|
|
1552
|
+
import { chmodSync, existsSync as existsSync5 } from "fs";
|
|
1135
1553
|
import { resolve as resolve6 } from "path";
|
|
1136
1554
|
var cached;
|
|
1137
1555
|
async function resolveRgPath() {
|
|
@@ -1141,15 +1559,15 @@ async function resolveRgPath() {
|
|
|
1141
1559
|
}
|
|
1142
1560
|
async function detect() {
|
|
1143
1561
|
const fromEnv = process.env.MINIMAL_AGENT_RIPGREP_PATH;
|
|
1144
|
-
if (fromEnv &&
|
|
1562
|
+
if (fromEnv && existsSync5(fromEnv)) return fromEnv;
|
|
1145
1563
|
const vendored = vendoredRgPath();
|
|
1146
|
-
if (vendored &&
|
|
1564
|
+
if (vendored && existsSync5(vendored)) {
|
|
1147
1565
|
ensureExecutable(vendored);
|
|
1148
1566
|
return vendored;
|
|
1149
1567
|
}
|
|
1150
1568
|
if (await trySpawn("rg")) return "rg";
|
|
1151
1569
|
for (const candidate of claudeCodeCandidates()) {
|
|
1152
|
-
if (
|
|
1570
|
+
if (existsSync5(candidate)) {
|
|
1153
1571
|
ensureExecutable(candidate);
|
|
1154
1572
|
return candidate;
|
|
1155
1573
|
}
|
|
@@ -1242,21 +1660,21 @@ function claudeCodeCandidates() {
|
|
|
1242
1660
|
}
|
|
1243
1661
|
|
|
1244
1662
|
// src/tools/grep/grep.ts
|
|
1245
|
-
var
|
|
1246
|
-
pattern:
|
|
1247
|
-
path:
|
|
1248
|
-
glob:
|
|
1249
|
-
type:
|
|
1250
|
-
output_mode:
|
|
1251
|
-
"-i":
|
|
1252
|
-
"-n":
|
|
1253
|
-
"-A":
|
|
1254
|
-
"-B":
|
|
1255
|
-
"-C":
|
|
1256
|
-
head_limit:
|
|
1663
|
+
var inputSchema5 = z5.object({
|
|
1664
|
+
pattern: z5.string().min(1).describe("\u6B63\u5219\u8868\u8FBE\u5F0F\uFF08ripgrep \u517C\u5BB9\u8BED\u6CD5\uFF09"),
|
|
1665
|
+
path: z5.string().optional().describe("\u641C\u7D22\u7684\u6839\u76EE\u5F55\u6216\u6587\u4EF6\uFF08\u9ED8\u8BA4\u5F53\u524D\u5DE5\u4F5C\u76EE\u5F55\uFF09"),
|
|
1666
|
+
glob: z5.string().optional().describe('\u6587\u4EF6\u540D glob \u8FC7\u6EE4\uFF0C\u5982 "*.ts"'),
|
|
1667
|
+
type: z5.string().optional().describe('rg \u7684\u6587\u4EF6\u7C7B\u578B\u5FEB\u6377\u540D\uFF0C\u5982 "py"\u3001"rust"\u3001"js"'),
|
|
1668
|
+
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"),
|
|
1669
|
+
"-i": z5.boolean().optional().describe("\u5FFD\u7565\u5927\u5C0F\u5199"),
|
|
1670
|
+
"-n": z5.boolean().optional().describe("\u663E\u793A\u884C\u53F7\uFF08\u4EC5 content \u6A21\u5F0F\uFF09"),
|
|
1671
|
+
"-A": z5.number().int().min(0).optional().describe("\u5339\u914D\u540E\u5C55\u793A\u51E0\u884C\u4E0A\u4E0B\u6587"),
|
|
1672
|
+
"-B": z5.number().int().min(0).optional().describe("\u5339\u914D\u524D\u5C55\u793A\u51E0\u884C\u4E0A\u4E0B\u6587"),
|
|
1673
|
+
"-C": z5.number().int().min(0).optional().describe("\u5339\u914D\u524D\u540E\u5404\u5C55\u793A\u51E0\u884C\uFF08\u8986\u76D6 -A/-B\uFF09"),
|
|
1674
|
+
head_limit: z5.number().int().positive().optional().describe("\u8F93\u51FA\u6700\u591A\u4FDD\u7559\u524D N \u884C\uFF08\u9632\u6B62\u7ED3\u679C\u8FC7\u5927\uFF09")
|
|
1257
1675
|
});
|
|
1258
|
-
var
|
|
1259
|
-
var
|
|
1676
|
+
var parameters5 = toToolParameters(inputSchema5);
|
|
1677
|
+
var description5 = `A powerful search tool built on ripgrep.
|
|
1260
1678
|
|
|
1261
1679
|
Usage:
|
|
1262
1680
|
- ALWAYS use Grep for content search tasks. Do NOT invoke \`grep\` or \`rg\` directly via Bash.
|
|
@@ -1264,7 +1682,7 @@ Usage:
|
|
|
1264
1682
|
- Filter files with glob parameter (e.g., "*.js", "**/*.tsx") or type parameter (e.g., "js", "py", "rust")
|
|
1265
1683
|
- Output modes: "content" shows matching lines, "files_with_matches" shows only file paths (default), "count" shows match counts
|
|
1266
1684
|
- Pattern syntax: Uses ripgrep (not classic grep)`;
|
|
1267
|
-
async function
|
|
1685
|
+
async function call5(input, signal) {
|
|
1268
1686
|
const args = [];
|
|
1269
1687
|
const mode = input.output_mode ?? "files_with_matches";
|
|
1270
1688
|
if (mode === "files_with_matches") args.push("-l");
|
|
@@ -1341,27 +1759,27 @@ async function call4(input, signal) {
|
|
|
1341
1759
|
}
|
|
1342
1760
|
var grepTool = {
|
|
1343
1761
|
name: "Grep",
|
|
1344
|
-
description:
|
|
1345
|
-
inputSchema:
|
|
1346
|
-
parameters:
|
|
1762
|
+
description: description5,
|
|
1763
|
+
inputSchema: inputSchema5,
|
|
1764
|
+
parameters: parameters5,
|
|
1347
1765
|
isReadOnly: true,
|
|
1348
1766
|
isConcurrencySafe: true,
|
|
1349
1767
|
maxResultSizeChars: DEFAULT_MAX_RESULT_SIZE_CHARS,
|
|
1350
|
-
call:
|
|
1768
|
+
call: call5
|
|
1351
1769
|
};
|
|
1352
1770
|
|
|
1353
1771
|
// src/tools/read/read.ts
|
|
1354
1772
|
import { createReadStream } from "fs";
|
|
1355
|
-
import { readFile as
|
|
1773
|
+
import { readFile as readFile8, stat as stat3 } from "fs/promises";
|
|
1356
1774
|
import { createInterface } from "readline";
|
|
1357
|
-
import { z as
|
|
1358
|
-
var
|
|
1359
|
-
file_path:
|
|
1360
|
-
offset:
|
|
1361
|
-
limit:
|
|
1775
|
+
import { z as z6 } from "zod";
|
|
1776
|
+
var inputSchema6 = z6.object({
|
|
1777
|
+
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"),
|
|
1778
|
+
offset: z6.number().int().positive().optional().describe("\u8D77\u59CB\u884C\u53F7\uFF081-indexed\uFF09\uFF1B\u4E0D\u586B\u5219\u4ECE\u6587\u4EF6\u5F00\u5934\u8BFB"),
|
|
1779
|
+
limit: z6.number().int().positive().optional().describe(`\u6700\u591A\u8BFB\u591A\u5C11\u884C\uFF1B\u4E0D\u586B\u5219\u7528\u9ED8\u8BA4\u503C ${MAX_LINES_TO_READ}`)
|
|
1362
1780
|
});
|
|
1363
|
-
var
|
|
1364
|
-
var
|
|
1781
|
+
var parameters6 = toToolParameters(inputSchema6);
|
|
1782
|
+
var description6 = `Reads a file from the local filesystem. You can access any file directly by using this tool.
|
|
1365
1783
|
Assume this tool is able to read all files on the machine. If the User provides a path to a file assume that path is valid. It is okay to read a file that does not exist; an error will be returned.
|
|
1366
1784
|
|
|
1367
1785
|
Usage:
|
|
@@ -1370,9 +1788,10 @@ Usage:
|
|
|
1370
1788
|
- You can optionally specify a line offset and limit (especially handy for long files)
|
|
1371
1789
|
- Results are returned using cat -n format, with line numbers starting at 1
|
|
1372
1790
|
- This tool can only read text files, not directories. To read a directory, use the Glob tool.
|
|
1373
|
-
- If you read a file that exists but has empty contents you will receive a warning in place of file contents
|
|
1791
|
+
- If you read a file that exists but has empty contents you will receive a warning in place of file contents.
|
|
1792
|
+
- This tool cannot read binary files. Files whose extensions are on the binary blocklist (images: .png/.jpg/.gif/.webp/..., documents: .pdf/.docx/.xlsx/..., executables: .exe/.dll/.so/..., archives: .zip/.tar/.gz/..., and others) will be rejected with a clear error. If the file is actually text despite the extension (e.g., a misnamed log), rename it or use Bash \`cat\` to read it directly \u2014 do not retry Read with the same path.`;
|
|
1374
1793
|
var STREAM_THRESHOLD = 1024 * 1024;
|
|
1375
|
-
async function
|
|
1794
|
+
async function call6(input) {
|
|
1376
1795
|
const offset = input.offset ?? 1;
|
|
1377
1796
|
const limit = input.limit ?? MAX_LINES_TO_READ;
|
|
1378
1797
|
const pathResult = validateAndResolvePath(input.file_path, getWorkingDir());
|
|
@@ -1383,6 +1802,12 @@ async function call5(input) {
|
|
|
1383
1802
|
if (isBlockedDevicePath(filePath)) {
|
|
1384
1803
|
return { ok: false, error: `\u4E0D\u5141\u8BB8\u8BFB\u53D6\u8BBE\u5907\u6587\u4EF6\uFF1A${filePath}\u3002\u8BE5\u8DEF\u5F84\u53EF\u80FD\u4EA7\u751F\u65E0\u9650\u8F93\u51FA\u6216\u963B\u585E\u8FDB\u7A0B\u3002` };
|
|
1385
1804
|
}
|
|
1805
|
+
if (hasBinaryExtension(filePath)) {
|
|
1806
|
+
return {
|
|
1807
|
+
ok: false,
|
|
1808
|
+
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`
|
|
1809
|
+
};
|
|
1810
|
+
}
|
|
1386
1811
|
let st;
|
|
1387
1812
|
try {
|
|
1388
1813
|
st = await stat3(filePath);
|
|
@@ -1408,6 +1833,7 @@ async function call5(input) {
|
|
|
1408
1833
|
numbered = result.numbered;
|
|
1409
1834
|
totalLines = result.totalLines;
|
|
1410
1835
|
if (result.isEmpty) {
|
|
1836
|
+
recordRead(filePath);
|
|
1411
1837
|
return { ok: true, content: "<file is empty>" };
|
|
1412
1838
|
}
|
|
1413
1839
|
} else {
|
|
@@ -1416,6 +1842,7 @@ async function call5(input) {
|
|
|
1416
1842
|
totalLines = result.totalLines;
|
|
1417
1843
|
}
|
|
1418
1844
|
if (totalLines === 0 || !numbered) {
|
|
1845
|
+
recordRead(filePath);
|
|
1419
1846
|
return { ok: true, content: "<file is empty>" };
|
|
1420
1847
|
}
|
|
1421
1848
|
let content = numbered;
|
|
@@ -1438,10 +1865,11 @@ async function call5(input) {
|
|
|
1438
1865
|
|
|
1439
1866
|
\u26A0\uFE0F \u6CE8\u610F\uFF1A\u8FD9\u662F\u4E00\u4E2A\u5927\u6587\u4EF6\uFF08${(st.size / 1024).toFixed(1)} KB\uFF09\u3002\u5EFA\u8BAE\u7528 offset/limit \u5206\u6BB5\u8BFB\u53D6\uFF0C\u4F8B\u5982\u5148\u8BFB\u5173\u952E\u90E8\u5206\uFF08imports\u3001exports\u3001\u51FD\u6570\u7B7E\u540D\uFF09\u3002`;
|
|
1440
1867
|
}
|
|
1868
|
+
recordRead(filePath);
|
|
1441
1869
|
return { ok: true, content };
|
|
1442
1870
|
}
|
|
1443
1871
|
async function readSmallFile(filePath, offset, limit) {
|
|
1444
|
-
const raw = await
|
|
1872
|
+
const raw = await readFile8(filePath, "utf8");
|
|
1445
1873
|
if (raw.length === 0) {
|
|
1446
1874
|
return { numbered: "", totalLines: 0, isEmpty: true };
|
|
1447
1875
|
}
|
|
@@ -1491,17 +1919,17 @@ async function readLargeFileStream(filePath, offset, limit) {
|
|
|
1491
1919
|
}
|
|
1492
1920
|
var readTool = {
|
|
1493
1921
|
name: "Read",
|
|
1494
|
-
description:
|
|
1495
|
-
inputSchema:
|
|
1496
|
-
parameters:
|
|
1922
|
+
description: description6,
|
|
1923
|
+
inputSchema: inputSchema6,
|
|
1924
|
+
parameters: parameters6,
|
|
1497
1925
|
isReadOnly: true,
|
|
1498
1926
|
isConcurrencySafe: true,
|
|
1499
1927
|
maxResultSizeChars: DEFAULT_MAX_RESULT_SIZE_CHARS,
|
|
1500
|
-
call:
|
|
1928
|
+
call: call6
|
|
1501
1929
|
};
|
|
1502
1930
|
|
|
1503
1931
|
// src/tools/webfetch/webfetch.ts
|
|
1504
|
-
import { z as
|
|
1932
|
+
import { z as z7 } from "zod";
|
|
1505
1933
|
|
|
1506
1934
|
// src/tools/webfetch/preapproved.ts
|
|
1507
1935
|
var PREAPPROVED_HOSTS = /* @__PURE__ */ new Set([
|
|
@@ -1786,12 +2214,12 @@ function cleanCache() {
|
|
|
1786
2214
|
}
|
|
1787
2215
|
}
|
|
1788
2216
|
}
|
|
1789
|
-
var
|
|
1790
|
-
url:
|
|
1791
|
-
prompt:
|
|
2217
|
+
var inputSchema7 = z7.object({
|
|
2218
|
+
url: z7.string().describe("\u8981\u83B7\u53D6\u5185\u5BB9\u7684 URL"),
|
|
2219
|
+
prompt: z7.string().describe("\u5BF9\u5185\u5BB9\u8FDB\u884C\u5904\u7406\u7684\u6307\u4EE4\uFF0C\u63CF\u8FF0\u4F60\u60F3\u4ECE\u9875\u9762\u63D0\u53D6\u4EC0\u4E48\u4FE1\u606F")
|
|
1792
2220
|
});
|
|
1793
|
-
var
|
|
1794
|
-
var
|
|
2221
|
+
var parameters7 = toToolParameters(inputSchema7);
|
|
2222
|
+
var description7 = `- Fetches content from a specified URL and processes it using an AI model.
|
|
1795
2223
|
- Takes a URL and a prompt as input.
|
|
1796
2224
|
- Fetches the URL content, converts HTML to markdown.
|
|
1797
2225
|
- Processes the content with the prompt (e.g., extract summary, find specific info).
|
|
@@ -1900,7 +2328,7 @@ async function htmlToMarkdown(html) {
|
|
|
1900
2328
|
const td = new TurndownService();
|
|
1901
2329
|
return td.turndown(html);
|
|
1902
2330
|
}
|
|
1903
|
-
async function
|
|
2331
|
+
async function call7(input, signal) {
|
|
1904
2332
|
const { url } = input;
|
|
1905
2333
|
const start = Date.now();
|
|
1906
2334
|
const cacheKey = url;
|
|
@@ -1998,17 +2426,17 @@ ${content}`;
|
|
|
1998
2426
|
}
|
|
1999
2427
|
var webfetchTool = {
|
|
2000
2428
|
name: "WebFetch",
|
|
2001
|
-
description:
|
|
2002
|
-
inputSchema:
|
|
2003
|
-
parameters:
|
|
2429
|
+
description: description7,
|
|
2430
|
+
inputSchema: inputSchema7,
|
|
2431
|
+
parameters: parameters7,
|
|
2004
2432
|
isReadOnly: true,
|
|
2005
2433
|
isConcurrencySafe: true,
|
|
2006
2434
|
maxResultSizeChars: DEFAULT_MAX_RESULT_SIZE_CHARS,
|
|
2007
|
-
call:
|
|
2435
|
+
call: call7
|
|
2008
2436
|
};
|
|
2009
2437
|
|
|
2010
2438
|
// src/tools/webbrowser/webbrowser.ts
|
|
2011
|
-
import { z as
|
|
2439
|
+
import { z as z8 } from "zod";
|
|
2012
2440
|
|
|
2013
2441
|
// src/tools/webbrowser/browser.ts
|
|
2014
2442
|
import os from "os";
|
|
@@ -2044,15 +2472,15 @@ function screenshotPath(prefix = "browser") {
|
|
|
2044
2472
|
}
|
|
2045
2473
|
|
|
2046
2474
|
// src/tools/webbrowser/webbrowser.ts
|
|
2047
|
-
var
|
|
2048
|
-
action:
|
|
2049
|
-
url:
|
|
2050
|
-
selector:
|
|
2051
|
-
value:
|
|
2052
|
-
timeout:
|
|
2475
|
+
var inputSchema8 = z8.object({
|
|
2476
|
+
action: z8.enum(["navigate", "screenshot", "getContent", "click", "fill", "submit"]).describe("Browser action to perform"),
|
|
2477
|
+
url: z8.string().url().optional().describe("URL to navigate to (required for navigate action)"),
|
|
2478
|
+
selector: z8.string().optional().describe("CSS selector for click/fill/submit actions"),
|
|
2479
|
+
value: z8.string().optional().describe("Value to fill in input fields"),
|
|
2480
|
+
timeout: z8.number().int().positive().optional().describe("Timeout in milliseconds (default: 30000)")
|
|
2053
2481
|
});
|
|
2054
|
-
var
|
|
2055
|
-
var
|
|
2482
|
+
var parameters8 = toToolParameters(inputSchema8);
|
|
2483
|
+
var description8 = `Control a headless web browser. Navigate to URLs, take screenshots, and interact with web pages.
|
|
2056
2484
|
|
|
2057
2485
|
When to use WebBrowser vs WebSearch:
|
|
2058
2486
|
- WebSearch: When you need to find information or discover URLs through search
|
|
@@ -2083,7 +2511,7 @@ Example actions:
|
|
|
2083
2511
|
- Take screenshot: { action: "screenshot" }
|
|
2084
2512
|
- Click element: { action: "click", selector: "#submit-btn" }
|
|
2085
2513
|
- Fill form field: { action: "fill", selector: "input[name='email']", value: "user@example.com" }`;
|
|
2086
|
-
async function
|
|
2514
|
+
async function call8(input, signal) {
|
|
2087
2515
|
const { action, url, selector, value, timeout = 3e4 } = input;
|
|
2088
2516
|
let page;
|
|
2089
2517
|
try {
|
|
@@ -2187,30 +2615,30 @@ Current URL: ${page.url()}`
|
|
|
2187
2615
|
}
|
|
2188
2616
|
var webbrowserTool = {
|
|
2189
2617
|
name: "WebBrowser",
|
|
2190
|
-
description:
|
|
2191
|
-
inputSchema:
|
|
2192
|
-
parameters:
|
|
2618
|
+
description: description8,
|
|
2619
|
+
inputSchema: inputSchema8,
|
|
2620
|
+
parameters: parameters8,
|
|
2193
2621
|
isReadOnly: false,
|
|
2194
2622
|
isConcurrencySafe: false,
|
|
2195
2623
|
maxResultSizeChars: DEFAULT_MAX_RESULT_SIZE_CHARS,
|
|
2196
|
-
call:
|
|
2624
|
+
call: call8
|
|
2197
2625
|
};
|
|
2198
2626
|
|
|
2199
2627
|
// src/tools/websearch/websearch.ts
|
|
2200
|
-
import { z as
|
|
2201
|
-
var
|
|
2202
|
-
query:
|
|
2203
|
-
max_results:
|
|
2204
|
-
search_depth:
|
|
2205
|
-
topic:
|
|
2628
|
+
import { z as z9 } from "zod";
|
|
2629
|
+
var inputSchema9 = z9.object({
|
|
2630
|
+
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"),
|
|
2631
|
+
max_results: z9.number().int().min(1).max(20).optional().describe("\u8FD4\u56DE\u7ED3\u679C\u6570\u91CF\uFF0C1-20\uFF0C\u9ED8\u8BA4 5"),
|
|
2632
|
+
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"),
|
|
2633
|
+
topic: z9.enum(["general", "news"]).optional().describe("general=\u901A\u7528\u7F51\u9875\uFF1Bnews=\u504F\u65B0\u95FB\u6E90\uFF1B\u9ED8\u8BA4 general")
|
|
2206
2634
|
});
|
|
2207
|
-
var
|
|
2208
|
-
var
|
|
2635
|
+
var parameters9 = toToolParameters(inputSchema9);
|
|
2636
|
+
var description9 = `- Searches the public web via the Tavily Search API and returns structured results.
|
|
2209
2637
|
- Use this when you need up-to-date information that is not in your training data, or when the user asks for recent news / docs / API references.
|
|
2210
2638
|
- Returns the top N results, each with a title, URL, and content snippet. With \`search_depth: "advanced"\` Tavily also returns a synthesized answer at the top.
|
|
2211
2639
|
- Prefer specific natural-language queries over keyword soup (e.g. "how does Bun handle .env files in version 1.1").
|
|
2212
2640
|
- Requires the TAVILY_API_KEY environment variable to be set; if missing the tool returns a friendly error.`;
|
|
2213
|
-
async function
|
|
2641
|
+
async function call9(input, signal) {
|
|
2214
2642
|
const apiKey = process.env.TAVILY_API_KEY;
|
|
2215
2643
|
if (!apiKey) {
|
|
2216
2644
|
return {
|
|
@@ -2292,37 +2720,42 @@ ${(r.content ?? "").trim()}`
|
|
|
2292
2720
|
}
|
|
2293
2721
|
var webSearchTool = {
|
|
2294
2722
|
name: "WebSearch",
|
|
2295
|
-
description:
|
|
2296
|
-
inputSchema:
|
|
2297
|
-
parameters:
|
|
2723
|
+
description: description9,
|
|
2724
|
+
inputSchema: inputSchema9,
|
|
2725
|
+
parameters: parameters9,
|
|
2298
2726
|
isReadOnly: true,
|
|
2299
2727
|
isConcurrencySafe: true,
|
|
2300
2728
|
maxResultSizeChars: DEFAULT_MAX_RESULT_SIZE_CHARS,
|
|
2301
|
-
call:
|
|
2729
|
+
call: call9
|
|
2302
2730
|
};
|
|
2303
2731
|
|
|
2304
2732
|
// src/tools/write/write.ts
|
|
2305
|
-
import { existsSync as
|
|
2306
|
-
import { mkdir as mkdir5, stat as stat4, writeFile as
|
|
2733
|
+
import { existsSync as existsSync6 } from "fs";
|
|
2734
|
+
import { mkdir as mkdir5, stat as stat4, writeFile as writeFile5 } from "fs/promises";
|
|
2307
2735
|
import { dirname as dirname6 } from "path";
|
|
2308
|
-
import { z as
|
|
2736
|
+
import { z as z10 } from "zod";
|
|
2309
2737
|
var MAX_WRITE_SIZE_BYTES = 1024 * 1024 * 1024;
|
|
2310
|
-
var
|
|
2311
|
-
file_path:
|
|
2312
|
-
content:
|
|
2738
|
+
var inputSchema10 = z10.object({
|
|
2739
|
+
file_path: z10.string().min(1).describe("\u8981\u5199\u5165\u7684\u6587\u4EF6\u8DEF\u5F84"),
|
|
2740
|
+
content: z10.string().describe("\u6587\u4EF6\u5B8C\u6574\u5185\u5BB9\uFF08\u4F1A\u8986\u76D6\u65E2\u6709\u5185\u5BB9\uFF09")
|
|
2313
2741
|
});
|
|
2314
|
-
var
|
|
2315
|
-
var
|
|
2742
|
+
var parameters10 = toToolParameters(inputSchema10);
|
|
2743
|
+
var description10 = `Writes a file to the local filesystem.
|
|
2316
2744
|
|
|
2317
2745
|
Usage:
|
|
2318
2746
|
- This tool will overwrite the existing file if there is one at the provided path.
|
|
2747
|
+
- If you intend to overwrite an existing file, you MUST use your \`Read\` tool to read its current content at least once in the current session BEFORE calling Write. This tool will error with a "\u8BF7\u5148 Read" message if you attempt to overwrite a file that has not been read. To create a new file (path does not yet exist), you can call Write directly without a prior Read.
|
|
2319
2748
|
- If the parent directory does not exist, it will be created recursively.
|
|
2320
2749
|
- ALWAYS prefer editing existing files in the codebase via the Edit tool. NEVER write new files unless explicitly required.
|
|
2321
2750
|
- NEVER create documentation files (*.md) or README files unless explicitly requested by the User.`;
|
|
2322
|
-
async function
|
|
2751
|
+
async function call10(input) {
|
|
2323
2752
|
const pathResult = validateAndResolvePath(input.file_path, getWorkingDir());
|
|
2324
2753
|
if (!pathResult.ok) return pathResult;
|
|
2325
2754
|
const filePath = pathResult.resolvedPath;
|
|
2755
|
+
const freshness = assertFresh(filePath);
|
|
2756
|
+
if (!freshness.ok) {
|
|
2757
|
+
return { ok: false, error: freshness.error };
|
|
2758
|
+
}
|
|
2326
2759
|
const contentSize = Buffer.byteLength(input.content, "utf8");
|
|
2327
2760
|
if (contentSize > MAX_WRITE_SIZE_BYTES) {
|
|
2328
2761
|
return {
|
|
@@ -2333,7 +2766,7 @@ async function call9(input) {
|
|
|
2333
2766
|
try {
|
|
2334
2767
|
await mkdir5(dirname6(filePath), { recursive: true });
|
|
2335
2768
|
let originalSize = 0;
|
|
2336
|
-
const fileExisted =
|
|
2769
|
+
const fileExisted = existsSync6(filePath);
|
|
2337
2770
|
if (fileExisted) {
|
|
2338
2771
|
try {
|
|
2339
2772
|
const st = await stat4(filePath);
|
|
@@ -2342,16 +2775,32 @@ async function call9(input) {
|
|
|
2342
2775
|
}
|
|
2343
2776
|
}
|
|
2344
2777
|
let contentToWrite = input.content;
|
|
2778
|
+
let bomEncoding = "utf8";
|
|
2345
2779
|
if (fileExisted) {
|
|
2780
|
+
const detected = await detectFileBomEncoding(filePath);
|
|
2781
|
+
if (detected === "utf16le") {
|
|
2782
|
+
return {
|
|
2783
|
+
ok: false,
|
|
2784
|
+
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"
|
|
2785
|
+
};
|
|
2786
|
+
}
|
|
2787
|
+
bomEncoding = detected;
|
|
2346
2788
|
const lineEnding = await detectFileLineEndings(filePath);
|
|
2347
2789
|
contentToWrite = applyLineEnding(input.content, lineEnding);
|
|
2348
2790
|
}
|
|
2349
|
-
|
|
2791
|
+
if (bomEncoding === "utf8-bom") {
|
|
2792
|
+
const bomBytes = Buffer.from([239, 187, 191]);
|
|
2793
|
+
const bodyBytes = Buffer.from(contentToWrite, "utf8");
|
|
2794
|
+
await writeFile5(filePath, Buffer.concat([bomBytes, bodyBytes]));
|
|
2795
|
+
} else {
|
|
2796
|
+
await writeFile5(filePath, contentToWrite, "utf8");
|
|
2797
|
+
}
|
|
2350
2798
|
const action = fileExisted ? "\u5DF2\u8986\u76D6" : "\u5DF2\u521B\u5EFA\u65B0\u6587\u4EF6";
|
|
2351
2799
|
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`;
|
|
2800
|
+
const bomInfo = bomEncoding === "utf8-bom" ? "\uFF0C\u5DF2\u4FDD\u7559 UTF-8 BOM" : "";
|
|
2352
2801
|
return {
|
|
2353
2802
|
ok: true,
|
|
2354
|
-
content: `${action} ${filePath}${sizeInfo}`
|
|
2803
|
+
content: `${action} ${filePath}${sizeInfo}${bomInfo}`
|
|
2355
2804
|
};
|
|
2356
2805
|
} catch (e) {
|
|
2357
2806
|
return { ok: false, error: `\u5199\u5165\u5931\u8D25\uFF1A${e.message}` };
|
|
@@ -2359,19 +2808,20 @@ async function call9(input) {
|
|
|
2359
2808
|
}
|
|
2360
2809
|
var writeTool = {
|
|
2361
2810
|
name: "Write",
|
|
2362
|
-
description:
|
|
2363
|
-
inputSchema:
|
|
2364
|
-
parameters:
|
|
2811
|
+
description: description10,
|
|
2812
|
+
inputSchema: inputSchema10,
|
|
2813
|
+
parameters: parameters10,
|
|
2365
2814
|
isReadOnly: false,
|
|
2366
2815
|
isConcurrencySafe: false,
|
|
2367
2816
|
maxResultSizeChars: DEFAULT_MAX_RESULT_SIZE_CHARS,
|
|
2368
|
-
call:
|
|
2817
|
+
call: call10
|
|
2369
2818
|
};
|
|
2370
2819
|
|
|
2371
2820
|
// src/tools/index.ts
|
|
2372
2821
|
var ALL_TOOLS = [
|
|
2373
2822
|
readTool,
|
|
2374
2823
|
editTool,
|
|
2824
|
+
multiEditTool,
|
|
2375
2825
|
writeTool,
|
|
2376
2826
|
globTool,
|
|
2377
2827
|
grepTool,
|
|
@@ -3780,7 +4230,7 @@ async function reactiveCompactIfApplicable(messages, provider, error, state = de
|
|
|
3780
4230
|
}
|
|
3781
4231
|
|
|
3782
4232
|
// src/plugins/commandRouter.ts
|
|
3783
|
-
import { readFile as
|
|
4233
|
+
import { readFile as readFile9, readdir as readdir3 } from "fs/promises";
|
|
3784
4234
|
import { join as join6 } from "path";
|
|
3785
4235
|
var PLUGINS_DIR = join6(findPackageRoot(import.meta.url), "plugins");
|
|
3786
4236
|
var pluginCache = /* @__PURE__ */ new Map();
|
|
@@ -3812,7 +4262,7 @@ async function loadPlugin(pluginDirPath) {
|
|
|
3812
4262
|
let manifestVersion;
|
|
3813
4263
|
let manifestDesc;
|
|
3814
4264
|
try {
|
|
3815
|
-
const raw = await
|
|
4265
|
+
const raw = await readFile9(manifestPath, "utf8");
|
|
3816
4266
|
const parsed = JSON.parse(raw);
|
|
3817
4267
|
manifestName = parsed.name ?? dirName;
|
|
3818
4268
|
manifestVersion = parsed.version;
|
|
@@ -3827,7 +4277,7 @@ async function loadPlugin(pluginDirPath) {
|
|
|
3827
4277
|
if (!entry.name.endsWith(".md")) continue;
|
|
3828
4278
|
const cmdPath = join6(commandsDir, entry.name);
|
|
3829
4279
|
try {
|
|
3830
|
-
const content = await
|
|
4280
|
+
const content = await readFile9(cmdPath, "utf8");
|
|
3831
4281
|
const fm = parseMarkdownFrontmatter(content);
|
|
3832
4282
|
const sep2 = content.indexOf("\n---", 4);
|
|
3833
4283
|
const body = sep2 >= 0 ? content.slice(sep2 + 4).trim() : content.trim();
|
|
@@ -3847,7 +4297,7 @@ async function loadPlugin(pluginDirPath) {
|
|
|
3847
4297
|
const hooksJsonPath = join6(pluginDirPath, "hooks", "hooks.json");
|
|
3848
4298
|
let hasStopHook = false;
|
|
3849
4299
|
try {
|
|
3850
|
-
const hooksRaw = await
|
|
4300
|
+
const hooksRaw = await readFile9(hooksJsonPath, "utf8");
|
|
3851
4301
|
const hooksParsed = JSON.parse(hooksRaw);
|
|
3852
4302
|
const stopHooks = hooksParsed?.hooks?.Stop;
|
|
3853
4303
|
if (Array.isArray(stopHooks) && stopHooks.length > 0) {
|
|
@@ -3940,13 +4390,13 @@ function getActiveStopHookPlugins() {
|
|
|
3940
4390
|
}
|
|
3941
4391
|
|
|
3942
4392
|
// src/plugins/stopHook.ts
|
|
3943
|
-
import { readFile as
|
|
4393
|
+
import { readFile as readFile10 } from "fs/promises";
|
|
3944
4394
|
import { join as join7 } from "path";
|
|
3945
4395
|
import { spawn as spawn4 } from "child_process";
|
|
3946
4396
|
async function loadStopHookConfig(pluginRoot) {
|
|
3947
4397
|
const hooksJsonPath = join7(pluginRoot, "hooks", "hooks.json");
|
|
3948
4398
|
try {
|
|
3949
|
-
const raw = await
|
|
4399
|
+
const raw = await readFile10(hooksJsonPath, "utf8");
|
|
3950
4400
|
const parsed = JSON.parse(raw);
|
|
3951
4401
|
const stopEntries = parsed?.hooks?.Stop;
|
|
3952
4402
|
if (!Array.isArray(stopEntries)) return [];
|
|
@@ -4028,7 +4478,7 @@ function runSingleStopHook(hookConfig, pluginRoot, transcriptText) {
|
|
|
4028
4478
|
}
|
|
4029
4479
|
|
|
4030
4480
|
// src/plugins/verificationGate.ts
|
|
4031
|
-
import { existsSync as
|
|
4481
|
+
import { existsSync as existsSync7, readFileSync } from "fs";
|
|
4032
4482
|
import { spawn as spawn5 } from "child_process";
|
|
4033
4483
|
function parseVerifyArg(arg) {
|
|
4034
4484
|
const colonIdx = arg.indexOf(":");
|
|
@@ -4105,7 +4555,7 @@ async function verifyShell(command, timeout) {
|
|
|
4105
4555
|
};
|
|
4106
4556
|
}
|
|
4107
4557
|
function verifyFileExists(file) {
|
|
4108
|
-
const exists =
|
|
4558
|
+
const exists = existsSync7(file);
|
|
4109
4559
|
return {
|
|
4110
4560
|
check: { type: "file_exists", file },
|
|
4111
4561
|
passed: exists,
|
|
@@ -4205,8 +4655,8 @@ function formatCheckName(check) {
|
|
|
4205
4655
|
}
|
|
4206
4656
|
|
|
4207
4657
|
// src/plugins/goalState.ts
|
|
4208
|
-
import { mkdir as mkdir6, appendFile, writeFile as
|
|
4209
|
-
import { existsSync as
|
|
4658
|
+
import { mkdir as mkdir6, appendFile, writeFile as writeFile6, unlink as unlink2, rmdir as rmdir2 } from "fs/promises";
|
|
4659
|
+
import { existsSync as existsSync8, readFileSync as readFileSync2 } from "fs";
|
|
4210
4660
|
import { join as join8 } from "path";
|
|
4211
4661
|
var Phase = /* @__PURE__ */ ((Phase2) => {
|
|
4212
4662
|
Phase2["PLAN"] = "plan";
|
|
@@ -4275,8 +4725,8 @@ ${goal}
|
|
|
4275
4725
|
};
|
|
4276
4726
|
for (const [name, content] of Object.entries(files)) {
|
|
4277
4727
|
const path2 = join8(this.dir, `${name}.md`);
|
|
4278
|
-
if (!
|
|
4279
|
-
await
|
|
4728
|
+
if (!existsSync8(path2)) {
|
|
4729
|
+
await writeFile6(path2, content);
|
|
4280
4730
|
}
|
|
4281
4731
|
}
|
|
4282
4732
|
}
|
|
@@ -4320,14 +4770,14 @@ ${goal}
|
|
|
4320
4770
|
`\u975E\u6CD5\u9636\u6BB5\u5207\u6362: ${current} \u2192 ${phase}\u3002\u8BE5\u76EE\u6807\u9636\u6BB5\u4E0D\u5728 PHASE_TRANSITIONS[${current}] \u7684\u53EF\u8FBE\u96C6\u5408\u5185\uFF0C\u9700\u8981\u8D70 forceSetPhase\u3002`
|
|
4321
4771
|
);
|
|
4322
4772
|
}
|
|
4323
|
-
await
|
|
4773
|
+
await writeFile6(join8(this.dir, "phase.md"), phase);
|
|
4324
4774
|
await this.appendProgress(`PHASE \u2192 ${phase}: ${reason}`);
|
|
4325
4775
|
}
|
|
4326
4776
|
async forceSetPhase(phase, reason) {
|
|
4327
4777
|
if (!VALID_PHASES.has(phase)) {
|
|
4328
4778
|
throw new Error(`Invalid phase: ${phase}`);
|
|
4329
4779
|
}
|
|
4330
|
-
await
|
|
4780
|
+
await writeFile6(join8(this.dir, "phase.md"), phase);
|
|
4331
4781
|
await this.appendProgress(`PHASE \u2192 ${phase}: ${reason}`);
|
|
4332
4782
|
}
|
|
4333
4783
|
async appendProgress(line) {
|
|
@@ -4945,6 +5395,7 @@ function useChat(args) {
|
|
|
4945
5395
|
setCompacting(false);
|
|
4946
5396
|
bump();
|
|
4947
5397
|
await clearContext();
|
|
5398
|
+
clearFileState();
|
|
4948
5399
|
}, [bump, isLoading]);
|
|
4949
5400
|
const compactNow = useCallback5(async () => {
|
|
4950
5401
|
if (isLoading) return;
|
|
@@ -5298,7 +5749,7 @@ async function main() {
|
|
|
5298
5749
|
const dirArg = extractCwdArg(args);
|
|
5299
5750
|
if (dirArg) {
|
|
5300
5751
|
const abs = resolve8(dirArg);
|
|
5301
|
-
if (!
|
|
5752
|
+
if (!existsSync9(abs)) {
|
|
5302
5753
|
mkdirSync(abs, { recursive: true });
|
|
5303
5754
|
}
|
|
5304
5755
|
process.chdir(abs);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "minimal-agent",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "最小化 Agent 系统 —— 单对话 +
|
|
3
|
+
"version": "0.1.8",
|
|
4
|
+
"description": "最小化 Agent 系统 —— 单对话 + 10 工具 + MultiEdit + Pre-read Guard + 自动压缩 + OpenAI 兼容 + Ink TUI",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"author": "Bill Wang <leiwang0359@gmail.com>",
|
|
7
7
|
"repository": {
|