never executed always true always false
    1 module PureClaw.Tools.Edit
    2   ( -- * Tool registration
    3     editTool
    4   ) where
    5 
    6 import Control.Exception
    7 import Data.Aeson
    8 import Data.Aeson.Types
    9 import Data.Text (Text)
   10 import Data.Text qualified as T
   11 import Data.Text.Encoding qualified as TE
   12 
   13 import PureClaw.Core.Types
   14 import PureClaw.Handles.File
   15 import PureClaw.Providers.Class
   16 import PureClaw.Security.Path
   17 import PureClaw.Tools.Registry
   18 
   19 -- | Create an edit tool that performs string replacement in files.
   20 -- The old string must be unique in the file — ambiguous edits are rejected.
   21 editTool :: WorkspaceRoot -> FileHandle -> (ToolDefinition, ToolHandler)
   22 editTool root fh = (def, handler)
   23   where
   24     def = ToolDefinition
   25       { _td_name        = "edit"
   26       , _td_description = "Replace a unique string in a file. The old_string must appear exactly once in the file."
   27       , _td_inputSchema = object
   28           [ "type" .= ("object" :: Text)
   29           , "properties" .= object
   30               [ "path" .= object
   31                   [ "type" .= ("string" :: Text)
   32                   , "description" .= ("The file path relative to the workspace root" :: Text)
   33                   ]
   34               , "old_string" .= object
   35                   [ "type" .= ("string" :: Text)
   36                   , "description" .= ("The exact string to find and replace (must be unique)" :: Text)
   37                   ]
   38               , "new_string" .= object
   39                   [ "type" .= ("string" :: Text)
   40                   , "description" .= ("The replacement string" :: Text)
   41                   ]
   42               ]
   43           , "required" .= (["path", "old_string", "new_string"] :: [Text])
   44           ]
   45       }
   46 
   47     handler = ToolHandler $ \input ->
   48       case parseEither parseInput input of
   49         Left err -> pure (T.pack err, True)
   50         Right (path, oldStr, newStr) -> do
   51           pathResult <- mkSafePath root (T.unpack path)
   52           case pathResult of
   53             Left pe -> pure (T.pack (show pe), True)
   54             Right sp -> do
   55               readResult <- try @SomeException (_fh_readFile fh sp)
   56               case readResult of
   57                 Left e -> pure (T.pack (show e), True)
   58                 Right bs -> case TE.decodeUtf8' bs of
   59                   Left _ -> pure ("Cannot edit binary file", True)
   60                   Right content ->
   61                     let count = countOccurrences oldStr content
   62                     in case count of
   63                       0 -> pure ("old_string not found in " <> path, True)
   64                       1 -> do
   65                         let newContent = replaceFirst oldStr newStr content
   66                         writeResult <- try @SomeException
   67                           (_fh_writeFile fh sp (TE.encodeUtf8 newContent))
   68                         case writeResult of
   69                           Left e -> pure (T.pack (show e), True)
   70                           Right () -> pure ("Edited " <> path, False)
   71                       n -> pure ("old_string not unique in " <> path
   72                                 <> " (" <> T.pack (show n) <> " occurrences)", True)
   73 
   74     parseInput :: Value -> Parser (Text, Text, Text)
   75     parseInput = withObject "EditInput" $ \o ->
   76       (,,) <$> o .: "path" <*> o .: "old_string" <*> o .: "new_string"
   77 
   78 -- | Count non-overlapping occurrences of a needle in a haystack.
   79 countOccurrences :: Text -> Text -> Int
   80 countOccurrences needle haystack
   81   | T.null needle = 0
   82   | otherwise     = go 0 haystack
   83   where
   84     go !n remaining =
   85       case T.breakOn needle remaining of
   86         (_, after)
   87           | T.null after -> n
   88           | otherwise    -> go (n + 1) (T.drop (T.length needle) after)
   89 
   90 -- | Replace the first occurrence of needle with replacement.
   91 replaceFirst :: Text -> Text -> Text -> Text
   92 replaceFirst needle replacement haystack =
   93   let (before, after) = T.breakOn needle haystack
   94   in before <> replacement <> T.drop (T.length needle) after