zizou-ai 0.1.1 → 0.1.2
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/cli.js +746 -59
- package/join.json +1 -0
- package/package.json +2 -2
- package/src/agent/run-turn.ts +33 -1
- package/src/context/build-system-prompt.ts +65 -3
- package/src/context/repo-map.ts +13 -3
- package/src/tools/edit-file.ts +9 -3
- package/src/tools/file-operations.ts +95 -0
- package/src/tools/glob.ts +84 -8
- package/src/tools/grep.ts +56 -9
- package/src/tools/index.ts +4 -0
- package/src/tools/manage-ports.ts +174 -0
- package/src/tools/manage-tasks.ts +98 -0
- package/src/tools/read-file.ts +3 -1
- package/src/tools/run-background.ts +70 -0
- package/src/tools/run-bash.ts +26 -3
- package/src/tools/task-registry.ts +139 -0
package/dist/cli.js
CHANGED
|
@@ -642,6 +642,7 @@ import { streamText, stepCountIs } from "ai";
|
|
|
642
642
|
|
|
643
643
|
// src/tools/read-file.ts
|
|
644
644
|
import { readFileSync } from "node:fs";
|
|
645
|
+
import { resolve } from "node:path";
|
|
645
646
|
import { z } from "zod";
|
|
646
647
|
import { tool } from "ai";
|
|
647
648
|
var readFile = tool({
|
|
@@ -656,7 +657,8 @@ var readFile = tool({
|
|
|
656
657
|
*/
|
|
657
658
|
execute: async ({ path }) => {
|
|
658
659
|
try {
|
|
659
|
-
const
|
|
660
|
+
const absPath = resolve(process.cwd(), path);
|
|
661
|
+
const contents = readFileSync(absPath, "utf-8");
|
|
660
662
|
return { success: true, contents };
|
|
661
663
|
} catch (err) {
|
|
662
664
|
const message = err instanceof Error ? err.message : "Unknown error reading file";
|
|
@@ -667,7 +669,7 @@ var readFile = tool({
|
|
|
667
669
|
|
|
668
670
|
// src/tools/write-file.ts
|
|
669
671
|
import { writeFileSync, mkdirSync } from "node:fs";
|
|
670
|
-
import { dirname, resolve } from "node:path";
|
|
672
|
+
import { dirname, resolve as resolve2 } from "node:path";
|
|
671
673
|
import { z as z2 } from "zod";
|
|
672
674
|
import { tool as tool2 } from "ai";
|
|
673
675
|
var writeFile = tool2({
|
|
@@ -678,7 +680,7 @@ var writeFile = tool2({
|
|
|
678
680
|
}),
|
|
679
681
|
execute: async ({ path: inputPath, contents }) => {
|
|
680
682
|
try {
|
|
681
|
-
const absPath =
|
|
683
|
+
const absPath = resolve2(process.cwd(), inputPath);
|
|
682
684
|
const dir = dirname(absPath);
|
|
683
685
|
mkdirSync(dir, { recursive: true });
|
|
684
686
|
writeFileSync(absPath, contents, "utf-8");
|
|
@@ -695,6 +697,7 @@ var writeFile = tool2({
|
|
|
695
697
|
|
|
696
698
|
// src/tools/edit-file.ts
|
|
697
699
|
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
700
|
+
import { resolve as resolve3 } from "node:path";
|
|
698
701
|
import { z as z3 } from "zod";
|
|
699
702
|
import { tool as tool3 } from "ai";
|
|
700
703
|
var editFile = tool3({
|
|
@@ -706,7 +709,10 @@ var editFile = tool3({
|
|
|
706
709
|
}),
|
|
707
710
|
execute: async ({ path, old_string, new_string }) => {
|
|
708
711
|
try {
|
|
709
|
-
const
|
|
712
|
+
const absPath = resolve3(process.cwd(), path);
|
|
713
|
+
let contents = readFileSync2(absPath, "utf-8");
|
|
714
|
+
contents = contents.replace(/\r\n/g, "\n");
|
|
715
|
+
old_string = old_string.replace(/\r\n/g, "\n");
|
|
710
716
|
let count = 0;
|
|
711
717
|
let pos = contents.indexOf(old_string);
|
|
712
718
|
while (pos !== -1) {
|
|
@@ -726,10 +732,10 @@ var editFile = tool3({
|
|
|
726
732
|
};
|
|
727
733
|
}
|
|
728
734
|
const updatedContents = contents.replace(old_string, new_string);
|
|
729
|
-
writeFileSync2(
|
|
735
|
+
writeFileSync2(absPath, updatedContents, "utf-8");
|
|
730
736
|
return {
|
|
731
737
|
success: true,
|
|
732
|
-
message: `Successfully replaced 1 occurrence in ${
|
|
738
|
+
message: `Successfully replaced 1 occurrence in ${absPath}`
|
|
733
739
|
};
|
|
734
740
|
} catch (err) {
|
|
735
741
|
const message = err instanceof Error ? err.message : "Unknown error editing file";
|
|
@@ -743,6 +749,45 @@ import { readdirSync, statSync } from "fs";
|
|
|
743
749
|
import { join, relative } from "path";
|
|
744
750
|
import { z as z4 } from "zod";
|
|
745
751
|
import { tool as tool4 } from "ai";
|
|
752
|
+
function globToRegex(pattern) {
|
|
753
|
+
let p = pattern.replace(/\\/g, "/");
|
|
754
|
+
const hasSlash = p.includes("/");
|
|
755
|
+
let regexStr = "";
|
|
756
|
+
let i = 0;
|
|
757
|
+
while (i < p.length) {
|
|
758
|
+
const char = p[i];
|
|
759
|
+
if (char === "*" && p[i + 1] === "*") {
|
|
760
|
+
if (p[i + 2] === "/") {
|
|
761
|
+
regexStr += "(.*/)?";
|
|
762
|
+
i += 3;
|
|
763
|
+
} else {
|
|
764
|
+
regexStr += ".*";
|
|
765
|
+
i += 2;
|
|
766
|
+
}
|
|
767
|
+
} else if (char === "*") {
|
|
768
|
+
regexStr += "[^/]*";
|
|
769
|
+
i++;
|
|
770
|
+
} else if (char === "?") {
|
|
771
|
+
regexStr += "[^/]";
|
|
772
|
+
i++;
|
|
773
|
+
} else if (char === ".") {
|
|
774
|
+
regexStr += "\\.";
|
|
775
|
+
i++;
|
|
776
|
+
} else if (char === "/" || char === "\\") {
|
|
777
|
+
regexStr += "/";
|
|
778
|
+
i++;
|
|
779
|
+
} else if (".+^${}()|[]\\".includes(char)) {
|
|
780
|
+
regexStr += "\\" + char;
|
|
781
|
+
i++;
|
|
782
|
+
} else {
|
|
783
|
+
regexStr += char;
|
|
784
|
+
i++;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
const finalPattern = hasSlash ? `^${regexStr}$` : `^(.*/)?${regexStr}$`;
|
|
788
|
+
const flags = process.platform === "win32" ? "i" : "";
|
|
789
|
+
return new RegExp(finalPattern, flags);
|
|
790
|
+
}
|
|
746
791
|
function simpleGlob(dir, pattern, results = [], root = dir) {
|
|
747
792
|
const SKIP = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".next"]);
|
|
748
793
|
let entries;
|
|
@@ -762,28 +807,78 @@ function simpleGlob(dir, pattern, results = [], root = dir) {
|
|
|
762
807
|
}
|
|
763
808
|
if (stat.isDirectory()) {
|
|
764
809
|
simpleGlob(fullPath, pattern, results, root);
|
|
765
|
-
} else
|
|
766
|
-
|
|
810
|
+
} else {
|
|
811
|
+
const relPath = relative(root, fullPath).replace(/\\/g, "/");
|
|
812
|
+
if (pattern.test(relPath) || pattern.test(entry)) {
|
|
813
|
+
results.push(relPath);
|
|
814
|
+
}
|
|
767
815
|
}
|
|
768
816
|
}
|
|
769
817
|
return results;
|
|
770
818
|
}
|
|
771
819
|
var glob = tool4({
|
|
772
|
-
description: "Find files in the project by filename pattern (e.g. '*.ts', '
|
|
820
|
+
description: "Find files in the project by filename pattern (e.g. '*.ts', '**/app.css')",
|
|
773
821
|
inputSchema: z4.object({
|
|
774
|
-
pattern: z4.string().describe("A simple glob-style pattern, e.g. '*.ts'")
|
|
822
|
+
pattern: z4.string().describe("A simple glob-style pattern, e.g. '*.ts' or '**/app.css'")
|
|
775
823
|
}),
|
|
776
824
|
execute: async ({ pattern }) => {
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
825
|
+
try {
|
|
826
|
+
const regex = globToRegex(pattern);
|
|
827
|
+
const matches = simpleGlob(".", regex);
|
|
828
|
+
return { success: true, files: matches.slice(0, 50) };
|
|
829
|
+
} catch (err) {
|
|
830
|
+
return { success: false, error: String(err) };
|
|
831
|
+
}
|
|
780
832
|
}
|
|
781
833
|
});
|
|
782
834
|
|
|
783
835
|
// src/tools/grep.ts
|
|
784
|
-
import {
|
|
836
|
+
import { readFileSync as readFileSync3, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
|
|
837
|
+
import { join as join2, relative as relative2 } from "path";
|
|
785
838
|
import { z as z5 } from "zod";
|
|
786
839
|
import { tool as tool5 } from "ai";
|
|
840
|
+
function simpleGrep(dir, regex, filePattern, results = [], root = dir) {
|
|
841
|
+
const SKIP = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".next", "coverage"]);
|
|
842
|
+
let entries;
|
|
843
|
+
try {
|
|
844
|
+
entries = readdirSync2(dir);
|
|
845
|
+
} catch {
|
|
846
|
+
return results;
|
|
847
|
+
}
|
|
848
|
+
for (const entry of entries) {
|
|
849
|
+
if (SKIP.has(entry)) continue;
|
|
850
|
+
const fullPath = join2(dir, entry);
|
|
851
|
+
let stat;
|
|
852
|
+
try {
|
|
853
|
+
stat = statSync2(fullPath);
|
|
854
|
+
} catch {
|
|
855
|
+
continue;
|
|
856
|
+
}
|
|
857
|
+
if (stat.isDirectory()) {
|
|
858
|
+
simpleGrep(fullPath, regex, filePattern, results, root);
|
|
859
|
+
} else {
|
|
860
|
+
if (stat.size > 1024 * 1024) continue;
|
|
861
|
+
const relPath = relative2(root, fullPath).replace(/\\/g, "/");
|
|
862
|
+
if (filePattern && !filePattern.test(relPath) && !filePattern.test(entry)) {
|
|
863
|
+
continue;
|
|
864
|
+
}
|
|
865
|
+
try {
|
|
866
|
+
const content = readFileSync3(fullPath, "utf-8");
|
|
867
|
+
if (content.indexOf("\0") !== -1) continue;
|
|
868
|
+
const lines = content.split("\n");
|
|
869
|
+
for (let i = 0; i < lines.length; i++) {
|
|
870
|
+
if (regex.test(lines[i])) {
|
|
871
|
+
results.push(`${relPath}:${i + 1}:${lines[i].trim()}`);
|
|
872
|
+
if (results.length >= 50) return results;
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
} catch {
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
if (results.length >= 50) break;
|
|
879
|
+
}
|
|
880
|
+
return results;
|
|
881
|
+
}
|
|
787
882
|
var grep = tool5({
|
|
788
883
|
description: "Search file contents for a text pattern across the project (like grep -r)",
|
|
789
884
|
inputSchema: z5.object({
|
|
@@ -792,12 +887,14 @@ var grep = tool5({
|
|
|
792
887
|
}),
|
|
793
888
|
execute: async ({ pattern, fileGlob }) => {
|
|
794
889
|
try {
|
|
795
|
-
const
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
890
|
+
const searchRegex = new RegExp(pattern);
|
|
891
|
+
let fileRegex = null;
|
|
892
|
+
if (fileGlob) {
|
|
893
|
+
const fp = "^" + fileGlob.replace(/\./g, "\\.").replace(/\*/g, ".*") + "$";
|
|
894
|
+
fileRegex = new RegExp(fp);
|
|
895
|
+
}
|
|
896
|
+
const matches = simpleGrep(".", searchRegex, fileRegex);
|
|
897
|
+
return { success: true, matches };
|
|
801
898
|
} catch (err) {
|
|
802
899
|
return { success: false, error: String(err) };
|
|
803
900
|
}
|
|
@@ -805,8 +902,8 @@ var grep = tool5({
|
|
|
805
902
|
});
|
|
806
903
|
|
|
807
904
|
// src/tools/list-dir.ts
|
|
808
|
-
import { readdirSync as
|
|
809
|
-
import { join as
|
|
905
|
+
import { readdirSync as readdirSync3, statSync as statSync3 } from "node:fs";
|
|
906
|
+
import { join as join3, resolve as resolve4 } from "node:path";
|
|
810
907
|
import { z as z6 } from "zod";
|
|
811
908
|
import { tool as tool6 } from "ai";
|
|
812
909
|
var listDir = tool6({
|
|
@@ -819,13 +916,13 @@ var listDir = tool6({
|
|
|
819
916
|
execute: async ({ path: inputPath }) => {
|
|
820
917
|
const SKIP = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".next", ".cache"]);
|
|
821
918
|
try {
|
|
822
|
-
const dir =
|
|
823
|
-
const entries =
|
|
919
|
+
const dir = resolve4(process.cwd(), inputPath ?? ".");
|
|
920
|
+
const entries = readdirSync3(dir);
|
|
824
921
|
const items = [];
|
|
825
922
|
for (const entry of entries) {
|
|
826
923
|
if (SKIP.has(entry)) continue;
|
|
827
924
|
try {
|
|
828
|
-
const stat =
|
|
925
|
+
const stat = statSync3(join3(dir, entry));
|
|
829
926
|
items.push({ name: entry, type: stat.isDirectory() ? "dir" : "file" });
|
|
830
927
|
} catch {
|
|
831
928
|
}
|
|
@@ -844,7 +941,7 @@ var listDir = tool6({
|
|
|
844
941
|
|
|
845
942
|
// src/tools/open-file.ts
|
|
846
943
|
import { spawn } from "node:child_process";
|
|
847
|
-
import { resolve as
|
|
944
|
+
import { resolve as resolve5, extname } from "node:path";
|
|
848
945
|
import { existsSync } from "node:fs";
|
|
849
946
|
import { z as z7 } from "zod";
|
|
850
947
|
import { tool as tool7 } from "ai";
|
|
@@ -862,7 +959,7 @@ var openFile = tool7({
|
|
|
862
959
|
}),
|
|
863
960
|
execute: async ({ path: inputPath }) => {
|
|
864
961
|
try {
|
|
865
|
-
const absPath =
|
|
962
|
+
const absPath = resolve5(process.cwd(), inputPath);
|
|
866
963
|
if (!existsSync(absPath)) {
|
|
867
964
|
return {
|
|
868
965
|
success: false,
|
|
@@ -895,10 +992,10 @@ import { tool as tool8 } from "ai";
|
|
|
895
992
|
import { z as z8 } from "zod";
|
|
896
993
|
|
|
897
994
|
// src/context/repo-map.ts
|
|
898
|
-
import { readFileSync as
|
|
899
|
-
import { join as
|
|
995
|
+
import { readFileSync as readFileSync4, readdirSync as readdirSync4, statSync as statSync4 } from "fs";
|
|
996
|
+
import { join as join4, relative as relative3 } from "path";
|
|
900
997
|
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".next", "coverage"]);
|
|
901
|
-
var CODE_FILE_PATTERN = /\.(ts|tsx|js|jsx|mjs|cjs)$/;
|
|
998
|
+
var CODE_FILE_PATTERN = /\.(ts|tsx|js|jsx|mjs|cjs|html|css)$/;
|
|
902
999
|
var SYMBOL_PATTERNS = [
|
|
903
1000
|
/^\s*(export\s+)?(async\s+)?function\s+(\w+)\s*\(/,
|
|
904
1001
|
/^\s*(export\s+)?const\s+(\w+)\s*=\s*(async\s*)?\(/,
|
|
@@ -935,25 +1032,34 @@ function scanRepo(rootDir) {
|
|
|
935
1032
|
function walk(dir) {
|
|
936
1033
|
let entries;
|
|
937
1034
|
try {
|
|
938
|
-
entries =
|
|
1035
|
+
entries = readdirSync4(dir);
|
|
939
1036
|
} catch {
|
|
940
1037
|
return;
|
|
941
1038
|
}
|
|
942
1039
|
for (const entry of entries) {
|
|
943
1040
|
if (SKIP_DIRS.has(entry)) continue;
|
|
944
|
-
const fullPath =
|
|
1041
|
+
const fullPath = join4(dir, entry);
|
|
945
1042
|
let stat;
|
|
946
1043
|
try {
|
|
947
|
-
stat =
|
|
1044
|
+
stat = statSync4(fullPath);
|
|
948
1045
|
} catch {
|
|
949
1046
|
continue;
|
|
950
1047
|
}
|
|
951
1048
|
if (stat.isDirectory()) {
|
|
952
1049
|
walk(fullPath);
|
|
953
1050
|
} else if (CODE_FILE_PATTERN.test(entry)) {
|
|
954
|
-
const content =
|
|
955
|
-
const relPath =
|
|
956
|
-
|
|
1051
|
+
const content = readFileSync4(fullPath, "utf-8");
|
|
1052
|
+
const relPath = relative3(rootDir, fullPath).replace(/\\/g, "/");
|
|
1053
|
+
const fileSymbols = extractSymbols(relPath, content);
|
|
1054
|
+
if (fileSymbols.length === 0) {
|
|
1055
|
+
allSymbols.push({
|
|
1056
|
+
file: relPath,
|
|
1057
|
+
line: 1,
|
|
1058
|
+
signature: "(no exported symbols extracted)"
|
|
1059
|
+
});
|
|
1060
|
+
} else {
|
|
1061
|
+
allSymbols.push(...fileSymbols);
|
|
1062
|
+
}
|
|
957
1063
|
}
|
|
958
1064
|
}
|
|
959
1065
|
}
|
|
@@ -983,11 +1089,11 @@ function buildRepoMap(rootDir) {
|
|
|
983
1089
|
}
|
|
984
1090
|
|
|
985
1091
|
// src/context/build-system-prompt.ts
|
|
986
|
-
import { existsSync as existsSync2, readFileSync as
|
|
987
|
-
import { resolve as
|
|
1092
|
+
import { existsSync as existsSync2, readFileSync as readFileSync5 } from "fs";
|
|
1093
|
+
import { resolve as resolve6 } from "path";
|
|
988
1094
|
var pinnedContextFiles = /* @__PURE__ */ new Set();
|
|
989
1095
|
function addPinnedFile(projectRoot, filePath) {
|
|
990
|
-
const absolutePath =
|
|
1096
|
+
const absolutePath = resolve6(projectRoot, filePath);
|
|
991
1097
|
if (!existsSync2(absolutePath)) {
|
|
992
1098
|
throw new Error(`File not found: ${absolutePath}`);
|
|
993
1099
|
}
|
|
@@ -997,6 +1103,12 @@ function addPinnedFile(projectRoot, filePath) {
|
|
|
997
1103
|
function clearPinnedFiles() {
|
|
998
1104
|
pinnedContextFiles.clear();
|
|
999
1105
|
}
|
|
1106
|
+
function getOSInfo() {
|
|
1107
|
+
const p = process.platform;
|
|
1108
|
+
if (p === "win32") return { os: "Windows", shell: "PowerShell" };
|
|
1109
|
+
if (p === "darwin") return { os: "macOS", shell: "bash/zsh" };
|
|
1110
|
+
return { os: "Linux", shell: "bash" };
|
|
1111
|
+
}
|
|
1000
1112
|
var BASE_INSTRUCTIONS = `You are Zizou, an AI coding agent operating inside a user's terminal,
|
|
1001
1113
|
with direct access to their project's filesystem through tools.
|
|
1002
1114
|
|
|
@@ -1013,6 +1125,12 @@ You have two complementary ways to understand the codebase:
|
|
|
1013
1125
|
|
|
1014
1126
|
When creating a new file or completely replacing a file's contents, use the writeFile tool. When modifying existing files in a targeted way, use editFile with an old_string that exactly matches existing content and is unique in the file \u2014 never rewrite whole files from scratch if you are only making minor edits. Before running any shell command, know that the user will be asked to approve it; explain briefly what a command will do if it's not obvious.
|
|
1015
1127
|
|
|
1128
|
+
IMPORTANT \u2014 "Read Before Edit" Rule:
|
|
1129
|
+
Before calling editFile, you MUST first readFile the file to see its current contents.
|
|
1130
|
+
The old_string parameter must EXACTLY match text currently in the file \u2014 including all
|
|
1131
|
+
whitespace, indentation, and line breaks. If you guess at the content without reading
|
|
1132
|
+
the file first, the edit will almost certainly fail.
|
|
1133
|
+
|
|
1016
1134
|
For general knowledge questions, conversational chat, or queries that do not require workspace files or terminal command execution, answer directly from your internal knowledge. Do NOT use tools (such as grep, glob, readFile, or runBash) unless the task specifically requires accessing the project codebase or executing commands.
|
|
1017
1135
|
|
|
1018
1136
|
CRITICAL INSTRUCTION FOR TOOL CALLING: You are interacting with a system that supports native tool calling (function calling). You MUST use the native tool calling protocol.
|
|
@@ -1025,9 +1143,12 @@ async function buildSystemPrompt(projectRoot) {
|
|
|
1025
1143
|
if (mode !== "light") {
|
|
1026
1144
|
repoMap = buildRepoMap(projectRoot);
|
|
1027
1145
|
}
|
|
1146
|
+
const { os, shell } = getOSInfo();
|
|
1028
1147
|
const SESSION_CONTEXT = `
|
|
1029
1148
|
--- SESSION CONTEXT ---
|
|
1030
1149
|
Workspace root (cwd): ${projectRoot}
|
|
1150
|
+
Operating system: ${os}
|
|
1151
|
+
Shell: ${shell}
|
|
1031
1152
|
All relative paths you provide to tools are resolved from this root.
|
|
1032
1153
|
When creating or writing a file, you can use either:
|
|
1033
1154
|
- An absolute path (e.g. ${projectRoot}/index.html)
|
|
@@ -1038,24 +1159,65 @@ TOOL GUIDE:
|
|
|
1038
1159
|
listDir(path?) \u2192 list immediate children of a directory. Use this FIRST
|
|
1039
1160
|
whenever you need to understand the folder structure or
|
|
1040
1161
|
decide where a new file should go.
|
|
1041
|
-
readFile(path) \u2192 read the full contents of an existing file.
|
|
1162
|
+
readFile(path) \u2192 read the full contents of an existing file. ALWAYS call
|
|
1163
|
+
this before editFile so you have the exact current text.
|
|
1042
1164
|
writeFile(path, contents) \u2192 create a NEW file or FULLY REPLACE an existing one.
|
|
1043
1165
|
Use this for brand-new files or when you want to overwrite everything.
|
|
1044
1166
|
editFile(path, old_string, new_string) \u2192 make a TARGETED replacement inside an existing
|
|
1045
1167
|
file. old_string must match exactly and be unique in the file.
|
|
1046
|
-
|
|
1047
|
-
|
|
1168
|
+
IMPORTANT: Always readFile first to get the exact text.
|
|
1169
|
+
glob(pattern) \u2192 find files recursively by name pattern (e.g. "*.ts",
|
|
1170
|
+
"**/app.css"). Supports ** for any depth matching.
|
|
1171
|
+
grep(pattern) \u2192 search file contents by text or regex pattern across the project.
|
|
1048
1172
|
runBash(command) \u2192 run a shell command (user must approve first).
|
|
1173
|
+
On ${os} this runs in ${shell}.
|
|
1174
|
+
Has a 120-second timeout \u2014 suitable for short/medium installation & builds.
|
|
1175
|
+
runBackground(command) \u2192 Run a shell command in the background (non-blocking).
|
|
1176
|
+
Perfect for long-running servers (e.g. 'npm run dev' or 'bun run dev').
|
|
1177
|
+
Returns a 'taskId' and 'pid' immediately.
|
|
1178
|
+
manageTasks(action, taskId?) \u2192 Manage background tasks.
|
|
1179
|
+
- action="list": Return details of all tasks spawned in this session.
|
|
1180
|
+
- action="logs": Return stdout/stderr buffer from a task (helps check server state/logs).
|
|
1181
|
+
- action="kill": Terminate a background task (and its children).
|
|
1182
|
+
managePorts(action, port) \u2192 Find and terminate processes on ports.
|
|
1183
|
+
- action="find": Find PID and name of process listening on 'port'.
|
|
1184
|
+
- action="kill": Kill the process listening on 'port'.
|
|
1185
|
+
Helps solve 'Address already in use' errors.
|
|
1186
|
+
fileOperations(action, source, destination?) \u2192 Native file management.
|
|
1187
|
+
- action="delete": Recursively delete a file/folder.
|
|
1188
|
+
- action="createDirectory": Recursively create folders.
|
|
1189
|
+
- action="copy": Recursively copy a file/folder to destination.
|
|
1190
|
+
- action="move": Move/rename a file/folder to destination.
|
|
1049
1191
|
openFile(path) \u2192 open a file in the OS default app (HTML \u2192 browser,
|
|
1050
1192
|
images \u2192 viewer, etc.). Use after creating a file so the
|
|
1051
1193
|
user can immediately preview it.
|
|
1194
|
+
|
|
1195
|
+
WORKFLOW FOR FINDING AND EDITING FILES:
|
|
1196
|
+
When the user asks you to modify a file you haven't seen yet:
|
|
1197
|
+
1. Use glob("**/filename") or listDir() to FIND the file path
|
|
1198
|
+
2. Use readFile(path) to READ its current contents
|
|
1199
|
+
3. Use editFile(path, old_string, new_string) to EDIT it
|
|
1200
|
+
Never skip step 2 \u2014 editFile needs an exact string match.
|
|
1201
|
+
|
|
1202
|
+
PROJECT SCAFFOLDING (React, Next.js, Vite, etc.):
|
|
1203
|
+
When the user asks you to create a new project with a framework:
|
|
1204
|
+
1. Use runBash to scaffold: e.g.
|
|
1205
|
+
- React/Vite: npx -y create-vite@latest my-app -- --template react
|
|
1206
|
+
- Next.js: npx -y create-next-app@latest my-app --yes --use-npm
|
|
1207
|
+
- Plain React: npx -y create-react-app my-app
|
|
1208
|
+
2. Use runBash("cd my-app && npm install") if dependencies weren't auto-installed
|
|
1209
|
+
3. Spin up development server in the background:
|
|
1210
|
+
- runBackground("cd my-app && npm run dev")
|
|
1211
|
+
4. Verify server running using:
|
|
1212
|
+
- manageTasks("list")
|
|
1213
|
+
- Wait a few seconds, then query logs using manageTasks("logs", taskId) to see server startup details.
|
|
1052
1214
|
--- END SESSION CONTEXT ---`;
|
|
1053
1215
|
let pinnedText = "";
|
|
1054
1216
|
if (pinnedContextFiles.size > 0) {
|
|
1055
1217
|
pinnedText = "\n\n--- PINNED FILES ---\nThe user has pinned the following files to your permanent context:\n";
|
|
1056
1218
|
for (const file of pinnedContextFiles) {
|
|
1057
1219
|
try {
|
|
1058
|
-
const contents =
|
|
1220
|
+
const contents = readFileSync5(file, "utf8");
|
|
1059
1221
|
pinnedText += `
|
|
1060
1222
|
File: ${file}
|
|
1061
1223
|
\`\`\`
|
|
@@ -1107,12 +1269,21 @@ var addFileToContext = tool8({
|
|
|
1107
1269
|
// src/tools/run-bash.ts
|
|
1108
1270
|
import { exec } from "node:child_process";
|
|
1109
1271
|
import { promisify } from "node:util";
|
|
1272
|
+
import { join as join5 } from "node:path";
|
|
1110
1273
|
import { z as z9 } from "zod";
|
|
1111
1274
|
import { tool as tool9 } from "ai";
|
|
1112
1275
|
var execAsync = promisify(exec);
|
|
1276
|
+
function getShellConfig() {
|
|
1277
|
+
if (process.platform === "win32") {
|
|
1278
|
+
const systemRoot = process.env.SystemRoot || "C:\\Windows";
|
|
1279
|
+
const powershellPath = join5(systemRoot, "System32\\WindowsPowerShell\\v1.0\\powershell.exe");
|
|
1280
|
+
return { shell: powershellPath };
|
|
1281
|
+
}
|
|
1282
|
+
return {};
|
|
1283
|
+
}
|
|
1113
1284
|
var createRunBashTool = (confirm) => {
|
|
1114
1285
|
return tool9({
|
|
1115
|
-
description: "Execute a shell command. This requires user confirmation, so only use it when necessary. Commands run with a
|
|
1286
|
+
description: "Execute a shell command. This requires user confirmation, so only use it when necessary. Commands run with a 120-second timeout. Output is truncated if too long. On Windows this runs in PowerShell; on macOS/Linux it uses the default shell.",
|
|
1116
1287
|
inputSchema: z9.object({
|
|
1117
1288
|
command: z9.string().describe("The shell command to execute")
|
|
1118
1289
|
}),
|
|
@@ -1130,10 +1301,13 @@ Allow?`
|
|
|
1130
1301
|
}
|
|
1131
1302
|
try {
|
|
1132
1303
|
const { stdout, stderr } = await execAsync(command, {
|
|
1133
|
-
timeout:
|
|
1134
|
-
//
|
|
1135
|
-
maxBuffer: 1024 * 1024
|
|
1304
|
+
timeout: 12e4,
|
|
1305
|
+
// 120 seconds — enough for npm install, npx create-*, etc.
|
|
1306
|
+
maxBuffer: 1024 * 1024,
|
|
1136
1307
|
// 1 MB
|
|
1308
|
+
cwd: process.cwd(),
|
|
1309
|
+
// explicit: run from the project root
|
|
1310
|
+
...getShellConfig()
|
|
1137
1311
|
});
|
|
1138
1312
|
const rawOutput = `STDOUT:
|
|
1139
1313
|
${stdout}
|
|
@@ -1163,6 +1337,487 @@ ${stderr}`;
|
|
|
1163
1337
|
});
|
|
1164
1338
|
};
|
|
1165
1339
|
|
|
1340
|
+
// src/tools/run-background.ts
|
|
1341
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
1342
|
+
import { join as join6 } from "node:path";
|
|
1343
|
+
import { z as z10 } from "zod";
|
|
1344
|
+
import { tool as tool10 } from "ai";
|
|
1345
|
+
|
|
1346
|
+
// src/tools/task-registry.ts
|
|
1347
|
+
var tasks = /* @__PURE__ */ new Map();
|
|
1348
|
+
var taskIdCounter = 1;
|
|
1349
|
+
var MAX_BUFFER_LENGTH = 1e4;
|
|
1350
|
+
function registerTask(command, process2) {
|
|
1351
|
+
const id = `task_${taskIdCounter++}`;
|
|
1352
|
+
const task = {
|
|
1353
|
+
id,
|
|
1354
|
+
command,
|
|
1355
|
+
pid: process2.pid,
|
|
1356
|
+
status: "running",
|
|
1357
|
+
exitCode: null,
|
|
1358
|
+
outputBuffer: "",
|
|
1359
|
+
startTime: /* @__PURE__ */ new Date(),
|
|
1360
|
+
process: process2
|
|
1361
|
+
};
|
|
1362
|
+
const appendToBuffer = (data) => {
|
|
1363
|
+
task.outputBuffer += data;
|
|
1364
|
+
if (task.outputBuffer.length > MAX_BUFFER_LENGTH) {
|
|
1365
|
+
task.outputBuffer = task.outputBuffer.slice(task.outputBuffer.length - MAX_BUFFER_LENGTH);
|
|
1366
|
+
}
|
|
1367
|
+
};
|
|
1368
|
+
process2.stdout?.setEncoding("utf-8");
|
|
1369
|
+
process2.stdout?.on("data", (chunk) => {
|
|
1370
|
+
appendToBuffer(chunk);
|
|
1371
|
+
});
|
|
1372
|
+
process2.stderr?.setEncoding("utf-8");
|
|
1373
|
+
process2.stderr?.on("data", (chunk) => {
|
|
1374
|
+
appendToBuffer(chunk);
|
|
1375
|
+
});
|
|
1376
|
+
process2.on("close", (code) => {
|
|
1377
|
+
task.exitCode = code;
|
|
1378
|
+
task.status = code === 0 ? "exited" : "failed";
|
|
1379
|
+
});
|
|
1380
|
+
process2.on("error", (err) => {
|
|
1381
|
+
task.status = "failed";
|
|
1382
|
+
appendToBuffer(`
|
|
1383
|
+
[Task Registry Error]: ${err.message}
|
|
1384
|
+
`);
|
|
1385
|
+
});
|
|
1386
|
+
tasks.set(id, task);
|
|
1387
|
+
return task;
|
|
1388
|
+
}
|
|
1389
|
+
function getTask(id) {
|
|
1390
|
+
return tasks.get(id);
|
|
1391
|
+
}
|
|
1392
|
+
function listTasks() {
|
|
1393
|
+
return Array.from(tasks.values()).map(({ id, command, pid, status, exitCode, outputBuffer, startTime }) => ({
|
|
1394
|
+
id,
|
|
1395
|
+
command,
|
|
1396
|
+
pid,
|
|
1397
|
+
status,
|
|
1398
|
+
exitCode,
|
|
1399
|
+
outputBuffer,
|
|
1400
|
+
startTime
|
|
1401
|
+
}));
|
|
1402
|
+
}
|
|
1403
|
+
function killTask(id) {
|
|
1404
|
+
const task = tasks.get(id);
|
|
1405
|
+
if (!task || task.status !== "running") return false;
|
|
1406
|
+
try {
|
|
1407
|
+
if (process.platform === "win32") {
|
|
1408
|
+
const { execSync } = __require("node:child_process");
|
|
1409
|
+
execSync(`taskkill /pid ${task.process.pid} /T /F`, { stdio: "ignore" });
|
|
1410
|
+
} else {
|
|
1411
|
+
task.process.kill("SIGTERM");
|
|
1412
|
+
setTimeout(() => {
|
|
1413
|
+
if (task.status === "running") {
|
|
1414
|
+
task.process.kill("SIGKILL");
|
|
1415
|
+
}
|
|
1416
|
+
}, 2e3);
|
|
1417
|
+
}
|
|
1418
|
+
task.status = "exited";
|
|
1419
|
+
return true;
|
|
1420
|
+
} catch (err) {
|
|
1421
|
+
try {
|
|
1422
|
+
task.process.kill();
|
|
1423
|
+
task.status = "exited";
|
|
1424
|
+
return true;
|
|
1425
|
+
} catch {
|
|
1426
|
+
return false;
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
function cleanup() {
|
|
1431
|
+
for (const [id, task] of tasks.entries()) {
|
|
1432
|
+
if (task.status === "running") {
|
|
1433
|
+
try {
|
|
1434
|
+
if (process.platform === "win32") {
|
|
1435
|
+
const { execSync } = __require("node:child_process");
|
|
1436
|
+
execSync(`taskkill /pid ${task.process.pid} /T /F`, { stdio: "ignore" });
|
|
1437
|
+
} else {
|
|
1438
|
+
task.process.kill("SIGKILL");
|
|
1439
|
+
}
|
|
1440
|
+
} catch {
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
process.on("exit", cleanup);
|
|
1446
|
+
process.on("SIGINT", () => {
|
|
1447
|
+
cleanup();
|
|
1448
|
+
process.exit(130);
|
|
1449
|
+
});
|
|
1450
|
+
process.on("SIGTERM", () => {
|
|
1451
|
+
cleanup();
|
|
1452
|
+
process.exit(143);
|
|
1453
|
+
});
|
|
1454
|
+
|
|
1455
|
+
// src/tools/run-background.ts
|
|
1456
|
+
function getShellConfig2() {
|
|
1457
|
+
if (process.platform === "win32") {
|
|
1458
|
+
const systemRoot = process.env.SystemRoot || "C:\\Windows";
|
|
1459
|
+
const powershellPath = join6(systemRoot, "System32\\WindowsPowerShell\\v1.0\\powershell.exe");
|
|
1460
|
+
return { shell: powershellPath };
|
|
1461
|
+
}
|
|
1462
|
+
return { shell: true };
|
|
1463
|
+
}
|
|
1464
|
+
var createRunBackgroundTool = (confirm) => {
|
|
1465
|
+
return tool10({
|
|
1466
|
+
description: "Execute a shell command in the background (non-blocking). This is perfect for starting development servers (e.g. npm run dev), or running long tasks like compilation. Returns a taskId immediately. Requires user confirmation. On Windows, runs in PowerShell; on macOS/Linux it uses the default shell.",
|
|
1467
|
+
inputSchema: z10.object({
|
|
1468
|
+
command: z10.string().describe("The shell command to execute in the background")
|
|
1469
|
+
}),
|
|
1470
|
+
execute: async ({ command }) => {
|
|
1471
|
+
const isApproved = await confirm(
|
|
1472
|
+
`The agent wants to run this command in the background:
|
|
1473
|
+
${command}
|
|
1474
|
+
Allow?`
|
|
1475
|
+
);
|
|
1476
|
+
if (!isApproved) {
|
|
1477
|
+
return {
|
|
1478
|
+
success: false,
|
|
1479
|
+
error: "User denied permission to run this command."
|
|
1480
|
+
};
|
|
1481
|
+
}
|
|
1482
|
+
try {
|
|
1483
|
+
const shellOpts = getShellConfig2();
|
|
1484
|
+
const child = spawn2(command, {
|
|
1485
|
+
...shellOpts,
|
|
1486
|
+
cwd: process.cwd(),
|
|
1487
|
+
detached: process.platform !== "win32",
|
|
1488
|
+
// detached group on Unix so child isn't killed immediately if parent detaches
|
|
1489
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1490
|
+
// pipe stdout and stderr
|
|
1491
|
+
});
|
|
1492
|
+
if (process.platform !== "win32") {
|
|
1493
|
+
child.unref();
|
|
1494
|
+
}
|
|
1495
|
+
const task = registerTask(command, child);
|
|
1496
|
+
return {
|
|
1497
|
+
success: true,
|
|
1498
|
+
message: `Successfully started background task.`,
|
|
1499
|
+
taskId: task.id,
|
|
1500
|
+
pid: task.pid
|
|
1501
|
+
};
|
|
1502
|
+
} catch (err) {
|
|
1503
|
+
const message = err instanceof Error ? err.message : "Unknown error starting background command";
|
|
1504
|
+
return { success: false, error: message };
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
});
|
|
1508
|
+
};
|
|
1509
|
+
|
|
1510
|
+
// src/tools/manage-tasks.ts
|
|
1511
|
+
import { z as z11 } from "zod";
|
|
1512
|
+
import { tool as tool11 } from "ai";
|
|
1513
|
+
var manageTasks = tool11({
|
|
1514
|
+
description: "Manage active background tasks spawned during this session. Can list running tasks, view their real-time output logs, or kill/terminate them.",
|
|
1515
|
+
inputSchema: z11.object({
|
|
1516
|
+
action: z11.enum(["list", "kill", "logs"]).describe("Action to perform: 'list' (all tasks), 'kill' (stop a task), 'logs' (get stdout/stderr output)"),
|
|
1517
|
+
taskId: z11.string().optional().describe("The taskId returned by runBackground. Required for 'kill' and 'logs'.")
|
|
1518
|
+
}),
|
|
1519
|
+
execute: async ({ action, taskId }) => {
|
|
1520
|
+
try {
|
|
1521
|
+
if (action === "list") {
|
|
1522
|
+
const activeTasks = listTasks();
|
|
1523
|
+
return {
|
|
1524
|
+
success: true,
|
|
1525
|
+
tasks: activeTasks.map((t) => ({
|
|
1526
|
+
id: t.id,
|
|
1527
|
+
command: t.command,
|
|
1528
|
+
pid: t.pid,
|
|
1529
|
+
status: t.status,
|
|
1530
|
+
exitCode: t.exitCode,
|
|
1531
|
+
startTime: t.startTime.toISOString(),
|
|
1532
|
+
logSize: t.outputBuffer.length
|
|
1533
|
+
}))
|
|
1534
|
+
};
|
|
1535
|
+
}
|
|
1536
|
+
if (!taskId) {
|
|
1537
|
+
return {
|
|
1538
|
+
success: false,
|
|
1539
|
+
error: "A 'taskId' is required for actions 'kill' and 'logs'."
|
|
1540
|
+
};
|
|
1541
|
+
}
|
|
1542
|
+
if (action === "logs") {
|
|
1543
|
+
const task = getTask(taskId);
|
|
1544
|
+
if (!task) {
|
|
1545
|
+
return {
|
|
1546
|
+
success: false,
|
|
1547
|
+
error: `Task ${taskId} not found.`
|
|
1548
|
+
};
|
|
1549
|
+
}
|
|
1550
|
+
return {
|
|
1551
|
+
success: true,
|
|
1552
|
+
taskId: task.id,
|
|
1553
|
+
command: task.command,
|
|
1554
|
+
status: task.status,
|
|
1555
|
+
logs: task.outputBuffer || "(No output logs yet)"
|
|
1556
|
+
};
|
|
1557
|
+
}
|
|
1558
|
+
if (action === "kill") {
|
|
1559
|
+
const task = getTask(taskId);
|
|
1560
|
+
if (!task) {
|
|
1561
|
+
return {
|
|
1562
|
+
success: false,
|
|
1563
|
+
error: `Task ${taskId} not found.`
|
|
1564
|
+
};
|
|
1565
|
+
}
|
|
1566
|
+
if (task.status !== "running") {
|
|
1567
|
+
return {
|
|
1568
|
+
success: false,
|
|
1569
|
+
error: `Task ${taskId} is not running (status: ${task.status}).`
|
|
1570
|
+
};
|
|
1571
|
+
}
|
|
1572
|
+
const killed = killTask(taskId);
|
|
1573
|
+
if (killed) {
|
|
1574
|
+
return {
|
|
1575
|
+
success: true,
|
|
1576
|
+
message: `Successfully terminated background task ${taskId}.`
|
|
1577
|
+
};
|
|
1578
|
+
} else {
|
|
1579
|
+
return {
|
|
1580
|
+
success: false,
|
|
1581
|
+
error: `Failed to terminate task ${taskId}.`
|
|
1582
|
+
};
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
return {
|
|
1586
|
+
success: false,
|
|
1587
|
+
error: `Unsupported action: ${action}`
|
|
1588
|
+
};
|
|
1589
|
+
} catch (err) {
|
|
1590
|
+
const message = err instanceof Error ? err.message : "Unknown error managing tasks";
|
|
1591
|
+
return { success: false, error: message };
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
});
|
|
1595
|
+
|
|
1596
|
+
// src/tools/manage-ports.ts
|
|
1597
|
+
import { exec as exec2 } from "node:child_process";
|
|
1598
|
+
import { promisify as promisify2 } from "node:util";
|
|
1599
|
+
import { z as z12 } from "zod";
|
|
1600
|
+
import { tool as tool12 } from "ai";
|
|
1601
|
+
var execAsync2 = promisify2(exec2);
|
|
1602
|
+
async function findProcessOnPort(port) {
|
|
1603
|
+
const results = [];
|
|
1604
|
+
if (process.platform === "win32") {
|
|
1605
|
+
try {
|
|
1606
|
+
const { stdout } = await execAsync2(`netstat -ano | findstr :${port}`);
|
|
1607
|
+
const lines = stdout.split("\n");
|
|
1608
|
+
const pids = /* @__PURE__ */ new Set();
|
|
1609
|
+
for (const line of lines) {
|
|
1610
|
+
const parts = line.trim().split(/\s+/);
|
|
1611
|
+
if (parts.length >= 5) {
|
|
1612
|
+
const localAddr = parts[1];
|
|
1613
|
+
if (localAddr.endsWith(`:${port}`)) {
|
|
1614
|
+
const pid = parseInt(parts[parts.length - 1], 10);
|
|
1615
|
+
if (!isNaN(pid) && pid > 0) {
|
|
1616
|
+
pids.add(pid);
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
for (const pid of pids) {
|
|
1622
|
+
let name = "Unknown";
|
|
1623
|
+
let details = "";
|
|
1624
|
+
try {
|
|
1625
|
+
const { stdout: taskStdout } = await execAsync2(`tasklist /FI "PID eq ${pid}" /NH`);
|
|
1626
|
+
const taskParts = taskStdout.trim().split(/\s+/);
|
|
1627
|
+
if (taskParts.length > 0 && taskParts[0] !== "INFO:") {
|
|
1628
|
+
name = taskParts[0];
|
|
1629
|
+
}
|
|
1630
|
+
details = `PID: ${pid}, Name: ${name}`;
|
|
1631
|
+
} catch {
|
|
1632
|
+
details = `PID: ${pid}`;
|
|
1633
|
+
}
|
|
1634
|
+
results.push({ pid, name, details });
|
|
1635
|
+
}
|
|
1636
|
+
} catch {
|
|
1637
|
+
}
|
|
1638
|
+
} else {
|
|
1639
|
+
try {
|
|
1640
|
+
const { stdout } = await execAsync2(`lsof -i :${port} -F p c`);
|
|
1641
|
+
const lines = stdout.trim().split("\n");
|
|
1642
|
+
let currentPid = null;
|
|
1643
|
+
for (const line of lines) {
|
|
1644
|
+
if (line.startsWith("p")) {
|
|
1645
|
+
currentPid = parseInt(line.substring(1), 10);
|
|
1646
|
+
} else if (line.startsWith("c") && currentPid !== null) {
|
|
1647
|
+
const name = line.substring(1);
|
|
1648
|
+
results.push({
|
|
1649
|
+
pid: currentPid,
|
|
1650
|
+
name,
|
|
1651
|
+
details: `PID: ${currentPid}, Command: ${name}`
|
|
1652
|
+
});
|
|
1653
|
+
currentPid = null;
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
if (results.length === 0 && lines.length > 0) {
|
|
1657
|
+
const pids = lines.filter((l) => l.startsWith("p")).map((l) => parseInt(l.substring(1), 10)).filter((pid) => !isNaN(pid));
|
|
1658
|
+
for (const pid of pids) {
|
|
1659
|
+
results.push({ pid, details: `PID: ${pid}` });
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
} catch {
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
return results;
|
|
1666
|
+
}
|
|
1667
|
+
async function killProcess(pid) {
|
|
1668
|
+
try {
|
|
1669
|
+
if (process.platform === "win32") {
|
|
1670
|
+
await execAsync2(`taskkill /F /PID ${pid}`);
|
|
1671
|
+
} else {
|
|
1672
|
+
await execAsync2(`kill -9 ${pid}`);
|
|
1673
|
+
}
|
|
1674
|
+
return true;
|
|
1675
|
+
} catch {
|
|
1676
|
+
return false;
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
var managePorts = tool12({
|
|
1680
|
+
description: "Find and terminate processes listening on a specific port. Useful for resolving 'Port already in use' errors during web server startup.",
|
|
1681
|
+
inputSchema: z12.object({
|
|
1682
|
+
action: z12.enum(["find", "kill"]).describe("Action to perform: 'find' (view what process is on the port), 'kill' (terminate the process on the port)"),
|
|
1683
|
+
port: z12.number().describe("The local port number (e.g. 3000, 8080)")
|
|
1684
|
+
}),
|
|
1685
|
+
execute: async ({ action, port }) => {
|
|
1686
|
+
try {
|
|
1687
|
+
if (action === "find") {
|
|
1688
|
+
const found = await findProcessOnPort(port);
|
|
1689
|
+
if (found.length === 0) {
|
|
1690
|
+
return {
|
|
1691
|
+
success: true,
|
|
1692
|
+
message: `No active processes found listening on port ${port}.`,
|
|
1693
|
+
processes: []
|
|
1694
|
+
};
|
|
1695
|
+
}
|
|
1696
|
+
return {
|
|
1697
|
+
success: true,
|
|
1698
|
+
message: `Found ${found.length} process(es) listening on port ${port}.`,
|
|
1699
|
+
processes: found
|
|
1700
|
+
};
|
|
1701
|
+
}
|
|
1702
|
+
if (action === "kill") {
|
|
1703
|
+
const found = await findProcessOnPort(port);
|
|
1704
|
+
if (found.length === 0) {
|
|
1705
|
+
return {
|
|
1706
|
+
success: false,
|
|
1707
|
+
error: `No active processes found listening on port ${port} to terminate.`
|
|
1708
|
+
};
|
|
1709
|
+
}
|
|
1710
|
+
const killedPids = [];
|
|
1711
|
+
const failedPids = [];
|
|
1712
|
+
for (const proc of found) {
|
|
1713
|
+
const ok = await killProcess(proc.pid);
|
|
1714
|
+
if (ok) {
|
|
1715
|
+
killedPids.push(proc.pid);
|
|
1716
|
+
} else {
|
|
1717
|
+
failedPids.push(proc.pid);
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
if (failedPids.length > 0) {
|
|
1721
|
+
return {
|
|
1722
|
+
success: false,
|
|
1723
|
+
error: `Successfully killed PID(s) ${killedPids.join(", ")}, but failed to kill PID(s) ${failedPids.join(", ")}.`
|
|
1724
|
+
};
|
|
1725
|
+
}
|
|
1726
|
+
return {
|
|
1727
|
+
success: true,
|
|
1728
|
+
message: `Successfully terminated all process(es) on port ${port} (PID(s): ${killedPids.join(", ")}).`
|
|
1729
|
+
};
|
|
1730
|
+
}
|
|
1731
|
+
return {
|
|
1732
|
+
success: false,
|
|
1733
|
+
error: `Unsupported action: ${action}`
|
|
1734
|
+
};
|
|
1735
|
+
} catch (err) {
|
|
1736
|
+
const message = err instanceof Error ? err.message : "Unknown error managing ports";
|
|
1737
|
+
return { success: false, error: message };
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
});
|
|
1741
|
+
|
|
1742
|
+
// src/tools/file-operations.ts
|
|
1743
|
+
import { rmSync, mkdirSync as mkdirSync2, cpSync, renameSync, existsSync as existsSync3 } from "node:fs";
|
|
1744
|
+
import { resolve as resolve7 } from "node:path";
|
|
1745
|
+
import { z as z13 } from "zod";
|
|
1746
|
+
import { tool as tool13 } from "ai";
|
|
1747
|
+
var fileOperations = tool13({
|
|
1748
|
+
description: "Perform safe, native filesystem operations (delete, copy, move, create directories) without running shell commands. Always resolves relative paths from the project root.",
|
|
1749
|
+
inputSchema: z13.object({
|
|
1750
|
+
action: z13.enum(["delete", "createDirectory", "copy", "move"]).describe("The filesystem action to perform"),
|
|
1751
|
+
source: z13.string().describe("The target path to delete/copy/move/create. Relative or absolute."),
|
|
1752
|
+
destination: z13.string().optional().describe("The destination path. Required only for 'copy' and 'move' actions.")
|
|
1753
|
+
}),
|
|
1754
|
+
execute: async ({ action, source, destination }) => {
|
|
1755
|
+
try {
|
|
1756
|
+
const absSource = resolve7(process.cwd(), source);
|
|
1757
|
+
if (action === "createDirectory") {
|
|
1758
|
+
mkdirSync2(absSource, { recursive: true });
|
|
1759
|
+
return {
|
|
1760
|
+
success: true,
|
|
1761
|
+
message: `Successfully created directory hierarchy at ${absSource}`
|
|
1762
|
+
};
|
|
1763
|
+
}
|
|
1764
|
+
if (action === "delete") {
|
|
1765
|
+
if (!existsSync3(absSource)) {
|
|
1766
|
+
return {
|
|
1767
|
+
success: false,
|
|
1768
|
+
error: `Path does not exist: ${absSource}`
|
|
1769
|
+
};
|
|
1770
|
+
}
|
|
1771
|
+
rmSync(absSource, { recursive: true, force: true });
|
|
1772
|
+
return {
|
|
1773
|
+
success: true,
|
|
1774
|
+
message: `Successfully deleted ${absSource}`
|
|
1775
|
+
};
|
|
1776
|
+
}
|
|
1777
|
+
if (!destination) {
|
|
1778
|
+
return {
|
|
1779
|
+
success: false,
|
|
1780
|
+
error: `A 'destination' path is required for '${action}' operations.`
|
|
1781
|
+
};
|
|
1782
|
+
}
|
|
1783
|
+
const absDest = resolve7(process.cwd(), destination);
|
|
1784
|
+
if (action === "copy") {
|
|
1785
|
+
if (!existsSync3(absSource)) {
|
|
1786
|
+
return {
|
|
1787
|
+
success: false,
|
|
1788
|
+
error: `Source path does not exist: ${absSource}`
|
|
1789
|
+
};
|
|
1790
|
+
}
|
|
1791
|
+
cpSync(absSource, absDest, { recursive: true });
|
|
1792
|
+
return {
|
|
1793
|
+
success: true,
|
|
1794
|
+
message: `Successfully copied ${absSource} to ${absDest}`
|
|
1795
|
+
};
|
|
1796
|
+
}
|
|
1797
|
+
if (action === "move") {
|
|
1798
|
+
if (!existsSync3(absSource)) {
|
|
1799
|
+
return {
|
|
1800
|
+
success: false,
|
|
1801
|
+
error: `Source path does not exist: ${absSource}`
|
|
1802
|
+
};
|
|
1803
|
+
}
|
|
1804
|
+
renameSync(absSource, absDest);
|
|
1805
|
+
return {
|
|
1806
|
+
success: true,
|
|
1807
|
+
message: `Successfully moved/renamed ${absSource} to ${absDest}`
|
|
1808
|
+
};
|
|
1809
|
+
}
|
|
1810
|
+
return {
|
|
1811
|
+
success: false,
|
|
1812
|
+
error: `Unsupported action: ${action}`
|
|
1813
|
+
};
|
|
1814
|
+
} catch (err) {
|
|
1815
|
+
const message = err instanceof Error ? err.message : "Unknown error in file operations";
|
|
1816
|
+
return { success: false, error: message };
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
});
|
|
1820
|
+
|
|
1166
1821
|
// src/agent/run-turn.ts
|
|
1167
1822
|
function sanitizeJsonString(jsonStr) {
|
|
1168
1823
|
let isInsideString = false;
|
|
@@ -1213,8 +1868,8 @@ async function* runTurn(options) {
|
|
|
1213
1868
|
};
|
|
1214
1869
|
try {
|
|
1215
1870
|
const { writeFileSync: writeFileSync3 } = await import("node:fs");
|
|
1216
|
-
const { resolve:
|
|
1217
|
-
debugPath =
|
|
1871
|
+
const { resolve: resolve8 } = await import("node:path");
|
|
1872
|
+
debugPath = resolve8(process.cwd(), "zizou-debug.log");
|
|
1218
1873
|
const toolSchemas = [
|
|
1219
1874
|
{
|
|
1220
1875
|
name: "readFile",
|
|
@@ -1271,6 +1926,34 @@ async function* runTurn(options) {
|
|
|
1271
1926
|
parameters: { command: "string \u2014 the shell command" },
|
|
1272
1927
|
returnShape: "{ success: true, output: string } | { success: false, error }",
|
|
1273
1928
|
howPassed: "ConfirmFn suspends the agent loop (via Promise) until user presses y/n in the TUI. If approved, uses child_process.exec with 15s timeout + 1MB buffer."
|
|
1929
|
+
},
|
|
1930
|
+
{
|
|
1931
|
+
name: "runBackground",
|
|
1932
|
+
description: "Execute a shell command in the background (non-blocking).",
|
|
1933
|
+
parameters: { command: "string \u2014 the shell command" },
|
|
1934
|
+
returnShape: "{ success: true, taskId: string, pid?: number } | { success: false, error: string }",
|
|
1935
|
+
howPassed: "Uses child_process.spawn to run command in the background, registers it in the task registry."
|
|
1936
|
+
},
|
|
1937
|
+
{
|
|
1938
|
+
name: "manageTasks",
|
|
1939
|
+
description: "Manage background tasks spawned in the session.",
|
|
1940
|
+
parameters: { action: "'list' | 'kill' | 'logs'", taskId: "string" },
|
|
1941
|
+
returnShape: "{ success: true, ... } | { success: false, error: string }",
|
|
1942
|
+
howPassed: "Queries active background task list, logs buffer, or terminates a task."
|
|
1943
|
+
},
|
|
1944
|
+
{
|
|
1945
|
+
name: "managePorts",
|
|
1946
|
+
description: "Find or terminate processes listening on a port.",
|
|
1947
|
+
parameters: { action: "'find' | 'kill'", port: "number" },
|
|
1948
|
+
returnShape: "{ success: true, ... } | { success: false, error: string }",
|
|
1949
|
+
howPassed: "Invokes platform commands (netstat, taskkill, lsof, kill) to find or stop processes on local ports."
|
|
1950
|
+
},
|
|
1951
|
+
{
|
|
1952
|
+
name: "fileOperations",
|
|
1953
|
+
description: "Perform filesystem operations natively (delete, copy, move, create directories).",
|
|
1954
|
+
parameters: { action: "'delete' | 'createDirectory' | 'copy' | 'move'", source: "string", destination: "string" },
|
|
1955
|
+
returnShape: "{ success: true, message: string } | { success: false, error: string }",
|
|
1956
|
+
howPassed: "Uses Node fs methods directly to modify directories and files."
|
|
1274
1957
|
}
|
|
1275
1958
|
];
|
|
1276
1959
|
const hr = (label) => `
|
|
@@ -1390,7 +2073,11 @@ ${"\u2500".repeat(60)}`;
|
|
|
1390
2073
|
listDir,
|
|
1391
2074
|
openFile,
|
|
1392
2075
|
addFileToContext,
|
|
1393
|
-
runBash: createRunBashTool(onConfirm)
|
|
2076
|
+
runBash: createRunBashTool(onConfirm),
|
|
2077
|
+
runBackground: createRunBackgroundTool(onConfirm),
|
|
2078
|
+
manageTasks,
|
|
2079
|
+
managePorts,
|
|
2080
|
+
fileOperations
|
|
1394
2081
|
};
|
|
1395
2082
|
const result = streamText({
|
|
1396
2083
|
model,
|
|
@@ -1589,8 +2276,8 @@ ${"\u2500".repeat(60)}`;
|
|
|
1589
2276
|
}
|
|
1590
2277
|
|
|
1591
2278
|
// src/commands/index.ts
|
|
1592
|
-
import { existsSync as
|
|
1593
|
-
import { join as
|
|
2279
|
+
import { existsSync as existsSync4, readdirSync as readdirSync5 } from "fs";
|
|
2280
|
+
import { join as join7 } from "path";
|
|
1594
2281
|
var SHORT_LABELS2 = {
|
|
1595
2282
|
groq: "Groq",
|
|
1596
2283
|
google: "Google Gemini",
|
|
@@ -1602,19 +2289,19 @@ var SHORT_LABELS2 = {
|
|
|
1602
2289
|
function getSkills() {
|
|
1603
2290
|
const skills = [];
|
|
1604
2291
|
const globalPath = "C:\\Users\\Arnv\\.gemini\\config\\skills";
|
|
1605
|
-
const localPath =
|
|
1606
|
-
if (
|
|
2292
|
+
const localPath = join7(process.cwd(), ".agents", "skills");
|
|
2293
|
+
if (existsSync4(globalPath)) {
|
|
1607
2294
|
try {
|
|
1608
|
-
const entries =
|
|
2295
|
+
const entries = readdirSync5(globalPath, { withFileTypes: true });
|
|
1609
2296
|
for (const entry of entries) {
|
|
1610
2297
|
if (entry.isDirectory()) skills.push(`global:${entry.name}`);
|
|
1611
2298
|
}
|
|
1612
2299
|
} catch {
|
|
1613
2300
|
}
|
|
1614
2301
|
}
|
|
1615
|
-
if (
|
|
2302
|
+
if (existsSync4(localPath)) {
|
|
1616
2303
|
try {
|
|
1617
|
-
const entries =
|
|
2304
|
+
const entries = readdirSync5(localPath, { withFileTypes: true });
|
|
1618
2305
|
for (const entry of entries) {
|
|
1619
2306
|
if (entry.isDirectory()) skills.push(`workspace:${entry.name}`);
|
|
1620
2307
|
}
|
|
@@ -1994,8 +2681,8 @@ function Chat({ onChangeKeys }) {
|
|
|
1994
2681
|
});
|
|
1995
2682
|
}, []);
|
|
1996
2683
|
const confirmFn = (description) => {
|
|
1997
|
-
return new Promise((
|
|
1998
|
-
setPendingConfirm({ description, resolve:
|
|
2684
|
+
return new Promise((resolve8) => {
|
|
2685
|
+
setPendingConfirm({ description, resolve: resolve8 });
|
|
1999
2686
|
});
|
|
2000
2687
|
};
|
|
2001
2688
|
useInput3((inputChar) => {
|