src / tools.ts
import { exec } from "node:child_process";
import { tool, type Tool, type ToolsProviderController } from "@lmstudio/sdk";
import { z } from "zod";
import { createSubagentTool } from "./subagent";
import { EXAMPLE_PATH, WorkspaceError, getWorkspace, setWorkspace } from "./workspace";
import * as ops from "./fsops";
function err(e: unknown): string {
if (e instanceof WorkspaceError) return e.message;
return `Error: ${e instanceof Error ? e.message : String(e)}`;
}
const RUN_CAP = 6000;
/** The seven core coding tools. Exported so the sub-agent can reuse a filtered subset. */
export function createCoreTools(): Array<Tool> {
const setWorkspaceTool = tool({
name: "set_workspace",
description:
"Set the project working folder. Must be called before any other code tool. " +
`Give an absolute path, e.g. "${EXAMPLE_PATH}" (spaces are fine, no extra quotes).` +
(process.platform === "win32" ? " Use forward slashes." : ""),
parameters: { path: z.string() },
implementation: async ({ path }) => {
try {
return `Workspace set to ${setWorkspace(path)}`;
} catch (e) {
return err(e);
}
},
});
const listDirTool = tool({
name: "list_dir",
description:
'List a directory in the working folder. path is relative to the working folder; omit it or use "." for the root. ' +
"Lines start with D=folder or F=file.",
parameters: { path: z.string().nullish() },
implementation: async ({ path }) => {
try {
return ops.listDir(path ?? ".");
} catch (e) {
return err(e);
}
},
});
const readFileTool = tool({
name: "read_file",
description:
`Read part of a text file (path relative to the working folder) with line numbers. Returns at most ${ops.MAX_READ_LINES} lines; use start to continue.`,
parameters: {
path: z.string(),
start: z.number().int().min(1).nullish(),
lines: z.number().int().min(1).max(ops.MAX_READ_LINES).nullish(),
},
implementation: async ({ path, start, lines }) => {
try {
return ops.readLines(path, start ?? 1, Math.min(lines ?? ops.DEFAULT_READ_LINES, ops.MAX_READ_LINES));
} catch (e) {
return err(e);
}
},
});
const writeFileTool = tool({
name: "write_file",
description: "Create or overwrite a file (path relative to the working folder). Creates missing parent folders.",
parameters: { path: z.string(), content: z.string() },
implementation: async ({ path, content }) => {
try {
return ops.writeFileSafe(path, content);
} catch (e) {
return err(e);
}
},
});
const editFileTool = tool({
name: "edit_file",
description:
"Replace an exact, unique text match in a file (path relative to the working folder). Omit replace (or '') to delete the matched text.",
parameters: { path: z.string(), find: z.string(), replace: z.string().nullish() },
implementation: async ({ path, find, replace }) => {
try {
return ops.editFileSafe(path, find, replace ?? "");
} catch (e) {
return err(e);
}
},
});
const searchTool = tool({
name: "search",
description:
"Search file contents for text in the working folder (case-insensitive). Optional path is a subfolder relative to the working folder. Returns path:line matches.",
parameters: { pattern: z.string().min(1), path: z.string().nullish() },
implementation: async ({ pattern, path }) => {
try {
return ops.searchFiles(pattern, path ?? ".");
} catch (e) {
return err(e);
}
},
});
const runTool = tool({
name: "run",
description: "Run a shell command in the working folder (builds, tests, install). Returns output.",
parameters: { cmd: z.string() },
implementation: async ({ cmd }) => {
let root: string;
try {
root = getWorkspace();
} catch (e) {
return err(e);
}
return await new Promise<string>((resolve) => {
exec(
cmd,
{ cwd: root, timeout: 120_000, maxBuffer: 1_000_000 },
(error, stdout, stderr) => {
const out = `${stdout}\n${stderr}`.trim();
if (!error && !out) resolve("(no output)");
else {
let text = out;
if (text.length > RUN_CAP) {
text = `...[truncated]...\n${text.slice(-RUN_CAP + 20)}`;
}
const code = error ? String(error.code ?? "error") : null;
resolve(code && code !== "0" ? `exit ${code}\n${text}` : text);
}
}
);
});
},
});
return [setWorkspaceTool, listDirTool, readFileTool, writeFileTool, editFileTool, searchTool, runTool];
}
export async function toolsProvider(ctl: ToolsProviderController) {
const core = createCoreTools();
return [...core, createSubagentTool(core, ctl.client)];
}src / tools.ts
import { exec } from "node:child_process";
import { tool, type Tool, type ToolsProviderController } from "@lmstudio/sdk";
import { z } from "zod";
import { createSubagentTool } from "./subagent";
import { EXAMPLE_PATH, WorkspaceError, getWorkspace, setWorkspace } from "./workspace";
import * as ops from "./fsops";
function err(e: unknown): string {
if (e instanceof WorkspaceError) return e.message;
return `Error: ${e instanceof Error ? e.message : String(e)}`;
}
const RUN_CAP = 6000;
/** The seven core coding tools. Exported so the sub-agent can reuse a filtered subset. */
export function createCoreTools(): Array<Tool> {
const setWorkspaceTool = tool({
name: "set_workspace",
description:
"Set the project working folder. Must be called before any other code tool. " +
`Give an absolute path, e.g. "${EXAMPLE_PATH}" (spaces are fine, no extra quotes).` +
(process.platform === "win32" ? " Use forward slashes." : ""),
parameters: { path: z.string() },
implementation: async ({ path }) => {
try {
return `Workspace set to ${setWorkspace(path)}`;
} catch (e) {
return err(e);
}
},
});
const listDirTool = tool({
name: "list_dir",
description:
'List a directory in the working folder. path is relative to the working folder; omit it or use "." for the root. ' +
"Lines start with D=folder or F=file.",
parameters: { path: z.string().nullish() },
implementation: async ({ path }) => {
try {
return ops.listDir(path ?? ".");
} catch (e) {
return err(e);
}
},
});
const readFileTool = tool({
name: "read_file",
description:
`Read part of a text file (path relative to the working folder) with line numbers. Returns at most ${ops.MAX_READ_LINES} lines; use start to continue.`,
parameters: {
path: z.string(),
start: z.number().int().min(1).nullish(),
lines: z.number().int().min(1).max(ops.MAX_READ_LINES).nullish(),
},
implementation: async ({ path, start, lines }) => {
try {
return ops.readLines(path, start ?? 1, Math.min(lines ?? ops.DEFAULT_READ_LINES, ops.MAX_READ_LINES));
} catch (e) {
return err(e);
}
},
});
const writeFileTool = tool({
name: "write_file",
description: "Create or overwrite a file (path relative to the working folder). Creates missing parent folders.",
parameters: { path: z.string(), content: z.string() },
implementation: async ({ path, content }) => {
try {
return ops.writeFileSafe(path, content);
} catch (e) {
return err(e);
}
},
});
const editFileTool = tool({
name: "edit_file",
description:
"Replace an exact, unique text match in a file (path relative to the working folder). Omit replace (or '') to delete the matched text.",
parameters: { path: z.string(), find: z.string(), replace: z.string().nullish() },
implementation: async ({ path, find, replace }) => {
try {
return ops.editFileSafe(path, find, replace ?? "");
} catch (e) {
return err(e);
}
},
});
const searchTool = tool({
name: "search",
description:
"Search file contents for text in the working folder (case-insensitive). Optional path is a subfolder relative to the working folder. Returns path:line matches.",
parameters: { pattern: z.string().min(1), path: z.string().nullish() },
implementation: async ({ pattern, path }) => {
try {
return ops.searchFiles(pattern, path ?? ".");
} catch (e) {
return err(e);
}
},
});
const runTool = tool({
name: "run",
description: "Run a shell command in the working folder (builds, tests, install). Returns output.",
parameters: { cmd: z.string() },
implementation: async ({ cmd }) => {
let root: string;
try {
root = getWorkspace();
} catch (e) {
return err(e);
}
return await new Promise<string>((resolve) => {
exec(
cmd,
{ cwd: root, timeout: 120_000, maxBuffer: 1_000_000 },
(error, stdout, stderr) => {
const out = `${stdout}\n${stderr}`.trim();
if (!error && !out) resolve("(no output)");
else {
let text = out;
if (text.length > RUN_CAP) {
text = `...[truncated]...\n${text.slice(-RUN_CAP + 20)}`;
}
const code = error ? String(error.code ?? "error") : null;
resolve(code && code !== "0" ? `exit ${code}\n${text}` : text);
}
}
);
});
},
});
return [setWorkspaceTool, listDirTool, readFileTool, writeFileTool, editFileTool, searchTool, runTool];
}
export async function toolsProvider(ctl: ToolsProviderController) {
const core = createCoreTools();
return [...core, createSubagentTool(core, ctl.client)];
}