/// import { join, dirname } from "https://deno.land/std@0.220.1/path/mod.ts"; import { red, yellow, green } from "https://deno.land/std@0.220.1/fmt/colors.ts"; import type { ToolResponse } from "../types.d.ts"; interface CommandConfig { desc: string; args: readonly string[]; } // Define allowed commands and their descriptions const ALLOWED_COMMANDS: Record = { 'npm': { desc: "Node package manager", args: ["install", "run", "test", "build"] }, 'git': { desc: "Version control", args: ["status", "add", "commit", "push", "pull", "clone", "checkout", "branch"] }, 'deno': { desc: "Deno runtime", args: ["run", "test", "fmt", "lint", "check", "compile", "bundle"] }, 'ls': { desc: "List directory contents", args: ["-l", "-a", "-la", "-lh"] }, 'cat': { desc: "Show file contents", args: [] }, 'echo': { desc: "Print text", args: [] } }; // Track commands that have been allowed for this session const alwaysAllowedCommands = new Set(); function isCommandAllowed(command: string): boolean { // Split command into parts const parts = command.trim().split(/\s+/); if (parts.length === 0) return false; // Get base command const baseCmd = parts[0]; if (!(baseCmd in ALLOWED_COMMANDS)) return false; // If command has arguments, check if they're allowed if (parts.length > 1 && ALLOWED_COMMANDS[baseCmd].args.length > 0) { const arg = parts[1]; return ALLOWED_COMMANDS[baseCmd].args.includes(arg); } return true; } async function promptForCommand(command: string): Promise { // Check if command has been previously allowed if (alwaysAllowedCommands.has(command)) { console.log(yellow("\nWarning: Running previously allowed command:"), red(command)); return true; } console.log(yellow("\nWarning: Command not in allowlist")); console.log("Command:", red(command)); console.log("\nAllowed commands:"); Object.entries(ALLOWED_COMMANDS).forEach(([cmd, { desc, args }]) => { console.log(` ${green(cmd)}: ${desc}`); if (args.length) { console.log(` Arguments: ${args.join(", ")}`); } }); const answer = prompt("\nDo you want to run this command? (y/n/always) "); if (answer?.toLowerCase() === 'always') { alwaysAllowedCommands.add(command); return true; } return answer?.toLowerCase() === 'y'; } export async function executeCommand(command: string): Promise { try { // Check if command is allowed if (!isCommandAllowed(command)) { // Prompt user for confirmation const shouldRun = await promptForCommand(command); if (!shouldRun) { return "Command execution cancelled by user"; } console.log(yellow("\nProceeding with command execution...")); } const process = new Deno.Command("sh", { args: ["-c", command], stdout: "piped", stderr: "piped", }); const { stdout, stderr } = await process.output(); const decoder = new TextDecoder(); return decoder.decode(stdout) + (stderr.length ? `\nStderr:\n${decoder.decode(stderr)}` : ""); } catch (error) { return `Error executing command: ${error instanceof Error ? error.message : String(error)}`; } } export async function readFile(workingDir: string, relativePath: string): Promise { try { const fullPath = join(workingDir, relativePath); const content = await Deno.readTextFile(fullPath); return content; } catch (error) { return `Error reading file: ${error instanceof Error ? error.message : String(error)}`; } } export async function writeFile(workingDir: string, relativePath: string, content: string): Promise { try { const fullPath = join(workingDir, relativePath); await Deno.mkdir(dirname(fullPath), { recursive: true }); await Deno.writeTextFile(fullPath, content); return `Successfully wrote to ${relativePath}`; } catch (error) { return `Error writing file: ${error instanceof Error ? error.message : String(error)}`; } } export async function searchFiles( workingDir: string, searchPath: string, regex: string, filePattern?: string ): Promise { try { const fullPath = join(workingDir, searchPath); const results: string[] = []; const regexObj = new RegExp(regex, "g"); const patternObj = filePattern ? new RegExp(filePattern) : null; for await (const entry of Deno.readDir(fullPath)) { if (entry.isFile && (!patternObj || patternObj.test(entry.name))) { const filePath = join(fullPath, entry.name); const content = await Deno.readTextFile(filePath); const matches = content.match(regexObj); if (matches) { results.push(`File: ${entry.name}\nMatches:\n${matches.join("\n")}\n`); } } } return results.join("\n") || "No matches found"; } catch (error) { return `Error searching files: ${error instanceof Error ? error.message : String(error)}`; } } export async function listFiles(workingDir: string, relativePath: string, recursive: boolean): Promise { try { const fullPath = join(workingDir, relativePath); const files: string[] = []; async function* walkDir(dir: string): AsyncGenerator { for await (const entry of Deno.readDir(dir)) { const entryPath = join(dir, entry.name); if (entry.isFile) { yield entryPath.replace(fullPath + "/", ""); } else if (recursive && entry.isDirectory) { yield* walkDir(entryPath); } } } for await (const file of walkDir(fullPath)) { files.push(file); } return files.join("\n") || "No files found"; } catch (error) { return `Error listing files: ${error instanceof Error ? error.message : String(error)}`; } } export async function listCodeDefinitions(workingDir: string, relativePath: string): Promise { try { const fullPath = join(workingDir, relativePath); const content = await Deno.readTextFile(fullPath); // Basic regex patterns for common code definitions const patterns = { function: /(?:function|const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*(?:=\s*(?:function|\([^)]*\)\s*=>)|[({])/g, class: /class\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/g, method: /(?:async\s+)?([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\([^)]*\)\s*{/g, }; const definitions: Record = { functions: [], classes: [], methods: [], }; let match; while ((match = patterns.function.exec(content)) !== null) { definitions.functions.push(match[1]); } while ((match = patterns.class.exec(content)) !== null) { definitions.classes.push(match[1]); } while ((match = patterns.method.exec(content)) !== null) { definitions.methods.push(match[1]); } return Object.entries(definitions) .map(([type, names]) => `${type}:\n${names.join("\n")}`) .join("\n\n"); } catch (error) { return `Error listing code definitions: ${error instanceof Error ? error.message : String(error)}`; } }