@tabbybyte/minion
Version:
A cross-runtime CLI tool for AI-powered command execution. Auto-detects and uses Bun for performance when available, falls back to Node.js.
413 lines (370 loc) • 10.9 kB
JavaScript
import { z } from "zod"
import {
safeReadFile,
safeWriteFile,
safeAppendFile,
fileExists,
listDirectory,
getFileStats,
isPathSafe,
getFileExtension
} from "./file-utils.js"
import { spawnProcess } from "./runtime-compat.js"
// Command allowlist for safety
const ALLOWED_COMMANDS = [
"ls", "dir", "pwd", "cd", "cat", "head", "tail", "grep", "find", "which", "whereis",
"echo", "date", "whoami", "id", "uname", "df", "du", "free", "ps", "top", "htop",
"curl", "wget", "ping", "nslookup", "dig", "ssh", "scp", "rsync",
"git", "npm", "yarn", "bun", "node", "python", "python3", "pip", "pip3",
"docker", "docker-compose", "kubectl", "helm",
"mkdir", "touch", "cp", "mv", "ln", "chmod", "chown",
"tar", "zip", "unzip", "gzip", "gunzip",
"vim", "nano", "emacs", "code", "subl"
]
// Dangerous command patterns to block
const DANGEROUS_PATTERNS = [
/rm\s+.*-rf?\s*\//, // rm -rf /
/:\(\)\{\s*:|:&\s*\};:/, // fork bomb
/>\s*\/dev\/sd[a-z]/, // writing to disk devices
/dd\s+.*of=\/dev/, // dd to devices
/mkfs/, // filesystem creation
/fdisk/, // disk partitioning
/format/, // disk formatting
/del\s+.*\/[sq]/, // Windows delete with force/quiet
/shutdown/, // system shutdown
/reboot/, // system reboot
/halt/, // system halt
/init\s+0/, // shutdown via init
/kill\s+-9\s+1/, // kill init process
/killall\s+-9/, // kill all processes
]
function setupTools(config) {
return {
run_safe_command: {
description: "Execute a shell command safely with built-in safety checks and allowlist validation",
parameters: z.object({
command: z.string().describe("The shell command to execute"),
}),
execute: async ({ command }) => runSafeCommand(command, config),
},
read_file: {
description: "Read the contents of a file safely with proper error handling",
parameters: z.object({
filePath: z.string().describe("Path to the file to read"),
encoding: z.string().optional().default("utf-8").describe("File encoding (default: utf-8)"),
}),
execute: async ({ filePath, encoding }) => readFileContents(filePath, encoding, config),
},
write_file: {
description: "Write content to a file safely, creating directories if needed",
parameters: z.object({
filePath: z.string().describe("Path to the file to write"),
content: z.string().describe("Content to write to the file"),
encoding: z.string().optional().default("utf-8").describe("File encoding (default: utf-8)"),
}),
execute: async ({ filePath, content, encoding }) => (
writeFileContents(filePath, content, encoding, config)
),
},
append_file: {
description: "Append content to a file safely",
parameters: z.object({
filePath: z.string().describe("Path to the file to append to"),
content: z.string().describe("Content to append to the file"),
encoding: z.string().optional().default("utf-8").describe("File encoding (default: utf-8)"),
}),
execute: async ({ filePath, content, encoding }) => (
appendFileContents(filePath, content, encoding, config)
),
},
list_files: {
description: "List files and directories in a given path",
parameters: z.object({
dirPath: z.string().describe("Path to the directory to list"),
showDetails: z.boolean().optional().default(false).describe("Show detailed file information"),
}),
execute: async ({ dirPath, showDetails }) => listFiles(dirPath, showDetails, config),
},
check_file_exists: {
description: "Check if a file or directory exists",
parameters: z.object({
filePath: z.string().describe("Path to check"),
}),
execute: async ({ filePath }) => checkFileExistence(filePath, config),
},
}
}
async function runSafeCommand(command, config) {
try {
// Parse the command to get the base command
const parts = command.trim().split(/\s+/)
const baseCommand = parts[0]
// Safety checks
if (!isCommandSafe(command, baseCommand)) {
return {
success: false,
error: "Command blocked by safety filters",
output: "",
stderr: ""
}
}
// if (config.dryRun) {
// if (config.debug) {
// console.log(`[DRY RUN] Would execute: ${command}`);
// }
// return {
// success: true,
// dryRun: true,
// command: command,
// output: `[DRY RUN] Would execute: ${command}`,
// stderr: ''
// };
// }
// Execute the command
if (config.debug) {
console.log(`🔧 Executing: ${command}`)
}
const result = await executeCommand(command, config)
if (config.debug) {
console.log(`✅ Command completed with exit code: ${result.exitCode}`)
}
return {
success: result.exitCode === 0,
exitCode: result.exitCode,
output: result.stdout,
stderr: result.stderr,
command
}
} catch (error) {
return {
success: false,
error: error.message,
output: "",
stderr: error.message,
command
}
}
}
function isCommandSafe(fullCommand, baseCommand) {
// Check against dangerous patterns
for (const pattern of DANGEROUS_PATTERNS) {
if (pattern.test(fullCommand)) {
return false
}
}
// Check allowlist
if (!ALLOWED_COMMANDS.includes(baseCommand)) {
return false
}
return true
}
function executeCommand(command, config) {
return new Promise((resolve, reject) => {
const shell = process.platform === "win32" ? "cmd" : "sh"
const shellFlag = process.platform === "win32" ? "/c" : "-c"
// Use async function to handle the spawn process
const runCommand = async () => {
try {
// Use cross-runtime spawn function for better compatibility
const result = await spawnProcess([shell, shellFlag, command], {
debug: config.debug
})
resolve({
exitCode: result.exitCode,
stdout: result.stdout.trim(),
stderr: result.stderr.trim()
})
} catch (error) {
reject(new Error(`Failed to execute command: ${error.message}`))
}
}
// Execute the command
runCommand()
})
}
// File operation implementations
async function readFileContents(filePath, encoding, config) {
try {
if (!isPathSafe(filePath)) {
return {
success: false,
error: "File path is not safe - outside allowed directory",
content: ""
}
}
if (config.debug) {
console.log(`📖 Reading file: ${filePath}`)
}
const content = await safeReadFile(filePath, encoding)
if (config.debug) {
console.log(`✅ File read successfully, ${content.length} characters`)
}
return {
success: true,
content,
filePath,
size: content.length
}
} catch (error) {
return {
success: false,
error: error.message,
content: "",
filePath
}
}
}
async function writeFileContents(filePath, content, encoding, config) {
try {
if (!isPathSafe(filePath)) {
return {
success: false,
error: "File path is not safe - outside allowed directory"
}
}
if (config.debug) {
console.log(`✏️ Writing file: ${filePath}`)
}
await safeWriteFile(filePath, content, encoding)
if (config.debug) {
console.log(`✅ File written successfully, ${content.length} characters`)
}
return {
success: true,
filePath,
size: content.length,
message: "File written successfully"
}
} catch (error) {
return {
success: false,
error: error.message,
filePath
}
}
}
async function appendFileContents(filePath, content, encoding, config) {
try {
if (!isPathSafe(filePath)) {
return {
success: false,
error: "File path is not safe - outside allowed directory"
}
}
if (config.debug) {
console.log(`➕ Appending to file: ${filePath}`)
}
await safeAppendFile(filePath, content, encoding)
if (config.debug) {
console.log(`✅ Content appended successfully, ${content.length} characters`)
}
return {
success: true,
filePath,
appendedSize: content.length,
message: "Content appended successfully"
}
} catch (error) {
return {
success: false,
error: error.message,
filePath
}
}
}
async function listFiles(dirPath, showDetails, config) {
try {
if (!isPathSafe(dirPath)) {
return {
success: false,
error: "Directory path is not safe - outside allowed directory",
files: []
}
}
if (config.debug) {
console.log(`📁 Listing directory: ${dirPath}`)
}
const files = await listDirectory(dirPath)
if (showDetails) {
const detailedFiles = []
for (const file of files) {
try {
const fullPath = `${dirPath}/${file}`
// eslint-disable-next-line no-await-in-loop
const stats = await getFileStats(fullPath)
detailedFiles.push({
name: file,
path: fullPath,
size: stats.size,
isDirectory: stats.isDirectory(),
isFile: stats.isFile(),
modified: stats.mtime.toISOString(),
extension: getFileExtension(file)
})
} catch (error) {
detailedFiles.push({
name: file,
error: `Could not get stats: ${error.message}`
})
}
}
if (config.debug) {
console.log(`✅ Listed ${detailedFiles.length} items with details`)
}
return {
success: true,
files: detailedFiles,
count: detailedFiles.length,
dirPath
}
}
if (config.debug) {
console.log(`✅ Listed ${files.length} items`)
}
return {
success: true,
files,
count: files.length,
dirPath
}
} catch (error) {
return {
success: false,
error: error.message,
files: [],
dirPath
}
}
}
async function checkFileExistence(filePath, config) {
try {
if (!isPathSafe(filePath)) {
return {
success: false,
error: "File path is not safe - outside allowed directory",
exists: false
}
}
if (config.debug) {
console.log(`🔍 Checking existence: ${filePath}`)
}
const exists = await fileExists(filePath)
if (config.debug) {
console.log(`✅ File ${exists ? "exists" : "does not exist"}`)
}
return {
success: true,
exists,
filePath
}
} catch (error) {
return {
success: false,
error: error.message,
exists: false,
filePath
}
}
}
// Exports
export { setupTools }