mod.ts 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  1. /// <reference lib="deno.ns" />
  2. import { join, dirname } from "https://deno.land/[email protected]/path/mod.ts";
  3. import { red, yellow, green } from "https://deno.land/[email protected]/fmt/colors.ts";
  4. import type { ToolResponse } from "../types.d.ts";
  5. interface CommandConfig {
  6. desc: string;
  7. args: readonly string[];
  8. }
  9. // Define allowed commands and their descriptions
  10. const ALLOWED_COMMANDS: Record<string, CommandConfig> = {
  11. 'npm': {
  12. desc: "Node package manager",
  13. args: ["install", "run", "test", "build"]
  14. },
  15. 'git': {
  16. desc: "Version control",
  17. args: ["status", "add", "commit", "push", "pull", "clone", "checkout", "branch"]
  18. },
  19. 'deno': {
  20. desc: "Deno runtime",
  21. args: ["run", "test", "fmt", "lint", "check", "compile", "bundle"]
  22. },
  23. 'ls': {
  24. desc: "List directory contents",
  25. args: ["-l", "-a", "-la", "-lh"]
  26. },
  27. 'cat': {
  28. desc: "Show file contents",
  29. args: []
  30. },
  31. 'echo': {
  32. desc: "Print text",
  33. args: []
  34. }
  35. };
  36. // Track commands that have been allowed for this session
  37. const alwaysAllowedCommands = new Set<string>();
  38. function isCommandAllowed(command: string): boolean {
  39. // Split command into parts
  40. const parts = command.trim().split(/\s+/);
  41. if (parts.length === 0) return false;
  42. // Get base command
  43. const baseCmd = parts[0];
  44. if (!(baseCmd in ALLOWED_COMMANDS)) return false;
  45. // If command has arguments, check if they're allowed
  46. if (parts.length > 1 && ALLOWED_COMMANDS[baseCmd].args.length > 0) {
  47. const arg = parts[1];
  48. return ALLOWED_COMMANDS[baseCmd].args.includes(arg);
  49. }
  50. return true;
  51. }
  52. async function promptForCommand(command: string): Promise<boolean> {
  53. // Check if command has been previously allowed
  54. if (alwaysAllowedCommands.has(command)) {
  55. console.log(yellow("\nWarning: Running previously allowed command:"), red(command));
  56. return true;
  57. }
  58. console.log(yellow("\nWarning: Command not in allowlist"));
  59. console.log("Command:", red(command));
  60. console.log("\nAllowed commands:");
  61. Object.entries(ALLOWED_COMMANDS).forEach(([cmd, { desc, args }]) => {
  62. console.log(` ${green(cmd)}: ${desc}`);
  63. if (args.length) {
  64. console.log(` Arguments: ${args.join(", ")}`);
  65. }
  66. });
  67. const answer = prompt("\nDo you want to run this command? (y/n/always) ");
  68. if (answer?.toLowerCase() === 'always') {
  69. alwaysAllowedCommands.add(command);
  70. return true;
  71. }
  72. return answer?.toLowerCase() === 'y';
  73. }
  74. export async function executeCommand(command: string): Promise<ToolResponse> {
  75. try {
  76. // Check if command is allowed
  77. if (!isCommandAllowed(command)) {
  78. // Prompt user for confirmation
  79. const shouldRun = await promptForCommand(command);
  80. if (!shouldRun) {
  81. return "Command execution cancelled by user";
  82. }
  83. console.log(yellow("\nProceeding with command execution..."));
  84. }
  85. const process = new Deno.Command("sh", {
  86. args: ["-c", command],
  87. stdout: "piped",
  88. stderr: "piped",
  89. });
  90. const { stdout, stderr } = await process.output();
  91. const decoder = new TextDecoder();
  92. return decoder.decode(stdout) + (stderr.length ? `\nStderr:\n${decoder.decode(stderr)}` : "");
  93. } catch (error) {
  94. return `Error executing command: ${error instanceof Error ? error.message : String(error)}`;
  95. }
  96. }
  97. export async function readFile(workingDir: string, relativePath: string): Promise<ToolResponse> {
  98. try {
  99. const fullPath = join(workingDir, relativePath);
  100. const content = await Deno.readTextFile(fullPath);
  101. return content;
  102. } catch (error) {
  103. return `Error reading file: ${error instanceof Error ? error.message : String(error)}`;
  104. }
  105. }
  106. export async function writeFile(workingDir: string, relativePath: string, content: string): Promise<ToolResponse> {
  107. try {
  108. const fullPath = join(workingDir, relativePath);
  109. await Deno.mkdir(dirname(fullPath), { recursive: true });
  110. await Deno.writeTextFile(fullPath, content);
  111. return `Successfully wrote to ${relativePath}`;
  112. } catch (error) {
  113. return `Error writing file: ${error instanceof Error ? error.message : String(error)}`;
  114. }
  115. }
  116. export async function searchFiles(
  117. workingDir: string,
  118. searchPath: string,
  119. regex: string,
  120. filePattern?: string
  121. ): Promise<ToolResponse> {
  122. try {
  123. const fullPath = join(workingDir, searchPath);
  124. const results: string[] = [];
  125. const regexObj = new RegExp(regex, "g");
  126. const patternObj = filePattern ? new RegExp(filePattern) : null;
  127. for await (const entry of Deno.readDir(fullPath)) {
  128. if (entry.isFile && (!patternObj || patternObj.test(entry.name))) {
  129. const filePath = join(fullPath, entry.name);
  130. const content = await Deno.readTextFile(filePath);
  131. const matches = content.match(regexObj);
  132. if (matches) {
  133. results.push(`File: ${entry.name}\nMatches:\n${matches.join("\n")}\n`);
  134. }
  135. }
  136. }
  137. return results.join("\n") || "No matches found";
  138. } catch (error) {
  139. return `Error searching files: ${error instanceof Error ? error.message : String(error)}`;
  140. }
  141. }
  142. export async function listFiles(workingDir: string, relativePath: string, recursive: boolean): Promise<ToolResponse> {
  143. try {
  144. const fullPath = join(workingDir, relativePath);
  145. const files: string[] = [];
  146. async function* walkDir(dir: string): AsyncGenerator<string> {
  147. for await (const entry of Deno.readDir(dir)) {
  148. const entryPath = join(dir, entry.name);
  149. if (entry.isFile) {
  150. yield entryPath.replace(fullPath + "/", "");
  151. } else if (recursive && entry.isDirectory) {
  152. yield* walkDir(entryPath);
  153. }
  154. }
  155. }
  156. for await (const file of walkDir(fullPath)) {
  157. files.push(file);
  158. }
  159. return files.join("\n") || "No files found";
  160. } catch (error) {
  161. return `Error listing files: ${error instanceof Error ? error.message : String(error)}`;
  162. }
  163. }
  164. export async function listCodeDefinitions(workingDir: string, relativePath: string): Promise<ToolResponse> {
  165. try {
  166. const fullPath = join(workingDir, relativePath);
  167. const content = await Deno.readTextFile(fullPath);
  168. // Basic regex patterns for common code definitions
  169. const patterns = {
  170. function: /(?:function|const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*(?:=\s*(?:function|\([^)]*\)\s*=>)|[({])/g,
  171. class: /class\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/g,
  172. method: /(?:async\s+)?([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\([^)]*\)\s*{/g,
  173. };
  174. const definitions: Record<string, string[]> = {
  175. functions: [],
  176. classes: [],
  177. methods: [],
  178. };
  179. let match;
  180. while ((match = patterns.function.exec(content)) !== null) {
  181. definitions.functions.push(match[1]);
  182. }
  183. while ((match = patterns.class.exec(content)) !== null) {
  184. definitions.classes.push(match[1]);
  185. }
  186. while ((match = patterns.method.exec(content)) !== null) {
  187. definitions.methods.push(match[1]);
  188. }
  189. return Object.entries(definitions)
  190. .map(([type, names]) => `${type}:\n${names.join("\n")}`)
  191. .join("\n\n");
  192. } catch (error) {
  193. return `Error listing code definitions: ${error instanceof Error ? error.message : String(error)}`;
  194. }
  195. }