packages feed

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

module LLM.Tools.MoveFile (moveFileToolTyped, MoveFileToolArgs (..)) 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 (doesDirectoryExist, doesFileExist, doesPathExist, renameFile)

data MoveFileToolArgs = MoveFileToolArgs
  { _mfSrc :: Text,
    _mfDst :: Text
  }
  deriving (Generic)
  deriving (FromJSON) via (AC.Autodocodec MoveFileToolArgs)

instance AC.HasCodec MoveFileToolArgs where
  codec =
    AC.object "move source to destination" $
      MoveFileToolArgs
        <$> AC.requiredField "source" "Relative path of the file to move" AC..= (._mfSrc)
        <*> AC.requiredField "destination" "Relative destination path (including filename)" AC..= (._mfDst)

moveFileToolTyped :: FsConfig -> TypedTool ctx MoveFileToolArgs
moveFileToolTyped cfg =
  TypedTool
    { ttoolName = "move_file",
      ttoolDescription =
        "Move (rename) a file from source to destination (both paths relative to the workspace). "
          <> "Creates parent directories at the destination as needed.",
      ttoolReadonly = False,
      ttoolExecute = const (moveFileExecTyped cfg)
    }

moveFileExecTyped :: FsConfig -> MoveFileToolArgs -> IO Text
moveFileExecTyped cfg args = do
  let src = args._mfSrc
      dst = args._mfDst
  srcResolved <- sandboxPath cfg (T.unpack src)
  -- Refuse to operate on symbolic-link sources: this would either move
  -- the link itself (surprising) or, after `sandboxPath` resolution,
  -- potentially target a file outside the sandbox.
  srcIsLink <- isSymlink srcResolved
  if srcIsLink
    then pure $ "Error: refusing to move 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)
              renameFile srcResolved dstResolved
              pure $ "Successfully moved " <> src <> " to " <> dst