src / toolsProvider.ts

import { tool, type ToolsProvider } from "@lmstudio/sdk";
import { z } from "zod";
import * as fs from "fs";
import * as path from "path";
import { listFiles, formatFilesList } from "./list-files";
import { regexSearchFiles } from "./ripgrep";
import { globalConfigSchematics } from "./config";

function resolvePath(targetPath: string, baseDirectory: string): string {
  const resolved = path.resolve(baseDirectory || ".", targetPath);
  if (baseDirectory) {
    const base = path.resolve(baseDirectory);
    if (!resolved.startsWith(base + path.sep) && resolved !== base) {
      throw new Error(`Access denied: "${resolved}" is outside the allowed base directory "${base}".`);
    }
  }
  return resolved;
}

export const toolsProvider: ToolsProvider = async (ctl) => {
  const globalConfig = ctl.getGlobalPluginConfig(globalConfigSchematics);
  const baseDirectory: string = globalConfig.get("baseDirectory") ?? "";

  const readFileTool = tool({
    name: "read_file",
    description: "Read the full contents of a file.",
    parameters: { path: z.string().describe("Path to the file to read.") },
    implementation: async ({ path: filePath }, ctx) => {
      ctx.status("Reading file...");
      try { return fs.readFileSync(resolvePath(filePath, baseDirectory), "utf-8"); }
      catch (err: any) { return `Error: ${err.message}`; }
    },
  });

  const writeFileTool = tool({
    name: "write_file",
    description: "Write (or overwrite) a file with the given content. Creates parent directories if needed.",
    parameters: {
      path: z.string().describe("Path to the file to write."),
      content: z.string().describe("Content to write into the file."),
    },
    implementation: async ({ path: filePath, content }, ctx) => {
      ctx.status("Writing file...");
      try {
        const resolved = resolvePath(filePath, baseDirectory);
        fs.mkdirSync(path.dirname(resolved), { recursive: true });
        fs.writeFileSync(resolved, content, "utf-8");
        return `File written: ${resolved}`;
      } catch (err: any) { return `Error: ${err.message}`; }
    },
  });

  const applyDiffTool = tool({
    name: "apply_diff",
    description: "Apply a targeted search-and-replace edit to a file. The match must be unique.",
    parameters: {
      path: z.string().describe("Path to the file to edit."),
      oldText: z.string().describe("Exact text to search for (must appear exactly once)."),
      newText: z.string().describe("Text to replace oldText with."),
    },
    implementation: async ({ path: filePath, oldText, newText }, ctx) => {
      ctx.status("Applying diff...");
      try {
        const resolved = resolvePath(filePath, baseDirectory);
        const original = fs.readFileSync(resolved, "utf-8");
        const count = original.split(oldText).length - 1;
        if (count === 0) return "Error: oldText not found in file.";
        if (count > 1) return `Error: oldText found ${count} times, make it more specific.`;
        fs.writeFileSync(resolved, original.replace(oldText, newText), "utf-8");
        return `Diff applied successfully to ${resolved}.`;
      } catch (err: any) { return `Error: ${err.message}`; }
    },
  });

  const replaceInFileTool = tool({
    name: "replace_in_file",
    description: "Make targeted edits using SEARCH/REPLACE blocks. Format: ------- SEARCH / [content] / ======= / [replacement] / +++++++ REPLACE",
    parameters: {
      path: z.string().describe("Path to the file to modify."),
      diff: z.string().describe("One or more SEARCH/REPLACE blocks."),
    },
    implementation: async ({ path: filePath, diff }, ctx) => {
      ctx.status("Applying replace...");
      try {
        const resolved = resolvePath(filePath, baseDirectory);
        let fileContent = fs.readFileSync(resolved, "utf-8");

        // Normalise les fins de ligne (\r\n et \r -> \n) pour comparer correctement
        const normalize = (s: string) => s.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
        const usedCRLF = fileContent.includes("\r\n");
        fileContent = normalize(fileContent);
        const normalizedDiff = normalize(diff);

        const blocks = normalizedDiff.match(/[-]{3,} SEARCH[\s\S]*?[+]{3,} REPLACE/g);
        if (!blocks || blocks.length === 0) return "Error: no valid SEARCH/REPLACE blocks found.";
        for (const block of blocks) {
          const sm = block.match(/[-]{3,} SEARCH\n?([\s\S]*?)\n?={3,}/);
          const rm = block.match(/={3,}\n?([\s\S]*?)\n?[+]{3,} REPLACE/);
          if (!sm || !rm) return "Error: malformed SEARCH/REPLACE block.";
          const searchText = sm[1];
          const replaceText = rm[1];
          if (!fileContent.includes(searchText)) return `Error: SEARCH block not found:\n${searchText}`;
          fileContent = fileContent.replace(searchText, replaceText);
        }

        // Rétablit les fins de ligne d'origine si le fichier utilisait \r\n
        const finalContent = usedCRLF ? fileContent.replace(/\n/g, "\r\n") : fileContent;
        fs.writeFileSync(resolved, finalContent, "utf-8");
        return "Replace applied successfully.";
      } catch (err: any) { return `Error: ${err.message}`; }
    },
  });

  const appendToFileTool = tool({
    name: "append_to_file",
    description: "Append content to the end of a file. Creates the file if it does not exist.",
    parameters: {
      path: z.string().describe("Path to the file."),
      content: z.string().describe("Content to append."),
      add_newline: z.boolean().optional().default(true).describe("Ensure content starts on a new line. Default: true."),
    },
    implementation: async ({ path: filePath, content, add_newline }, ctx) => {
      ctx.status("Appending...");
      try {
        const resolved = resolvePath(filePath, baseDirectory);
        fs.mkdirSync(path.dirname(resolved), { recursive: true });
        let toAppend = content;
        if (add_newline && fs.existsSync(resolved)) {
          const existing = fs.readFileSync(resolved, "utf-8");
          if (existing.length > 0 && !existing.endsWith("\n")) toAppend = "\n" + toAppend;
        }
        fs.appendFileSync(resolved, toAppend, "utf-8");
        return "Content appended successfully.";
      } catch (err: any) { return `Error: ${err.message}`; }
    },
  });

  const copyFileTool = tool({
    name: "copy_file",
    description: "Copy a file from one location to another. Creates parent directories if needed.",
    parameters: {
      source: z.string().describe("Path of the source file."),
      destination: z.string().describe("Path of the destination file."),
      overwrite: z.boolean().optional().default(false).describe("Overwrite if exists. Default: false."),
    },
    implementation: async ({ source, destination, overwrite }, ctx) => {
      ctx.status("Copying file...");
      try {
        const src = resolvePath(source, baseDirectory);
        const dst = resolvePath(destination, baseDirectory);
        if (!fs.existsSync(src)) return `Error: source not found: ${src}`;
        if (!overwrite && fs.existsSync(dst)) return "Error: destination already exists. Use overwrite: true.";
        fs.mkdirSync(path.dirname(dst), { recursive: true });
        fs.copyFileSync(src, dst);
        return `File copied: ${src} -> ${dst}`;
      } catch (err: any) { return `Error: ${err.message}`; }
    },
  });

  const fileExistsTool = tool({
    name: "file_exists",
    description: "Check whether a file or directory exists. Returns existence status and type.",
    parameters: { path: z.string().describe("Path to check.") },
    implementation: async ({ path: filePath }, ctx) => {
      ctx.status("Checking...");
      try {
        const resolved = resolvePath(filePath, baseDirectory);
        if (!fs.existsSync(resolved)) return JSON.stringify({ exists: false, type: null, path: resolved });
        const s = fs.statSync(resolved);
        return JSON.stringify({ exists: true, type: s.isDirectory() ? "directory" : "file", path: resolved });
      } catch (err: any) { return `Error: ${err.message}`; }
    },
  });

  const createDirectoryTool = tool({
    name: "create_directory",
    description: "Create a directory (and all missing parent directories) at the given path.",
    parameters: { path: z.string().describe("Path of the directory to create.") },
    implementation: async ({ path: dirPath }, ctx) => {
      ctx.status("Creating directory...");
      try {
        const resolved = resolvePath(dirPath, baseDirectory);
        fs.mkdirSync(resolved, { recursive: true });
        return `Directory created: ${resolved}`;
      } catch (err: any) { return `Error: ${err.message}`; }
    },
  });

  const listFilesTool = tool({
    name: "list_files",
    description: "List files and directories at a given path. Set recursive=true to include subdirectories.",
    parameters: {
      path: z.string().describe("Directory to list."),
      recursive: z.boolean().optional().default(false).describe("Whether to list files recursively."),
    },
    implementation: async ({ path: dirPath, recursive }, ctx) => {
      ctx.status("Listing files...");
      try {
        const resolved = resolvePath(dirPath, baseDirectory);
        const [files, hitLimit] = await listFiles(resolved, recursive ?? false, 500);
        return formatFilesList(resolved, files, hitLimit);
      } catch (err: any) { return `Error: ${err.message}`; }
    },
  });

  const searchFilesTool = tool({
    name: "search_files",
    description: "Search for a regex pattern inside files using ripgrep.",
    parameters: {
      path: z.string().describe("Directory to search in."),
      regex: z.string().describe("Regular expression to search for."),
      filePattern: z.string().optional().describe("Glob pattern to restrict which files are searched (e.g. '*.ts')."),
    },
    implementation: async ({ path: dirPath, regex, filePattern }, ctx) => {
      ctx.status("Searching files...");
      try {
        const resolved = resolvePath(dirPath, baseDirectory);
        const cwd = path.resolve(__dirname, "..");
        return await regexSearchFiles(cwd, resolved, regex, filePattern);
      } catch (err: any) { return `Error: ${err.message}`; }
    },
  });

  const deleteFileTool = tool({
    name: "delete_file",
    description: "Delete a file at the given path.",
    parameters: { path: z.string().describe("Path to the file to delete.") },
    implementation: async ({ path: filePath }, ctx) => {
      ctx.status("Deleting file...");
      try {
        fs.rmSync(resolvePath(filePath, baseDirectory), { force: true });
        return "File deleted.";
      } catch (err: any) { return `Error: ${err.message}`; }
    },
  });

  const moveFileTool = tool({
    name: "move_file",
    description: "Move or rename a file or directory.",
    parameters: {
      source: z.string().describe("Current path of the file or directory."),
      destination: z.string().describe("New path (target location)."),
    },
    implementation: async ({ source, destination }, ctx) => {
      ctx.status("Moving file...");
      try {
        const src = resolvePath(source, baseDirectory);
        const dst = resolvePath(destination, baseDirectory);
        fs.mkdirSync(path.dirname(dst), { recursive: true });
        fs.renameSync(src, dst);
        return `Moved "${src}" -> "${dst}"`;
      } catch (err: any) { return `Error: ${err.message}`; }
    },
  });

  const fileInfoTool = tool({
    name: "file_info",
    description: "Get metadata about a file or directory (size, type, dates).",
    parameters: { path: z.string().describe("Path to inspect.") },
    implementation: async ({ path: filePath }, ctx) => {
      ctx.status("Getting file info...");
      try {
        const resolved = resolvePath(filePath, baseDirectory);
        const stat = fs.statSync(resolved);
        return JSON.stringify({ path: resolved, type: stat.isDirectory() ? "directory" : "file", sizeBytes: stat.size, created: stat.birthtime.toISOString(), modified: stat.mtime.toISOString(), accessed: stat.atime.toISOString() }, null, 2);
      } catch (err: any) { return `Error: ${err.message}`; }
    },
  });

  return [
    readFileTool, writeFileTool, applyDiffTool,
    replaceInFileTool, appendToFileTool, copyFileTool, fileExistsTool,
    createDirectoryTool, listFilesTool, searchFilesTool,
    deleteFileTool, moveFileTool, fileInfoTool,
  ];
};