src / tools / core.ts
src / tools / core.ts
import { tool, type Tool } from "@lmstudio/sdk";
import { existsSync } from "fs";
import { readdir, readFile, stat } from "fs/promises";
import { join, sep } from "path";
import { z } from "zod";
import { clamp, firstText, formatBytes, type Workspace } from "../workspace";
/**
* Reading and listing -- the only tools that every caller needs, including
* subagents, which is why they live apart from the main provider.
*/
export function coreTools(ws: Workspace): Tool[] {
return [
tool({
name: "list_directory",
description:
"List the files and subdirectories at a path inside the workspace. Use this to explore " +
"before reading files. Paths are relative to the workspace root.",
parameters: {
path: z
.string()
.default(".")
.describe("Directory path relative to the workspace root. Use '.' for the root itself."),
directory: z.string().optional().describe("Alias for path."),
},
implementation: async ({ path, directory }, ctx) => {
const dir = ws.resolveInRoot(firstText(directory, path, "."));
ctx.status(`Listing ${ws.rel(dir)}`);
if (!existsSync(dir)) {
return `Error: "${path}" does not exist. Use directory_tree or find_file to locate it.`;
}
const entries = await readdir(dir, { withFileTypes: true });
if (entries.length === 0) return `"${path}" is empty.`;
const lines: string[] = [];
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
if (entry.isDirectory()) {
lines.push(`${entry.name}${sep}`);
} else {
const info = await stat(join(dir, entry.name));
lines.push(`${entry.name} (${formatBytes(info.size)})`);
}
}
return lines.join("\n");
},
}),
tool({
name: "read_file",
description:
"Read a text file from the workspace, returned with line numbers so you can refer to exact " +
"lines when editing. Always read a file before editing it. For a big file, read a range " +
"with start_line and max_lines instead of the whole thing.",
parameters: {
path: z.string().default("").describe("File path relative to the workspace root."),
file_path: z.string().optional().describe("Alias for path."),
start_line: z
.number()
.int()
.min(1)
.default(1)
.describe("1-based line to start reading from."),
max_lines: z
.number()
.int()
.min(1)
.default(500)
.describe("How many lines to return, starting at start_line."),
},
implementation: async ({ path, file_path, start_line, max_lines }, ctx) => {
const target = firstText(path, file_path);
if (target === "") return "Error: no file path given. Pass path=\"src/thing.ts\".";
const filePath = ws.resolveInRoot(target);
ctx.status(`Reading ${ws.rel(filePath)}`);
if (!existsSync(filePath)) {
return `Error: "${target}" does not exist. Use find_file or glob_files to locate it.`;
}
const info = await stat(filePath);
if (info.isDirectory()) {
return `Error: "${target}" is a directory. Use list_directory instead.`;
}
if (info.size > ws.maxBytes) {
ctx.warn(`${target} is ${formatBytes(info.size)}; reading a window of it.`);
}
const content = await readFile(filePath, "utf-8");
const lines = content.split(/\r?\n/);
const from = Math.min(start_line, lines.length);
const slice = lines.slice(from - 1, from - 1 + max_lines);
const width = String(from + slice.length - 1).length;
const body = slice
.map((line, index) => `${String(from + index).padStart(width, " ")}\t${line}`)
.join("\n");
const shownTo = from + slice.length - 1;
const header = `${ws.rel(filePath)} -- lines ${from}-${shownTo} of ${lines.length} (${formatBytes(info.size)})`;
const footer =
shownTo < lines.length
? `\n\n[${lines.length - shownTo} more line(s). Read on with start_line=${shownTo + 1}.]`
: "";
return clamp(`${header}\n\n${body}${footer}`, ws.maxBytes, "file window");
},
}),
];
}
import { tool, type Tool } from "@lmstudio/sdk";
import { existsSync } from "fs";
import { readdir, readFile, stat } from "fs/promises";
import { join, sep } from "path";
import { z } from "zod";
import { clamp, firstText, formatBytes, type Workspace } from "../workspace";
/**
* Reading and listing -- the only tools that every caller needs, including
* subagents, which is why they live apart from the main provider.
*/
export function coreTools(ws: Workspace): Tool[] {
return [
tool({
name: "list_directory",
description:
"List the files and subdirectories at a path inside the workspace. Use this to explore " +
"before reading files. Paths are relative to the workspace root.",
parameters: {
path: z
.string()
.default(".")
.describe("Directory path relative to the workspace root. Use '.' for the root itself."),
directory: z.string().optional().describe("Alias for path."),
},
implementation: async ({ path, directory }, ctx) => {
const dir = ws.resolveInRoot(firstText(directory, path, "."));
ctx.status(`Listing ${ws.rel(dir)}`);
if (!existsSync(dir)) {
return `Error: "${path}" does not exist. Use directory_tree or find_file to locate it.`;
}
const entries = await readdir(dir, { withFileTypes: true });
if (entries.length === 0) return `"${path}" is empty.`;
const lines: string[] = [];
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
if (entry.isDirectory()) {
lines.push(`${entry.name}${sep}`);
} else {
const info = await stat(join(dir, entry.name));
lines.push(`${entry.name} (${formatBytes(info.size)})`);
}
}
return lines.join("\n");
},
}),
tool({
name: "read_file",
description:
"Read a text file from the workspace, returned with line numbers so you can refer to exact " +
"lines when editing. Always read a file before editing it. For a big file, read a range " +
"with start_line and max_lines instead of the whole thing.",
parameters: {
path: z.string().default("").describe("File path relative to the workspace root."),
file_path: z.string().optional().describe("Alias for path."),
start_line: z
.number()
.int()
.min(1)
.default(1)
.describe("1-based line to start reading from."),
max_lines: z
.number()
.int()
.min(1)
.default(500)
.describe("How many lines to return, starting at start_line."),
},
implementation: async ({ path, file_path, start_line, max_lines }, ctx) => {
const target = firstText(path, file_path);
if (target === "") return "Error: no file path given. Pass path=\"src/thing.ts\".";
const filePath = ws.resolveInRoot(target);
ctx.status(`Reading ${ws.rel(filePath)}`);
if (!existsSync(filePath)) {
return `Error: "${target}" does not exist. Use find_file or glob_files to locate it.`;
}
const info = await stat(filePath);
if (info.isDirectory()) {
return `Error: "${target}" is a directory. Use list_directory instead.`;
}
if (info.size > ws.maxBytes) {
ctx.warn(`${target} is ${formatBytes(info.size)}; reading a window of it.`);
}
const content = await readFile(filePath, "utf-8");
const lines = content.split(/\r?\n/);
const from = Math.min(start_line, lines.length);
const slice = lines.slice(from - 1, from - 1 + max_lines);
const width = String(from + slice.length - 1).length;
const body = slice
.map((line, index) => `${String(from + index).padStart(width, " ")}\t${line}`)
.join("\n");
const shownTo = from + slice.length - 1;
const header = `${ws.rel(filePath)} -- lines ${from}-${shownTo} of ${lines.length} (${formatBytes(info.size)})`;
const footer =
shownTo < lines.length
? `\n\n[${lines.length - shownTo} more line(s). Read on with start_line=${shownTo + 1}.]`
: "";
return clamp(`${header}\n\n${body}${footer}`, ws.maxBytes, "file window");
},
}),
];
}