src / toolsProvider.ts

import { tool, Tool, ToolsProviderController } from "@lmstudio/sdk";
import { exec, ExecOptions } from "child_process";
import { z } from "zod";
import path from "path";
import { configSchematics } from "./config";

interface ExecResult {
  stdout: string;
  stderr: string;
  error: Error | null;
  timedOut: boolean;
}

const runShellCommand = (command: string, options: ExecOptions, timeoutMs: number): Promise<ExecResult> => {
  return new Promise((resolve) => {
    // NODEJS EXEC WRAPPER
    const child = exec(command, { ...options, timeout: timeoutMs }, (error, stdout, stderr) => {
      resolve({
        stdout: stdout.trim(),
        stderr: stderr.trim(),
        error,
        timedOut: false,
      });
    });

    child.on('error', (err: any) => {
      if (err.code === 'ETIMEDOUT') {
        resolve({ stdout: '', stderr: 'Command timed out.', error: err, timedOut: true });
      }
    });
  });
};

export async function toolsProvider(ctl: ToolsProviderController): Promise<Tool[]> {
  const config = ctl.getPluginConfig(configSchematics);
  const osType = config.get("operatingSystem");
  
  // --- 1. DETERMINE SHELL EXECUTABLE ---
  // Reverted to CMD.exe for Windows to preserve native PATH behavior (fixes 'cargo' not found)
  const shellExecutable = osType === "windows" ? "cmd.exe" : "/bin/bash";

  // --- 2. OS HINTS ---
  let osHints = "";
  if (osType === "windows") {
    osHints = `
    TARGET SHELL: WINDOWS CMD (Command Prompt).
    - Standard Syntax: Use 'dir', 'type', 'copy', 'cargo', 'python'.
    - POWERSHELL: This shell does NOT support PowerShell syntax (ls, Get-ChildItem) directly.
      IF YOU NEED POWERSHELL: You must prefix the command like this:
      powershell -Command "Get-ChildItem"
    - Chaining: Use '&&' (not ';').
    `;
  } else {
    osHints = `
    TARGET SHELL: LINUX/MACOS (Bash).
    - Usage: Standard bash commands.
    - List: 'ls -la'
    `;
  }

  // --- 3. SANDBOX SETUP ---
  const sandboxRootStr = config.get("homeDirectory");
  const sandboxRoot = sandboxRootStr && sandboxRootStr.trim() !== "" 
    ? path.resolve(sandboxRootStr) 
    : null; 

  let sandboxWarning = "";
  if (sandboxRoot) {
    sandboxWarning = `
    SECURITY RESTRICTION ACTIVE:
    - Working Directory: "${sandboxRoot}"
    - You CANNOT access files outside this folder.
    - DO NOT use absolute paths.
    `;
  }

  const dynamicDescription = `
  FALLBACK TOOL: Executes a command in the system shell.
  PRIORITY: Check for specialized tools (like 'read_file') BEFORE using this.
  ${sandboxWarning}
  ${osHints}
  `;

  const executeCommandTool = tool({
    name: "run_shell_command", 
    description: dynamicDescription,
    parameters: {
      command: z.string().describe(`The command to run.`),
      cwd: z.string().optional().describe("Subdirectory to run in. Must be inside the Sandbox."),
      timeout: z.number().optional().describe("Execution timeout in milliseconds."),
    },
    implementation: async ({ command, cwd, timeout }, { status, warn }) => {
      const currentConfig = ctl.getPluginConfig(configSchematics);
      const allowAuto = currentConfig.get("allowAutoExecution");
      
      const policy = currentConfig.get("executionPolicy"); // "allow_all" or "allow_only"
      const allowedStr = currentConfig.get("allowedCommands");
      const forbiddenStr = currentConfig.get("forbiddenCommands");
      const extraPathsStr = currentConfig.get("additionalSearchPaths");

      // --- CHECK 1: AUTO EXECUTION ---
      if (!allowAuto) {
        warn(`Blocked command: "${command}"`);
        return `PERMISSION DENIED: 'Allow Automatic Execution' is disabled.`;
      }

      // --- CHECK 2: BLACKLIST ---
      if (forbiddenStr && forbiddenStr.trim().length > 0) {
        const forbiddenList = forbiddenStr.split(",").map(s => s.trim().toLowerCase());
        const commandLower = command.toLowerCase();
        const isForbidden = forbiddenList.some(badCmd => {
             return commandLower.startsWith(badCmd + " ") || 
                    commandLower === badCmd ||
                    commandLower.includes(" " + badCmd + " "); 
        });
        if (isForbidden) return `SECURITY ERROR: The command '${command}' is forbidden by the Blacklist.`;
      }

      // --- CHECK 3: WHITELIST ---
      if (policy === "allow_only") {
        if (!allowedStr || allowedStr.trim() === "") {
            return `SECURITY ERROR: Policy is 'Allow Only' but the Allowed Commands list is empty.`;
        }
        const allowedList = allowedStr.split(",").map(s => s.trim().toLowerCase());
        const commandLower = command.toLowerCase();
        
        // For whitelist, we check if the command starts with an allowed token.
        // We also need to handle "powershell -Command" cases specially if we want to allow them.
        const isAllowed = allowedList.some(allowedCmd => {
            // Check matches
            const match = commandLower === allowedCmd || commandLower.startsWith(allowedCmd + " ");
            // Special case: If user whitelisted "powershell", allow "powershell -Command ..."
            return match;
        });

        if (!isAllowed) {
            return `SECURITY ERROR: The command '${command}' is NOT in the Allowed Commands list.`;
        }
      }

      // --- CHECK 4: SANDBOX ---
      const effectiveRoot = sandboxRoot || process.cwd();
      let targetDir = effectiveRoot;
      if (cwd) targetDir = path.resolve(effectiveRoot, cwd);

      if (sandboxRoot) {
        const relativePath = path.relative(sandboxRoot, targetDir);
        if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) {
           return `SECURITY ERROR: Access denied. Target '${targetDir}' is outside sandbox '${sandboxRoot}'.`;
        }
        if (/[a-zA-Z]:/.test(command)) return `SECURITY ERROR: Absolute paths (Drive Letters) are forbidden while sandboxed.`;
        if (command.includes("..")) return `SECURITY ERROR: Parent directory traversal ('..') is forbidden while sandboxed.`;
      }

      // --- SETUP ENVIRONMENT ---
      // We still include this helper, just in case CMD also needs help finding path variables.
      const env = { ...process.env };
      if (extraPathsStr && extraPathsStr.trim() !== "") {
        const extraPaths = extraPathsStr.split(",").map(p => p.trim()).join(path.delimiter);
        const pathKey = process.platform === 'win32' ? 'Path' : 'PATH'; 
        const actualPathKey = Object.keys(env).find(k => k.toLowerCase() === 'path') || pathKey;
        env[actualPathKey] = `${env[actualPathKey] || ''}${path.delimiter}${extraPaths}`;
      }

      status(`Running: ${command.slice(0, 50)}...`);

      try {
        const timeLimit = timeout || currentConfig.get("defaultTimeout");
        
        const result = await runShellCommand(command, { 
            cwd: targetDir,
            shell: shellExecutable, // Back to cmd.exe
            env: env 
        }, timeLimit);

        let response = "";
        
        if (result.stdout) {
          const maxLen = 4000;
          const output = result.stdout.length > maxLen 
            ? result.stdout.slice(0, maxLen) + "\n...[Output Truncated]" 
            : result.stdout;
          response += `STDOUT:\n${output}\n`;
        }

        if (result.stderr) {
          response += `STDERR:\n${result.stderr}\n`;
        }

        if (result.error) {
           response += `EXIT_CODE: ${"code" in result.error ? result.error.code : "Unknown"}\n`;
           if (!result.stderr) response += `ERROR: ${result.error.message}\n`;
        } else {
           if (!result.stdout && !result.stderr) response = "Command executed successfully.";
        }

        return response.trim();

      } catch (err: any) {
        return `PLUGIN EXCEPTION: ${err.message}`;
      }
    },
  });

  return [executeCommandTool];
}