import { spawn } from "child_process"; import fs from "fs"; let toolSpec; export function listTools() { if (!toolSpec) { toolSpec = JSON.parse(fs.readFileSync("tools.json", "utf-8")); } return toolSpec; } /** * Stream a child process over SSE. * send(eventType, payload) writes to the SSE connection. */ function streamProcess(cmd, args, options, send) { const child = spawn(cmd, args, { shell: false, ...options }); child.stdout.on("data", (chunk) => { send("output", { data: chunk.toString() }); }); child.stderr.on("data", (chunk) => { send("error", { data: chunk.toString() }); }); child.on("close", (code) => { send("done", { code }); }); child.on("error", (err) => { send("error", { data: String(err) }); send("done", { code: -1 }); }); return child; } /** * Execute a tool call and stream results. * tool: "shell" | "python" * args: { command } | { code } */ export function executeTool(tool, args, send) { if (tool === "shell") { if (!args?.command || typeof args.command !== "string") { send("error", { data: "Missing 'command' string." }); return send("done", { code: 2 }); } // Use /bin/bash -lc to enable pipes, redirects, env, etc. return streamProcess("/bin/bash", ["-lc", args.command], {}, send); } if (tool === "python") { if (!args?.code || typeof args.code !== "string") { send("error", { data: "Missing 'code' string." }); return send("done", { code: 2 }); } // Run python inline, escape newlines safely by passing as stdin const child = spawn("python3", ["-"], { stdio: ["pipe", "pipe", "pipe"] }); child.stdin.write(args.code); child.stdin.end(); child.stdout.on("data", (chunk) => send("output", { data: chunk.toString() })); child.stderr.on("data", (chunk) => send("error", { data: chunk.toString() })); child.on("close", (code) => send("done", { code })); child.on("error", (err) => { send("error", { data: String(err) }); send("done", { code: -1 }); }); return child; } send("error", { data: `Unknown tool '${tool}'.` }); send("done", { code: 2 }); }