src / toolsProvider.ts
import { tool, Tool, ToolsProviderController } from "@lmstudio/sdk";
import { existsSync, statSync } from "fs";
import { mkdir, readFile, readdir, writeFile } from "fs/promises";
import { join, basename, dirname, resolve, sep } from "path";
import { z } from "zod";
import { configSchematics } from "./config";
// Helper function to check if a path is within a base directory
function isPathWithinBaseDir(baseDir: string, targetPath: string): boolean {
// First, resolve both paths to absolute paths to handle all path variations
const resolvedBaseDir = resolve(baseDir);
const resolvedTargetPath = resolve(targetPath);
// Normalize base directory to end with path separator for accurate prefix matching
// This prevents "/home/user/workspace" from matching "/home/user/workspace-evil"
const normalizedBaseDir = resolvedBaseDir.endsWith(sep)
? resolvedBaseDir
: resolvedBaseDir + sep;
// Check if target is exactly the base directory or starts with base directory + separator
if (resolvedTargetPath !== resolvedBaseDir && !resolvedTargetPath.startsWith(normalizedBaseDir)) {
return false;
}
// Additional security check: prevent directory traversal by ensuring the
// relative path doesn't contain '..' segments
const relativePath = resolvedTargetPath.substring(resolvedBaseDir.length);
const pathSegments = relativePath.split(/[\/\\]/).filter(segment => segment !== '');
// Check if any path segment is '..', which would indicate directory traversal
for (const segment of pathSegments) {
if (segment === '..') {
return false;
}
}
return true;
}
export async function toolsProvider(ctl: ToolsProviderController) {
const tools: Tool[] = [];
// === WRITE FILE TOOL ===
// Writes files to the configured directory
const writeFileTool = tool({
name: `write_file`,
description: "Write or update a file with the given name and content. Creates the file if it doesn't exist. Supports subdirectories.",
parameters: {
file_name: z.string().min(1, "File name cannot be empty").regex(/^[\w./-]+$/, "File name can only contain letters, numbers, underscores, hyphens, dots, and forward slashes"),
content: z.string()
},
implementation: async ({ file_name, content }) => {
console.log("write_file tool called with parameters:", { file_name, content });
// Check if directory is set
const folderName = ctl.getPluginConfig(configSchematics).get("folderName");
if (!folderName) {
return "Error: Directory not set. Use set_directory first.";
}
// Validate that the file path is within the configured directory
const fullPath = join(folderName, file_name);
// Security check: ensure the path is within the configured directory
// Allow paths with "/" in filenames (subdirectories) but prevent traversal outside the folder
if (!isPathWithinBaseDir(folderName, fullPath)) {
return "Error: File path is outside the configured directory.";
}
// Create directory structure if needed
const fileDir = dirname(fullPath);
if (!existsSync(fileDir)) {
await mkdir(fileDir, { recursive: true });
}
// Write file (creates or overwrites)
await writeFile(fullPath, content, "utf-8");
return "File created or updated successfully";
},
});
tools.push(writeFileTool);
// === READ FILE TOOL ===
// Reads files from the configured directory
const readFileTool = tool({
name: `read_file`,
description: "Read the content of a file from the configured directory.",
parameters: {
file_name: z.string().min(1, "File name cannot be empty").regex(/^[\w./-]+$/, "File name can only contain letters, numbers, underscores, hyphens, dots, and forward slashes")
},
implementation: async ({ file_name }) => {
console.log("read_file tool called with parameters:", { file_name });
// Check if directory is set
const folderName = ctl.getPluginConfig(configSchematics).get("folderName");
if (!folderName) {
return "Error: Directory not set. Use set_directory first.";
}
// Validate that the file path is within the configured directory
const fullPath = join(folderName, file_name);
// Security check: ensure the path is within the configured directory
if (!isPathWithinBaseDir(folderName, fullPath)) {
return "Error: File path is outside the configured directory.";
}
// Build file path
const filePath = fullPath;
// Check if file exists
if (!existsSync(filePath)) {
return "Error: File does not exist";
}
// Read and return content
return await readFile(filePath, "utf-8");
},
});
tools.push(readFileTool);
// === LIST FILES TOOL ===
// Lists all files in the configured directory
const listFilesTool = tool({
name: `list_files`,
description: "List all files in the configured directory.",
parameters: {},
implementation: async () => {
console.log("list_files tool called");
// Check if directory is set
const folderName = ctl.getPluginConfig(configSchematics).get("folderName");
if (!folderName || !existsSync(folderName)) {
return "Error: Directory not set or does not exist";
}
// Get file list
const files = await readdir(folderName);
if (files.length === 0) {
return "Directory is empty";
}
return `Files found:\n${files.map(f => `- ${f}`).join("\n")}`;
},
});
tools.push(listFilesTool);
// === CREATE DIRECTORY TOOL ===
// Creates a subdirectory within the configured directory
const createDirectoryTool = tool({
name: `create_directory`,
description: "Create a new subdirectory within the configured directory.",
parameters: {
directory_name: z.string().min(1, "Directory name cannot be empty").regex(/^[\w./-]+$/, "Directory name can only contain letters, numbers, underscores, hyphens, dots, and forward slashes")
},
implementation: async ({ directory_name }) => {
console.log("create_directory tool called with parameters:", { directory_name });
// Check if directory is set
const folderName = ctl.getPluginConfig(configSchematics).get("folderName");
if (!folderName) {
return "Error: Directory not set. Use set_directory first.";
}
// Validate that the directory path is within the configured directory
const fullPath = join(folderName, directory_name);
// Security check: ensure the path is within the configured directory
if (!isPathWithinBaseDir(folderName, fullPath)) {
return "Error: Directory path is outside the configured directory.";
}
// Create directory
await mkdir(fullPath, { recursive: true });
return `Directory '${directory_name}' created successfully`;
},
});
tools.push(createDirectoryTool);
return tools;
}