jinzd-ai-cli 0.4.23 → 0.4.25
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/chunk-4BKXL7SM.js +98 -0
- package/dist/{chunk-PDVX5QJA.js → chunk-5GZQLJAY.js} +1068 -201
- package/dist/{chunk-UA4BVWKV.js → chunk-AHH5I2U6.js} +1 -1
- package/dist/{chunk-XMTMCMAP.js → chunk-ETMUP3CY.js} +1 -1
- package/dist/chunk-SKET65WZ.js +96 -0
- package/dist/{chunk-GBMVHLPA.js → chunk-SS7BQZ5R.js} +2 -198
- package/dist/{hub-YN245LMP.js → hub-JOYPSPR2.js} +1 -1
- package/dist/index.js +170 -500
- package/dist/{run-tests-2S6SYL2M.js → run-tests-25BZE3KQ.js} +1 -1
- package/dist/{run-tests-7ZBI4ZTU.js → run-tests-L3JNRB6X.js} +1 -1
- package/dist/{server-SD5ICBFP.js → server-V3IZSAMO.js} +126 -7
- package/dist/{task-orchestrator-C472QXTJ.js → task-orchestrator-4N5UUA6L.js} +3 -2
- package/dist/web/client/app.js +16 -3
- package/package.json +1 -1
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
fileCheckpoints
|
|
4
|
+
} from "./chunk-4BKXL7SM.js";
|
|
2
5
|
import {
|
|
3
6
|
CONFIG_DIR_NAME,
|
|
4
7
|
MEMORY_FILE_NAME,
|
|
@@ -6,7 +9,7 @@ import {
|
|
|
6
9
|
SUBAGENT_DEFAULT_MAX_ROUNDS,
|
|
7
10
|
SUBAGENT_MAX_ROUNDS_LIMIT,
|
|
8
11
|
runTestsTool
|
|
9
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-AHH5I2U6.js";
|
|
10
13
|
|
|
11
14
|
// src/tools/builtin/bash.ts
|
|
12
15
|
import { execSync } from "child_process";
|
|
@@ -586,37 +589,849 @@ Cannot extract text from this PDF, but found alternative text versions:
|
|
|
586
589
|
` + textAlts.map((p) => ` \u2192 ${basename(p)}`).join("\n") + `
|
|
587
590
|
Please use read_file to read the above files.`;
|
|
588
591
|
}
|
|
589
|
-
return `[PDF file: ${filePath}]
|
|
590
|
-
Cannot extract text from this PDF (requires pdftotext or pdfminer.six).
|
|
591
|
-
Suggestion: read existing text versions (.md / .txt) in the project, or install extraction tools via bash and retry.`;
|
|
592
|
-
}
|
|
593
|
-
if (BINARY_EXTENSIONS.has(ext)) {
|
|
594
|
-
return `[Binary file: ${filePath} (${ext})]
|
|
595
|
-
This is a binary file and cannot be read as text. If there is a text version (.md / .txt) in the project, please read that instead.`;
|
|
596
|
-
}
|
|
597
|
-
const { readFile: readFile2 } = await import("fs/promises");
|
|
598
|
-
const buf = size > 1048576 ? await readFile2(normalizedPath) : readFileSync2(normalizedPath);
|
|
599
|
-
if (encoding === "base64") {
|
|
600
|
-
return `[File: ${filePath} | base64]
|
|
601
|
-
|
|
602
|
-
${buf.toString("base64")}`;
|
|
592
|
+
return `[PDF file: ${filePath}]
|
|
593
|
+
Cannot extract text from this PDF (requires pdftotext or pdfminer.six).
|
|
594
|
+
Suggestion: read existing text versions (.md / .txt) in the project, or install extraction tools via bash and retry.`;
|
|
595
|
+
}
|
|
596
|
+
if (BINARY_EXTENSIONS.has(ext)) {
|
|
597
|
+
return `[Binary file: ${filePath} (${ext})]
|
|
598
|
+
This is a binary file and cannot be read as text. If there is a text version (.md / .txt) in the project, please read that instead.`;
|
|
599
|
+
}
|
|
600
|
+
const { readFile: readFile2 } = await import("fs/promises");
|
|
601
|
+
const buf = size > 1048576 ? await readFile2(normalizedPath) : readFileSync2(normalizedPath);
|
|
602
|
+
if (encoding === "base64") {
|
|
603
|
+
return `[File: ${filePath} | base64]
|
|
604
|
+
|
|
605
|
+
${buf.toString("base64")}`;
|
|
606
|
+
}
|
|
607
|
+
if (isBinaryBuffer(buf)) {
|
|
608
|
+
return `[Binary file: ${filePath}]
|
|
609
|
+
This file contains binary data and cannot be read as text.
|
|
610
|
+
If needed, use the bash tool to run an appropriate conversion program.`;
|
|
611
|
+
}
|
|
612
|
+
const content = buf.toString(encoding);
|
|
613
|
+
const lines = content.split("\n").length;
|
|
614
|
+
return `${sensitiveWarning}[File: ${filePath} | ${lines} lines]
|
|
615
|
+
|
|
616
|
+
${content}`;
|
|
617
|
+
}
|
|
618
|
+
};
|
|
619
|
+
|
|
620
|
+
// src/tools/builtin/write-file.ts
|
|
621
|
+
import { writeFileSync as writeFileSync2, appendFileSync, mkdirSync } from "fs";
|
|
622
|
+
import { dirname as dirname2 } from "path";
|
|
623
|
+
|
|
624
|
+
// src/tools/executor.ts
|
|
625
|
+
import chalk3 from "chalk";
|
|
626
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
|
|
627
|
+
|
|
628
|
+
// src/tools/types.ts
|
|
629
|
+
function isFileWriteTool(name) {
|
|
630
|
+
return name === "write_file" || name === "edit_file";
|
|
631
|
+
}
|
|
632
|
+
function getDangerLevel(toolName, args) {
|
|
633
|
+
if (toolName.startsWith("mcp__")) return "safe";
|
|
634
|
+
if (toolName === "bash") {
|
|
635
|
+
const cmd = String(args["command"] ?? "");
|
|
636
|
+
if (/\brm\s+[^\n]*(?:-\w*[rRfF]\w*|--recursive|--force)\b/.test(cmd)) return "destructive";
|
|
637
|
+
if (/\brm\s+\S/.test(cmd)) return "destructive";
|
|
638
|
+
if (/\brmdir\b|\bformat\b|\bmkfs\b/.test(cmd)) return "destructive";
|
|
639
|
+
if (/\bRemove-Item\b.*(?:-Recurse|-Force)|\bri\s+.*-(?:Recurse|Force)\b|\brd\s+\/s\b|\brmdir\s+\/s\b/i.test(cmd)) return "destructive";
|
|
640
|
+
if (/\bdel\s+\S/.test(cmd)) return "destructive";
|
|
641
|
+
if (/\becho\b.*>>?|\btee\b|\bcp\b|\bmv\b/.test(cmd)) return "write";
|
|
642
|
+
if (/\bSet-Content\b|\bOut-File\b|\bAdd-Content\b|\bCopy-Item\b|\bMove-Item\b/i.test(cmd)) return "write";
|
|
643
|
+
return "safe";
|
|
644
|
+
}
|
|
645
|
+
if (toolName === "write_file") return "write";
|
|
646
|
+
if (toolName === "edit_file") return "write";
|
|
647
|
+
if (toolName === "save_last_response") return "write";
|
|
648
|
+
if (toolName === "run_interactive") {
|
|
649
|
+
const exe = String(args["executable"] ?? "").toLowerCase();
|
|
650
|
+
if (/\b(rm|rmdir|del|format|mkfs|Remove-Item)\b/i.test(exe)) return "destructive";
|
|
651
|
+
if (/\b(bash|sh|zsh|cmd|powershell|pwsh|python|node|ruby|perl)\b/i.test(exe)) return "write";
|
|
652
|
+
return "write";
|
|
653
|
+
}
|
|
654
|
+
if (toolName === "task_create" || toolName === "task_stop") return "write";
|
|
655
|
+
if (toolName === "task_list") return "safe";
|
|
656
|
+
if (toolName === "read_file" || toolName === "list_dir" || toolName === "grep_files" || toolName === "glob_files" || toolName === "web_fetch" || toolName === "save_memory" || toolName === "ask_user" || toolName === "write_todos" || toolName === "google_search" || toolName === "spawn_agent" || toolName === "run_tests") return "safe";
|
|
657
|
+
return "write";
|
|
658
|
+
}
|
|
659
|
+
function schemaToJsonSchema(schema) {
|
|
660
|
+
const result = {
|
|
661
|
+
type: schema.type,
|
|
662
|
+
description: schema.description
|
|
663
|
+
};
|
|
664
|
+
if (schema.enum) result["enum"] = schema.enum;
|
|
665
|
+
if (schema.items) result["items"] = schemaToJsonSchema(schema.items);
|
|
666
|
+
if (schema.properties) {
|
|
667
|
+
result["properties"] = Object.fromEntries(
|
|
668
|
+
Object.entries(schema.properties).map(([k, v]) => [k, schemaToJsonSchema(v)])
|
|
669
|
+
);
|
|
670
|
+
}
|
|
671
|
+
return result;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// src/tools/diff-utils.ts
|
|
675
|
+
import chalk from "chalk";
|
|
676
|
+
function renderDiff(oldText, newText, opts = {}) {
|
|
677
|
+
const contextLines = opts.contextLines ?? 3;
|
|
678
|
+
const maxLines = opts.maxLines ?? 120;
|
|
679
|
+
const filePath = opts.filePath ?? "";
|
|
680
|
+
const oldLines = oldText.split("\n");
|
|
681
|
+
const newLines = newText.split("\n");
|
|
682
|
+
const hunks = computeHunks(oldLines, newLines, contextLines);
|
|
683
|
+
if (hunks.length === 0) {
|
|
684
|
+
return chalk.dim(" (no changes)");
|
|
685
|
+
}
|
|
686
|
+
const output = [];
|
|
687
|
+
if (filePath) {
|
|
688
|
+
output.push(chalk.bold.white(`--- ${filePath} (before)`));
|
|
689
|
+
output.push(chalk.bold.white(`+++ ${filePath} (after)`));
|
|
690
|
+
}
|
|
691
|
+
let totalDisplayed = 0;
|
|
692
|
+
for (const hunk of hunks) {
|
|
693
|
+
if (totalDisplayed >= maxLines) {
|
|
694
|
+
output.push(chalk.dim(` ... (diff truncated, too many changes)`));
|
|
695
|
+
break;
|
|
696
|
+
}
|
|
697
|
+
output.push(
|
|
698
|
+
chalk.cyan(
|
|
699
|
+
`@@ -${hunk.oldStart + 1},${hunk.oldCount} +${hunk.newStart + 1},${hunk.newCount} @@`
|
|
700
|
+
)
|
|
701
|
+
);
|
|
702
|
+
for (const line of hunk.lines) {
|
|
703
|
+
if (totalDisplayed >= maxLines) break;
|
|
704
|
+
totalDisplayed++;
|
|
705
|
+
if (line.type === "context") {
|
|
706
|
+
output.push(chalk.dim(` ${line.text}`));
|
|
707
|
+
} else if (line.type === "remove") {
|
|
708
|
+
output.push(chalk.red(`- ${line.text}`));
|
|
709
|
+
} else {
|
|
710
|
+
output.push(chalk.green(`+ ${line.text}`));
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
return output.join("\n");
|
|
715
|
+
}
|
|
716
|
+
function computeHunks(oldLines, newLines, contextLines) {
|
|
717
|
+
const edits = diffLines(oldLines, newLines);
|
|
718
|
+
if (edits.every((e) => e.type === "context")) return [];
|
|
719
|
+
const hunks = [];
|
|
720
|
+
let i = 0;
|
|
721
|
+
while (i < edits.length) {
|
|
722
|
+
if (edits[i].type === "context") {
|
|
723
|
+
i++;
|
|
724
|
+
continue;
|
|
725
|
+
}
|
|
726
|
+
const start = Math.max(0, i - contextLines);
|
|
727
|
+
let end = i;
|
|
728
|
+
while (end < edits.length) {
|
|
729
|
+
if (edits[end].type !== "context") {
|
|
730
|
+
end++;
|
|
731
|
+
} else {
|
|
732
|
+
let hasMoreChange = false;
|
|
733
|
+
for (let j = end + 1; j < Math.min(edits.length, end + contextLines * 2 + 1); j++) {
|
|
734
|
+
if (edits[j].type !== "context") {
|
|
735
|
+
hasMoreChange = true;
|
|
736
|
+
break;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
if (hasMoreChange) {
|
|
740
|
+
end++;
|
|
741
|
+
} else {
|
|
742
|
+
break;
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
end = Math.min(edits.length, end + contextLines);
|
|
747
|
+
const hunkEdits = edits.slice(start, end);
|
|
748
|
+
let oldStart = 0;
|
|
749
|
+
let newStart = 0;
|
|
750
|
+
for (let k = 0; k < start; k++) {
|
|
751
|
+
if (edits[k].type !== "add") oldStart++;
|
|
752
|
+
if (edits[k].type !== "remove") newStart++;
|
|
753
|
+
}
|
|
754
|
+
let oldCount = 0;
|
|
755
|
+
let newCount = 0;
|
|
756
|
+
for (const e of hunkEdits) {
|
|
757
|
+
if (e.type !== "add") oldCount++;
|
|
758
|
+
if (e.type !== "remove") newCount++;
|
|
759
|
+
}
|
|
760
|
+
hunks.push({
|
|
761
|
+
oldStart,
|
|
762
|
+
oldCount,
|
|
763
|
+
newStart,
|
|
764
|
+
newCount,
|
|
765
|
+
lines: hunkEdits.map((e) => ({ type: e.type, text: e.text }))
|
|
766
|
+
});
|
|
767
|
+
i = end;
|
|
768
|
+
}
|
|
769
|
+
return hunks;
|
|
770
|
+
}
|
|
771
|
+
function diffLines(oldLines, newLines) {
|
|
772
|
+
const n = oldLines.length;
|
|
773
|
+
const m = newLines.length;
|
|
774
|
+
if (n * m > 25e4) {
|
|
775
|
+
return simpleDiff(oldLines, newLines);
|
|
776
|
+
}
|
|
777
|
+
const dp = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
|
|
778
|
+
for (let i2 = 1; i2 <= n; i2++) {
|
|
779
|
+
for (let j2 = 1; j2 <= m; j2++) {
|
|
780
|
+
if (oldLines[i2 - 1] === newLines[j2 - 1]) {
|
|
781
|
+
dp[i2][j2] = dp[i2 - 1][j2 - 1] + 1;
|
|
782
|
+
} else {
|
|
783
|
+
dp[i2][j2] = Math.max(dp[i2 - 1][j2], dp[i2][j2 - 1]);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
const result = [];
|
|
788
|
+
let i = n;
|
|
789
|
+
let j = m;
|
|
790
|
+
while (i > 0 || j > 0) {
|
|
791
|
+
if (i > 0 && j > 0 && oldLines[i - 1] === newLines[j - 1]) {
|
|
792
|
+
result.unshift({ type: "context", text: oldLines[i - 1] });
|
|
793
|
+
i--;
|
|
794
|
+
j--;
|
|
795
|
+
} else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) {
|
|
796
|
+
result.unshift({ type: "add", text: newLines[j - 1] });
|
|
797
|
+
j--;
|
|
798
|
+
} else {
|
|
799
|
+
result.unshift({ type: "remove", text: oldLines[i - 1] });
|
|
800
|
+
i--;
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
return result;
|
|
804
|
+
}
|
|
805
|
+
function simpleDiff(oldLines, newLines) {
|
|
806
|
+
const result = [];
|
|
807
|
+
const maxLen = Math.max(oldLines.length, newLines.length);
|
|
808
|
+
for (let i = 0; i < maxLen; i++) {
|
|
809
|
+
const o = oldLines[i];
|
|
810
|
+
const n = newLines[i];
|
|
811
|
+
if (o !== void 0 && n !== void 0) {
|
|
812
|
+
if (o === n) {
|
|
813
|
+
result.push({ type: "context", text: o });
|
|
814
|
+
} else {
|
|
815
|
+
result.push({ type: "remove", text: o });
|
|
816
|
+
result.push({ type: "add", text: n });
|
|
817
|
+
}
|
|
818
|
+
} else if (o !== void 0) {
|
|
819
|
+
result.push({ type: "remove", text: o });
|
|
820
|
+
} else if (n !== void 0) {
|
|
821
|
+
result.push({ type: "add", text: n });
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
return result;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// src/tools/hooks.ts
|
|
828
|
+
import { execSync as execSync3 } from "child_process";
|
|
829
|
+
function shellEscape(value) {
|
|
830
|
+
return "'" + value.replace(/'/g, "'\\''") + "'";
|
|
831
|
+
}
|
|
832
|
+
function runHook(template, vars) {
|
|
833
|
+
if (!template) return;
|
|
834
|
+
let cmd = template;
|
|
835
|
+
cmd = cmd.replace(/\{tool\}/g, shellEscape(vars.tool));
|
|
836
|
+
cmd = cmd.replace(/\{dangerLevel\}/g, shellEscape(vars.dangerLevel ?? ""));
|
|
837
|
+
cmd = cmd.replace(/\{args\}/g, shellEscape(vars.args ?? ""));
|
|
838
|
+
cmd = cmd.replace(/\{status\}/g, shellEscape(vars.status ?? ""));
|
|
839
|
+
try {
|
|
840
|
+
execSync3(cmd, {
|
|
841
|
+
timeout: 5e3,
|
|
842
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
843
|
+
encoding: "utf-8"
|
|
844
|
+
});
|
|
845
|
+
} catch {
|
|
846
|
+
process.stderr.write(`\u26A0 Hook failed: ${cmd.slice(0, 100)}
|
|
847
|
+
`);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
// src/tools/permissions.ts
|
|
852
|
+
function checkPermission(toolName, args, dangerLevel, rules, defaultAction = "confirm") {
|
|
853
|
+
for (const rule of rules) {
|
|
854
|
+
if (rule.tool !== "*" && rule.tool !== toolName) continue;
|
|
855
|
+
if (rule.when) {
|
|
856
|
+
if (rule.when.dangerLevel && rule.when.dangerLevel !== dangerLevel) continue;
|
|
857
|
+
if (rule.when.pathPattern) {
|
|
858
|
+
const path = String(args["path"] ?? args["command"] ?? "");
|
|
859
|
+
if (!path.includes(rule.when.pathPattern)) continue;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
return rule.action;
|
|
863
|
+
}
|
|
864
|
+
return defaultAction;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
// src/tools/truncate.ts
|
|
868
|
+
var DEFAULT_MAX_TOOL_OUTPUT_CHARS = 12e3;
|
|
869
|
+
function getMaxOutputChars(contextWindow) {
|
|
870
|
+
if (!contextWindow || contextWindow <= 0) return DEFAULT_MAX_TOOL_OUTPUT_CHARS;
|
|
871
|
+
return Math.max(DEFAULT_MAX_TOOL_OUTPUT_CHARS, Math.min(Math.floor(contextWindow / 4), 12e4));
|
|
872
|
+
}
|
|
873
|
+
var activeMaxChars = DEFAULT_MAX_TOOL_OUTPUT_CHARS;
|
|
874
|
+
function setContextWindow(contextWindow) {
|
|
875
|
+
activeMaxChars = getMaxOutputChars(contextWindow);
|
|
876
|
+
}
|
|
877
|
+
function getActiveMaxChars() {
|
|
878
|
+
return activeMaxChars;
|
|
879
|
+
}
|
|
880
|
+
function truncateOutput(content, toolName, maxChars) {
|
|
881
|
+
const limit = maxChars ?? activeMaxChars;
|
|
882
|
+
if (content.length <= limit) return content;
|
|
883
|
+
const keepHead = Math.floor(limit * 0.7);
|
|
884
|
+
const keepTail = Math.floor(limit * 0.2);
|
|
885
|
+
const omitted = content.length - keepHead - keepTail;
|
|
886
|
+
const lines = content.split("\n").length;
|
|
887
|
+
const head = content.slice(0, keepHead);
|
|
888
|
+
const tail = content.slice(content.length - keepTail);
|
|
889
|
+
return head + `
|
|
890
|
+
|
|
891
|
+
... [Output truncated: ${content.length} chars / ${lines} lines total, ${omitted} chars omitted. Use read_file to view full content in segments` + (toolName === "bash" ? ", or narrow the command scope" : "") + `] ...
|
|
892
|
+
|
|
893
|
+
` + tail;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
// src/repl/theme.ts
|
|
897
|
+
import chalk2 from "chalk";
|
|
898
|
+
var DARK_THEME = {
|
|
899
|
+
prompt: chalk2.green,
|
|
900
|
+
info: chalk2.cyan,
|
|
901
|
+
warning: chalk2.yellow,
|
|
902
|
+
error: chalk2.red,
|
|
903
|
+
success: chalk2.green,
|
|
904
|
+
dim: chalk2.dim,
|
|
905
|
+
accent: chalk2.cyan,
|
|
906
|
+
toolCall: chalk2.yellow,
|
|
907
|
+
toolResult: chalk2.green,
|
|
908
|
+
heading: chalk2.bold.cyan
|
|
909
|
+
};
|
|
910
|
+
var LIGHT_THEME = {
|
|
911
|
+
prompt: chalk2.blue,
|
|
912
|
+
info: chalk2.blueBright,
|
|
913
|
+
warning: chalk2.yellow,
|
|
914
|
+
error: chalk2.red,
|
|
915
|
+
success: chalk2.green,
|
|
916
|
+
dim: chalk2.gray,
|
|
917
|
+
accent: chalk2.blueBright,
|
|
918
|
+
toolCall: chalk2.magenta,
|
|
919
|
+
toolResult: chalk2.green,
|
|
920
|
+
heading: chalk2.bold.blue
|
|
921
|
+
};
|
|
922
|
+
function resolveColor(name) {
|
|
923
|
+
if (name.startsWith("#")) return chalk2.hex(name);
|
|
924
|
+
const parts = name.split(".");
|
|
925
|
+
let result = chalk2;
|
|
926
|
+
for (const part of parts) {
|
|
927
|
+
const obj = result;
|
|
928
|
+
if (obj && typeof obj[part] !== "undefined") {
|
|
929
|
+
result = obj[part];
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
if (typeof result !== "function") {
|
|
933
|
+
process.stderr.write(`[theme] Warning: unrecognized color "${name}", using default.
|
|
934
|
+
`);
|
|
935
|
+
return chalk2;
|
|
936
|
+
}
|
|
937
|
+
return result;
|
|
938
|
+
}
|
|
939
|
+
function buildCustomTheme(base, overrides) {
|
|
940
|
+
if (!overrides) return base;
|
|
941
|
+
const result = { ...base };
|
|
942
|
+
for (const [key, colorName] of Object.entries(overrides)) {
|
|
943
|
+
if (key in result && colorName) {
|
|
944
|
+
result[key] = resolveColor(colorName);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
return result;
|
|
948
|
+
}
|
|
949
|
+
var _currentTheme = DARK_THEME;
|
|
950
|
+
function initTheme(themeId = "dark", customColors) {
|
|
951
|
+
switch (themeId) {
|
|
952
|
+
case "light":
|
|
953
|
+
_currentTheme = LIGHT_THEME;
|
|
954
|
+
break;
|
|
955
|
+
case "custom":
|
|
956
|
+
_currentTheme = buildCustomTheme(DARK_THEME, customColors);
|
|
957
|
+
break;
|
|
958
|
+
default:
|
|
959
|
+
_currentTheme = DARK_THEME;
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
var theme = new Proxy(DARK_THEME, {
|
|
963
|
+
get(_target, prop) {
|
|
964
|
+
return _currentTheme[prop];
|
|
965
|
+
}
|
|
966
|
+
});
|
|
967
|
+
|
|
968
|
+
// src/tools/executor.ts
|
|
969
|
+
var ToolExecutor = class {
|
|
970
|
+
constructor(registry) {
|
|
971
|
+
this.registry = registry;
|
|
972
|
+
}
|
|
973
|
+
/** 当前 session 消息索引,由 repl.ts / session-handler.ts 在每轮工具执行前设置 */
|
|
974
|
+
static currentMessageIndex = 0;
|
|
975
|
+
round = 0;
|
|
976
|
+
totalRounds = 0;
|
|
977
|
+
/** readline 接口引用,由 repl.ts 注入,用于 confirm() 读取用户输入 */
|
|
978
|
+
rl = null;
|
|
979
|
+
/**
|
|
980
|
+
* confirm() 进行中标志。
|
|
981
|
+
* repl.ts 的主循环 line handler 在此为 true 时忽略输入,
|
|
982
|
+
* 防止用户输入 "y"+Enter 被同时触发 once('line') 和主循环 on('line')。
|
|
983
|
+
*/
|
|
984
|
+
confirming = false;
|
|
985
|
+
/** 在 confirm 期间用户输入的 slash 命令,由 repl.ts 主循环消费 */
|
|
986
|
+
pendingSlashCommand = null;
|
|
987
|
+
/** confirm() 的取消回调,由 SIGINT handler 调用 */
|
|
988
|
+
cancelConfirmFn = null;
|
|
989
|
+
/**
|
|
990
|
+
* 会话级 auto-approve:跳过所有 write/destructive 确认(仅当前会话有效)。
|
|
991
|
+
* 通过 /yolo 命令切换。destructive 操作仍会显示警告但不阻塞。
|
|
992
|
+
*/
|
|
993
|
+
sessionAutoApprove = false;
|
|
994
|
+
/**
|
|
995
|
+
* 由外部(repl.ts SIGINT handler)调用,将当前 confirm() 等待视为用户按 N 取消。
|
|
996
|
+
* 若当前没有 confirm() 进行中,无操作。
|
|
997
|
+
*/
|
|
998
|
+
cancelConfirm() {
|
|
999
|
+
if (this.cancelConfirmFn) {
|
|
1000
|
+
this.cancelConfirmFn();
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
setRoundInfo(current, total) {
|
|
1004
|
+
this.round = current;
|
|
1005
|
+
this.totalRounds = total;
|
|
1006
|
+
}
|
|
1007
|
+
/**
|
|
1008
|
+
* 注入 readline 接口,供 confirm() 使用。
|
|
1009
|
+
* 必须在 start() 之前调用,rl 初始化后立即注入。
|
|
1010
|
+
*/
|
|
1011
|
+
setReadline(rl) {
|
|
1012
|
+
this.rl = rl;
|
|
1013
|
+
}
|
|
1014
|
+
/** 钩子配置(可选) */
|
|
1015
|
+
hookConfig;
|
|
1016
|
+
/** 权限规则(可选) */
|
|
1017
|
+
permissionRules = [];
|
|
1018
|
+
defaultPermission = "confirm";
|
|
1019
|
+
/** 注入 hooks 和 permission rules 配置 */
|
|
1020
|
+
setConfig(opts) {
|
|
1021
|
+
this.hookConfig = opts.hookConfig;
|
|
1022
|
+
if (opts.permissionRules) this.permissionRules = opts.permissionRules;
|
|
1023
|
+
if (opts.defaultPermission) this.defaultPermission = opts.defaultPermission;
|
|
1024
|
+
}
|
|
1025
|
+
async execute(call) {
|
|
1026
|
+
const tool = this.registry.get(call.name);
|
|
1027
|
+
if (!tool) {
|
|
1028
|
+
return {
|
|
1029
|
+
callId: call.id,
|
|
1030
|
+
content: `Unknown tool: ${call.name}`,
|
|
1031
|
+
isError: true
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
1034
|
+
const dangerLevel = getDangerLevel(call.name, call.arguments);
|
|
1035
|
+
runHook(this.hookConfig?.preToolExecution, {
|
|
1036
|
+
tool: call.name,
|
|
1037
|
+
dangerLevel,
|
|
1038
|
+
args: JSON.stringify(call.arguments).slice(0, 200)
|
|
1039
|
+
});
|
|
1040
|
+
if (this.permissionRules.length > 0) {
|
|
1041
|
+
const action = checkPermission(call.name, call.arguments, dangerLevel, this.permissionRules, this.defaultPermission);
|
|
1042
|
+
if (action === "deny") {
|
|
1043
|
+
return { callId: call.id, content: `[Permission denied] Tool ${call.name} is blocked by permission rules. Do not retry.`, isError: true };
|
|
1044
|
+
}
|
|
1045
|
+
if (action === "auto-approve") {
|
|
1046
|
+
this.printToolCall(call);
|
|
1047
|
+
try {
|
|
1048
|
+
const rawContent = await tool.execute(call.arguments);
|
|
1049
|
+
const content = truncateOutput(rawContent, call.name);
|
|
1050
|
+
const wasTruncated = content !== rawContent;
|
|
1051
|
+
this.printToolResult(call.name, rawContent, false, wasTruncated);
|
|
1052
|
+
runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "ok" });
|
|
1053
|
+
return { callId: call.id, content, isError: false };
|
|
1054
|
+
} catch (err) {
|
|
1055
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1056
|
+
this.printToolResult(call.name, message, true, false);
|
|
1057
|
+
runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "error" });
|
|
1058
|
+
return { callId: call.id, content: message, isError: true };
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
if (this.sessionAutoApprove && dangerLevel !== "safe") {
|
|
1063
|
+
this.printToolCall(call);
|
|
1064
|
+
if (dangerLevel === "write") this.printDiffPreview(call);
|
|
1065
|
+
console.log(theme.warning(" \u26A1 Auto-approved (session /yolo mode)"));
|
|
1066
|
+
try {
|
|
1067
|
+
const rawContent = await tool.execute(call.arguments);
|
|
1068
|
+
const content = truncateOutput(rawContent, call.name);
|
|
1069
|
+
const wasTruncated = content !== rawContent;
|
|
1070
|
+
this.printToolResult(call.name, rawContent, false, wasTruncated);
|
|
1071
|
+
runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "ok" });
|
|
1072
|
+
return { callId: call.id, content, isError: false };
|
|
1073
|
+
} catch (err) {
|
|
1074
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1075
|
+
this.printToolResult(call.name, message, true, false);
|
|
1076
|
+
runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "error" });
|
|
1077
|
+
return { callId: call.id, content: message, isError: true };
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
if (dangerLevel === "write") {
|
|
1081
|
+
this.printToolCall(call);
|
|
1082
|
+
this.printDiffPreview(call);
|
|
1083
|
+
const confirmed = await this.confirm(call, dangerLevel);
|
|
1084
|
+
if (!confirmed) {
|
|
1085
|
+
return {
|
|
1086
|
+
callId: call.id,
|
|
1087
|
+
content: `[User cancelled] The user declined the ${call.name} operation. Do not retry without asking.`,
|
|
1088
|
+
isError: true
|
|
1089
|
+
};
|
|
1090
|
+
}
|
|
1091
|
+
} else if (dangerLevel === "destructive") {
|
|
1092
|
+
const confirmed = await this.confirm(call, dangerLevel);
|
|
1093
|
+
if (!confirmed) {
|
|
1094
|
+
return {
|
|
1095
|
+
callId: call.id,
|
|
1096
|
+
content: `[User cancelled] The user declined the destructive ${call.name} operation. Do not retry without asking.`,
|
|
1097
|
+
isError: true
|
|
1098
|
+
};
|
|
1099
|
+
}
|
|
1100
|
+
this.printToolCall(call);
|
|
1101
|
+
} else {
|
|
1102
|
+
this.printToolCall(call);
|
|
1103
|
+
}
|
|
1104
|
+
try {
|
|
1105
|
+
const rawContent = await tool.execute(call.arguments);
|
|
1106
|
+
const content = truncateOutput(rawContent, call.name);
|
|
1107
|
+
const wasTruncated = content !== rawContent;
|
|
1108
|
+
this.printToolResult(call.name, rawContent, false, wasTruncated);
|
|
1109
|
+
runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "ok" });
|
|
1110
|
+
return { callId: call.id, content, isError: false };
|
|
1111
|
+
} catch (err) {
|
|
1112
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1113
|
+
this.printToolResult(call.name, message, true, false);
|
|
1114
|
+
runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "error" });
|
|
1115
|
+
return { callId: call.id, content: message, isError: true };
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
async executeAll(calls) {
|
|
1119
|
+
const safeCalls = [];
|
|
1120
|
+
const fileWriteCalls = [];
|
|
1121
|
+
const otherCalls = [];
|
|
1122
|
+
for (let i = 0; i < calls.length; i++) {
|
|
1123
|
+
const call = calls[i];
|
|
1124
|
+
const level = getDangerLevel(call.name, call.arguments);
|
|
1125
|
+
if (level === "safe") {
|
|
1126
|
+
safeCalls.push({ idx: i, call });
|
|
1127
|
+
} else if (isFileWriteTool(call.name) && level === "write") {
|
|
1128
|
+
fileWriteCalls.push({ idx: i, call });
|
|
1129
|
+
} else {
|
|
1130
|
+
otherCalls.push({ idx: i, call });
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
const results = new Array(calls.length);
|
|
1134
|
+
await Promise.all(
|
|
1135
|
+
safeCalls.map(async ({ idx, call }) => {
|
|
1136
|
+
results[idx] = await this.execute(call);
|
|
1137
|
+
})
|
|
1138
|
+
);
|
|
1139
|
+
if (fileWriteCalls.length === 1) {
|
|
1140
|
+
const { idx, call } = fileWriteCalls[0];
|
|
1141
|
+
results[idx] = await this.execute(call);
|
|
1142
|
+
} else if (fileWriteCalls.length >= 2) {
|
|
1143
|
+
const batchResults = await this.executeBatchFileWrites(fileWriteCalls.map((f) => f.call));
|
|
1144
|
+
for (let i = 0; i < fileWriteCalls.length; i++) {
|
|
1145
|
+
results[fileWriteCalls[i].idx] = batchResults[i];
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
for (const { idx, call } of otherCalls) {
|
|
1149
|
+
results[idx] = await this.execute(call);
|
|
1150
|
+
}
|
|
1151
|
+
return results;
|
|
1152
|
+
}
|
|
1153
|
+
/**
|
|
1154
|
+
* 批量文件写入:展示所有文件的编号列表 + diff 预览,
|
|
1155
|
+
* 然后让用户 approve all / reject all / 选择性 approve。
|
|
1156
|
+
*/
|
|
1157
|
+
async executeBatchFileWrites(calls) {
|
|
1158
|
+
console.log();
|
|
1159
|
+
console.log(theme.heading(`\u270E Batch file writes (${calls.length} files):`));
|
|
1160
|
+
console.log(theme.dim("\u2500".repeat(50)));
|
|
1161
|
+
for (let i = 0; i < calls.length; i++) {
|
|
1162
|
+
const call = calls[i];
|
|
1163
|
+
const filePath = String(call.arguments["path"] ?? "");
|
|
1164
|
+
console.log(theme.warning(` [${i + 1}] `) + chalk3.white(call.name) + theme.dim(": ") + theme.accent(filePath));
|
|
1165
|
+
this.printDiffPreview(call);
|
|
1166
|
+
}
|
|
1167
|
+
console.log(theme.dim("\u2500".repeat(50)));
|
|
1168
|
+
const decision = this.sessionAutoApprove ? "all" : await this.batchConfirm(calls.length);
|
|
1169
|
+
if (this.sessionAutoApprove) {
|
|
1170
|
+
console.log(theme.warning(" \u26A1 All auto-approved (session /yolo mode)"));
|
|
1171
|
+
}
|
|
1172
|
+
const results = [];
|
|
1173
|
+
for (let i = 0; i < calls.length; i++) {
|
|
1174
|
+
const call = calls[i];
|
|
1175
|
+
const approved = decision === "all" || decision !== "none" && decision.has(i + 1);
|
|
1176
|
+
if (approved) {
|
|
1177
|
+
const tool = this.registry.get(call.name);
|
|
1178
|
+
if (!tool) {
|
|
1179
|
+
results.push({ callId: call.id, content: `Unknown tool: ${call.name}`, isError: true });
|
|
1180
|
+
continue;
|
|
1181
|
+
}
|
|
1182
|
+
try {
|
|
1183
|
+
const rawContent = await tool.execute(call.arguments);
|
|
1184
|
+
const content = truncateOutput(rawContent, call.name);
|
|
1185
|
+
const wasTruncated = content !== rawContent;
|
|
1186
|
+
this.printToolResult(call.name, rawContent, false, wasTruncated);
|
|
1187
|
+
results.push({ callId: call.id, content, isError: false });
|
|
1188
|
+
} catch (err) {
|
|
1189
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1190
|
+
this.printToolResult(call.name, message, true, false);
|
|
1191
|
+
results.push({ callId: call.id, content: message, isError: true });
|
|
1192
|
+
}
|
|
1193
|
+
} else {
|
|
1194
|
+
console.log(theme.dim(` [${i + 1}] `) + theme.dim("rejected"));
|
|
1195
|
+
results.push({ callId: call.id, content: `[User rejected] The user rejected this ${call.name} operation. Do not retry without asking.`, isError: true });
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
return results;
|
|
1199
|
+
}
|
|
1200
|
+
/**
|
|
1201
|
+
* 批量确认:让用户选择 approve all / reject all / 指定编号。
|
|
1202
|
+
* 返回 'all' | 'none' | Set<number>(1-based 编号)
|
|
1203
|
+
*/
|
|
1204
|
+
batchConfirm(count) {
|
|
1205
|
+
const prompt = theme.warning(` [a]pprove all [r]eject all [1,${count > 1 ? count : "2"},..] approve specific: `);
|
|
1206
|
+
if (!this.rl) {
|
|
1207
|
+
process.stdout.write(theme.warning("No readline: auto-rejected.\n"));
|
|
1208
|
+
return Promise.resolve("none");
|
|
1209
|
+
}
|
|
1210
|
+
const rl = this.rl;
|
|
1211
|
+
const rlAny = rl;
|
|
1212
|
+
const savedOutput = rlAny.output;
|
|
1213
|
+
rlAny.output = process.stdout;
|
|
1214
|
+
rl.resume();
|
|
1215
|
+
process.stdout.write(prompt);
|
|
1216
|
+
this.confirming = true;
|
|
1217
|
+
return new Promise((resolve4) => {
|
|
1218
|
+
let completed = false;
|
|
1219
|
+
const cleanup = (result) => {
|
|
1220
|
+
if (completed) return;
|
|
1221
|
+
completed = true;
|
|
1222
|
+
rl.removeListener("line", onLine);
|
|
1223
|
+
this.cancelConfirmFn = null;
|
|
1224
|
+
rl.pause();
|
|
1225
|
+
rlAny.output = savedOutput;
|
|
1226
|
+
this.confirming = false;
|
|
1227
|
+
resolve4(result);
|
|
1228
|
+
};
|
|
1229
|
+
const onLine = (line) => {
|
|
1230
|
+
const trimmed = line.trim();
|
|
1231
|
+
if (trimmed.startsWith("/")) {
|
|
1232
|
+
this.pendingSlashCommand = trimmed;
|
|
1233
|
+
process.stdout.write(theme.dim(`
|
|
1234
|
+
(command "${trimmed}" queued, will execute after current operation)
|
|
1235
|
+
`));
|
|
1236
|
+
cleanup("none");
|
|
1237
|
+
return;
|
|
1238
|
+
}
|
|
1239
|
+
const input = trimmed.toLowerCase();
|
|
1240
|
+
if (input === "a" || input === "all" || input === "y") {
|
|
1241
|
+
cleanup("all");
|
|
1242
|
+
} else if (input === "r" || input === "reject" || input === "n" || input === "") {
|
|
1243
|
+
cleanup("none");
|
|
1244
|
+
} else {
|
|
1245
|
+
const nums = input.split(/[,\s]+/).map(Number).filter((n) => !isNaN(n) && n >= 1 && n <= count);
|
|
1246
|
+
if (nums.length > 0) {
|
|
1247
|
+
cleanup(new Set(nums));
|
|
1248
|
+
} else {
|
|
1249
|
+
cleanup("none");
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
};
|
|
1253
|
+
this.cancelConfirmFn = () => {
|
|
1254
|
+
process.stdout.write(theme.dim("\n(cancelled)\n"));
|
|
1255
|
+
cleanup("none");
|
|
1256
|
+
};
|
|
1257
|
+
try {
|
|
1258
|
+
rl.once("line", onLine);
|
|
1259
|
+
} catch {
|
|
1260
|
+
cleanup("none");
|
|
1261
|
+
}
|
|
1262
|
+
});
|
|
1263
|
+
}
|
|
1264
|
+
printToolCall(call) {
|
|
1265
|
+
const dangerLevel = getDangerLevel(call.name, call.arguments);
|
|
1266
|
+
console.log();
|
|
1267
|
+
const icon = dangerLevel === "write" ? theme.toolCall("\u270E Tool: ") : theme.heading(theme.accent("\u2699 Tool: "));
|
|
1268
|
+
const roundBadge = this.totalRounds > 0 ? theme.dim(` [${this.round}/${this.totalRounds}]`) : "";
|
|
1269
|
+
console.log(icon + chalk3.white(call.name) + roundBadge);
|
|
1270
|
+
for (const [key, val] of Object.entries(call.arguments)) {
|
|
1271
|
+
let valStr;
|
|
1272
|
+
if (Array.isArray(val)) {
|
|
1273
|
+
const json = JSON.stringify(val);
|
|
1274
|
+
valStr = json.length > 160 ? json.slice(0, 160) + "..." : json;
|
|
1275
|
+
} else if (typeof val === "string" && val.length > 120) {
|
|
1276
|
+
valStr = val.slice(0, 120) + "...";
|
|
1277
|
+
} else {
|
|
1278
|
+
valStr = String(val);
|
|
1279
|
+
}
|
|
1280
|
+
console.log(theme.dim(` ${key}: `) + chalk3.white(valStr));
|
|
603
1281
|
}
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
1282
|
+
}
|
|
1283
|
+
/**
|
|
1284
|
+
* 对 write_file / edit_file 在执行前展示 diff 预览。
|
|
1285
|
+
* - write_file:比较旧文件内容与新内容
|
|
1286
|
+
* - edit_file (replace):比较旧字符串与新字符串
|
|
1287
|
+
* - edit_file (insert/delete):显示操作摘要,不做 diff(变化明确)
|
|
1288
|
+
*/
|
|
1289
|
+
printDiffPreview(call) {
|
|
1290
|
+
if (call.name === "write_file") {
|
|
1291
|
+
const filePath = String(call.arguments["path"] ?? "");
|
|
1292
|
+
const newContent = String(call.arguments["content"] ?? "");
|
|
1293
|
+
if (!filePath) return;
|
|
1294
|
+
if (existsSync4(filePath)) {
|
|
1295
|
+
let oldContent;
|
|
1296
|
+
try {
|
|
1297
|
+
oldContent = readFileSync3(filePath, "utf-8");
|
|
1298
|
+
} catch {
|
|
1299
|
+
return;
|
|
1300
|
+
}
|
|
1301
|
+
if (oldContent === newContent) {
|
|
1302
|
+
console.log(theme.dim(" (file content unchanged)"));
|
|
1303
|
+
return;
|
|
1304
|
+
}
|
|
1305
|
+
const diff = renderDiff(oldContent, newContent, { filePath, contextLines: 3 });
|
|
1306
|
+
console.log(theme.dim(" \u2500\u2500 diff preview \u2500\u2500"));
|
|
1307
|
+
console.log(diff);
|
|
1308
|
+
console.log();
|
|
1309
|
+
} else {
|
|
1310
|
+
const lines = newContent.split("\n");
|
|
1311
|
+
const preview = lines.slice(0, 20).map((l) => theme.success(`+ ${l}`)).join("\n");
|
|
1312
|
+
const more = lines.length > 20 ? theme.dim(`
|
|
1313
|
+
... (+${lines.length - 20} more lines)`) : "";
|
|
1314
|
+
console.log(theme.dim(" \u2500\u2500 new file preview \u2500\u2500"));
|
|
1315
|
+
console.log(preview + more);
|
|
1316
|
+
console.log();
|
|
1317
|
+
}
|
|
1318
|
+
} else if (call.name === "edit_file") {
|
|
1319
|
+
const filePath = String(call.arguments["path"] ?? "");
|
|
1320
|
+
if (!filePath || !existsSync4(filePath)) return;
|
|
1321
|
+
const oldStr = call.arguments["old_str"];
|
|
1322
|
+
const newStr = call.arguments["new_str"];
|
|
1323
|
+
if (oldStr !== void 0) {
|
|
1324
|
+
const diff = renderDiff(
|
|
1325
|
+
String(oldStr),
|
|
1326
|
+
String(newStr ?? ""),
|
|
1327
|
+
{ filePath, contextLines: 2 }
|
|
1328
|
+
);
|
|
1329
|
+
console.log(theme.dim(" \u2500\u2500 diff preview \u2500\u2500"));
|
|
1330
|
+
console.log(diff);
|
|
1331
|
+
console.log();
|
|
1332
|
+
} else if (call.arguments["insert_after_line"] !== void 0) {
|
|
1333
|
+
const line = Number(call.arguments["insert_after_line"]);
|
|
1334
|
+
const insertContent = String(call.arguments["insert_content"] ?? "");
|
|
1335
|
+
const insertLines = insertContent.split("\n");
|
|
1336
|
+
const preview = insertLines.slice(0, 5).map((l) => theme.success(`+ ${l}`)).join("\n");
|
|
1337
|
+
const more = insertLines.length > 5 ? theme.dim(`
|
|
1338
|
+
... (+${insertLines.length - 5} more lines)`) : "";
|
|
1339
|
+
console.log(theme.dim(` \u2500\u2500 insert after line ${line} \u2500\u2500`));
|
|
1340
|
+
console.log(preview + more);
|
|
1341
|
+
console.log();
|
|
1342
|
+
} else if (call.arguments["delete_from_line"] !== void 0) {
|
|
1343
|
+
const from = Number(call.arguments["delete_from_line"]);
|
|
1344
|
+
const to = Number(call.arguments["delete_to_line"] ?? from);
|
|
1345
|
+
let fileContent;
|
|
1346
|
+
try {
|
|
1347
|
+
fileContent = readFileSync3(filePath, "utf-8");
|
|
1348
|
+
} catch {
|
|
1349
|
+
return;
|
|
1350
|
+
}
|
|
1351
|
+
const fileLines = fileContent.split("\n");
|
|
1352
|
+
const deleted = fileLines.slice(from - 1, to);
|
|
1353
|
+
const preview = deleted.slice(0, 5).map((l) => theme.error(`- ${l}`)).join("\n");
|
|
1354
|
+
const more = deleted.length > 5 ? theme.dim(`
|
|
1355
|
+
... (-${deleted.length - 5} more lines)`) : "";
|
|
1356
|
+
console.log(theme.dim(` \u2500\u2500 delete lines ${from}\u2013${to} \u2500\u2500`));
|
|
1357
|
+
console.log(preview + more);
|
|
1358
|
+
console.log();
|
|
1359
|
+
}
|
|
608
1360
|
}
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
1361
|
+
}
|
|
1362
|
+
printToolResult(name, content, isError, wasTruncated) {
|
|
1363
|
+
if (isError) {
|
|
1364
|
+
console.log(theme.error(`\u26A0 ${name} error: `) + theme.dim(content.slice(0, 300)));
|
|
1365
|
+
} else {
|
|
1366
|
+
const lines = content.split("\n");
|
|
1367
|
+
const maxLines = name === "run_interactive" ? 40 : 8;
|
|
1368
|
+
const preview = lines.slice(0, maxLines).join("\n");
|
|
1369
|
+
const moreLines = lines.length > maxLines ? theme.dim(`
|
|
1370
|
+
... (${lines.length - maxLines} more lines)`) : "";
|
|
1371
|
+
const truncatedNote = wasTruncated ? theme.warning(`
|
|
1372
|
+
\u26A1 Output truncated to ${getActiveMaxChars()} chars before sending to AI`) : "";
|
|
1373
|
+
console.log(theme.toolResult("\u2713 Result: ") + theme.dim(preview) + moreLines + truncatedNote);
|
|
1374
|
+
}
|
|
1375
|
+
console.log();
|
|
1376
|
+
}
|
|
1377
|
+
confirm(call, level) {
|
|
1378
|
+
const color = level === "destructive" ? theme.error : theme.warning;
|
|
1379
|
+
const label = level === "destructive" ? "\u26A0 DESTRUCTIVE" : "\u270E Write";
|
|
1380
|
+
console.log();
|
|
1381
|
+
console.log(color(`${label} operation: `) + theme.heading(call.name));
|
|
1382
|
+
for (const [key, val] of Object.entries(call.arguments)) {
|
|
1383
|
+
const valStr = typeof val === "string" && val.length > 200 ? val.slice(0, 200) + "..." : String(val);
|
|
1384
|
+
console.log(theme.dim(` ${key}: `) + valStr);
|
|
1385
|
+
}
|
|
1386
|
+
if (!this.rl) {
|
|
1387
|
+
process.stdout.write(theme.warning("No readline: auto-rejected.\n"));
|
|
1388
|
+
return Promise.resolve(false);
|
|
1389
|
+
}
|
|
1390
|
+
const rl = this.rl;
|
|
1391
|
+
const rlAny = rl;
|
|
1392
|
+
const savedOutput = rlAny.output;
|
|
1393
|
+
rlAny.output = process.stdout;
|
|
1394
|
+
rl.resume();
|
|
1395
|
+
process.stdout.write(color("Proceed? [y/N] (type y + Enter to confirm) "));
|
|
1396
|
+
this.confirming = true;
|
|
1397
|
+
return new Promise((resolve4) => {
|
|
1398
|
+
let completed = false;
|
|
1399
|
+
const cleanup = (answer) => {
|
|
1400
|
+
if (completed) return;
|
|
1401
|
+
completed = true;
|
|
1402
|
+
rl.removeListener("line", onLine);
|
|
1403
|
+
this.cancelConfirmFn = null;
|
|
1404
|
+
rl.pause();
|
|
1405
|
+
rlAny.output = savedOutput;
|
|
1406
|
+
this.confirming = false;
|
|
1407
|
+
resolve4(answer === "y");
|
|
1408
|
+
};
|
|
1409
|
+
const onLine = (line) => {
|
|
1410
|
+
const trimmed = line.trim();
|
|
1411
|
+
if (trimmed.startsWith("/")) {
|
|
1412
|
+
this.pendingSlashCommand = trimmed;
|
|
1413
|
+
process.stdout.write(theme.dim(`
|
|
1414
|
+
(command "${trimmed}" queued, will execute after current operation)
|
|
1415
|
+
`));
|
|
1416
|
+
cleanup("n");
|
|
1417
|
+
return;
|
|
1418
|
+
}
|
|
1419
|
+
cleanup(trimmed.toLowerCase());
|
|
1420
|
+
};
|
|
1421
|
+
this.cancelConfirmFn = () => {
|
|
1422
|
+
process.stdout.write(theme.dim("\n(cancelled)\n"));
|
|
1423
|
+
cleanup("n");
|
|
1424
|
+
};
|
|
1425
|
+
try {
|
|
1426
|
+
rl.once("line", onLine);
|
|
1427
|
+
} catch {
|
|
1428
|
+
cleanup("n");
|
|
1429
|
+
}
|
|
1430
|
+
});
|
|
614
1431
|
}
|
|
615
1432
|
};
|
|
616
1433
|
|
|
617
1434
|
// src/tools/builtin/write-file.ts
|
|
618
|
-
import { writeFileSync as writeFileSync2, appendFileSync, mkdirSync } from "fs";
|
|
619
|
-
import { dirname as dirname2 } from "path";
|
|
620
1435
|
var writeFileTool = {
|
|
621
1436
|
definition: {
|
|
622
1437
|
name: "write_file",
|
|
@@ -654,6 +1469,7 @@ Important: For long content (over 500 lines or 3000 chars), you MUST split into
|
|
|
654
1469
|
const appendMode = String(args["append"] ?? "false").toLowerCase() === "true";
|
|
655
1470
|
if (!filePath) throw new ToolError("write_file", "path is required");
|
|
656
1471
|
undoStack.push(filePath, `write_file${appendMode ? " (append)" : ""}: ${filePath}`);
|
|
1472
|
+
fileCheckpoints.snapshot(filePath, ToolExecutor.currentMessageIndex);
|
|
657
1473
|
mkdirSync(dirname2(filePath), { recursive: true });
|
|
658
1474
|
if (appendMode) {
|
|
659
1475
|
appendFileSync(filePath, content, encoding);
|
|
@@ -667,7 +1483,7 @@ Important: For long content (over 500 lines or 3000 chars), you MUST split into
|
|
|
667
1483
|
};
|
|
668
1484
|
|
|
669
1485
|
// src/tools/builtin/edit-file.ts
|
|
670
|
-
import { readFileSync as
|
|
1486
|
+
import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, existsSync as existsSync5 } from "fs";
|
|
671
1487
|
function similarityScore(a, b) {
|
|
672
1488
|
if (a === b) return 1;
|
|
673
1489
|
if (a.length < 2 || b.length < 2) return 0;
|
|
@@ -793,8 +1609,8 @@ Note: Path can be absolute or relative to the current working directory.`,
|
|
|
793
1609
|
const filePath = String(args["path"] ?? "");
|
|
794
1610
|
const encoding = args["encoding"] ?? "utf-8";
|
|
795
1611
|
if (!filePath) throw new ToolError("edit_file", "path is required");
|
|
796
|
-
if (!
|
|
797
|
-
const original =
|
|
1612
|
+
if (!existsSync5(filePath)) throw new ToolError("edit_file", `File not found: ${filePath}`);
|
|
1613
|
+
const original = readFileSync4(filePath, encoding);
|
|
798
1614
|
if (args["old_str"] !== void 0) {
|
|
799
1615
|
const oldStr = String(args["old_str"]);
|
|
800
1616
|
const newStr = String(args["new_str"] ?? "");
|
|
@@ -818,6 +1634,7 @@ Please read the file first and use exact text.`;
|
|
|
818
1634
|
return `ERROR: old_str matches multiple locations with whitespace-tolerant matching. Please include more surrounding context to make it unique.`;
|
|
819
1635
|
}
|
|
820
1636
|
undoStack.push(filePath, `edit_file (ws-replace): ${filePath}`);
|
|
1637
|
+
fileCheckpoints.snapshot(filePath, ToolExecutor.currentMessageIndex);
|
|
821
1638
|
const before = fileLines.slice(0, matchStart);
|
|
822
1639
|
const after = fileLines.slice(matchStart + searchLines.length);
|
|
823
1640
|
const updated2 = [...before, newStr, ...after].join("\n");
|
|
@@ -839,6 +1656,7 @@ ${similar.join("\n")}` : "";
|
|
|
839
1656
|
Please read the file first and use exact text.`;
|
|
840
1657
|
}
|
|
841
1658
|
undoStack.push(filePath, `edit_file (replace_all): ${filePath}`);
|
|
1659
|
+
fileCheckpoints.snapshot(filePath, ToolExecutor.currentMessageIndex);
|
|
842
1660
|
const updated2 = original.split(oldStr).join(newStr);
|
|
843
1661
|
writeFileSync3(filePath, updated2, encoding);
|
|
844
1662
|
return `Successfully edited ${filePath} (replace_all)
|
|
@@ -862,6 +1680,7 @@ Tip: You can also try ignore_whitespace: true to match ignoring indentation diff
|
|
|
862
1680
|
return `ERROR: old_str appears multiple times in file (at least at positions ${firstIndex} and ${secondIndex}). Please include more surrounding context to make it unique.`;
|
|
863
1681
|
}
|
|
864
1682
|
undoStack.push(filePath, `edit_file (replace): ${filePath}`);
|
|
1683
|
+
fileCheckpoints.snapshot(filePath, ToolExecutor.currentMessageIndex);
|
|
865
1684
|
const updated = original.slice(0, firstIndex) + newStr + original.slice(firstIndex + oldStr.length);
|
|
866
1685
|
writeFileSync3(filePath, updated, encoding);
|
|
867
1686
|
const oldLines = oldStr.split("\n").length;
|
|
@@ -881,6 +1700,7 @@ Tip: You can also try ignore_whitespace: true to match ignoring indentation diff
|
|
|
881
1700
|
throw new ToolError("edit_file", `insert_after_line ${afterLine} is out of range (file has ${lines.length} lines)`);
|
|
882
1701
|
}
|
|
883
1702
|
undoStack.push(filePath, `edit_file (insert): ${filePath}`);
|
|
1703
|
+
fileCheckpoints.snapshot(filePath, ToolExecutor.currentMessageIndex);
|
|
884
1704
|
lines.splice(afterLine, 0, content);
|
|
885
1705
|
writeFileSync3(filePath, lines.join("\n"), encoding);
|
|
886
1706
|
return `Successfully inserted ${content.split("\n").length} line(s) after line ${afterLine} in ${filePath}`;
|
|
@@ -896,6 +1716,7 @@ Tip: You can also try ignore_whitespace: true to match ignoring indentation diff
|
|
|
896
1716
|
);
|
|
897
1717
|
}
|
|
898
1718
|
undoStack.push(filePath, `edit_file (delete): ${filePath}`);
|
|
1719
|
+
fileCheckpoints.snapshot(filePath, ToolExecutor.currentMessageIndex);
|
|
899
1720
|
const deleted = lines.splice(fromLine - 1, toLine - fromLine + 1);
|
|
900
1721
|
writeFileSync3(filePath, lines.join("\n"), encoding);
|
|
901
1722
|
return `Successfully deleted lines ${fromLine}-${toLine} (${deleted.length} lines) from ${filePath}`;
|
|
@@ -913,7 +1734,7 @@ function truncatePreview(str, maxLen = 80) {
|
|
|
913
1734
|
}
|
|
914
1735
|
|
|
915
1736
|
// src/tools/builtin/list-dir.ts
|
|
916
|
-
import { readdirSync as readdirSync3, statSync as statSync3, existsSync as
|
|
1737
|
+
import { readdirSync as readdirSync3, statSync as statSync3, existsSync as existsSync6 } from "fs";
|
|
917
1738
|
import { join, basename as basename2 } from "path";
|
|
918
1739
|
var listDirTool = {
|
|
919
1740
|
definition: {
|
|
@@ -936,7 +1757,7 @@ var listDirTool = {
|
|
|
936
1757
|
async execute(args) {
|
|
937
1758
|
const dirPath = String(args["path"] ?? process.cwd());
|
|
938
1759
|
const recursive = Boolean(args["recursive"] ?? false);
|
|
939
|
-
if (!
|
|
1760
|
+
if (!existsSync6(dirPath)) {
|
|
940
1761
|
const targetName = basename2(dirPath).toLowerCase();
|
|
941
1762
|
const cwd = process.cwd();
|
|
942
1763
|
const suggestions = [];
|
|
@@ -1014,7 +1835,7 @@ function formatSize(bytes) {
|
|
|
1014
1835
|
}
|
|
1015
1836
|
|
|
1016
1837
|
// src/tools/builtin/grep-files.ts
|
|
1017
|
-
import { readdirSync as readdirSync4, readFileSync as
|
|
1838
|
+
import { readdirSync as readdirSync4, readFileSync as readFileSync5, statSync as statSync4, existsSync as existsSync7 } from "fs";
|
|
1018
1839
|
import { readFile } from "fs/promises";
|
|
1019
1840
|
import { join as join2, relative } from "path";
|
|
1020
1841
|
var grepFilesTool = {
|
|
@@ -1067,7 +1888,7 @@ Supports regex. Automatically skips node_modules, dist, .git directories.`,
|
|
|
1067
1888
|
const contextLines = Math.max(0, Number(args["context_lines"] ?? 0));
|
|
1068
1889
|
const maxResults = Math.max(1, Number(args["max_results"] ?? 50));
|
|
1069
1890
|
if (!pattern) throw new ToolError("grep_files", "pattern is required");
|
|
1070
|
-
if (!
|
|
1891
|
+
if (!existsSync7(rootPath)) throw new ToolError("grep_files", `Path not found: ${rootPath}`);
|
|
1071
1892
|
const MAX_PATTERN_LENGTH = 1e3;
|
|
1072
1893
|
if (pattern.length > MAX_PATTERN_LENGTH) {
|
|
1073
1894
|
throw new ToolError("grep_files", `Pattern too long (${pattern.length} chars, max ${MAX_PATTERN_LENGTH}). Use a shorter pattern.`);
|
|
@@ -1220,7 +2041,7 @@ function searchInFile(fullPath, displayPath, regex, contextLines, maxResults, re
|
|
|
1220
2041
|
}
|
|
1221
2042
|
let content;
|
|
1222
2043
|
try {
|
|
1223
|
-
content =
|
|
2044
|
+
content = readFileSync5(fullPath, "utf-8");
|
|
1224
2045
|
} catch {
|
|
1225
2046
|
return;
|
|
1226
2047
|
}
|
|
@@ -1251,7 +2072,7 @@ function searchInFile(fullPath, displayPath, regex, contextLines, maxResults, re
|
|
|
1251
2072
|
}
|
|
1252
2073
|
|
|
1253
2074
|
// src/tools/builtin/glob-files.ts
|
|
1254
|
-
import { readdirSync as readdirSync5, statSync as statSync5, existsSync as
|
|
2075
|
+
import { readdirSync as readdirSync5, statSync as statSync5, existsSync as existsSync8 } from "fs";
|
|
1255
2076
|
import { join as join3, relative as relative2, basename as basename3 } from "path";
|
|
1256
2077
|
var globFilesTool = {
|
|
1257
2078
|
definition: {
|
|
@@ -1285,7 +2106,7 @@ Results sorted by most recent modification time. Automatically skips node_module
|
|
|
1285
2106
|
const rootPath = String(args["path"] ?? process.cwd());
|
|
1286
2107
|
const maxResults = Math.max(1, Number(args["max_results"] ?? 100));
|
|
1287
2108
|
if (!pattern) throw new ToolError("glob_files", "pattern is required");
|
|
1288
|
-
if (!
|
|
2109
|
+
if (!existsSync8(rootPath)) throw new ToolError("glob_files", `Path not found: ${rootPath}`);
|
|
1289
2110
|
const regex = globToRegex(pattern);
|
|
1290
2111
|
const matches = [];
|
|
1291
2112
|
collectMatchingFiles(rootPath, rootPath, regex, matches, maxResults);
|
|
@@ -1754,7 +2575,7 @@ var saveLastResponseTool = {
|
|
|
1754
2575
|
};
|
|
1755
2576
|
|
|
1756
2577
|
// src/tools/builtin/save-memory.ts
|
|
1757
|
-
import { existsSync as
|
|
2578
|
+
import { existsSync as existsSync9, statSync as statSync6, appendFileSync as appendFileSync2, mkdirSync as mkdirSync3 } from "fs";
|
|
1758
2579
|
import { join as join4 } from "path";
|
|
1759
2580
|
import { homedir as homedir2 } from "os";
|
|
1760
2581
|
function getMemoryFilePath() {
|
|
@@ -1783,7 +2604,7 @@ var saveMemoryTool = {
|
|
|
1783
2604
|
if (!content) throw new ToolError("save_memory", "content is required");
|
|
1784
2605
|
const memoryPath = getMemoryFilePath();
|
|
1785
2606
|
const configDir = join4(homedir2(), CONFIG_DIR_NAME);
|
|
1786
|
-
if (!
|
|
2607
|
+
if (!existsSync9(configDir)) {
|
|
1787
2608
|
mkdirSync3(configDir, { recursive: true });
|
|
1788
2609
|
}
|
|
1789
2610
|
const timestamp = formatTimestamp();
|
|
@@ -1798,7 +2619,7 @@ ${content}
|
|
|
1798
2619
|
};
|
|
1799
2620
|
|
|
1800
2621
|
// src/tools/builtin/ask-user.ts
|
|
1801
|
-
import
|
|
2622
|
+
import chalk4 from "chalk";
|
|
1802
2623
|
var askUserContext = {
|
|
1803
2624
|
prompting: false
|
|
1804
2625
|
};
|
|
@@ -1835,8 +2656,8 @@ function promptUser(rl, question) {
|
|
|
1835
2656
|
rl.resume();
|
|
1836
2657
|
askUserContext.prompting = true;
|
|
1837
2658
|
console.log();
|
|
1838
|
-
console.log(
|
|
1839
|
-
process.stdout.write(
|
|
2659
|
+
console.log(chalk4.cyan("\u2753 ") + chalk4.bold(question));
|
|
2660
|
+
process.stdout.write(chalk4.cyan("> "));
|
|
1840
2661
|
return new Promise((resolve4) => {
|
|
1841
2662
|
let completed = false;
|
|
1842
2663
|
const cleanup = (answer) => {
|
|
@@ -1853,7 +2674,7 @@ function promptUser(rl, question) {
|
|
|
1853
2674
|
cleanup(line);
|
|
1854
2675
|
};
|
|
1855
2676
|
askUserContext.cancelFn = () => {
|
|
1856
|
-
process.stdout.write(
|
|
2677
|
+
process.stdout.write(chalk4.gray("\n(cancelled)\n"));
|
|
1857
2678
|
cleanup(null);
|
|
1858
2679
|
};
|
|
1859
2680
|
rl.once("line", onLine);
|
|
@@ -1861,7 +2682,7 @@ function promptUser(rl, question) {
|
|
|
1861
2682
|
}
|
|
1862
2683
|
|
|
1863
2684
|
// src/tools/builtin/write-todos.ts
|
|
1864
|
-
import
|
|
2685
|
+
import chalk5 from "chalk";
|
|
1865
2686
|
var VALID_STATUSES = /* @__PURE__ */ new Set(["pending", "in_progress", "completed"]);
|
|
1866
2687
|
var currentTodos = [];
|
|
1867
2688
|
var writeTodosTool = {
|
|
@@ -1924,25 +2745,25 @@ function renderTodoList(todos) {
|
|
|
1924
2745
|
const total = todos.length;
|
|
1925
2746
|
console.log();
|
|
1926
2747
|
console.log(
|
|
1927
|
-
|
|
2748
|
+
chalk5.bold.cyan("\u{1F4CB} Todo List") + chalk5.dim(` (${completed}/${total} completed)`)
|
|
1928
2749
|
);
|
|
1929
|
-
console.log(
|
|
2750
|
+
console.log(chalk5.dim(" " + "\u2500".repeat(40)));
|
|
1930
2751
|
for (const todo of todos) {
|
|
1931
2752
|
let icon;
|
|
1932
2753
|
let text;
|
|
1933
2754
|
switch (todo.status) {
|
|
1934
2755
|
case "completed":
|
|
1935
|
-
icon =
|
|
1936
|
-
text =
|
|
2756
|
+
icon = chalk5.green(" \u2713 ");
|
|
2757
|
+
text = chalk5.strikethrough.gray(todo.title);
|
|
1937
2758
|
break;
|
|
1938
2759
|
case "in_progress":
|
|
1939
|
-
icon =
|
|
1940
|
-
text =
|
|
2760
|
+
icon = chalk5.yellow(" \u2192 ");
|
|
2761
|
+
text = chalk5.white(todo.title);
|
|
1941
2762
|
break;
|
|
1942
2763
|
case "pending":
|
|
1943
2764
|
default:
|
|
1944
|
-
icon =
|
|
1945
|
-
text =
|
|
2765
|
+
icon = chalk5.gray(" \u25CB ");
|
|
2766
|
+
text = chalk5.gray(todo.title);
|
|
1946
2767
|
break;
|
|
1947
2768
|
}
|
|
1948
2769
|
console.log(icon + text);
|
|
@@ -2117,151 +2938,6 @@ function formatResults(query, data, requested) {
|
|
|
2117
2938
|
return header + "\n" + results.join("\n\n");
|
|
2118
2939
|
}
|
|
2119
2940
|
|
|
2120
|
-
// src/tools/types.ts
|
|
2121
|
-
function isFileWriteTool(name) {
|
|
2122
|
-
return name === "write_file" || name === "edit_file";
|
|
2123
|
-
}
|
|
2124
|
-
function getDangerLevel(toolName, args) {
|
|
2125
|
-
if (toolName.startsWith("mcp__")) return "safe";
|
|
2126
|
-
if (toolName === "bash") {
|
|
2127
|
-
const cmd = String(args["command"] ?? "");
|
|
2128
|
-
if (/\brm\s+[^\n]*(?:-\w*[rRfF]\w*|--recursive|--force)\b/.test(cmd)) return "destructive";
|
|
2129
|
-
if (/\brm\s+\S/.test(cmd)) return "destructive";
|
|
2130
|
-
if (/\brmdir\b|\bformat\b|\bmkfs\b/.test(cmd)) return "destructive";
|
|
2131
|
-
if (/\bRemove-Item\b.*(?:-Recurse|-Force)|\bri\s+.*-(?:Recurse|Force)\b|\brd\s+\/s\b|\brmdir\s+\/s\b/i.test(cmd)) return "destructive";
|
|
2132
|
-
if (/\bdel\s+\S/.test(cmd)) return "destructive";
|
|
2133
|
-
if (/\becho\b.*>>?|\btee\b|\bcp\b|\bmv\b/.test(cmd)) return "write";
|
|
2134
|
-
if (/\bSet-Content\b|\bOut-File\b|\bAdd-Content\b|\bCopy-Item\b|\bMove-Item\b/i.test(cmd)) return "write";
|
|
2135
|
-
return "safe";
|
|
2136
|
-
}
|
|
2137
|
-
if (toolName === "write_file") return "write";
|
|
2138
|
-
if (toolName === "edit_file") return "write";
|
|
2139
|
-
if (toolName === "save_last_response") return "write";
|
|
2140
|
-
if (toolName === "run_interactive") {
|
|
2141
|
-
const exe = String(args["executable"] ?? "").toLowerCase();
|
|
2142
|
-
if (/\b(rm|rmdir|del|format|mkfs|Remove-Item)\b/i.test(exe)) return "destructive";
|
|
2143
|
-
if (/\b(bash|sh|zsh|cmd|powershell|pwsh|python|node|ruby|perl)\b/i.test(exe)) return "write";
|
|
2144
|
-
return "write";
|
|
2145
|
-
}
|
|
2146
|
-
if (toolName === "read_file" || toolName === "list_dir" || toolName === "grep_files" || toolName === "glob_files" || toolName === "web_fetch" || toolName === "save_memory" || toolName === "ask_user" || toolName === "write_todos" || toolName === "google_search" || toolName === "spawn_agent" || toolName === "run_tests") return "safe";
|
|
2147
|
-
return "write";
|
|
2148
|
-
}
|
|
2149
|
-
function schemaToJsonSchema(schema) {
|
|
2150
|
-
const result = {
|
|
2151
|
-
type: schema.type,
|
|
2152
|
-
description: schema.description
|
|
2153
|
-
};
|
|
2154
|
-
if (schema.enum) result["enum"] = schema.enum;
|
|
2155
|
-
if (schema.items) result["items"] = schemaToJsonSchema(schema.items);
|
|
2156
|
-
if (schema.properties) {
|
|
2157
|
-
result["properties"] = Object.fromEntries(
|
|
2158
|
-
Object.entries(schema.properties).map(([k, v]) => [k, schemaToJsonSchema(v)])
|
|
2159
|
-
);
|
|
2160
|
-
}
|
|
2161
|
-
return result;
|
|
2162
|
-
}
|
|
2163
|
-
|
|
2164
|
-
// src/tools/truncate.ts
|
|
2165
|
-
var DEFAULT_MAX_TOOL_OUTPUT_CHARS = 12e3;
|
|
2166
|
-
function getMaxOutputChars(contextWindow) {
|
|
2167
|
-
if (!contextWindow || contextWindow <= 0) return DEFAULT_MAX_TOOL_OUTPUT_CHARS;
|
|
2168
|
-
return Math.max(DEFAULT_MAX_TOOL_OUTPUT_CHARS, Math.min(Math.floor(contextWindow / 4), 12e4));
|
|
2169
|
-
}
|
|
2170
|
-
var activeMaxChars = DEFAULT_MAX_TOOL_OUTPUT_CHARS;
|
|
2171
|
-
function setContextWindow(contextWindow) {
|
|
2172
|
-
activeMaxChars = getMaxOutputChars(contextWindow);
|
|
2173
|
-
}
|
|
2174
|
-
function getActiveMaxChars() {
|
|
2175
|
-
return activeMaxChars;
|
|
2176
|
-
}
|
|
2177
|
-
function truncateOutput(content, toolName, maxChars) {
|
|
2178
|
-
const limit = maxChars ?? activeMaxChars;
|
|
2179
|
-
if (content.length <= limit) return content;
|
|
2180
|
-
const keepHead = Math.floor(limit * 0.7);
|
|
2181
|
-
const keepTail = Math.floor(limit * 0.2);
|
|
2182
|
-
const omitted = content.length - keepHead - keepTail;
|
|
2183
|
-
const lines = content.split("\n").length;
|
|
2184
|
-
const head = content.slice(0, keepHead);
|
|
2185
|
-
const tail = content.slice(content.length - keepTail);
|
|
2186
|
-
return head + `
|
|
2187
|
-
|
|
2188
|
-
... [Output truncated: ${content.length} chars / ${lines} lines total, ${omitted} chars omitted. Use read_file to view full content in segments` + (toolName === "bash" ? ", or narrow the command scope" : "") + `] ...
|
|
2189
|
-
|
|
2190
|
-
` + tail;
|
|
2191
|
-
}
|
|
2192
|
-
|
|
2193
|
-
// src/repl/theme.ts
|
|
2194
|
-
import chalk3 from "chalk";
|
|
2195
|
-
var DARK_THEME = {
|
|
2196
|
-
prompt: chalk3.green,
|
|
2197
|
-
info: chalk3.cyan,
|
|
2198
|
-
warning: chalk3.yellow,
|
|
2199
|
-
error: chalk3.red,
|
|
2200
|
-
success: chalk3.green,
|
|
2201
|
-
dim: chalk3.dim,
|
|
2202
|
-
accent: chalk3.cyan,
|
|
2203
|
-
toolCall: chalk3.yellow,
|
|
2204
|
-
toolResult: chalk3.green,
|
|
2205
|
-
heading: chalk3.bold.cyan
|
|
2206
|
-
};
|
|
2207
|
-
var LIGHT_THEME = {
|
|
2208
|
-
prompt: chalk3.blue,
|
|
2209
|
-
info: chalk3.blueBright,
|
|
2210
|
-
warning: chalk3.yellow,
|
|
2211
|
-
error: chalk3.red,
|
|
2212
|
-
success: chalk3.green,
|
|
2213
|
-
dim: chalk3.gray,
|
|
2214
|
-
accent: chalk3.blueBright,
|
|
2215
|
-
toolCall: chalk3.magenta,
|
|
2216
|
-
toolResult: chalk3.green,
|
|
2217
|
-
heading: chalk3.bold.blue
|
|
2218
|
-
};
|
|
2219
|
-
function resolveColor(name) {
|
|
2220
|
-
if (name.startsWith("#")) return chalk3.hex(name);
|
|
2221
|
-
const parts = name.split(".");
|
|
2222
|
-
let result = chalk3;
|
|
2223
|
-
for (const part of parts) {
|
|
2224
|
-
const obj = result;
|
|
2225
|
-
if (obj && typeof obj[part] !== "undefined") {
|
|
2226
|
-
result = obj[part];
|
|
2227
|
-
}
|
|
2228
|
-
}
|
|
2229
|
-
if (typeof result !== "function") {
|
|
2230
|
-
process.stderr.write(`[theme] Warning: unrecognized color "${name}", using default.
|
|
2231
|
-
`);
|
|
2232
|
-
return chalk3;
|
|
2233
|
-
}
|
|
2234
|
-
return result;
|
|
2235
|
-
}
|
|
2236
|
-
function buildCustomTheme(base, overrides) {
|
|
2237
|
-
if (!overrides) return base;
|
|
2238
|
-
const result = { ...base };
|
|
2239
|
-
for (const [key, colorName] of Object.entries(overrides)) {
|
|
2240
|
-
if (key in result && colorName) {
|
|
2241
|
-
result[key] = resolveColor(colorName);
|
|
2242
|
-
}
|
|
2243
|
-
}
|
|
2244
|
-
return result;
|
|
2245
|
-
}
|
|
2246
|
-
var _currentTheme = DARK_THEME;
|
|
2247
|
-
function initTheme(themeId = "dark", customColors) {
|
|
2248
|
-
switch (themeId) {
|
|
2249
|
-
case "light":
|
|
2250
|
-
_currentTheme = LIGHT_THEME;
|
|
2251
|
-
break;
|
|
2252
|
-
case "custom":
|
|
2253
|
-
_currentTheme = buildCustomTheme(DARK_THEME, customColors);
|
|
2254
|
-
break;
|
|
2255
|
-
default:
|
|
2256
|
-
_currentTheme = DARK_THEME;
|
|
2257
|
-
}
|
|
2258
|
-
}
|
|
2259
|
-
var theme = new Proxy(DARK_THEME, {
|
|
2260
|
-
get(_target, prop) {
|
|
2261
|
-
return _currentTheme[prop];
|
|
2262
|
-
}
|
|
2263
|
-
});
|
|
2264
|
-
|
|
2265
2941
|
// src/tools/builtin/spawn-agent.ts
|
|
2266
2942
|
var spawnAgentContext = {
|
|
2267
2943
|
provider: null,
|
|
@@ -2516,9 +3192,194 @@ var spawnAgentTool = {
|
|
|
2516
3192
|
}
|
|
2517
3193
|
};
|
|
2518
3194
|
|
|
3195
|
+
// src/tools/builtin/task-manager.ts
|
|
3196
|
+
import { spawn as spawn2 } from "child_process";
|
|
3197
|
+
import { randomUUID } from "crypto";
|
|
3198
|
+
import { platform as platform3 } from "os";
|
|
3199
|
+
var MAX_OUTPUT_CHARS = 1e4;
|
|
3200
|
+
var MAX_TASKS = 20;
|
|
3201
|
+
var tasks = /* @__PURE__ */ new Map();
|
|
3202
|
+
function appendOutput(task, field, chunk) {
|
|
3203
|
+
task[field] += chunk;
|
|
3204
|
+
if (task[field].length > MAX_OUTPUT_CHARS) {
|
|
3205
|
+
task[field] = "... [truncated] ...\n" + task[field].slice(-MAX_OUTPUT_CHARS);
|
|
3206
|
+
}
|
|
3207
|
+
}
|
|
3208
|
+
function createTask(command, description) {
|
|
3209
|
+
if (tasks.size >= MAX_TASKS) {
|
|
3210
|
+
for (const [id2, t] of tasks) {
|
|
3211
|
+
if (t.status !== "running") {
|
|
3212
|
+
tasks.delete(id2);
|
|
3213
|
+
break;
|
|
3214
|
+
}
|
|
3215
|
+
}
|
|
3216
|
+
}
|
|
3217
|
+
const id = randomUUID().slice(0, 8);
|
|
3218
|
+
const isWin = platform3() === "win32";
|
|
3219
|
+
const shell = isWin ? "cmd" : "sh";
|
|
3220
|
+
const shellFlag = isWin ? "/c" : "-c";
|
|
3221
|
+
const proc = spawn2(shell, [shellFlag, command], {
|
|
3222
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
3223
|
+
detached: false,
|
|
3224
|
+
env: { ...process.env, PYTHONUTF8: "1" }
|
|
3225
|
+
});
|
|
3226
|
+
const task = {
|
|
3227
|
+
id,
|
|
3228
|
+
command,
|
|
3229
|
+
description,
|
|
3230
|
+
process: proc,
|
|
3231
|
+
startTime: Date.now(),
|
|
3232
|
+
endTime: null,
|
|
3233
|
+
status: "running",
|
|
3234
|
+
exitCode: null,
|
|
3235
|
+
stdout: "",
|
|
3236
|
+
stderr: ""
|
|
3237
|
+
};
|
|
3238
|
+
proc.stdout?.on("data", (chunk) => appendOutput(task, "stdout", chunk.toString("utf-8")));
|
|
3239
|
+
proc.stderr?.on("data", (chunk) => appendOutput(task, "stderr", chunk.toString("utf-8")));
|
|
3240
|
+
proc.on("close", (code) => {
|
|
3241
|
+
task.endTime = Date.now();
|
|
3242
|
+
task.exitCode = code;
|
|
3243
|
+
task.status = code === 0 ? "completed" : "failed";
|
|
3244
|
+
});
|
|
3245
|
+
proc.on("error", (err) => {
|
|
3246
|
+
task.endTime = Date.now();
|
|
3247
|
+
task.status = "failed";
|
|
3248
|
+
task.stderr += `
|
|
3249
|
+
Process error: ${err.message}`;
|
|
3250
|
+
});
|
|
3251
|
+
tasks.set(id, task);
|
|
3252
|
+
return task;
|
|
3253
|
+
}
|
|
3254
|
+
function listTasks() {
|
|
3255
|
+
return [...tasks.values()].map(({ process: _p, ...rest }) => rest);
|
|
3256
|
+
}
|
|
3257
|
+
function getTaskOutput(id) {
|
|
3258
|
+
const task = tasks.get(id);
|
|
3259
|
+
if (!task) return null;
|
|
3260
|
+
return { stdout: task.stdout, stderr: task.stderr, status: task.status };
|
|
3261
|
+
}
|
|
3262
|
+
function stopTask(id) {
|
|
3263
|
+
const task = tasks.get(id);
|
|
3264
|
+
if (!task || task.status !== "running") return false;
|
|
3265
|
+
try {
|
|
3266
|
+
task.process.kill("SIGTERM");
|
|
3267
|
+
setTimeout(() => {
|
|
3268
|
+
if (task.status === "running") {
|
|
3269
|
+
try {
|
|
3270
|
+
task.process.kill("SIGKILL");
|
|
3271
|
+
} catch {
|
|
3272
|
+
}
|
|
3273
|
+
}
|
|
3274
|
+
}, 3e3);
|
|
3275
|
+
task.status = "stopped";
|
|
3276
|
+
task.endTime = Date.now();
|
|
3277
|
+
return true;
|
|
3278
|
+
} catch {
|
|
3279
|
+
return false;
|
|
3280
|
+
}
|
|
3281
|
+
}
|
|
3282
|
+
|
|
3283
|
+
// src/tools/builtin/task-create.ts
|
|
3284
|
+
var taskCreateTool = {
|
|
3285
|
+
definition: {
|
|
3286
|
+
name: "task_create",
|
|
3287
|
+
description: `Start a command running in the background. Returns a task ID for monitoring with task_list. Use this to run long-running processes (dev servers, builds, tests) while continuing other work.`,
|
|
3288
|
+
parameters: {
|
|
3289
|
+
command: {
|
|
3290
|
+
type: "string",
|
|
3291
|
+
description: "Shell command to run in the background",
|
|
3292
|
+
required: true
|
|
3293
|
+
},
|
|
3294
|
+
description: {
|
|
3295
|
+
type: "string",
|
|
3296
|
+
description: "Brief description of what this task does",
|
|
3297
|
+
required: true
|
|
3298
|
+
}
|
|
3299
|
+
},
|
|
3300
|
+
dangerous: false
|
|
3301
|
+
},
|
|
3302
|
+
async execute(args) {
|
|
3303
|
+
const command = String(args["command"] ?? "");
|
|
3304
|
+
const description = String(args["description"] ?? command);
|
|
3305
|
+
if (!command) throw new ToolError("task_create", "command is required");
|
|
3306
|
+
const task = createTask(command, description);
|
|
3307
|
+
return `Background task started.
|
|
3308
|
+
ID: ${task.id}
|
|
3309
|
+
Command: ${command}
|
|
3310
|
+
Description: ${description}
|
|
3311
|
+
Use task_list to check status, task_stop to terminate.`;
|
|
3312
|
+
}
|
|
3313
|
+
};
|
|
3314
|
+
|
|
3315
|
+
// src/tools/builtin/task-list.ts
|
|
3316
|
+
var taskListTool = {
|
|
3317
|
+
definition: {
|
|
3318
|
+
name: "task_list",
|
|
3319
|
+
description: `List all background tasks with their status. Provide an ID to get detailed output for a specific task.`,
|
|
3320
|
+
parameters: {
|
|
3321
|
+
id: {
|
|
3322
|
+
type: "string",
|
|
3323
|
+
description: "Optional: specific task ID to get detailed stdout/stderr output",
|
|
3324
|
+
required: false
|
|
3325
|
+
}
|
|
3326
|
+
},
|
|
3327
|
+
dangerous: false
|
|
3328
|
+
},
|
|
3329
|
+
async execute(args) {
|
|
3330
|
+
const id = args["id"] ? String(args["id"]) : void 0;
|
|
3331
|
+
if (id) {
|
|
3332
|
+
const output = getTaskOutput(id);
|
|
3333
|
+
if (!output) return `Task not found: ${id}`;
|
|
3334
|
+
const parts = [`Task ${id} (${output.status})`];
|
|
3335
|
+
if (output.stdout) parts.push(`
|
|
3336
|
+
\u2500\u2500 stdout \u2500\u2500
|
|
3337
|
+
${output.stdout}`);
|
|
3338
|
+
if (output.stderr) parts.push(`
|
|
3339
|
+
\u2500\u2500 stderr \u2500\u2500
|
|
3340
|
+
${output.stderr}`);
|
|
3341
|
+
if (!output.stdout && !output.stderr) parts.push("\n(no output yet)");
|
|
3342
|
+
return parts.join("");
|
|
3343
|
+
}
|
|
3344
|
+
const all = listTasks();
|
|
3345
|
+
if (all.length === 0) return "No background tasks.";
|
|
3346
|
+
const lines = [`Background tasks (${all.length}):
|
|
3347
|
+
`];
|
|
3348
|
+
for (const t of all) {
|
|
3349
|
+
const elapsed = ((t.endTime ?? Date.now()) - t.startTime) / 1e3;
|
|
3350
|
+
const status = t.status === "running" ? "\u{1F7E2} running" : t.status === "completed" ? "\u2705 completed" : t.status === "stopped" ? "\u23F9 stopped" : "\u274C failed";
|
|
3351
|
+
lines.push(` [${t.id}] ${status} (${elapsed.toFixed(1)}s) \u2014 ${t.description}`);
|
|
3352
|
+
if (t.exitCode !== null) lines.push(` exit code: ${t.exitCode}`);
|
|
3353
|
+
}
|
|
3354
|
+
return lines.join("\n");
|
|
3355
|
+
}
|
|
3356
|
+
};
|
|
3357
|
+
|
|
3358
|
+
// src/tools/builtin/task-stop.ts
|
|
3359
|
+
var taskStopTool = {
|
|
3360
|
+
definition: {
|
|
3361
|
+
name: "task_stop",
|
|
3362
|
+
description: `Stop a running background task by its ID. Use task_list to find the ID.`,
|
|
3363
|
+
parameters: {
|
|
3364
|
+
id: {
|
|
3365
|
+
type: "string",
|
|
3366
|
+
description: "Task ID to stop",
|
|
3367
|
+
required: true
|
|
3368
|
+
}
|
|
3369
|
+
},
|
|
3370
|
+
dangerous: false
|
|
3371
|
+
},
|
|
3372
|
+
async execute(args) {
|
|
3373
|
+
const id = String(args["id"] ?? "");
|
|
3374
|
+
if (!id) throw new ToolError("task_stop", "id is required");
|
|
3375
|
+
const success = stopTask(id);
|
|
3376
|
+
return success ? `Task ${id} stopped.` : `Task ${id} not found or already completed.`;
|
|
3377
|
+
}
|
|
3378
|
+
};
|
|
3379
|
+
|
|
2519
3380
|
// src/tools/registry.ts
|
|
2520
3381
|
import { pathToFileURL } from "url";
|
|
2521
|
-
import { existsSync as
|
|
3382
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync4, readdirSync as readdirSync6 } from "fs";
|
|
2522
3383
|
import { join as join5 } from "path";
|
|
2523
3384
|
var ToolRegistry = class {
|
|
2524
3385
|
tools = /* @__PURE__ */ new Map();
|
|
@@ -2541,6 +3402,9 @@ var ToolRegistry = class {
|
|
|
2541
3402
|
this.register(googleSearchTool);
|
|
2542
3403
|
this.register(spawnAgentTool);
|
|
2543
3404
|
this.register(runTestsTool);
|
|
3405
|
+
this.register(taskCreateTool);
|
|
3406
|
+
this.register(taskListTool);
|
|
3407
|
+
this.register(taskStopTool);
|
|
2544
3408
|
}
|
|
2545
3409
|
register(tool) {
|
|
2546
3410
|
this.tools.set(tool.definition.name, tool);
|
|
@@ -2592,7 +3456,7 @@ var ToolRegistry = class {
|
|
|
2592
3456
|
* Returns the number of successfully loaded plugins.
|
|
2593
3457
|
*/
|
|
2594
3458
|
async loadPlugins(pluginsDir, allowPlugins = false) {
|
|
2595
|
-
if (!
|
|
3459
|
+
if (!existsSync10(pluginsDir)) {
|
|
2596
3460
|
try {
|
|
2597
3461
|
mkdirSync4(pluginsDir, { recursive: true });
|
|
2598
3462
|
} catch {
|
|
@@ -2663,12 +3527,15 @@ export {
|
|
|
2663
3527
|
initTheme,
|
|
2664
3528
|
theme,
|
|
2665
3529
|
undoStack,
|
|
3530
|
+
renderDiff,
|
|
3531
|
+
runHook,
|
|
3532
|
+
checkPermission,
|
|
3533
|
+
setContextWindow,
|
|
3534
|
+
truncateOutput,
|
|
3535
|
+
ToolExecutor,
|
|
2666
3536
|
lastResponseStore,
|
|
2667
3537
|
askUserContext,
|
|
2668
3538
|
googleSearchContext,
|
|
2669
|
-
setContextWindow,
|
|
2670
|
-
getActiveMaxChars,
|
|
2671
|
-
truncateOutput,
|
|
2672
3539
|
spawnAgentContext,
|
|
2673
3540
|
ToolRegistry
|
|
2674
3541
|
};
|