src / toolsProvider.ts
import { text, tool, type ToolsProviderController } from "@lmstudio/sdk";
import { z } from "zod";
import { configSchematics } from "./configSchematics";
import {
describeEnvironment,
formatRunResult,
runShell,
type ShellSettings,
} from "./shell";
export async function toolsProvider(ctl: ToolsProviderController) {
const config = ctl.getPluginConfig(configSchematics);
const getSettings = (): ShellSettings => ({
shell: config.get("shell"),
loginShell: config.get("loginShell"),
defaultCwd: config.get("defaultCwd"),
timeoutMs: config.get("timeoutMs"),
maxOutputBytes: config.get("maxOutputBytes"),
});
const shellExecTool = tool({
name: "shell_exec",
description: text`
Run a command in the user's local shell and return its stdout, stderr, and exit code.
The command string is passed to the shell with -c, so pipes, redirects, &&, ||, globs,
command substitution, and quoting all work as in a normal terminal.
Each call spawns a fresh shell process — there is no persistent session, so cwd and
environment changes do not carry over between calls. To run multiple steps with shared
state, chain them in one command (e.g. "cd /tmp && ls && cat foo.txt") or pass cwd.
The user's login profile is sourced by default, so PATH and version managers are
available; aliases defined only in interactive rc files (.zshrc, .bashrc) typically are
not. Output is capped per stream — long outputs are truncated with a marker.
`,
parameters: {
command: z.string().min(1).describe(
"Shell command to run, exactly as you would type it at the terminal.",
),
cwd: z.string().optional().describe(
"Absolute path to run the command in. Defaults to the plugin's configured working directory (or the user's home).",
),
timeout_ms: z.number().int().min(100).max(600000).optional().describe(
"Override the default timeout for this call. The command is killed if it runs longer.",
),
env: z.record(z.string()).optional().describe(
"Extra environment variables to set for this command, merged on top of the inherited environment.",
),
},
implementation: async (
{ command, cwd, timeout_ms, env },
{ signal, status },
) => {
status(`Running: ${command.length > 80 ? command.slice(0, 77) + "..." : command}`);
const settings = getSettings();
const result = await runShell(settings, command, {
signal,
cwd,
timeoutMs: timeout_ms,
env,
});
return formatRunResult(result, settings.maxOutputBytes);
},
});
const shellInfoTool = tool({
name: "shell_info",
description: text`
Return information about the shell environment commands will run in: which shell binary
is used, the args it is invoked with, the default working directory, the user, the
hostname, and the platform. Useful as a first call to orient yourself before running
anything destructive.
`,
parameters: {},
implementation: async () => {
return describeEnvironment(getSettings());
},
});
return [shellExecTool, shellInfoTool];
}