src / tools / project.ts
src / tools / project.ts
import { tool, type Tool } from "@lmstudio/sdk";
import { existsSync } from "fs";
import { open, readdir, stat } from "fs/promises";
import { extname, join } from "path";
import { z } from "zod";
import { clamp, formatBytes, IGNORED_DIRS, type Workspace } from "../workspace";
const MANIFEST_HEAD_BYTES = 64 * 1024;
const OVERVIEW_CHARS = 6000;
const TREE_CHARS = 6000;
const INSTRUCTIONS_CHARS = 8000;
const SURVEY_MAX_FILES = 5000;
const SURVEY_MAX_DIRS = 4000;
const SURVEY_MAX_DEPTH = 6;
const TOP_LEVEL_SHOWN = 40;
const PER_DIR_SHOWN = 60;
const LINE_SCAN_CAP = 32 * 1024 * 1024;
/** Instruction files, in the order a coding agent should prefer them. */
const INSTRUCTION_FILES = [
"AGENTS.md",
"CLAUDE.md",
".cursorrules",
"CONTRIBUTING.md",
"README.md",
];
/** Manifests worth a presence note even though nothing here parses them. */
const EXTRA_MANIFESTS = [
"build.gradle",
"build.gradle.kts",
"composer.json",
"Dockerfile",
"docker-compose.yml",
"docker-compose.yaml",
"deno.json",
"flake.nix",
];
interface HeadRead {
text: string;
size: number;
truncated: boolean;
}
/**
* Reads at most `maxBytes` from the front of a file. Every manifest parser here
* goes through this so a pathological 40 MB package.json cannot be slurped into
* memory just to find a name field.
*/
async function readHead(absPath: string, maxBytes: number): Promise<HeadRead> {
const info = await stat(absPath);
const want = Math.min(info.size, maxBytes);
if (want === 0) return { text: "", size: info.size, truncated: false };
const handle = await open(absPath, "r");
try {
const buffer = Buffer.alloc(want);
await handle.read(buffer, 0, want, 0);
return { text: buffer.toString("utf-8"), size: info.size, truncated: info.size > want };
} finally {
await handle.close();
}
}
async function readManifest(absPath: string): Promise<string> {
const head = await readHead(absPath, MANIFEST_HEAD_BYTES);
return head.text;
}
function fileExists(root: string, name: string): boolean {
return existsSync(join(root, name));
}
function short(value: string, max: number): string {
const flat = value.replace(/\s+/g, " ").trim();
return flat.length <= max ? flat : `${flat.slice(0, max - 1)}…`;
}
function plural(count: number, one: string, many: string): string {
return count === 1 ? one : many;
}
interface StackReport {
/** Short label used in the "Detected stack" line. */
label: string;
lines: string[];
/** Concrete command lines the model can hand to run_command. */
commands: string[];
}
async function detectNode(root: string): Promise<StackReport | undefined> {
const manifestPath = join(root, "package.json");
if (!existsSync(manifestPath)) return undefined;
const lines: string[] = [];
const commands: string[] = [];
let parsed: Record<string, unknown> = {};
try {
parsed = JSON.parse(await readManifest(manifestPath)) as Record<string, unknown>;
} catch {
lines.push("package.json is present but is not valid JSON.");
return { label: "Node", lines, commands };
}
const name = typeof parsed.name === "string" ? parsed.name : "(unnamed)";
const version = typeof parsed.version === "string" ? ` v${parsed.version}` : "";
const type = typeof parsed.type === "string" ? parsed.type : "commonjs";
lines.push(`package.json: ${name}${version} (module type: ${type})`);
const runner = fileExists(root, "pnpm-lock.yaml")
? "pnpm"
: fileExists(root, "yarn.lock")
? "yarn"
: fileExists(root, "bun.lockb")
? "bun"
: "npm";
lines.push(`Package manager (from lockfile): ${runner}`);
const scripts = parsed.scripts;
if (typeof scripts === "object" && scripts !== null) {
const entries = Object.entries(scripts as Record<string, unknown>).filter(
([, value]) => typeof value === "string",
);
if (entries.length === 0) {
lines.push("No npm scripts defined.");
} else {
const shown = entries.slice(0, 12);
lines.push(`Scripts (${entries.length}):`);
for (const [key, value] of shown) {
lines.push(` ${key} -> ${short(String(value), 70)}`);
commands.push(`${runner} run ${key}`);
}
if (entries.length > shown.length) {
lines.push(` ... ${entries.length - shown.length} more script(s) not shown`);
}
}
}
const deps = Object.keys((parsed.dependencies as Record<string, unknown>) ?? {});
const devDeps = Object.keys((parsed.devDependencies as Record<string, unknown>) ?? {});
const all = [...deps, ...devDeps];
if (all.length > 0) {
lines.push(
`Dependencies: ${deps.length} runtime, ${devDeps.length} dev. ` +
`Notable: ${all.slice(0, 15).join(", ")}${all.length > 15 ? ", ..." : ""}`,
);
}
const isTs = fileExists(root, "tsconfig.json") || all.includes("typescript");
return { label: isTs ? "Node/TypeScript" : "Node/JavaScript", lines, commands };
}
async function detectRust(root: string): Promise<StackReport | undefined> {
const manifestPath = join(root, "Cargo.toml");
if (!existsSync(manifestPath)) return undefined;
const text = await readManifest(manifestPath);
const lines: string[] = [];
const name = /^\s*name\s*=\s*"([^"]+)"/m.exec(text);
const version = /^\s*version\s*=\s*"([^"]+)"/m.exec(text);
const edition = /^\s*edition\s*=\s*"([^"]+)"/m.exec(text);
lines.push(
`Cargo.toml: ${name === null ? "(name not found)" : name[1]}` +
`${version === null ? "" : ` v${version[1]}`}` +
`${edition === null ? "" : ` (edition ${edition[1]})`}`,
);
if (/^\s*\[workspace\]/m.test(text)) lines.push("This is a Cargo workspace.");
return {
label: "Rust",
lines,
commands: ["cargo build", "cargo test", "cargo run"],
};
}
async function detectPython(root: string): Promise<StackReport | undefined> {
const hasPyproject = fileExists(root, "pyproject.toml");
const hasRequirements = fileExists(root, "requirements.txt");
if (!hasPyproject && !hasRequirements) return undefined;
const lines: string[] = [];
const commands: string[] = [];
if (hasPyproject) {
const text = await readManifest(join(root, "pyproject.toml"));
const name = /^\s*name\s*=\s*"([^"]+)"/m.exec(text);
const backend = /^\s*\[tool\.([A-Za-z0-9_]+)\]/m.exec(text);
lines.push(
`pyproject.toml: ${name === null ? "(name not found)" : name[1]}` +
`${backend === null ? "" : ` (tooling: ${backend[1]})`}`,
);
if (/pytest/.test(text)) commands.push("python -m pytest");
}
if (hasRequirements) {
const text = await readManifest(join(root, "requirements.txt"));
const packages = text
.split(/\r?\n/)
.map((line) => line.trim())
.filter((line) => line !== "" && !line.startsWith("#"))
.map((line) => line.split(/[=<>!~\[ ]/)[0]);
lines.push(
`requirements.txt: ${packages.length} ${plural(packages.length, "package", "packages")}` +
`${packages.length === 0 ? "" : ` (${packages.slice(0, 10).join(", ")}${packages.length > 10 ? ", ..." : ""})`}`,
);
commands.push("pip install -r requirements.txt");
}
return { label: "Python", lines, commands };
}
async function detectGo(root: string): Promise<StackReport | undefined> {
const manifestPath = join(root, "go.mod");
if (!existsSync(manifestPath)) return undefined;
const text = await readManifest(manifestPath);
const module = /^\s*module\s+(\S+)/m.exec(text);
const version = /^\s*go\s+(\S+)/m.exec(text);
return {
label: "Go",
lines: [
`go.mod: module ${module === null ? "(not found)" : module[1]}` +
`${version === null ? "" : ` (go ${version[1]})`}`,
],
commands: ["go build ./...", "go test ./..."],
};
}
async function detectMaven(root: string): Promise<StackReport | undefined> {
const manifestPath = join(root, "pom.xml");
if (!existsSync(manifestPath)) return undefined;
const text = await readManifest(manifestPath);
const artifact = /<artifactId>([^<]+)<\/artifactId>/.exec(text);
const group = /<groupId>([^<]+)<\/groupId>/.exec(text);
return {
label: "Java/Maven",
lines: [
`pom.xml: ${group === null ? "" : `${group[1]}:`}` +
`${artifact === null ? "(artifactId not found)" : artifact[1]}`,
],
commands: ["mvn -q compile", "mvn -q test"],
};
}
async function detectMake(root: string): Promise<StackReport | undefined> {
const name = ["Makefile", "makefile", "GNUmakefile"].find((candidate) =>
fileExists(root, candidate),
);
if (name === undefined) return undefined;
const text = await readManifest(join(root, name));
const targets: string[] = [];
const pattern = /^([A-Za-z0-9_][A-Za-z0-9_.\-/]*)\s*:(?!=)/gm;
let match = pattern.exec(text);
while (match !== null && targets.length < 20) {
if (!targets.includes(match[1])) targets.push(match[1]);
match = pattern.exec(text);
}
return {
label: "Make",
lines: [
`${name}: ${targets.length === 0 ? "no targets parsed" : `targets ${targets.join(", ")}`}`,
],
commands: targets.slice(0, 8).map((target) => `make ${target}`),
};
}
async function detectCMake(root: string): Promise<StackReport | undefined> {
if (!fileExists(root, "CMakeLists.txt")) return undefined;
const text = await readManifest(join(root, "CMakeLists.txt"));
const project = /project\s*\(\s*([A-Za-z0-9_.\-]+)/i.exec(text);
return {
label: "CMake",
lines: [`CMakeLists.txt: project ${project === null ? "(name not found)" : project[1]}`],
commands: ["cmake -S . -B build", "cmake --build build"],
};
}
async function detectRuby(root: string): Promise<StackReport | undefined> {
if (!fileExists(root, "Gemfile")) return undefined;
const text = await readManifest(join(root, "Gemfile"));
const gems = [...text.matchAll(/^\s*gem\s+["']([^"']+)["']/gm)].map((m) => m[1]);
return {
label: "Ruby",
lines: [
`Gemfile: ${gems.length} ${plural(gems.length, "gem", "gems")}` +
`${gems.length === 0 ? "" : ` (${gems.slice(0, 10).join(", ")}${gems.length > 10 ? ", ..." : ""})`}`,
],
commands: ["bundle install", "bundle exec rake"],
};
}
function detectDotnet(topLevelFiles: string[]): StackReport | undefined {
const projects = topLevelFiles.filter((name) => /\.(csproj|fsproj|vbproj|sln)$/i.test(name));
if (projects.length === 0) return undefined;
return {
label: ".NET",
lines: [`Project files: ${projects.slice(0, 10).join(", ")}`],
commands: ["dotnet build", "dotnet test"],
};
}
/** Reads the branch straight out of .git rather than shelling out, so this works with shell access off. */
async function describeGit(root: string): Promise<string> {
const gitPath = join(root, ".git");
if (!existsSync(gitPath)) return "Not a git repository (no .git here).";
const info = await stat(gitPath);
if (!info.isDirectory()) {
return "Git repository (linked worktree or submodule; branch not read).";
}
const headPath = join(gitPath, "HEAD");
if (!existsSync(headPath)) return "Git repository (no HEAD file).";
const head = (await readHead(headPath, 4096)).text.trim();
const ref = /^ref:\s*refs\/heads\/(.+)$/.exec(head);
if (ref !== null) return `Git repository, on branch ${ref[1]}.`;
return `Git repository, detached HEAD at ${head.slice(0, 8)}.`;
}
interface Survey {
files: number;
dirs: number;
byExt: Map<string, number>;
capped: boolean;
}
/**
* Counts files by extension with an explicit stack and hard caps, so an
* enormous or symlink-looped tree can never hang the plugin. Dirents report
* symlinks as symlinks, so nothing here follows one.
*/
async function surveyFiles(rootDir: string): Promise<Survey> {
const byExt = new Map<string, number>();
const stack: Array<{ dir: string; depth: number }> = [{ dir: rootDir, depth: 0 }];
let files = 0;
let dirs = 0;
let capped = false;
while (stack.length > 0) {
const current = stack.pop();
if (current === undefined) break;
if (files >= SURVEY_MAX_FILES) {
capped = true;
break;
}
let entries;
try {
entries = await readdir(current.dir, { withFileTypes: true });
} catch {
continue;
}
for (const entry of entries) {
if (entry.isDirectory()) {
if (IGNORED_DIRS.has(entry.name)) continue;
dirs++;
if (current.depth + 1 <= SURVEY_MAX_DEPTH) {
stack.push({ dir: join(current.dir, entry.name), depth: current.depth + 1 });
} else {
capped = true;
}
continue;
}
if (!entry.isFile()) continue;
files++;
if (files >= SURVEY_MAX_FILES) {
capped = true;
break;
}
const ext = extname(entry.name).toLowerCase();
const key = ext === "" ? "(no extension)" : ext;
byExt.set(key, (byExt.get(key) ?? 0) + 1);
}
}
return { files, dirs, byExt, capped };
}
async function countChildren(dir: string): Promise<number> {
try {
return (await readdir(dir)).length;
} catch {
return 0;
}
}
interface TreeResult {
lines: string[];
budgetExhausted: boolean;
}
async function buildTree(startDir: string, maxDepth: number, limit: number): Promise<TreeResult> {
const lines: string[] = [];
let budget = limit;
const walk = async (dir: string, prefix: string, depth: number): Promise<void> => {
let entries;
try {
entries = await readdir(dir, { withFileTypes: true });
} catch {
lines.push(`${prefix}[unreadable directory]`);
return;
}
const dirsFirst = [
...entries.filter((e) => e.isDirectory()).sort((a, b) => a.name.localeCompare(b.name)),
...entries.filter((e) => !e.isDirectory()).sort((a, b) => a.name.localeCompare(b.name)),
];
const shown = dirsFirst.slice(0, PER_DIR_SHOWN);
let printed = 0;
for (const entry of shown) {
if (budget <= 0) break;
const full = join(dir, entry.name);
if (entry.isDirectory()) {
if (IGNORED_DIRS.has(entry.name)) {
lines.push(`${prefix}${entry.name}/ [not walked]`);
budget--;
printed++;
continue;
}
if (depth >= maxDepth) {
const childCount = await countChildren(full);
lines.push(
`${prefix}${entry.name}/ [${childCount} ${plural(childCount, "entry", "entries")}, depth limit]`,
);
budget--;
printed++;
continue;
}
lines.push(`${prefix}${entry.name}/`);
budget--;
printed++;
await walk(full, `${prefix} `, depth + 1);
continue;
}
lines.push(`${prefix}${entry.name}`);
budget--;
printed++;
}
const elided = dirsFirst.length - printed;
if (elided > 0) {
lines.push(`${prefix}... ${elided} more ${plural(elided, "entry", "entries")} not shown`);
}
};
await walk(startDir, "", 0);
return { lines, budgetExhausted: budget <= 0 };
}
interface LineCount {
lines: number;
size: number;
scannedBytes: number;
binary: boolean;
truncated: boolean;
}
/** Counts newlines in fixed-size chunks so a huge file never lands in memory whole. */
async function countFileLines(absPath: string): Promise<LineCount> {
const info = await stat(absPath);
const handle = await open(absPath, "r");
try {
const chunk = Buffer.alloc(64 * 1024);
let position = 0;
let lines = 0;
let binary = false;
let lastByte = 0;
const cap = Math.min(info.size, LINE_SCAN_CAP);
while (position < cap) {
const { bytesRead } = await handle.read(chunk, 0, Math.min(chunk.length, cap - position), position);
if (bytesRead === 0) break;
for (let i = 0; i < bytesRead; i++) {
const byte = chunk[i];
if (byte === 0x0a) lines++;
else if (byte === 0) binary = true;
}
lastByte = chunk[bytesRead - 1];
position += bytesRead;
}
if (position > 0 && lastByte !== 0x0a) lines++;
return {
lines,
size: info.size,
scannedBytes: position,
binary,
truncated: info.size > cap,
};
} finally {
await handle.close();
}
}
export function projectTools(ws: Workspace): Tool[] {
const tools: Tool[] = [];
tools.push(
tool({
name: "project_overview",
description:
"CALL THIS FIRST in an unfamiliar workspace. Returns one dense orientation report: the " +
"workspace root, the detected stack (package.json, Cargo.toml, pyproject.toml, go.mod, " +
"pom.xml, Makefile, CMakeLists.txt, Gemfile, .csproj), the commands you can actually run, " +
"whether it is a git repo and which branch, the top-level layout, and a file count by " +
"extension. Read-only and safe to call more than once.",
parameters: {},
implementation: async (_params, ctx) => {
try {
ctx.status("Reading project overview");
const root = ws.root;
const sections: string[] = [`Workspace root: ${root}`];
let topEntries;
try {
topEntries = await readdir(root, { withFileTypes: true });
} catch (caught) {
return `Error: cannot read the workspace root ${root} (${(caught as Error).message}). Check the plugin's rootDirectory setting.`;
}
const topDirs = topEntries
.filter((e) => e.isDirectory())
.map((e) => e.name)
.sort((a, b) => a.localeCompare(b));
const topFiles = topEntries
.filter((e) => !e.isDirectory())
.map((e) => e.name)
.sort((a, b) => a.localeCompare(b));
const reports = (
await Promise.all([
detectNode(root),
detectRust(root),
detectPython(root),
detectGo(root),
detectMaven(root),
detectMake(root),
detectCMake(root),
detectRuby(root),
])
).filter((report): report is StackReport => report !== undefined);
const dotnet = detectDotnet(topFiles);
if (dotnet !== undefined) reports.push(dotnet);
sections.push(
reports.length === 0
? "Detected stack: none recognised (no known manifest at the top level)."
: `Detected stack: ${reports.map((r) => r.label).join(", ")}`,
);
for (const report of reports) sections.push(report.lines.join("\n"));
const commands = reports.flatMap((report) => report.commands).slice(0, 16);
sections.push(
commands.length === 0
? "Runnable targets: none found. Look for a CI config or the README before guessing."
: `Runnable targets (pass one to run_command):\n${commands.map((c) => ` ${c}`).join("\n")}`,
);
const extras = EXTRA_MANIFESTS.filter((name) => topFiles.includes(name));
if (extras.length > 0) {
sections.push(`Other manifests present (not parsed): ${extras.join(", ")}`);
}
sections.push(await describeGit(root));
const layout: string[] = ["Top-level layout:"];
for (const name of topDirs.slice(0, TOP_LEVEL_SHOWN)) {
layout.push(` ${name}/${IGNORED_DIRS.has(name) ? " [not walked]" : ""}`);
}
for (const name of topFiles.slice(0, TOP_LEVEL_SHOWN)) {
layout.push(` ${name}`);
}
const hiddenCount =
Math.max(0, topDirs.length - TOP_LEVEL_SHOWN) +
Math.max(0, topFiles.length - TOP_LEVEL_SHOWN);
if (hiddenCount > 0) {
layout.push(` ... ${hiddenCount} more top-level ${plural(hiddenCount, "entry", "entries")} not shown`);
}
sections.push(layout.join("\n"));
ctx.status("Counting files by extension");
const survey = await surveyFiles(root);
const ranked = [...survey.byExt.entries()].sort((a, b) => b[1] - a[1]).slice(0, 10);
const census: string[] = [
`Files counted: ${survey.files} across ${survey.dirs} ${plural(survey.dirs, "directory", "directories")}` +
`${survey.capped ? ` (walk stopped at ${SURVEY_MAX_FILES} files / depth ${SURVEY_MAX_DEPTH}; real totals are higher)` : ""}`,
];
if (ranked.length > 0) {
census.push("Top extensions:");
for (const [ext, count] of ranked) census.push(` ${ext} ${count}`);
}
sections.push(census.join("\n"));
sections.push(
"Next: read_project_instructions for the house rules, then directory_tree to see the layout.",
);
return clamp(sections.join("\n\n"), OVERVIEW_CHARS, "overview");
} catch (caught) {
return `Error: could not build the project overview (${(caught as Error).message}). Try list_directory on '.' instead.`;
}
},
}),
);
tools.push(
tool({
name: "directory_tree",
description:
"Show the folder structure as an indented tree, skipping noise like node_modules, .git " +
"and build output. Use it after project_overview to find where the source lives. Says " +
"how many entries were left out at each cut, so you know when to look deeper with a " +
"specific path.",
parameters: {
path: z
.string()
.default(".")
.describe("Directory to start from, relative to the workspace root. Use '.' for the root."),
max_depth: z
.number()
.int()
.min(1)
.max(6)
.default(3)
.describe("How many levels deep to descend. 3 is usually enough."),
limit: z
.number()
.int()
.min(10)
.max(500)
.default(200)
.describe("Maximum number of entries to print before stopping."),
},
implementation: async ({ path, max_depth, limit }, ctx) => {
const start = ws.resolveInRoot(path);
try {
ctx.status(`Mapping ${ws.rel(start)}`);
if (!existsSync(start)) {
return `Error: "${path}" does not exist. Call project_overview or directory_tree('.') to see what is there.`;
}
const info = await stat(start);
if (!info.isDirectory()) {
return `Error: "${path}" is a file, not a directory. Use count_lines or read_file on it instead.`;
}
const { lines, budgetExhausted } = await buildTree(start, max_depth, limit);
const header = `${ws.rel(start)}/`;
const footer = budgetExhausted
? `\n\n[stopped at the ${limit}-entry limit; call directory_tree again on a subfolder for the rest]`
: "";
return clamp(`${header}\n${lines.join("\n")}${footer}`, TREE_CHARS, "tree");
} catch (caught) {
return `Error: could not walk "${path}" (${(caught as Error).message}). Try list_directory on it instead.`;
}
},
}),
);
tools.push(
tool({
name: "read_project_instructions",
description:
"Read this project's house rules before writing any code. Looks for AGENTS.md, CLAUDE.md, " +
".cursorrules, CONTRIBUTING.md, then README.md, and returns the first one it finds along " +
"with its name. Long files are truncated. Read-only and safe to call more than once.",
parameters: {},
implementation: async (_params, ctx) => {
try {
ctx.status("Looking for project instructions");
const present = INSTRUCTION_FILES.filter((name) => existsSync(join(ws.root, name)));
if (present.length === 0) {
return (
"No instruction file found (looked for " +
`${INSTRUCTION_FILES.join(", ")}). There are no written house rules; ` +
"match the style of the existing code instead."
);
}
const chosen = present[0];
const head = await readHead(join(ws.root, chosen), ws.maxBytes);
const others = present.slice(1);
const alsoLine =
others.length === 0
? ""
: `\nAlso present (not shown): ${others.join(", ")} -- read one with read_file if you need it.`;
const sizeNote = head.truncated
? `\n[only the first ${formatBytes(ws.maxBytes)} of ${formatBytes(head.size)} was read]`
: "";
return clamp(
`Instructions from ${chosen} (${formatBytes(head.size)}):${alsoLine}${sizeNote}\n\n${head.text}`,
INSTRUCTIONS_CHARS,
chosen,
);
} catch (caught) {
return `Error: could not read the project instructions (${(caught as Error).message}). Try read_file on README.md.`;
}
},
}),
);
tools.push(
tool({
name: "count_lines",
description:
"Report the line count and size of one file WITHOUT returning its contents. Call this " +
"before reading an unfamiliar file so you can decide whether to read it whole or in " +
"ranges, and to avoid dumping a huge or binary file into the conversation.",
parameters: {
path: z.string().describe("File path relative to the workspace root."),
},
implementation: async ({ path }, ctx) => {
const filePath = ws.resolveInRoot(path);
try {
ctx.status(`Measuring ${ws.rel(filePath)}`);
if (!existsSync(filePath)) {
return `Error: "${path}" does not exist. Use directory_tree to find the right path.`;
}
const info = await stat(filePath);
if (info.isDirectory()) {
return `Error: "${path}" is a directory. Use directory_tree on it instead.`;
}
if (!info.isFile()) {
return `Error: "${path}" is not a regular file.`;
}
const counted = await countFileLines(filePath);
if (counted.binary) {
return (
`${ws.rel(filePath)}: ${formatBytes(counted.size)}, looks BINARY (contains null bytes). ` +
"Do not read it as text."
);
}
const parts = [
`${ws.rel(filePath)}: ${counted.lines} ${plural(counted.lines, "line", "lines")}, ${formatBytes(counted.size)}.`,
counted.truncated
? `Only the first ${formatBytes(counted.scannedBytes)} was scanned, so the real line count is higher.`
: "",
counted.size > ws.maxBytes
? `Bigger than the ${ws.maxFileSizeKb} KB read limit -- read_file will truncate it.`
: counted.lines > 400
? "Long file: search it for the part you need rather than reading it all."
: "Small enough to read in full.",
].filter((part) => part !== "");
return parts.join(" ");
} catch (caught) {
return `Error: could not measure "${path}" (${(caught as Error).message}). Check the path with directory_tree.`;
}
},
}),
);
return tools;
}
import { tool, type Tool } from "@lmstudio/sdk";
import { existsSync } from "fs";
import { open, readdir, stat } from "fs/promises";
import { extname, join } from "path";
import { z } from "zod";
import { clamp, formatBytes, IGNORED_DIRS, type Workspace } from "../workspace";
const MANIFEST_HEAD_BYTES = 64 * 1024;
const OVERVIEW_CHARS = 6000;
const TREE_CHARS = 6000;
const INSTRUCTIONS_CHARS = 8000;
const SURVEY_MAX_FILES = 5000;
const SURVEY_MAX_DIRS = 4000;
const SURVEY_MAX_DEPTH = 6;
const TOP_LEVEL_SHOWN = 40;
const PER_DIR_SHOWN = 60;
const LINE_SCAN_CAP = 32 * 1024 * 1024;
/** Instruction files, in the order a coding agent should prefer them. */
const INSTRUCTION_FILES = [
"AGENTS.md",
"CLAUDE.md",
".cursorrules",
"CONTRIBUTING.md",
"README.md",
];
/** Manifests worth a presence note even though nothing here parses them. */
const EXTRA_MANIFESTS = [
"build.gradle",
"build.gradle.kts",
"composer.json",
"Dockerfile",
"docker-compose.yml",
"docker-compose.yaml",
"deno.json",
"flake.nix",
];
interface HeadRead {
text: string;
size: number;
truncated: boolean;
}
/**
* Reads at most `maxBytes` from the front of a file. Every manifest parser here
* goes through this so a pathological 40 MB package.json cannot be slurped into
* memory just to find a name field.
*/
async function readHead(absPath: string, maxBytes: number): Promise<HeadRead> {
const info = await stat(absPath);
const want = Math.min(info.size, maxBytes);
if (want === 0) return { text: "", size: info.size, truncated: false };
const handle = await open(absPath, "r");
try {
const buffer = Buffer.alloc(want);
await handle.read(buffer, 0, want, 0);
return { text: buffer.toString("utf-8"), size: info.size, truncated: info.size > want };
} finally {
await handle.close();
}
}
async function readManifest(absPath: string): Promise<string> {
const head = await readHead(absPath, MANIFEST_HEAD_BYTES);
return head.text;
}
function fileExists(root: string, name: string): boolean {
return existsSync(join(root, name));
}
function short(value: string, max: number): string {
const flat = value.replace(/\s+/g, " ").trim();
return flat.length <= max ? flat : `${flat.slice(0, max - 1)}…`;
}
function plural(count: number, one: string, many: string): string {
return count === 1 ? one : many;
}
interface StackReport {
/** Short label used in the "Detected stack" line. */
label: string;
lines: string[];
/** Concrete command lines the model can hand to run_command. */
commands: string[];
}
async function detectNode(root: string): Promise<StackReport | undefined> {
const manifestPath = join(root, "package.json");
if (!existsSync(manifestPath)) return undefined;
const lines: string[] = [];
const commands: string[] = [];
let parsed: Record<string, unknown> = {};
try {
parsed = JSON.parse(await readManifest(manifestPath)) as Record<string, unknown>;
} catch {
lines.push("package.json is present but is not valid JSON.");
return { label: "Node", lines, commands };
}
const name = typeof parsed.name === "string" ? parsed.name : "(unnamed)";
const version = typeof parsed.version === "string" ? ` v${parsed.version}` : "";
const type = typeof parsed.type === "string" ? parsed.type : "commonjs";
lines.push(`package.json: ${name}${version} (module type: ${type})`);
const runner = fileExists(root, "pnpm-lock.yaml")
? "pnpm"
: fileExists(root, "yarn.lock")
? "yarn"
: fileExists(root, "bun.lockb")
? "bun"
: "npm";
lines.push(`Package manager (from lockfile): ${runner}`);
const scripts = parsed.scripts;
if (typeof scripts === "object" && scripts !== null) {
const entries = Object.entries(scripts as Record<string, unknown>).filter(
([, value]) => typeof value === "string",
);
if (entries.length === 0) {
lines.push("No npm scripts defined.");
} else {
const shown = entries.slice(0, 12);
lines.push(`Scripts (${entries.length}):`);
for (const [key, value] of shown) {
lines.push(` ${key} -> ${short(String(value), 70)}`);
commands.push(`${runner} run ${key}`);
}
if (entries.length > shown.length) {
lines.push(` ... ${entries.length - shown.length} more script(s) not shown`);
}
}
}
const deps = Object.keys((parsed.dependencies as Record<string, unknown>) ?? {});
const devDeps = Object.keys((parsed.devDependencies as Record<string, unknown>) ?? {});
const all = [...deps, ...devDeps];
if (all.length > 0) {
lines.push(
`Dependencies: ${deps.length} runtime, ${devDeps.length} dev. ` +
`Notable: ${all.slice(0, 15).join(", ")}${all.length > 15 ? ", ..." : ""}`,
);
}
const isTs = fileExists(root, "tsconfig.json") || all.includes("typescript");
return { label: isTs ? "Node/TypeScript" : "Node/JavaScript", lines, commands };
}
async function detectRust(root: string): Promise<StackReport | undefined> {
const manifestPath = join(root, "Cargo.toml");
if (!existsSync(manifestPath)) return undefined;
const text = await readManifest(manifestPath);
const lines: string[] = [];
const name = /^\s*name\s*=\s*"([^"]+)"/m.exec(text);
const version = /^\s*version\s*=\s*"([^"]+)"/m.exec(text);
const edition = /^\s*edition\s*=\s*"([^"]+)"/m.exec(text);
lines.push(
`Cargo.toml: ${name === null ? "(name not found)" : name[1]}` +
`${version === null ? "" : ` v${version[1]}`}` +
`${edition === null ? "" : ` (edition ${edition[1]})`}`,
);
if (/^\s*\[workspace\]/m.test(text)) lines.push("This is a Cargo workspace.");
return {
label: "Rust",
lines,
commands: ["cargo build", "cargo test", "cargo run"],
};
}
async function detectPython(root: string): Promise<StackReport | undefined> {
const hasPyproject = fileExists(root, "pyproject.toml");
const hasRequirements = fileExists(root, "requirements.txt");
if (!hasPyproject && !hasRequirements) return undefined;
const lines: string[] = [];
const commands: string[] = [];
if (hasPyproject) {
const text = await readManifest(join(root, "pyproject.toml"));
const name = /^\s*name\s*=\s*"([^"]+)"/m.exec(text);
const backend = /^\s*\[tool\.([A-Za-z0-9_]+)\]/m.exec(text);
lines.push(
`pyproject.toml: ${name === null ? "(name not found)" : name[1]}` +
`${backend === null ? "" : ` (tooling: ${backend[1]})`}`,
);
if (/pytest/.test(text)) commands.push("python -m pytest");
}
if (hasRequirements) {
const text = await readManifest(join(root, "requirements.txt"));
const packages = text
.split(/\r?\n/)
.map((line) => line.trim())
.filter((line) => line !== "" && !line.startsWith("#"))
.map((line) => line.split(/[=<>!~\[ ]/)[0]);
lines.push(
`requirements.txt: ${packages.length} ${plural(packages.length, "package", "packages")}` +
`${packages.length === 0 ? "" : ` (${packages.slice(0, 10).join(", ")}${packages.length > 10 ? ", ..." : ""})`}`,
);
commands.push("pip install -r requirements.txt");
}
return { label: "Python", lines, commands };
}
async function detectGo(root: string): Promise<StackReport | undefined> {
const manifestPath = join(root, "go.mod");
if (!existsSync(manifestPath)) return undefined;
const text = await readManifest(manifestPath);
const module = /^\s*module\s+(\S+)/m.exec(text);
const version = /^\s*go\s+(\S+)/m.exec(text);
return {
label: "Go",
lines: [
`go.mod: module ${module === null ? "(not found)" : module[1]}` +
`${version === null ? "" : ` (go ${version[1]})`}`,
],
commands: ["go build ./...", "go test ./..."],
};
}
async function detectMaven(root: string): Promise<StackReport | undefined> {
const manifestPath = join(root, "pom.xml");
if (!existsSync(manifestPath)) return undefined;
const text = await readManifest(manifestPath);
const artifact = /<artifactId>([^<]+)<\/artifactId>/.exec(text);
const group = /<groupId>([^<]+)<\/groupId>/.exec(text);
return {
label: "Java/Maven",
lines: [
`pom.xml: ${group === null ? "" : `${group[1]}:`}` +
`${artifact === null ? "(artifactId not found)" : artifact[1]}`,
],
commands: ["mvn -q compile", "mvn -q test"],
};
}
async function detectMake(root: string): Promise<StackReport | undefined> {
const name = ["Makefile", "makefile", "GNUmakefile"].find((candidate) =>
fileExists(root, candidate),
);
if (name === undefined) return undefined;
const text = await readManifest(join(root, name));
const targets: string[] = [];
const pattern = /^([A-Za-z0-9_][A-Za-z0-9_.\-/]*)\s*:(?!=)/gm;
let match = pattern.exec(text);
while (match !== null && targets.length < 20) {
if (!targets.includes(match[1])) targets.push(match[1]);
match = pattern.exec(text);
}
return {
label: "Make",
lines: [
`${name}: ${targets.length === 0 ? "no targets parsed" : `targets ${targets.join(", ")}`}`,
],
commands: targets.slice(0, 8).map((target) => `make ${target}`),
};
}
async function detectCMake(root: string): Promise<StackReport | undefined> {
if (!fileExists(root, "CMakeLists.txt")) return undefined;
const text = await readManifest(join(root, "CMakeLists.txt"));
const project = /project\s*\(\s*([A-Za-z0-9_.\-]+)/i.exec(text);
return {
label: "CMake",
lines: [`CMakeLists.txt: project ${project === null ? "(name not found)" : project[1]}`],
commands: ["cmake -S . -B build", "cmake --build build"],
};
}
async function detectRuby(root: string): Promise<StackReport | undefined> {
if (!fileExists(root, "Gemfile")) return undefined;
const text = await readManifest(join(root, "Gemfile"));
const gems = [...text.matchAll(/^\s*gem\s+["']([^"']+)["']/gm)].map((m) => m[1]);
return {
label: "Ruby",
lines: [
`Gemfile: ${gems.length} ${plural(gems.length, "gem", "gems")}` +
`${gems.length === 0 ? "" : ` (${gems.slice(0, 10).join(", ")}${gems.length > 10 ? ", ..." : ""})`}`,
],
commands: ["bundle install", "bundle exec rake"],
};
}
function detectDotnet(topLevelFiles: string[]): StackReport | undefined {
const projects = topLevelFiles.filter((name) => /\.(csproj|fsproj|vbproj|sln)$/i.test(name));
if (projects.length === 0) return undefined;
return {
label: ".NET",
lines: [`Project files: ${projects.slice(0, 10).join(", ")}`],
commands: ["dotnet build", "dotnet test"],
};
}
/** Reads the branch straight out of .git rather than shelling out, so this works with shell access off. */
async function describeGit(root: string): Promise<string> {
const gitPath = join(root, ".git");
if (!existsSync(gitPath)) return "Not a git repository (no .git here).";
const info = await stat(gitPath);
if (!info.isDirectory()) {
return "Git repository (linked worktree or submodule; branch not read).";
}
const headPath = join(gitPath, "HEAD");
if (!existsSync(headPath)) return "Git repository (no HEAD file).";
const head = (await readHead(headPath, 4096)).text.trim();
const ref = /^ref:\s*refs\/heads\/(.+)$/.exec(head);
if (ref !== null) return `Git repository, on branch ${ref[1]}.`;
return `Git repository, detached HEAD at ${head.slice(0, 8)}.`;
}
interface Survey {
files: number;
dirs: number;
byExt: Map<string, number>;
capped: boolean;
}
/**
* Counts files by extension with an explicit stack and hard caps, so an
* enormous or symlink-looped tree can never hang the plugin. Dirents report
* symlinks as symlinks, so nothing here follows one.
*/
async function surveyFiles(rootDir: string): Promise<Survey> {
const byExt = new Map<string, number>();
const stack: Array<{ dir: string; depth: number }> = [{ dir: rootDir, depth: 0 }];
let files = 0;
let dirs = 0;
let capped = false;
while (stack.length > 0) {
const current = stack.pop();
if (current === undefined) break;
if (files >= SURVEY_MAX_FILES) {
capped = true;
break;
}
let entries;
try {
entries = await readdir(current.dir, { withFileTypes: true });
} catch {
continue;
}
for (const entry of entries) {
if (entry.isDirectory()) {
if (IGNORED_DIRS.has(entry.name)) continue;
dirs++;
if (current.depth + 1 <= SURVEY_MAX_DEPTH) {
stack.push({ dir: join(current.dir, entry.name), depth: current.depth + 1 });
} else {
capped = true;
}
continue;
}
if (!entry.isFile()) continue;
files++;
if (files >= SURVEY_MAX_FILES) {
capped = true;
break;
}
const ext = extname(entry.name).toLowerCase();
const key = ext === "" ? "(no extension)" : ext;
byExt.set(key, (byExt.get(key) ?? 0) + 1);
}
}
return { files, dirs, byExt, capped };
}
async function countChildren(dir: string): Promise<number> {
try {
return (await readdir(dir)).length;
} catch {
return 0;
}
}
interface TreeResult {
lines: string[];
budgetExhausted: boolean;
}
async function buildTree(startDir: string, maxDepth: number, limit: number): Promise<TreeResult> {
const lines: string[] = [];
let budget = limit;
const walk = async (dir: string, prefix: string, depth: number): Promise<void> => {
let entries;
try {
entries = await readdir(dir, { withFileTypes: true });
} catch {
lines.push(`${prefix}[unreadable directory]`);
return;
}
const dirsFirst = [
...entries.filter((e) => e.isDirectory()).sort((a, b) => a.name.localeCompare(b.name)),
...entries.filter((e) => !e.isDirectory()).sort((a, b) => a.name.localeCompare(b.name)),
];
const shown = dirsFirst.slice(0, PER_DIR_SHOWN);
let printed = 0;
for (const entry of shown) {
if (budget <= 0) break;
const full = join(dir, entry.name);
if (entry.isDirectory()) {
if (IGNORED_DIRS.has(entry.name)) {
lines.push(`${prefix}${entry.name}/ [not walked]`);
budget--;
printed++;
continue;
}
if (depth >= maxDepth) {
const childCount = await countChildren(full);
lines.push(
`${prefix}${entry.name}/ [${childCount} ${plural(childCount, "entry", "entries")}, depth limit]`,
);
budget--;
printed++;
continue;
}
lines.push(`${prefix}${entry.name}/`);
budget--;
printed++;
await walk(full, `${prefix} `, depth + 1);
continue;
}
lines.push(`${prefix}${entry.name}`);
budget--;
printed++;
}
const elided = dirsFirst.length - printed;
if (elided > 0) {
lines.push(`${prefix}... ${elided} more ${plural(elided, "entry", "entries")} not shown`);
}
};
await walk(startDir, "", 0);
return { lines, budgetExhausted: budget <= 0 };
}
interface LineCount {
lines: number;
size: number;
scannedBytes: number;
binary: boolean;
truncated: boolean;
}
/** Counts newlines in fixed-size chunks so a huge file never lands in memory whole. */
async function countFileLines(absPath: string): Promise<LineCount> {
const info = await stat(absPath);
const handle = await open(absPath, "r");
try {
const chunk = Buffer.alloc(64 * 1024);
let position = 0;
let lines = 0;
let binary = false;
let lastByte = 0;
const cap = Math.min(info.size, LINE_SCAN_CAP);
while (position < cap) {
const { bytesRead } = await handle.read(chunk, 0, Math.min(chunk.length, cap - position), position);
if (bytesRead === 0) break;
for (let i = 0; i < bytesRead; i++) {
const byte = chunk[i];
if (byte === 0x0a) lines++;
else if (byte === 0) binary = true;
}
lastByte = chunk[bytesRead - 1];
position += bytesRead;
}
if (position > 0 && lastByte !== 0x0a) lines++;
return {
lines,
size: info.size,
scannedBytes: position,
binary,
truncated: info.size > cap,
};
} finally {
await handle.close();
}
}
export function projectTools(ws: Workspace): Tool[] {
const tools: Tool[] = [];
tools.push(
tool({
name: "project_overview",
description:
"CALL THIS FIRST in an unfamiliar workspace. Returns one dense orientation report: the " +
"workspace root, the detected stack (package.json, Cargo.toml, pyproject.toml, go.mod, " +
"pom.xml, Makefile, CMakeLists.txt, Gemfile, .csproj), the commands you can actually run, " +
"whether it is a git repo and which branch, the top-level layout, and a file count by " +
"extension. Read-only and safe to call more than once.",
parameters: {},
implementation: async (_params, ctx) => {
try {
ctx.status("Reading project overview");
const root = ws.root;
const sections: string[] = [`Workspace root: ${root}`];
let topEntries;
try {
topEntries = await readdir(root, { withFileTypes: true });
} catch (caught) {
return `Error: cannot read the workspace root ${root} (${(caught as Error).message}). Check the plugin's rootDirectory setting.`;
}
const topDirs = topEntries
.filter((e) => e.isDirectory())
.map((e) => e.name)
.sort((a, b) => a.localeCompare(b));
const topFiles = topEntries
.filter((e) => !e.isDirectory())
.map((e) => e.name)
.sort((a, b) => a.localeCompare(b));
const reports = (
await Promise.all([
detectNode(root),
detectRust(root),
detectPython(root),
detectGo(root),
detectMaven(root),
detectMake(root),
detectCMake(root),
detectRuby(root),
])
).filter((report): report is StackReport => report !== undefined);
const dotnet = detectDotnet(topFiles);
if (dotnet !== undefined) reports.push(dotnet);
sections.push(
reports.length === 0
? "Detected stack: none recognised (no known manifest at the top level)."
: `Detected stack: ${reports.map((r) => r.label).join(", ")}`,
);
for (const report of reports) sections.push(report.lines.join("\n"));
const commands = reports.flatMap((report) => report.commands).slice(0, 16);
sections.push(
commands.length === 0
? "Runnable targets: none found. Look for a CI config or the README before guessing."
: `Runnable targets (pass one to run_command):\n${commands.map((c) => ` ${c}`).join("\n")}`,
);
const extras = EXTRA_MANIFESTS.filter((name) => topFiles.includes(name));
if (extras.length > 0) {
sections.push(`Other manifests present (not parsed): ${extras.join(", ")}`);
}
sections.push(await describeGit(root));
const layout: string[] = ["Top-level layout:"];
for (const name of topDirs.slice(0, TOP_LEVEL_SHOWN)) {
layout.push(` ${name}/${IGNORED_DIRS.has(name) ? " [not walked]" : ""}`);
}
for (const name of topFiles.slice(0, TOP_LEVEL_SHOWN)) {
layout.push(` ${name}`);
}
const hiddenCount =
Math.max(0, topDirs.length - TOP_LEVEL_SHOWN) +
Math.max(0, topFiles.length - TOP_LEVEL_SHOWN);
if (hiddenCount > 0) {
layout.push(` ... ${hiddenCount} more top-level ${plural(hiddenCount, "entry", "entries")} not shown`);
}
sections.push(layout.join("\n"));
ctx.status("Counting files by extension");
const survey = await surveyFiles(root);
const ranked = [...survey.byExt.entries()].sort((a, b) => b[1] - a[1]).slice(0, 10);
const census: string[] = [
`Files counted: ${survey.files} across ${survey.dirs} ${plural(survey.dirs, "directory", "directories")}` +
`${survey.capped ? ` (walk stopped at ${SURVEY_MAX_FILES} files / depth ${SURVEY_MAX_DEPTH}; real totals are higher)` : ""}`,
];
if (ranked.length > 0) {
census.push("Top extensions:");
for (const [ext, count] of ranked) census.push(` ${ext} ${count}`);
}
sections.push(census.join("\n"));
sections.push(
"Next: read_project_instructions for the house rules, then directory_tree to see the layout.",
);
return clamp(sections.join("\n\n"), OVERVIEW_CHARS, "overview");
} catch (caught) {
return `Error: could not build the project overview (${(caught as Error).message}). Try list_directory on '.' instead.`;
}
},
}),
);
tools.push(
tool({
name: "directory_tree",
description:
"Show the folder structure as an indented tree, skipping noise like node_modules, .git " +
"and build output. Use it after project_overview to find where the source lives. Says " +
"how many entries were left out at each cut, so you know when to look deeper with a " +
"specific path.",
parameters: {
path: z
.string()
.default(".")
.describe("Directory to start from, relative to the workspace root. Use '.' for the root."),
max_depth: z
.number()
.int()
.min(1)
.max(6)
.default(3)
.describe("How many levels deep to descend. 3 is usually enough."),
limit: z
.number()
.int()
.min(10)
.max(500)
.default(200)
.describe("Maximum number of entries to print before stopping."),
},
implementation: async ({ path, max_depth, limit }, ctx) => {
const start = ws.resolveInRoot(path);
try {
ctx.status(`Mapping ${ws.rel(start)}`);
if (!existsSync(start)) {
return `Error: "${path}" does not exist. Call project_overview or directory_tree('.') to see what is there.`;
}
const info = await stat(start);
if (!info.isDirectory()) {
return `Error: "${path}" is a file, not a directory. Use count_lines or read_file on it instead.`;
}
const { lines, budgetExhausted } = await buildTree(start, max_depth, limit);
const header = `${ws.rel(start)}/`;
const footer = budgetExhausted
? `\n\n[stopped at the ${limit}-entry limit; call directory_tree again on a subfolder for the rest]`
: "";
return clamp(`${header}\n${lines.join("\n")}${footer}`, TREE_CHARS, "tree");
} catch (caught) {
return `Error: could not walk "${path}" (${(caught as Error).message}). Try list_directory on it instead.`;
}
},
}),
);
tools.push(
tool({
name: "read_project_instructions",
description:
"Read this project's house rules before writing any code. Looks for AGENTS.md, CLAUDE.md, " +
".cursorrules, CONTRIBUTING.md, then README.md, and returns the first one it finds along " +
"with its name. Long files are truncated. Read-only and safe to call more than once.",
parameters: {},
implementation: async (_params, ctx) => {
try {
ctx.status("Looking for project instructions");
const present = INSTRUCTION_FILES.filter((name) => existsSync(join(ws.root, name)));
if (present.length === 0) {
return (
"No instruction file found (looked for " +
`${INSTRUCTION_FILES.join(", ")}). There are no written house rules; ` +
"match the style of the existing code instead."
);
}
const chosen = present[0];
const head = await readHead(join(ws.root, chosen), ws.maxBytes);
const others = present.slice(1);
const alsoLine =
others.length === 0
? ""
: `\nAlso present (not shown): ${others.join(", ")} -- read one with read_file if you need it.`;
const sizeNote = head.truncated
? `\n[only the first ${formatBytes(ws.maxBytes)} of ${formatBytes(head.size)} was read]`
: "";
return clamp(
`Instructions from ${chosen} (${formatBytes(head.size)}):${alsoLine}${sizeNote}\n\n${head.text}`,
INSTRUCTIONS_CHARS,
chosen,
);
} catch (caught) {
return `Error: could not read the project instructions (${(caught as Error).message}). Try read_file on README.md.`;
}
},
}),
);
tools.push(
tool({
name: "count_lines",
description:
"Report the line count and size of one file WITHOUT returning its contents. Call this " +
"before reading an unfamiliar file so you can decide whether to read it whole or in " +
"ranges, and to avoid dumping a huge or binary file into the conversation.",
parameters: {
path: z.string().describe("File path relative to the workspace root."),
},
implementation: async ({ path }, ctx) => {
const filePath = ws.resolveInRoot(path);
try {
ctx.status(`Measuring ${ws.rel(filePath)}`);
if (!existsSync(filePath)) {
return `Error: "${path}" does not exist. Use directory_tree to find the right path.`;
}
const info = await stat(filePath);
if (info.isDirectory()) {
return `Error: "${path}" is a directory. Use directory_tree on it instead.`;
}
if (!info.isFile()) {
return `Error: "${path}" is not a regular file.`;
}
const counted = await countFileLines(filePath);
if (counted.binary) {
return (
`${ws.rel(filePath)}: ${formatBytes(counted.size)}, looks BINARY (contains null bytes). ` +
"Do not read it as text."
);
}
const parts = [
`${ws.rel(filePath)}: ${counted.lines} ${plural(counted.lines, "line", "lines")}, ${formatBytes(counted.size)}.`,
counted.truncated
? `Only the first ${formatBytes(counted.scannedBytes)} was scanned, so the real line count is higher.`
: "",
counted.size > ws.maxBytes
? `Bigger than the ${ws.maxFileSizeKb} KB read limit -- read_file will truncate it.`
: counted.lines > 400
? "Long file: search it for the part you need rather than reading it all."
: "Small enough to read in full.",
].filter((part) => part !== "");
return parts.join(" ");
} catch (caught) {
return `Error: could not measure "${path}" (${(caught as Error).message}). Check the path with directory_tree.`;
}
},
}),
);
return tools;
}