Project Files
src / tools / codeTools.ts
/**
* @file codeTools.ts
* Code execution tools: JavaScript (Deno), Python, shell commands, terminal, tests.
* Exports originalRunJs/PyImplementation for use by the sub-agent dispatcher.
*/
import { text, tool, type Tool } from "@lmstudio/sdk";
import { spawn } from "child_process";
import { rm, writeFile } from "fs/promises";
import { join } from "path";
import { z } from "zod";
import { createSafeToolImplementation, type ToolContext } from "./shared";
import { findLMStudioHome } from "./findLMStudioHome";
function getDenoPath() {
const lmstudioHome = findLMStudioHome();
const utilPath = join(lmstudioHome, ".internal", "utils");
return join(utilPath, process.platform === "win32" ? "deno.exe" : "deno");
}
export interface CodeToolsResult {
tools: Tool[];
/** Exposed for sub-agent tool dispatch. */
originalRunJavascript: (params: { javascript: string; timeout_seconds?: number }) => Promise<{ stdout: string; stderr: string }>;
originalRunPython: (params: { python: string; timeout_seconds?: number }) => Promise<{ stdout: string; stderr: string }>;
}
export function createCodeTools(
ctx: ToolContext,
config: { allowJavascript: boolean; allowPython: boolean; allowShell: boolean; allowTerminal: boolean },
): CodeToolsResult {
const tools: Tool[] = [];
const MAX_OUTPUT = 4000;
const originalRunJavascript = async ({ javascript, timeout_seconds }: { javascript: string; timeout_seconds?: number }) => {
const scriptFileName = `temp_script_${Date.now()}.ts`;
const scriptFilePath = join(ctx.cwd, scriptFileName);
try {
await writeFile(scriptFilePath, javascript, "utf-8");
const childProcess = 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);
await new Promise<void>((resolve, reject) => {
childProcess.on("close", code => code === 0 ? resolve() : reject(new Error(`Process exited with code ${code}. Stderr: ${stderr}`)));
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,
};
} finally {
await rm(scriptFilePath, { force: true }).catch(() => {});
}
};
tools.push(tool({
name: "run_javascript",
description: 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: z.string(), timeout_seconds: z.number().min(0.1).max(60).optional() },
implementation: createSafeToolImplementation(originalRunJavascript, config.allowJavascript, "run_javascript"),
}));
const originalRunPython = async ({ python, timeout_seconds }: { python: string; timeout_seconds?: number }) => {
const scriptFileName = `temp_script_${Date.now()}.py`;
const scriptFilePath = join(ctx.cwd, scriptFileName);
try {
await writeFile(scriptFilePath, python, "utf-8");
const childProcess = spawn("python", [scriptFilePath], {
cwd: ctx.cwd, timeout: (timeout_seconds ?? 5) * 1000, stdio: "pipe",
});
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);
await new Promise<void>((resolve, reject) => {
childProcess.on("close", code => code === 0 ? resolve() : reject(new Error(`Process exited with code ${code}. Stderr: ${stderr}`)));
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,
};
} finally {
await rm(scriptFilePath, { force: true }).catch(() => {});
}
};
tools.push(tool({
name: "run_python",
description: 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: z.string(), timeout_seconds: z.number().min(0.1).max(60).optional() },
implementation: createSafeToolImplementation(originalRunPython, config.allowPython, "run_python"),
}));
const originalExecuteCommand = async ({ command, input, timeout_seconds }: { command: string; input?: string; timeout_seconds?: number }) => {
const childProcess = spawn(command, [], {
cwd: ctx.cwd, shell: true, timeout: (timeout_seconds ?? 5) * 1000, stdio: "pipe",
});
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);
await new Promise<void>((resolve, reject) => {
childProcess.on("close", code => code === 0 ? resolve() : reject(new Error(`Process exited with code ${code}. Stderr: ${stderr}`)));
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,
};
};
tools.push(tool({
name: "execute_command",
description: 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: z.string(),
input: z.string().optional().describe("Input text to pipe to the command's stdin."),
timeout_seconds: z.number().min(0.1).max(60).optional().describe("Timeout in seconds (default: 5, max: 60)"),
},
implementation: createSafeToolImplementation(originalExecuteCommand, config.allowShell, "execute_command"),
}));
const originalRunInTerminal = async ({ command }: { command: string }) => {
if (process.platform === "win32") {
const escapedDir = ctx.cwd.replace(/"/g, '""');
const escapedCmd = command.replace(/"/g, '""');
const child = 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 = 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 = spawn("x-terminal-emulator", ["-e", "bash", "-c", bashScript], { detached: true, stdio: "ignore" });
child.on("error", () => {
const child2 = 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(tool({
name: "run_in_terminal",
description: text`Launch a command in a new, separate interactive terminal window.`,
parameters: { command: z.string() },
implementation: createSafeToolImplementation(originalRunInTerminal, config.allowTerminal, "run_in_terminal"),
}));
tools.push(tool({
name: "run_test_command",
description: "Execute a test command (like 'npm test') and return the results.",
parameters: { command: z.string().describe("The test command to run (e.g., 'npm test', 'pytest').") },
implementation: async ({ command }) => {
return new Promise((resolve) => {
const parts = command.split(" ");
const TIMEOUT_MS = 120_000;
const child = spawn(parts[0], parts.slice(1), { cwd: ctx.cwd, shell: true, env: { ...process.env, CI: 'true' } });
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);
const out = stdout.trim();
const err = stderr.trim();
resolve({
command, exit_code: code, passed: code === 0,
stdout: out.length > MAX_OUTPUT ? out.substring(0, MAX_OUTPUT) + `\n... (truncated, ${out.length} chars total)` : out,
stderr: err.length > MAX_OUTPUT ? err.substring(0, MAX_OUTPUT) + `\n... (truncated)` : err,
...(timedOut ? { warning: `Process killed after ${TIMEOUT_MS / 1000}s timeout` } : {}),
});
});
child.on("error", err => { clearTimeout(timer); resolve({ command, error: err.message, passed: false }); });
});
},
}));
return { tools, originalRunJavascript, originalRunPython };
}