packages feed

llm-simple-0.1.0.1: src/LLM/Tools/CopyFile.hs

module LLM.Tools.CopyFile (copyFileToolTyped, CopyFileToolArgs (..)) where

import Autodocodec qualified as AC
import Data.Aeson (FromJSON)
import Data.Text (Text)
import Data.Text qualified as T
import GHC.Generics (Generic)
import LLM.Core.Types (TypedTool (..))
import LLM.Tools.FsConfig (FsConfig, isSymlink, sandboxPath, sandboxWritePath)
import System.Directory (copyFile, doesDirectoryExist, doesFileExist, doesPathExist)

data CopyFileToolArgs = CopyFileToolArgs
  { _cfSrc :: Text,
    _cfDst :: Text
  }
  deriving (Generic)
  deriving (FromJSON) via (AC.Autodocodec CopyFileToolArgs)

instance AC.HasCodec CopyFileToolArgs where
  codec =
    AC.object "copy source to destination" $
      CopyFileToolArgs
        <$> AC.requiredField "source" "Relative path of the file to copy" AC..= (._cfSrc)
        <*> AC.requiredField "destination" "Relative destination path (including filename)" AC..= (._cfDst)

copyFileToolTyped :: FsConfig -> TypedTool ctx CopyFileToolArgs
copyFileToolTyped cfg =
  TypedTool
    { ttoolName = "copy_file",
      ttoolDescription =
        "Copy a file from source to destination (both paths relative to the workspace). "
          <> "Overwrites the destination if it already exists. "
          <> "Creates parent directories at the destination as needed.",
      ttoolReadonly = False,
      ttoolExecute = const (copyFileExecTyped cfg)
    }

copyFileExecTyped :: FsConfig -> CopyFileToolArgs -> IO Text
copyFileExecTyped cfg args = do
  let src = args._cfSrc
      dst = args._cfDst
  srcResolved <- sandboxPath cfg (T.unpack src)
  -- Refuse symbolic-link sources: copying would either dereference the
  -- link (potentially pulling content from outside the sandbox) or be
  -- ambiguous about whether the link itself should be reproduced.
  srcIsLink <- isSymlink srcResolved
  if srcIsLink
    then pure $ "Error: refusing to copy symbolic link source: " <> src
    else do
      srcExists <- doesPathExist srcResolved
      if not srcExists
        then pure $ "Error: source does not exist: " <> src
        else do
          srcIsFile <- doesFileExist srcResolved
          srcIsDir <- doesDirectoryExist srcResolved
          case (srcIsFile, srcIsDir) of
            (False, True) -> pure $ "Error: source is a directory, not a regular file: " <> src
            (False, False) -> pure $ "Error: source is not a regular file: " <> src
            _ -> do
              dstResolved <- sandboxWritePath cfg (T.unpack dst)
              copyFile srcResolved dstResolved
              pure $ "Successfully copied " <> src <> " to " <> dst