Forked from brdcastro/maestro
"use strict";
/**
* @file codeTools.ts
* Code execution tools: JavaScript (Deno), Python, shell commands, terminal, tests.
* Exports originalRunJs/PyImplementation for use by the sub-agent dispatcher.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.createCodeTools = createCodeTools;
const sdk_1 = require("@lmstudio/sdk");
const child_process_1 = require("child_process");
const promises_1 = require("fs/promises");
const path_1 = require("path");
const zod_1 = require("zod");
const shared_1 = require("./shared");
const findLMStudioHome_1 = require("./findLMStudioHome");
const spillToDisk_1 = require("./spillToDisk");
/**
* Spawn a shell command via the user's login shell so it inherits the full
* PATH from .zshrc / .bash_profile (homebrew, nvm, volta, asdf, etc.). The
* LM Studio plugin process otherwise gets a minimal /bin/sh PATH that
* excludes /opt/homebrew/bin and Node version manager paths — which is why
* the model's `npx hyperframes render` and `which node` calls failed in the
* Apr 28 Micro Plásticos session.
*
* On Windows, falls back to standard `shell: true` (cmd.exe), which already
* uses the user's PATH and has no login-shell concept.
*/
function spawnInLoginShell(command, options = {}) {
if (process.platform === "win32") {
return (0, child_process_1.spawn)(command, [], { shell: true, ...options });
}
const userShell = process.env.SHELL || "/bin/zsh";
return (0, child_process_1.spawn)(userShell, ["-lc", command], options);
}
function getDenoPath() {
const lmstudioHome = (0, findLMStudioHome_1.findLMStudioHome)();
const utilPath = (0, path_1.join)(lmstudioHome, ".internal", "utils");
return (0, path_1.join)(utilPath, process.platform === "win32" ? "deno.exe" : "deno");
}
function createCodeTools(ctx, config, limits) {
const tools = [];
const MAX_OUTPUT = limits?.maxOutput ?? 4_000;
const originalRunJavascript = async ({ javascript, timeout_seconds }) => {
const scriptFileName = `temp_script_${Date.now()}.ts`;
const scriptFilePath = (0, path_1.join)(ctx.cwd, scriptFileName);
try {
await (0, promises_1.writeFile)(scriptFilePath, javascript, "utf-8");
const childProcess = (0, child_process_1.spawn)(getDenoPath(), ["run", "--allow-read=.", "--allow-write=.", "--no-prompt", "--deny-net", "--deny-env", "--deny-sys", "--deny-run", "--deny-ffi", scriptFilePath], {
cwd: ctx.cwd, timeout: (timeout_seconds ?? 5) * 1000, stdio: "pipe", env: { NO_COLOR: "true" },
});
let stdout = "", stderr = "";
childProcess.stdout.setEncoding("utf-8");
childProcess.stderr.setEncoding("utf-8");
childProcess.stdout.on("data", d => stdout += d);
childProcess.stderr.on("data", d => stderr += d);
const exitCode = await new Promise((resolve, reject) => {
childProcess.on("close", code => resolve(code ?? -1));
childProcess.on("error", reject);
});
const outJs = stdout.trim(), errJs = stderr.trim();
return {
stdout: outJs.length > MAX_OUTPUT ? outJs.substring(0, MAX_OUTPUT) + `\n... (truncated, ${outJs.length} chars total)` : outJs,
stderr: errJs.length > MAX_OUTPUT ? errJs.substring(0, MAX_OUTPUT) + `\n... (truncated)` : errJs,
...(exitCode !== 0 ? { exitCode, error: `Process exited with code ${exitCode}` } : {}),
};
}
finally {
await (0, promises_1.rm)(scriptFilePath, { force: true }).catch(() => { });
}
};
tools.push((0, sdk_1.tool)({
name: "run_javascript",
description: (0, sdk_1.text) `
Run a JavaScript code snippet using deno. You cannot import external modules but you have
read/write access to the current working directory.
Pass the code you wish to run as a string in the 'javascript' parameter.
By default, the code will timeout in 5 seconds. You can extend this timeout by setting the
'timeout_seconds' parameter to a higher value in seconds, up to a maximum of 60 seconds.
You will get the stdout and stderr output of the code execution, thus please print the output
you wish to return using 'console.log' or 'console.error'.
`,
parameters: { javascript: zod_1.z.string(), timeout_seconds: zod_1.z.number().min(0.1).max(60).optional() },
implementation: (0, shared_1.createSafeToolImplementation)(originalRunJavascript, config.allowJavascript, "run_javascript"),
}));
const originalRunPython = async ({ python, timeout_seconds }) => {
const scriptFileName = `temp_script_${Date.now()}.py`;
const scriptFilePath = (0, path_1.join)(ctx.cwd, scriptFileName);
try {
await (0, promises_1.writeFile)(scriptFilePath, python, "utf-8");
const pythonCmd = process.platform === "win32" ? "python" : "python3";
const childProcess = (0, child_process_1.spawn)(pythonCmd, [scriptFilePath], {
cwd: ctx.cwd, timeout: (timeout_seconds ?? 5) * 1000, stdio: "pipe",
env: { PATH: process.env.PATH || "/usr/bin:/bin", HOME: process.env.HOME || "", TMPDIR: process.env.TMPDIR || "/tmp", LANG: process.env.LANG || "" },
});
let stdout = "", stderr = "";
childProcess.stdout.setEncoding("utf-8");
childProcess.stderr.setEncoding("utf-8");
childProcess.stdout.on("data", d => stdout += d);
childProcess.stderr.on("data", d => stderr += d);
const exitCode = await new Promise((resolve, reject) => {
childProcess.on("close", code => resolve(code ?? -1));
childProcess.on("error", reject);
});
const outStr = stdout.trim(), errStr = stderr.trim();
return {
stdout: outStr.length > MAX_OUTPUT ? outStr.substring(0, MAX_OUTPUT) + `\n... (truncated, ${outStr.length} chars total)` : outStr,
stderr: errStr.length > MAX_OUTPUT ? errStr.substring(0, MAX_OUTPUT) + `\n... (truncated)` : errStr,
...(exitCode !== 0 ? { exitCode, error: `Process exited with code ${exitCode}` } : {}),
};
}
finally {
await (0, promises_1.rm)(scriptFilePath, { force: true }).catch(() => { });
}
};
tools.push((0, sdk_1.tool)({
name: "run_python",
description: (0, sdk_1.text) `
Run a Python code snippet. You cannot import external modules but you have
read/write access to the current working directory.
Pass the code you wish to run as a string in the 'python' parameter.
By default, the code will timeout in 5 seconds. You can extend this timeout by setting the
'timeout_seconds' parameter to a higher value in seconds, up to a maximum of 60 seconds.
You will get the stdout and stderr output of the code execution, thus please print the output
you wish to return using 'print()'.
`,
parameters: { python: zod_1.z.string(), timeout_seconds: zod_1.z.number().min(0.1).max(60).optional() },
implementation: (0, shared_1.createSafeToolImplementation)(originalRunPython, config.allowPython, "run_python"),
}));
const originalExecuteCommand = async ({ command, input, timeout_seconds }) => {
const childProcess = spawnInLoginShell(command, {
cwd: ctx.cwd, timeout: (timeout_seconds ?? 5) * 1000, stdio: "pipe",
});
// stdio: "pipe" is set explicitly above, so stdin/stdout/stderr are
// guaranteed non-null at runtime. Non-null assertions satisfy the
// generic ChildProcess return type from spawnInLoginShell.
if (input) {
childProcess.stdin.write(input);
childProcess.stdin.end();
}
else {
childProcess.stdin.end();
}
let stdout = "", stderr = "";
childProcess.stdout.setEncoding("utf-8");
childProcess.stderr.setEncoding("utf-8");
childProcess.stdout.on("data", d => stdout += d);
childProcess.stderr.on("data", d => stderr += d);
const exitCode = await new Promise((resolve, reject) => {
childProcess.on("close", code => resolve(code ?? -1));
childProcess.on("error", reject);
});
const outStr = stdout.trim();
const errStr = stderr.trim();
return {
stdout: outStr.length > MAX_OUTPUT ? outStr.substring(0, MAX_OUTPUT) + `\n... (truncated, ${outStr.length} chars total)` : outStr,
stderr: errStr.length > MAX_OUTPUT ? errStr.substring(0, MAX_OUTPUT) + `\n... (truncated)` : errStr,
...(exitCode !== 0 ? { exitCode, error: `Process exited with code ${exitCode}` } : {}),
};
};
tools.push((0, sdk_1.tool)({
name: "execute_command",
description: (0, sdk_1.text) `
Execute a shell command in the current working directory.
Returns the stdout and stderr output of the command.
You can optionally provide input to be piped to the command's stdin.
IMPORTANT: The host operating system is '${process.platform}'.
If the OS is 'win32' (Windows), do NOT use 'bash' or 'sh' commands unless you are certain WSL is available.
Instead, use standard Windows 'cmd' or 'powershell' syntax.
`,
parameters: {
command: zod_1.z.string(),
input: zod_1.z.string().optional().describe("Input text to pipe to the command's stdin."),
timeout_seconds: zod_1.z.number().min(0.1).max(60).optional().describe("Timeout in seconds (default: 5, max: 60)"),
},
implementation: (0, shared_1.createSafeToolImplementation)(originalExecuteCommand, config.allowShell, "execute_command"),
}));
const originalRunInTerminal = async ({ command }) => {
if (process.platform === "win32") {
const escapedDir = ctx.cwd.replace(/"/g, '""');
const escapedCmd = command.replace(/"/g, '""');
const child = (0, child_process_1.spawn)("cmd.exe", ["/c", `start "" /D "${escapedDir}" cmd.exe /k "${escapedCmd}"`], { detached: true, stdio: "ignore", windowsHide: false });
child.unref();
}
else if (process.platform === "darwin") {
const safeCmd = command.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
const safeCwd = ctx.cwd.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
const child = (0, child_process_1.spawn)("osascript", ["-e", `tell application "Terminal"\ndo script "cd \\"${safeCwd}\\" && ${safeCmd}"\nactivate\nend tell`], { detached: true, stdio: "ignore" });
child.unref();
}
else {
const safeCwd = ctx.cwd.replace(/'/g, "'\\''");
const safeCmd = command.replace(/'/g, "'\\''");
const bashScript = `cd '${safeCwd}' && ${safeCmd}; bash`;
const child = (0, child_process_1.spawn)("x-terminal-emulator", ["-e", "bash", "-c", bashScript], { detached: true, stdio: "ignore" });
child.on("error", () => {
const child2 = (0, child_process_1.spawn)("gnome-terminal", ["--", "bash", "-c", bashScript], { detached: true, stdio: "ignore" });
child2.unref();
});
child.unref();
}
return { success: true, message: "Terminal window launched. Please check your taskbar." };
};
tools.push((0, sdk_1.tool)({
name: "run_in_terminal",
description: (0, sdk_1.text) `Launch a command in a new, separate interactive terminal window.`,
parameters: { command: zod_1.z.string() },
implementation: (0, shared_1.createSafeToolImplementation)(originalRunInTerminal, config.allowTerminal, "run_in_terminal"),
}));
tools.push((0, sdk_1.tool)({
name: "run_test_command",
description: "Execute a test command (like 'npm test') and return the results.",
parameters: { command: zod_1.z.string().describe("The test command to run (e.g., 'npm test', 'pytest').") },
implementation: async ({ command }) => {
const raw = await new Promise((resolve) => {
const TIMEOUT_MS = 120_000;
const child = spawnInLoginShell(command, { cwd: ctx.cwd, env: { ...process.env, CI: 'true' }, stdio: "pipe" });
let stdout = "", stderr = "", timedOut = false;
const timer = setTimeout(() => { timedOut = true; child.kill("SIGTERM"); }, TIMEOUT_MS);
child.stdout.on("data", d => stdout += d.toString());
child.stderr.on("data", d => stderr += d.toString());
child.on("close", code => {
clearTimeout(timer);
resolve({ stdout: stdout.trim(), stderr: stderr.trim(), code, timedOut });
});
child.on("error", err => { clearTimeout(timer); resolve({ stdout: "", stderr: err.message, code: -1, timedOut: false, spawnError: true }); });
});
const outSpill = await (0, spillToDisk_1.spillIfNeeded)(raw.stdout, MAX_OUTPUT, "test-stdout");
const errSpill = await (0, spillToDisk_1.spillIfNeeded)(raw.stderr, MAX_OUTPUT, "test-stderr");
return {
command, exit_code: raw.code, passed: raw.code === 0,
stdout: outSpill.preview,
stderr: errSpill.preview,
...(outSpill.spilled ? { stdout_full: outSpill.spillPath } : {}),
...(errSpill.spilled ? { stderr_full: errSpill.spillPath } : {}),
...(raw.timedOut ? { warning: `Process killed after 120s timeout` } : {}),
};
},
}));
tools.push((0, sdk_1.tool)({
name: "run_background",
description: (0, sdk_1.text) `
Launch a command in the background (fire-and-forget).
Returns the process ID immediately without waiting for completion.
Useful for starting dev servers, watchers, or long-running processes.
`,
parameters: {
command: zod_1.z.string().describe("The command to run (e.g., 'npm run dev', 'python server.py')."),
},
implementation: (0, shared_1.createSafeToolImplementation)(async ({ command }) => {
const child = spawnInLoginShell(command, {
cwd: ctx.cwd,
detached: true,
stdio: "ignore",
env: { ...process.env },
});
child.unref();
return {
pid: child.pid,
command,
message: `Process started in background with PID ${child.pid}. It will continue running independently.`,
};
}, config.allowShell, "run_background"),
}));
return { tools, originalRunJavascript, originalRunPython };
}
//# sourceMappingURL=codeTools.js.map