A-gent 0.11.0.11 → 0.11.0.12
raw patch · 11 files changed
+1530/−606 lines, 11 filesPVP: major bump suggested
API removals or changes: PVP suggests a major version bump
API changes (from Hackage documentation)
- Agent.LLM: [exit] :: Context a -> !Exit
- Agent.LLM: [file] :: Context a -> !Maybe File
- Agent.LLM: [root] :: Context a -> !Maybe Root
- Agent.LLM: [view] :: History -> ![View]
- Agent.LLM: combineFiles :: FilePaths -> FilePaths -> FilePaths
- Agent.LLM: getFile :: File -> (FilePath, [FileLine])
- Agent.LLM: rootPath :: RIO (Maybe Root) -> IO (Maybe Root)
- Agent.Utils.Common: chats :: Bool -> [String] -> [(String, String)]
- Agent.Utils.Common: chits :: Bool -> [String] -> [(String, String)]
- Agent.Utils.Common: combine :: [(String, String)] -> [String]
- Agent.Utils.Common: file :: Bool -> String -> [(String, String)]
- Agent.Utils.Common: files :: String -> Bool -> [String] -> [(String, String)]
- Agent.Utils.Common: index :: Int -> Int -> String
- Agent.Utils.Common: leftpad :: Char -> Int -> [Char] -> [Char]
- Agent.Utils.Common: linesOptNums :: String -> Bool -> Int -> [String] -> [(String, String)]
- Agent.Utils.Common: split :: Char -> String -> [String]
+ Agent.IO.Effects: class Monad m => LlmCodeConf (m :: Type -> Type)
+ Agent.IO.Effects: class LlmCodeConf m => LlmCodePost (m :: Type -> Type)
+ Agent.IO.Effects: class Monad m => LlmCodeTmpl (m :: Type -> Type)
+ Agent.IO.Effects: llmCodeAPI :: LlmCodeConf m => m (Maybe String)
+ Agent.IO.Effects: llmCodeExa :: LlmCodeTmpl m => m [File]
+ Agent.IO.Effects: llmCodeGit :: LlmCodeRead m => m (Either String String)
+ Agent.IO.Effects: llmCodeIns :: LlmCodeTmpl m => m [File]
+ Agent.IO.Effects: llmCodeKey :: LlmCodeConf m => m (Maybe String)
+ Agent.IO.Effects: llmCodeWeb :: LlmCodePost m => String -> m (Either String String)
+ Agent.IO.Restricted: llmCodeAPI :: LlmCodeConf m => m (Maybe String)
+ Agent.IO.Restricted: llmCodeExa :: LlmCodeTmpl m => m [File]
+ Agent.IO.Restricted: llmCodeGit :: LlmCodeRead m => m (Either String String)
+ Agent.IO.Restricted: llmCodeIns :: LlmCodeTmpl m => m [File]
+ Agent.IO.Restricted: llmCodeKey :: LlmCodeConf m => m (Maybe String)
+ Agent.IO.Restricted: llmCodePut :: LlmCodeSave m => String -> Files -> m (Either String String)
+ Agent.IO.Restricted: llmCodeWeb :: LlmCodePost m => String -> m (Either String String)
+ Agent.LLM: Atom :: !String -> !Files -> Message
+ Agent.LLM: Branch :: !String -> !Files -> Action
+ Agent.LLM: Exit :: Action
+ Agent.LLM: File :: !File -> Action
+ Agent.LLM: Help :: Action
+ Agent.LLM: Hist :: !(Bool, Bool) -> Action
+ Agent.LLM: List :: !Maybe Filter -> Message
+ Agent.LLM: Message :: !Message -> Action
+ Agent.LLM: Mode :: !Char -> !String -> Action
+ Agent.LLM: None :: Action
+ Agent.LLM: Path :: !Bool -> !Maybe AbsoluteFilePath -> Message
+ Agent.LLM: Paths :: !FilePaths -> Action
+ Agent.LLM: Pile :: Action
+ Agent.LLM: Prompt :: !String -> Action
+ Agent.LLM: Repo :: Message
+ Agent.LLM: Root :: Message
+ Agent.LLM: Ruck :: !String -> Action
+ Agent.LLM: Send :: ![String] -> Message
+ Agent.LLM: Template :: !TextToFile -> ![File] -> ![File] -> ![File] -> Action
+ Agent.LLM: TemplateFiles :: !Maybe [AbsoluteFilePath] -> Message
+ Agent.LLM: Text :: !String -> Message
+ Agent.LLM: UnknownCmd :: !String -> Action
+ Agent.LLM: Wipe :: Action
+ Agent.LLM: [ruck] :: Context a -> !Maybe Files
+ Agent.LLM: [uuid] :: Context a -> !UUID
+ Agent.LLM: data Action
+ Agent.LLM: data Message
+ Agent.LLM: exit :: Action
+ Agent.LLM: file :: File -> Action
+ Agent.LLM: instance GHC.Show.Show Agent.LLM.Message
+ Agent.LLM: none :: Action
+ Agent.LLM: paths :: FilePaths -> Action
+ Agent.LLM: root :: Root -> Action
+ Agent.LLM: template :: TextToFile -> [File] -> [File] -> [File] -> Action
+ Agent.LLM: text :: String -> Action
- Agent.IO.Effects: llmCodeGet :: LlmCodeRead m => String -> m (Either String File)
+ Agent.IO.Effects: llmCodeGet :: LlmCodeRead m => AbsoluteFilePath -> m (Either String File)
- Agent.IO.Effects: llmCodePut :: LlmCodeSave m => m [(String, String)] -> m [Either String ()]
+ Agent.IO.Effects: llmCodePut :: LlmCodeSave m => String -> Files -> m (Either String String)
- Agent.IO.Restricted: llmCodeGet :: LlmCodeRead m => String -> m (Either String File)
+ Agent.IO.Restricted: llmCodeGet :: LlmCodeRead m => AbsoluteFilePath -> m (Either String File)
- Agent.LLM: Context :: !Exit -> !Maybe Root -> !Mode -> !Load a -> !History -> !Maybe Filter -> !Maybe FilePaths -> !Maybe File -> Context a
+ Agent.LLM: Context :: !UUID -> !Mode -> !Load a -> !History -> !Maybe Filter -> !Maybe FilePaths -> !Maybe Files -> Context a
- Agent.LLM: History :: ![View] -> !Chit -> !Chat -> History
+ Agent.LLM: History :: !Chit -> !Chat -> History
- Agent.LLM: repl :: Eval a -> Maybe Root -> IO ()
+ Agent.LLM: repl :: Eval a -> IO ()
- Agent.LLM: replWithMode :: Mode -> Eval a -> Maybe Root -> IO ()
+ Agent.LLM: replWithMode :: Mode -> Eval a -> IO ()
- Agent.LLM: type Eval a = Context a -> String -> RIO (Context a, String)
+ Agent.LLM: type Eval a = Context a -> Message -> RIO (Context a, Action)
Files
- A-gent.cabal +3/−3
- CHANGELOG.md +51/−2
- README.md +51/−0
- src/Agent/Data/ANSI/EscapeCode.hs +1/−0
- src/Agent/IO/Effects.hs +42/−5
- src/Agent/IO/Restricted.hs +12/−2
- src/Agent/LLM.hs +667/−329
- src/Agent/Utils/Common.hs +0/−133
- src/Internal/LLM.hs +72/−43
- src/Internal/RIO.hs +498/−89
- src/Internal/Utils.hs +133/−0
A-gent.cabal view
@@ -9,7 +9,7 @@ build-type: Simple name: A-gent-version: 0.11.0.11+version: 0.11.0.12 synopsis: Polite & well educated LLM agent with excellent manners that always behaves well description: Polite and well educated LLM agent with excellent manners that always behaves well@@ -154,6 +154,8 @@ Internal.RIO -- Internal.LLM+ --+ Internal.Utils exposed-modules: Agent.Control.Concurrent Agent.Control.IFC@@ -164,8 +166,6 @@ -- Agent.IO.Effects Agent.IO.Restricted- --- Agent.Utils.Common -- Agent.LLM
CHANGELOG.md view
@@ -1,15 +1,64 @@ # Revision history for Λ-gent +## 0.11.0.12 -- 2026-05-12++There have been made some **major changes** to this library and therefore there+is no backwards compatibility with previous versions:++* Added support for `Code` mode, as in `read` and `write` files back and forth+ to the `git` repository. A new tab will be added to the website with a script+ explaining usage.++* Added support for script integrity as known from `HTML`:++ ```+ <script integrity="sha256-YBbjT3uJjnS6ofC6HIal+4vNvgA9vZBfC/KMVN51URc=" …></script>+ ```+ + If the `LLM_SIGN_SHA` environment variable is provided on execution, the+ script will check it's own file `SHA-256` hash and compare to the provided:+ + ```+ # Λ-gent integrity failed:+ > 2d3c326e2991da5876b820fe128c3840d3c0664c43907ce725f3f3f26f2fbabc (expected)+ > 8f89a79f891ce329a6142c68962fcad0b438e7d0b80c9410f0141cc6c02437d0 (observed)+ ```++* Adding `uuid` to main context future purposes.++* Due to refactoring, the context lost a few properties (ex: `exit`, `hist`,+ `root`, …) and some of the internals have changed. Still same output, but,+ less chaotic. Furthermore, `rootPath` is no longer needed.++* Due to refactoring, some of the exposed utils are no longer+ relevant. Therefore, they are moved to an internal module.++* Removed quotes from `curl` headers as some `LLM` API's would parse them, but+ others would not.++* Followed up on binary flags as we need to have the **least common** between+ `Linux` distros and `macOS`.++* Added the `/repo` command to help parse and store files received from LLM's+ `atomically` with help of `git worktree`.++* Added `message` and `action` types simplify and make more robust the+ interactions between user and `Λ-gent`.++* Wrapped `withExitCode…` functions in `try` (`Control.Exception`) to ensure the+ `Λ-gents` don't crash and burn. Also wrapped `read` and `write` files as well+ with `try`.+ ## 0.11.0.11 -- 2026-04-19 * Fixed a few misspells. * Refactored `Files` to `FilePaths`. -* Refactored `Document(s)` to already existing `File(s)` for simplicity+* Refactored `Document(s)` to already existing `File(s)` for simplicity. * Excluding non-files (symbolic links) from search as this could bypass the- limitation of the current working directory+ limitation of the current working directory. ## 0.11.0.10 -- 2026-04-17
README.md view
@@ -26,6 +26,57 @@ [oxford-gentleman]: https://www.oxfordlearnersdictionaries.com/definition/english/gentleman [llm-wiki]: https://en.wikipedia.org/wiki/Large_language_model +Project structure+=================++```sh+├── doc+│ └── TODO.md+├── src+│ ├── Agent+│ │ ├── Control+│ │ │ ├── Concurrent.hs+│ │ │ ├── IFC.hs+│ │ │ └── MAC.hs+│ │ ├── Data+│ │ │ ├── ANSI+│ │ │ │ └── EscapeCode.hs+│ │ │ └── JSON.hs+│ │ ├── IO+│ │ │ ├── Effects.hs+│ │ │ └── Restricted.hs+│ │ └── LLM.hs+│ └── Internal+│ ├── GaloisInc+│ │ └── Text+│ │ ├── JSON+│ │ │ ├── Generic.hs+│ │ │ ├── String.hs+│ │ │ └── Types.hs+│ │ ├── JSON.hs+│ │ ├── LICENSE+│ │ └── NOTES.md+│ ├── GlasgowUniversity+│ │ ├── Data+│ │ │ └── Generics+│ │ │ └── Aliases.hs+│ │ ├── LICENSE+│ │ └── NOTES.md+│ ├── LLM.hs+│ ├── RIO.hs+│ └── Utils.hs+├── uat+│ └── TODO.md+├── A-gent.cabal+├── CHANGELOG.md+├── LICENSE.md+├── LICENSE_AGPL-3.0-only.txt+├── LICENSE_SSPL-1.0.txt+├── README.md+├── Setup.hs+└── todo.org+```+ License =======
src/Agent/Data/ANSI/EscapeCode.hs view
@@ -161,6 +161,7 @@ -> SelectGraphicRendition --------------------------------------------------------------------------------+ sgr = SGR bg fg bo fa it un bl where
src/Agent/IO/Effects.hs view
@@ -22,8 +22,11 @@ , -- * LLM (Code) LlmCodeRoot(llmCodeDir) , LlmCodeMask(llmCodeMsk)- , LlmCodeRead(llmCodeSeq, llmCodeGet)+ , LlmCodeTmpl(llmCodeIns, llmCodeExa)+ , LlmCodeRead(llmCodeSeq, llmCodeGet, llmCodeGit) , LlmCodeSave(llmCodePut)+ , LlmCodeConf(llmCodeKey, llmCodeAPI)+ , LlmCodePost(llmCodeWeb) , -- * LLM (Plan) LlmPlanRoot(llmPlanDir) , LlmPlanMask(llmPlanMsk)@@ -35,7 +38,6 @@ -------------------------------------------------------------------------------- - import qualified Internal.LLM as LLM --------------------------------------------------------------------------------@@ -91,6 +93,17 @@ -------------------------------------------------------------------------------- class+ Monad m+ => LlmCodeTmpl m+ where+ llmCodeIns+ :: m [LLM.File]+ llmCodeExa+ :: m [LLM.File]++--------------------------------------------------------------------------------++class ( LlmConf m , LlmCodeRoot m , LlmCodeMask m@@ -101,8 +114,10 @@ :: Maybe LLM.Filter -> m (Either [String] LLM.FilePaths) llmCodeGet- :: String+ :: LLM.AbsoluteFilePath -> m (Either String LLM.File)+ llmCodeGit+ :: m (Either String String) -------------------------------------------------------------------------------- @@ -113,8 +128,30 @@ => LlmCodeSave m where llmCodePut- :: m [ (String, String) ]- -> m [ (Either String ()) ]+ :: String+ -> LLM.Files+ -> m (Either String String)++--------------------------------------------------------------------------------++class+ Monad m+ => LlmCodeConf m+ where+ llmCodeAPI+ :: m (Maybe String)+ llmCodeKey+ :: m (Maybe String)++--------------------------------------------------------------------------------++class+ LlmCodeConf m+ => LlmCodePost m+ where+ llmCodeWeb+ :: String+ -> m (Either String String) -------------------------------------------------------------------------------- --------------------------------------------------------------------------------
src/Agent/IO/Restricted.hs view
@@ -39,7 +39,6 @@ module Agent.IO.Restricted ( RIO- -- , run -- * Environment , getEnvVar -- * LLM (Config)@@ -50,7 +49,11 @@ -- * LLM (Code) , llmCodeDir , llmCodeMsk- , llmCodeSeq, llmCodeGet+ , llmCodeIns, llmCodeExa+ , llmCodeSeq, llmCodeGet, llmCodeGit+ , llmCodePut+ , llmCodeKey, llmCodeAPI+ , llmCodeWeb -- * LLM (Plan) , llmPlanDir , llmPlanMsk@@ -70,10 +73,17 @@ ( llmChatAPI , llmChatKey , llmChatWeb+ , llmCodeAPI , llmCodeDir+ , llmCodeExa , llmCodeGet+ , llmCodeGit+ , llmCodeIns+ , llmCodeKey , llmCodeMsk+ , llmCodePut , llmCodeSeq+ , llmCodeWeb , llmPathCWD , llmPlanAPI , llmPlanDir
src/Agent/LLM.hs view
@@ -28,14 +28,20 @@ , History(..) , Context(..) -- * Paramenters+ , Action(..)+ , Message(..)+ , none+ , exit+ , file+ , root+ , text+ , paths+ , template , Eval -- * Methods , repl , replWithMode- , rootPath- -- * Helpers- , combineFiles- , getFile+ -- * DEBUG ) where @@ -49,7 +55,13 @@ ) import Data.Char ( toLower, toUpper )+import Data.Maybe ( fromMaybe ) import GHC.IO.Encoding ( setLocaleEncoding )+import System.Environment ( getProgName )+import qualified System.Environment.Blank as ENV+import System.Exit+ ( ExitCode (ExitFailure, ExitSuccess)+ ) import System.IO ( BufferMode (LineBuffering, NoBuffering) , hFlush@@ -60,28 +72,26 @@ , stdout , utf8 )+import System.Process+ ( readProcessWithExitCode+ ) import Text.Read ( readMaybe ) import Internal.GaloisInc.Text.JSON.Generic ( Data ) import Internal.LLM ( Chit (..) , History (..)- , Template (..)- , View (..) , modes ) import qualified Internal.LLM as INT import Internal.RIO ( RIO (..)- , findFiles , input , output- , readFileStrict )+import qualified Internal.Utils as UTL import qualified Agent.Data.ANSI.EscapeCode as AEC-import qualified Agent.Data.JSON as JSON-import qualified Agent.Utils.Common as COM -------------------------------------------------------------------------------- @@ -93,32 +103,102 @@ data Context a = Context- { exit :: !INT.Exit- , root :: !(Maybe INT.Root)+ { uuid :: !INT.UUID , mode :: !INT.Mode , load :: !(Load a) , hist :: !INT.History , list :: !(Maybe INT.Filter) , pile :: !(Maybe INT.FilePaths)- , file :: !(Maybe INT.File)+ , ruck :: !(Maybe INT.Files) } +data Message+ = Path !Bool !(Maybe INT.AbsoluteFilePath)+ | List !(Maybe INT.Filter)+ | Text !String+ | Send ![String]+ | TemplateFiles !(Maybe [INT.AbsoluteFilePath])+ | Root+ | Atom !String !INT.Files+ | Repo++instance Show Message where+ show (Path _ _) = "Path"+ show (List _) = "List"+ show (Text _) = "Text"+ show (Send _) = "Send"+ -- DEBUG+ show (TemplateFiles _) = "TemplateFiles"+ -- DEBUG+ show Root = "Root"+ -- DEBUG+ show (Atom _ _) = "Atom"+ -- DEBUG+ show Repo = "Repo"++type TextToFile = String -> [(FilePath, [INT.FileLine])]++data Action+ = None+ | Exit+ | File !INT.File+ | Hist !(Bool, Bool)+ | Help+ | Mode !Char !String+ | Paths !INT.FilePaths+ | Pile+ | Branch !String !INT.Files+ | Ruck !String+ | Wipe+ | UnknownCmd !String+ | Message !Message+ -- DEBUG+ | Prompt !String+ | Template !TextToFile ![INT.File] ![INT.File] ![INT.File]++-- DEBUG+none :: Action+none =+ None++exit :: Action+exit =+ Exit++file :: INT.File -> Action+file =+ File++root :: INT.Root -> Action+root (INT.Root afp) =+ Paths $ INT.FilePaths [afp]++text :: String -> Action+text =+ Message . Text++paths :: INT.FilePaths -> Action+paths =+ Paths++template+ :: TextToFile+ -> [INT.File]+ -> [INT.File]+ -> [INT.File]+ -> Action+template =+ Template+ type Eval a = Context a- -> String- -> RIO (Context a, String)+ -> Message+ -> RIO (Context a, Action) -------------------------------------------------------------------------------- -rootPath- :: RIO (Maybe INT.Root)- -> IO (Maybe INT.Root)-rootPath =- run- repl :: Eval a- -> Maybe INT.Root -> IO () repl = replWithMode INT.Chat@@ -126,56 +206,43 @@ replWithMode :: INT.Mode -> Eval a- -> Maybe INT.Root -> IO ()-replWithMode mod proc mroot =+replWithMode mod proc = do- setLocaleEncoding utf8- hSetBuffering stderr NoBuffering- hSetBuffering stdin NoBuffering- hSetBuffering stdout LineBuffering -- NOTE: logic based on `hFlush stdout`- -- NOTE: No default output when typing- hSetEcho stdin False- putStrLn head >> hFlush stdout- run $ loop ctx proc- where- ctx =- Context- { exit = INT.Exit False- , root = mroot- , mode = mod- , load = Nothing- , hist =- History- { view = []- , chit =+ constraint <- integrity+ case constraint of+ Right __ ->+ do+ setLocaleEncoding utf8+ hSetBuffering stderr NoBuffering+ hSetBuffering stdin NoBuffering+ -- NOTE: logic based on `hFlush stdout`+ hSetBuffering stdout LineBuffering+ -- NOTE: No default output when typing+ hSetEcho stdin False+ putStrLn head >> hFlush stdout+ run $ loop ctx proc+ Left err ->+ putStrLn err >> hFlush stdout+ where+ ctx =+ Context+ { uuid = INT.UUID []+ , mode = mod+ , load = Nothing+ , hist =+ History+ { chit = Chit { prev = [] , next = [] }- , chat = []- }- , list = Nothing- , pile = Just $ INT.FilePaths []- , file = Nothing- }-------------------------------------------------------------------------------------- HELPERS (public)--combineFiles- :: INT.FilePaths- -> INT.FilePaths- -> INT.FilePaths-combineFiles (INT.FilePaths xs) (INT.FilePaths ys) =- INT.FilePaths $ xs ++ ys--getFile- :: INT.File- -> (FilePath, [INT.FileLine])-getFile (INT.File f) =- f+ , chat = []+ }+ , list = Nothing+ , pile = Just $ INT.FilePaths []+ , ruck = Just $ INT.Files []+ } -------------------------------------------------------------------------------- @@ -189,292 +256,563 @@ sync . (:[]) <$> fork task -} +head :: String+head =+ "# Exit Λ-gent with /e or /exit. For more commands, type /? or /help."++help :: String+help =+ unlines+ [ "# Supported commands:"+ , ds+ , "/? or /help | This message"+ , "/e or /exit | Exit Λ-gent"+ , "/w or /wipe | Clear screen"+ , ds+ , "/m m or /mode m | Change to {" ++ ms ++ "}. Ex: /m plan"+ , ds+ , "/h or /hist | View history (input and output)"+ , " /chit | View history (only input) "+ , " /chat | View history (only output)"+ , ds+ , "/l f or /list f | List of files, limited by mode, file masks and filter"+ , "/p or /pile | Show stored list of files. See /list above"+ , "/f i or /file i | Show file with the given index. See /pile above"+ , "/n i or /nums i | Show file with line numbers. See /file above"+ , ds+ , "/s m or /send m | Sending /pile to LLM as context to the message"+ , "/r o or /ruck o | View files (opt index) from LLM response. See /send"+ , "/a d or /atom d | Atomically save /ruck files to a GIT branch (+ description)"+ , " /repo | List GIT branches (+ description). See /atom above"+ , ds+ ]+ where+ ds = replicate 80 '-'+ ms =+ foldl1 (\ x y -> x ++ "|" ++ y) $ map (map toLower . show) modes+ loop :: Context a -> Eval a -> RIO () loop ctx eval =- case ctx of- Context { exit = INT.Exit True } ->- return ()- Context { hist = History { view = [ Chits, Chats ] } } ->- printLn "* Chits:" >>- ( mapM_ printLn . COM.combine . COM.chits True . prev . chit . hist- ) ctx >>- printLn "* Chats:" >>- ( mapM_ printLn . COM.combine . COM.chats True . chat . hist- ) ctx >>- loop (ctx { hist = ((hist ctx) { view = [] }) }) eval- Context { hist = History { view = [ Chits ] } } ->- printLn "* Chits:" >>- ( mapM_ printLn . COM.combine . COM.chits True . prev . chit . hist- ) ctx >>- loop (ctx { hist = ((hist ctx) { view = [] }) }) eval- Context { hist = History { view = [ Chats ] } } ->- printLn "* Chats:" >>- ( mapM_ printLn . COM.combine . COM.chats True . chat . hist- ) ctx >>- loop (ctx { hist = ((hist ctx) { view = [] }) }) eval- Context { list = Just _, pile = Just _ } ->- eval ctx [ ] >>= \ (upd, _) ->- ( mapM_ printLn .- COM.combine .- COM.files [] True .- -- NOTE: Show relative paths- map (drop (length cwd)) .- concatMap INT.filePaths .- pile- ) upd >>- loop (upd { list = Nothing }) eval+ print prompt >>+ read hps hns >>= \ txt ->+ printLn [ ] >>+ case txt2act txt of+ -- TODO: Refactoring needed++ -- Nested -- Start+ None ->+ loop (nxt txt ctx Nothing) eval+ File _ ->+ loop (nxt txt ctx Nothing) eval+ Template _ _ _ _ ->+ loop (nxt txt ctx Nothing) eval+ -- Nested -- Stop++ Exit -> caseExit+ Help -> caseHelp txt+ Hist (chits, chats) -> caseHist txt chits chats+ Mode c cs -> caseMode txt c cs+ Paths (INT.FilePaths fps) -> casePaths txt fps+ Pile -> casePile txt+ Ruck mdix -> caseRuck txt mdix+ Wipe -> caseWipe txt+ UnknownCmd cmd -> caseUnknownCmd txt cmd++ Branch msg fs ->+ eval ctx (Atom msg fs) >>= \ (upd, action) ->+ case action of+ Message (Text cs) ->+ printLn cs >>+ loop (nxt txt upd (Just cs)) eval+ None ->+ loop (nxt txt upd Nothing) eval+ _________________________ ->+ -- NOTE: Unexpected error+ printLn "Unexpected error" >>+ loop (nxt txt upd (Just [])) eval++ Prompt msg ->++ eval ctx (TemplateFiles afps) >>= \ (upd0, action0) ->+ case action0 of+ Template f is es fs ->++ eval ctx (Send xml) >>= \ (upd1, action1) ->+ case action1 of+ Message (Text cs) ->+ printLn cs >>+ loop+ ( nxt txt+ ( upd1 { ruck = Just $ pfs }+ )+ ( Just cs+ )+ ) eval+ where+ pfs =+ case (map INT.File . f) cs of+ [] -> INT.Files []+ xs -> INT.Files xs+ None ->+ loop (nxt txt upd1 Nothing) eval+ _________________________ ->+ -- NOTE: Unexpected error+ printLn "Unexpected error" >>+ loop (nxt txt upd1 (Just [])) eval+ where+ ils = map ( \ (INT.File (_, ls)) -> unlines ls) is+ els = map ( \ (INT.File (_, ls)) -> unlines ls) es+ tpl = INT.Template msg ils els fs+ xml = INT.tpl2xmls tpl++ Message (Text cs) ->+ printLn cs >>+ loop (nxt txt upd0 (Just cs)) eval+ None ->+ loop (nxt txt upd0 Nothing) eval+ _________________________ ->+ -- NOTE: Unexpected error+ printLn "Unexpected error" >>+ loop (nxt txt upd0 (Just [])) eval where- cwd =- case root ctx of- Just (INT.Root cs) -> cs- Nothing -> []- Context { file = Just _ } ->- eval ctx [ ] >>= \ (upd, res) ->- printLn res >>- loop (upd { file = Nothing }) eval- __________________________________________ ->- print prompt >>- read hps hns >>= \ txt ->- printLn [ ] >>+ afps =+ ( \ (INT.FilePaths fps) -> map INT.AbsoluteFilePath fps+ )+ <$> pile ctx++ Message (Path ns afp) ->+ eval ctx (Path ns afp) >>= \ (upd, action) ->+ case action of+ File (INT.File (_,ls)) ->+ caseFile txt ns ls+ Message (Text cs) ->+ printLn cs >>+ loop (nxt txt upd (Just cs)) eval+ None ->+ loop (nxt txt upd Nothing) eval+ _________________________ ->+ -- NOTE: Unexpected error+ printLn "Unexpected error" >>+ loop (nxt txt upd (Just [])) eval++ Message (List fil) ->+ eval ctx (List fil) >>= \ (upd, action) ->+ case action of+ Paths (INT.FilePaths fps) ->+ casePaths txt fps+ Message (Text cs) ->+ printLn cs >>+ loop (nxt txt upd (Just cs)) eval+ None ->+ loop (nxt txt upd Nothing) eval+ _________________________ ->+ -- NOTE: Unexpected error+ printLn "Unexpected error" >>+ loop (nxt txt upd (Just [])) eval++ Message Repo ->+ eval ctx Repo >>= \ (upd, action) ->+ case action of+ Message (Text cs) ->+ printLn cs >>+ loop (nxt txt upd (Just cs)) eval+ None ->+ loop (nxt txt upd Nothing) eval+ _________________________ ->+ -- NOTE: Unexpected error+ printLn "Unexpected error" >>+ loop (nxt txt upd (Just [])) eval++ Message msg ->+ eval ctx msg >>= \ (upd, action) ->+ case action of+ Message (Text cs) ->+ printLn cs >>+ loop (nxt txt upd (Just cs)) eval+ None ->+ loop (nxt txt upd Nothing) eval+ _________________________ ->+ -- NOTE: Unexpected error+ printLn "Unexpected error" >>+ loop (nxt txt upd (Just [])) eval+ where++ -- TODO: Refactoring needed+ txt2act txt =+ -- TODO: Refactoring needed case txt of- '/':'m':'o':'d':'e':' ':c:cs -> caseMode txt c cs- '/':'m' :' ':c:cs -> caseMode txt c cs- '/':'s':'e':'n':'d':' ':mesg -> caseSend txt mesg- '/':'s' :' ':mesg -> caseSend txt mesg- '/':'f':'i':'l':'e' :midx -> caseFile txt midx False- '/':'f' :midx -> caseFile txt midx False- '/':'n':'u':'m':'s' :midx -> caseFile txt midx True- '/':'n' :midx -> caseFile txt midx True- '/':'l':'i':'s':'t' :mfil -> caseList txt mfil- '/':'l' :mfil -> caseList txt mfil- "/pile" -> casePile txt- "/p" -> casePile txt- "/hist" -> caseHist txt [ Chits, Chats ]- "/h" -> caseHist txt [ Chits, Chats ]- "/chit" -> caseHist txt [ Chits ]- "/chat" -> caseHist txt [ Chats ]- "/wipe" -> caseWipe txt- "/w" -> caseWipe txt- "/help" -> caseHelp txt- "/?" -> caseHelp txt- "/exit" -> caseExit txt- "/e" -> caseExit txt- '/':cmd ->- printLn msg >>- loop (nxt txt ctx Nothing) eval++ '/':'m':'o':'d':'e':' ':c:cs -> Mode c cs+ '/':'m' :' ':c:cs -> Mode c cs++ '/':'l':'i':'s':'t' :mfil ->+ -- TODO: DRY+ Message (List fil) where- msg = "Command not recognized: " ++ cmd- ____________________________ ->- eval ctx xml >>= \ (upd, res) ->- printLn res >>- loop (nxt txt upd (Just res)) eval+ fil =+ case mfil of+ [ ] -> Just $ INT.Filter mfil+ ' ':cs -> Just $ INT.Filter cs+ ______ -> Nothing+ '/':'l' :mfil ->+ -- TODO: DRY+ Message (List fil) where- tpl =- Template- { purpose = txt- , instructions = []- , examples = []- , files = []- }- xml = INT.tpl2xml tpl- where- his = hist ctx- chi = chit his- hps = prev chi- hns = next chi- nxt p c o =- c- { hist =- (hist c)- { chit =- (chit $ hist c)- { prev = p : hps- }- , chat =- case o of- Nothing -> chat $ hist c- Just r -> (r:) $ chat $ hist c- }- }- caseMode txt c cs =- case mmod of- Just mod ->- printLn ("Changed to " ++ low ++ "-mode") >>- loop (nxt txt (ctx { mode = mod, pile = Nothing }) Nothing) eval+ fil =+ case mfil of+ [ ] -> Just $ INT.Filter mfil+ ' ':cs -> Just $ INT.Filter cs+ ______ -> Nothing++ '/':'f':'i':'l':'e':' ':midx ->+ -- TODO: DRY+ case oidx of+ Nothing ->+ Message (Path False Nothing)+ Just idx ->+ if 0 <= idx && idx < length pfs then+ Message (Path False (Just $ INT.AbsoluteFilePath $ pfs !! idx))+ else+ Message (Path False Nothing) where- low = map toLower $ show mod- Nothing ->- printLn ("Invalid mode: " ++ c:cs) >>- loop (nxt txt ctx Nothing) eval+ pfs = concatMap INT.filePaths $ pile ctx where- mmod = readMaybe (toUpper c : map toLower cs) :: Maybe INT.Mode- caseSend txt msg =- pds >>= \ ds ->- tis >>= \ is ->- eval ctx (xml ds (concat is)) >>= \ (upd, res) ->- printLn res >>- loop (nxt txt upd (Just res)) eval+ oidx =+ case midx of+ [] -> Nothing+ cs -> readMaybe cs :: Maybe Int+ '/':'f' :' ':midx ->+ -- TODO: DRY+ case oidx of+ Nothing ->+ Message (Path False Nothing)+ Just idx ->+ if 0 <= idx && idx < length pfs then+ Message (Path False (Just $ INT.AbsoluteFilePath $ pfs !! idx))+ else+ Message (Path False Nothing)+ where+ pfs = concatMap INT.filePaths $ pile ctx where- cwd =- case root ctx of- Just (INT.Root cs) -> cs- Nothing -> []- pds =- ( mapM- ( \ fap ->- INT.File .- (,) (drop (length cwd) fap) .- lines- <$> readFileStrict fap- ) .- concatMap INT.filePaths .- pile- ) ctx-- tis =- -- TODO: This need to be refactored- findFiles cwd ("*/tpl/" ++ mod ++ "_instructions_*.json") >>= \ efs ->- case efs of- Right aps ->- mapM (\ ap -> dec <$> readFileStrict ap) aps- Left ___ ->- pure []+ oidx =+ case midx of+ [] -> Nothing+ cs -> readMaybe cs :: Maybe Int+ '/':'n':'u':'m':'s':' ':midx ->+ -- TODO: DRY+ case oidx of+ Nothing ->+ Message (Path True Nothing)+ Just idx ->+ if 0 <= idx && idx < length pfs then+ Message (Path True (Just $ INT.AbsoluteFilePath $ pfs !! idx))+ else+ Message (Path True Nothing) where- mod = (map toLower . show . mode) ctx- dec x =- case JSON.decode x :: Either JSON.DecodeError [String] of- Right xs -> xs- Left __ -> []- tpl fs is =- Template- { purpose = msg- , instructions = is- , examples =- -- TODO: Look in {root}/tpl/{mode}_examples_*.md- []- , files = fs+ pfs = concatMap INT.filePaths $ pile ctx+ where+ oidx =+ case midx of+ [] -> Nothing+ cs -> readMaybe cs :: Maybe Int+ '/':'n' :' ':midx ->+ -- TODO: DRY+ case oidx of+ Nothing ->+ Message (Path True Nothing)+ Just idx ->+ if 0 <= idx && idx < length pfs then+ Message (Path True (Just $ INT.AbsoluteFilePath $ pfs !! idx))+ else+ Message (Path True Nothing)+ where+ pfs = concatMap INT.filePaths $ pile ctx+ where+ oidx =+ case midx of+ [] -> Nothing+ cs -> readMaybe cs :: Maybe Int++ '/':'s':'e':'n':'d':' ':mesg -> Prompt mesg+ '/':'s' :' ':mesg -> Prompt mesg++ '/':'a':'t':'o':'m':' ':mesg ->+ -- TODO: DRY+ Branch mesg fs+ where+ fs = fromMaybe (INT.Files []) (ruck ctx)+ '/':'a' :' ':mesg ->+ -- TODO: DRY+ Branch mesg fs+ where+ fs = fromMaybe (INT.Files []) (ruck ctx)++ '/':'r':'e':'p':'o' :[ ] -> Message Repo++ '/':'r':'u':'c':'k' :midx -> Ruck midx+ '/':'r' :midx -> Ruck midx++ "/exit" -> Exit+ "/e" -> Exit+ "/help" -> Help+ "/?" -> Help+ "/hist" -> Hist ( True, True )+ "/h" -> Hist ( True, True )+ "/chit" -> Hist ( True, False )+ "/chat" -> Hist ( False, True )+ "/pile" -> Pile+ "/p" -> Pile+ "/wipe" -> Wipe+ "/w" -> Wipe++ '/':cmd -> UnknownCmd cmd+ ____________________________ -> Message (Text txt)++ -- TODO: Refactoring needed+ his = hist ctx+ chi = chit his+ hps = prev chi+ hns = next chi+ nxt p c o =+ c+ { hist =+ (hist c)+ { chit =+ (chit $ hist c)+ { prev = p : hps }- xml xs ys = INT.tpl2xml $ tpl xs ys+ , chat =+ case o of+ Nothing -> chat $ hist c+ Just r -> (r:) $ chat $ hist c+ }+ } - caseFile txt midx nums =+ -- TODO: Refactoring needed+ caseMode txt c cs =+ case mmod of+ Just mod ->+ printLn ("Changed to " ++ low ++ "-mode") >>+ loop+ ( (nxt txt ctx Nothing)+ { mode = mod+ , pile = Just $ INT.FilePaths []+ , ruck = Just $ INT.Files []+ }+ ) eval+ where+ low = map toLower $ show mod+ Nothing ->+ printLn ("Invalid mode: " ++ c:cs) >>+ loop ctx eval+ where+ mmod = readMaybe (toUpper c : map toLower cs) :: Maybe INT.Mode++ caseHist txt chits chats =+ ( if chits then+ printLn "* Chits:" >>+ ( mapM_ printLn . UTL.combine . UTL.chits True . prev . chit . hist+ ) ctx+ else+ pure ()+ ) >>+ ( if chats then+ printLn "* Chats:" >>+ ( mapM_ printLn . UTL.combine . UTL.chats True . chat . hist+ ) ctx+ else+ pure ()+ ) >>+ loop (nxt txt ctx Nothing) eval++ caseExit =+ printLn "Λ-gent will shutdown" >>+ return ()+ caseFile txt ns ls =+ ( mapM_ printLn .+ UTL.combine .+ UTL.file ns+ ) (unlines ls) >>+ loop (nxt txt ctx Nothing) eval+ caseHelp txt =+ printLn help >>+ loop (nxt txt ctx Nothing) eval++ casePaths txt fps =+ eval ctx Root >>= \ (upd, action) ->+ case action of+ Paths (INT.FilePaths rfps) ->+ ( case rfps of+ [cwd] ->+ mapM_ printLn .+ UTL.combine .+ UTL.files [] True .+ -- NOTE: Show relative paths+ map (drop (length cwd)) $+ fps+ _____ ->+ printLn "Missing root path"+ ) >>+ loop+ ( (nxt txt upd Nothing)+ { pile = Just $ INT.FilePaths fps+ }+ ) eval+ Message (Text cs) ->+ printLn cs >>+ loop (nxt txt upd (Just cs)) eval+ None ->+ loop (nxt txt upd Nothing) eval+ _________________________ ->+ -- NOTE: Unexpected error+ printLn "Unexpected error" >>+ loop (nxt txt upd (Just [])) eval++ casePile txt =+ eval ctx Root >>= \ (upd, action) ->+ case action of+ Paths (INT.FilePaths fps) ->+ ( case fps of+ [cwd] ->+ mapM_ printLn .+ UTL.combine .+ UTL.files [] True .+ -- NOTE: Show relative paths+ map (drop (length cwd)) .+ concatMap INT.filePaths .+ pile $+ upd+ _____ ->+ printLn "Missing root path"+ ) >>+ loop (nxt txt upd Nothing) eval+ Message (Text cs) ->+ printLn cs >>+ loop (nxt txt upd (Just cs)) eval+ None ->+ loop (nxt txt upd Nothing) eval+ _________________________ ->+ -- NOTE: Unexpected error+ printLn "Unexpected error" >>+ loop (nxt txt upd (Just [])) eval++ caseRuck txt midx =+ ( if null midx then+ -- NOTE: If no optional index provided, show only paths+ ( mapM_ printLn .+ UTL.combine .+ UTL.files [] True .+ concatMap fps .+ ruck+ ) ctx+ else+ -- NOTE: Otherwise, show content or index error ( case idx of Just i ->- if 0 <= i && i < length pfs then- let- cwd =- case root ctx of- Just (INT.Root cs) -> cs- Nothing -> []- fap = pfs !! i- in- readFileStrict fap >>= \ f ->- ( mapM_ printLn .- COM.combine .- COM.file nums- ) f >>- loop- ( nxt txt- ( ctx- { file =- Just- ( INT.File- -- NOTE: Store relative path- ( drop (length cwd) fap- , lines f- )- )- }- ) Nothing- ) eval+ if 0 <= i && i < len then+ ( mapM_ printLn .+ UTL.combine .+ UTL.file True .+ concatMap f .+ ruck+ ) ctx else let m = "Index (" ++ show i ++ ") is out of bounds." in- printLn m >>- loop (nxt txt ctx Nothing) eval+ printLn m+ where+ f (INT.Files fs) =+ case fs !! i of+ INT.File (_,ls) -> unlines ls Nothing ->- printLn "Invalid index" >>- loop (nxt txt ctx Nothing) eval+ printLn "Invalid use of /ruck. Use space between cmd and index" )- where- pfs = concatMap INT.filePaths $ pile ctx- idx =- case midx of- [ ] -> Nothing- ' ':cs -> readMaybe cs :: Maybe Int- ______ -> Nothing- caseList txt mfil =- ( case fil of- Nothing ->- printLn "Invalid use of list. Use space between cmd and filter"- _______ ->- pure ()- ) >>- loop (nxt txt (ctx { list = fil }) Nothing) eval- where- fil =- case mfil of- [ ] -> Just $ INT.Filter mfil- ' ':cs -> Just $ INT.Filter cs- ______ -> Nothing- casePile txt =- ( mapM_ printLn .- COM.combine .- COM.files [] True .- -- NOTE: Show relative paths- map (drop (length cwd)) .- concatMap INT.filePaths .- pile- ) ctx >>- loop (nxt txt ctx Nothing) eval- where- cwd =- case root ctx of- Just (INT.Root cs) -> cs- Nothing -> []- caseHist txt v =- loop (nxt txt (ctx { hist = his { view = v } }) Nothing) eval- caseWipe txt =- printLn clear >>- loop (nxt txt ctx Nothing) eval- caseHelp txt =- printLn help >>- loop (nxt txt ctx Nothing) eval- caseExit txt =- printLn "Λ-gent will shutdown" >>- loop (nxt txt (ctx { exit = INT.Exit True }) Nothing) eval- where- -- NOTE: Clear screen & Move top-left- clear = "\^[[H\^[[2J" -- NOTE: See `infocmp -x`- prompt =- (show . AEC.bold . AEC.sgr)- ("Λ-" ++ (map toLower . show . mode) ctx ++ "> ")- read = input- print = output . show . AEC.faint . AEC.sgr- printLn x = print $ x ++ "\n"+ ) >>+ loop (nxt txt ctx Nothing) eval+ where+ idx =+ case midx of+ [ ] -> Nothing+ ' ':cs -> readMaybe cs :: Maybe Int+ ______ -> Nothing+ len =+ case ruck ctx of+ Just (INT.Files fs) -> length fs+ Nothing -> 0+ fps (INT.Files fs) =+ map ( \ (INT.File (p,_)) -> p) fs -head :: String-head =- "# Exit Λ-gent with /e or /exit. For more commands, type /? or /help."+ caseWipe txt =+ printLn clear >>+ loop (nxt txt ctx Nothing) eval+ caseUnknownCmd txt cmd =+ printLn msg >>+ loop (nxt txt ctx Nothing) eval+ where+ msg = "Command not recognized: " ++ cmd -help :: String-help =- "# Supported commands:\n" ++- "/? or /help | This message\n" ++- "/e or /exit | Exit Λ-gent\n" ++- "/w or /wipe | Clear screen\n" ++- "/m m or /mode m | Change to {" ++ ms ++ "}. Ex: /m code\n" ++- "/h or /hist | View chit-chat (input and output) history\n" ++- " /chit | View chit (input) history\n" ++- " /chat | View chat (output) history\n" ++- "/l f or /list f | List of files, limited by mode, file masks and filter\n" ++- "/p or /pile | Show stored list of files. See /list above\n" ++- "/f i or /file i | Show file with the given index. See, /pile above\n" ++- "/n i or /nums i | Show file with line numbers. See /file above\n" ++- "/s m or /send m | Sending /pile to LLM as context to the message"+ -- NOTE: Clear screen & Move top-left+ clear = "\^[[H\^[[2J" -- NOTE: See `infocmp -x`+ prompt =+ (show . AEC.bold . AEC.sgr)+ ("Λ-" ++ (map toLower . show . mode) ctx ++ "> ")+ read = input+ print = output . show . AEC.faint . AEC.sgr+ printLn x = print $ x ++ "\n"++integrity :: IO (Either String ())+integrity =+ do+ ohi <- ENV.getEnv "LLM_SIGN_SHA"+ case ohi of+ Nothing -> pure $ Right ()+ Just hi ->+ do+ op <- cwd+ pn <- getProgName+ case op of+ Left e -> pure $ Left e+ Right p ->+ do+ ehf <- sha ap+ case ehf of+ Left ef -> pure $ Left ef+ Right hf ->+ if hi == h64 then+ pure $ Right ()+ else+ pure $+ Left $+ "# Λ-gent integrity failed:\n" +++ "> " ++ hi ++ " (expected)\n" +++ "> " ++ h64 ++ " (observed)"+ where+ h64 = take 64 hf+ where+ ap =+ concatMap id+ $ lines p ++ [ "/" ] ++ lines pn where- ms =- foldl1 (\ x y -> x ++ "|" ++ y) $ map (map toLower . show) modes+ -- TODO: Move "HELPERS (private)" private out from Internal/RIO.hs to+ -- Internal/IO.hs and then just wrap them with `RestrictedIO`. This will+ -- allow for logic like this to be simplified+ cwd =+ do+ (exitcode, out, err) <- readProcessWithExitCode "pwd" [] []+ case exitcode of+ ExitSuccess ->+ pure $ Right out+ ExitFailure _ ->+ pure $ Left err+ sha p =+ do+ (exitcode, out, err) <- readProcessWithExitCode "sha256sum" [ p ] []+ case exitcode of+ ExitSuccess ->+ pure $ Right out+ ExitFailure _ ->+ pure $ Left err
− src/Agent/Utils/Common.hs
@@ -1,133 +0,0 @@-{-# OPTIONS_GHC -Wall -Werror #-}--{-# LANGUAGE NoGeneralizedNewtypeDeriving #-}-{-# LANGUAGE Safe #-}--{-# LANGUAGE DeriveDataTypeable #-}-------------------------------------------------------------------------------------- |--- Copyright : (c) 2026 SPISE MISU ApS--- License : SSPL-1.0 OR AGPL-3.0-only--- Maintainer : SPISE MISU <mail+hackage@spisemisu.com>--- Stability : experimental------------------------------------------------------------------------------------module Agent.Utils.Common- ( -- * Files- files- , file- -- * History- , chits- , chats- -- * Helpers- , combine- , leftpad- , index- , split- , linesOptNums- )-where------------------------------------------------------------------------------------import Prelude hiding ( lines )-import qualified Prelude as Prelude------------------------------------------------------------------------------------combine- :: [ (String, String) ]- -> [ String ]-combine =- map- ( \ (x, y) ->- if null x then- y- else- x ++ ": " ++ y- )--leftpad- :: Char- -> Int- -> [Char]- -> [Char]-leftpad chr rep cs =- replicate (rep - length xs) chr ++ xs- where- xs = take rep cs--index- :: Int- -> Int- -> String-index rep idx =- leftpad '0' rep $ show idx--split- :: Char- -> String- -> [String]-split sep str =- case dropWhile (== sep) str of- [] -> []- cs ->- b : split sep bs- where- (b, bs) = break (== sep) cs------------------------------------------------------------------------------------files- :: String- -> Bool- -> [ String ]- -> [ (String, String) ]-files prefix nums fs =- linesOptNums prefix nums 0 fs--file- :: Bool- -> String- -> [ (String, String) ]-file nums f =- linesOptNums [] nums 1 $ Prelude.lines f------------------------------------------------------------------------------------chits- :: Bool- -> [ String ]- -> [ (String, String) ]-chits nums fs =- linesOptNums [] nums 0 $ reverse fs--chats- :: Bool- -> [ String ]- -> [ (String, String) ]-chats nums fs =- linesOptNums [] nums 0 $ reverse fs-------------------------------------------------------------------------------------- HELPERS (private)--linesOptNums- :: String- -> Bool- -> Int- -> [ String ]- -> [ (String, String) ]-linesOptNums prefix nums origo xs =- zipWith (\ idx rel -> (prefix ++ f idx, rel)) [origo..] xs- where- len = length $ show $ length xs- f i =- if nums then- index len i- else- []
src/Internal/LLM.hs view
@@ -14,29 +14,31 @@ -------------------------------------------------------------------------------- module Internal.LLM- ( Exit (..)+ ( UUID (..) , Root (..) , Filter (..) , Mode (..)- , View (..) , Chit (..) , Chat , History (..) , Mask (..) , File (..)+ , Files (..) , FileLine , FilePaths (..)+ , AbsoluteFilePath (..) , Template (..) , modes- , tpl2xml+ , tpl2xmls ) where --------------------------------------------------------------------------------+-------------------------------------------------------------------------------- -newtype Exit = Exit Bool+newtype UUID = UUID String -newtype Root = Root FilePath+newtype Root = Root FilePath deriving Eq newtype Filter = Filter String @@ -50,10 +52,6 @@ | Test deriving (Bounded, Enum, Eq, Read, Show) -data View- = Chits- | Chats- data Chit = Chit { prev :: ![String]@@ -64,16 +62,17 @@ data History = History- { view :: ![View]- , chit :: !Chit+ { chit :: !Chit , chat :: !Chat } --------------------------------------------------------------------------------+-------------------------------------------------------------------------------- newtype Mask = Mask [String] --------------------------------------------------------------------------------+-------------------------------------------------------------------------------- type Markdown = String @@ -81,8 +80,36 @@ newtype File = File (FilePath, [FileLine]) -newtype FilePaths = FilePaths { filePaths :: [FilePath] }+-------------------------------------------------------------------------------- +newtype Files = Files [File]++instance Semigroup Files where+ Files xs <> Files ys =+ Files (xs ++ ys)++instance Monoid Files where+ mempty = Files []++--------------------------------------------------------------------------------++newtype AbsoluteFilePath = AbsoluteFilePath FilePath++--------------------------------------------------------------------------------++-- NOTE: Then use `mconcat` to combine++data FilePaths = FilePaths { filePaths :: [FilePath] }++instance Semigroup FilePaths where+ FilePaths xs <> FilePaths ys =+ FilePaths (xs ++ ys)++instance Monoid FilePaths where+ mempty = FilePaths []++--------------------------------------------------------------------------------+ data Template = Template { purpose :: !String@@ -92,41 +119,43 @@ } --------------------------------------------------------------------------------+-------------------------------------------------------------------------------- modes :: [Mode] modes = [ minBound .. maxBound ] -tpl2xml+tpl2xmls :: Template- -> String-tpl2xml tpl =- ( if eis && ees && efs then- pur- else- "<purpose>" ++ pur ++ "</purpose>"- ) ++- ( if eis then- []- else- "<instructions>" ++- tis ++- "</instructions>"- ) ++- ( if ees then- []- else- "<examples>" ++- tes ++- "</examples>"- ) ++- ( if efs then- []- else- "<files>" ++- concatMap aux tfs ++- "</files>"- )+ -> [String]+tpl2xmls tpl =+ [ ( if eis && ees && efs then+ pur+ else+ "<purpose>" ++ pur ++ "</purpose>"+ ) +++ ( if eis then+ []+ else+ "<instructions>" +++ tis +++ "</instructions>"+ ) +++ ( if ees then+ []+ else+ "<examples>" +++ tes +++ "</examples>"+ ) +++ ( if efs then+ []+ else+ "<files>" +++ concatMap aux tfs +++ "</files>"+ )+ ] where pur = purpose tpl ++@@ -138,12 +167,12 @@ ( if ees then [] else- ". Output MUST match provided `examples`"+ ". Output MUST match scaffolding from provided `examples`" ) ++ ( if efs then [] else- ". List of provided `files` for context"+ ". Apply to each of the `files`" ) eis = null tis ees = null tes
src/Internal/RIO.hs view
@@ -58,7 +58,11 @@ -- * LLM (Code) , llmCodeDir , llmCodeMsk- , llmCodeSeq, llmCodeGet+ , llmCodeIns, llmCodeExa+ , llmCodeSeq, llmCodeGet, llmCodeGit+ , llmCodePut+ , llmCodeKey, llmCodeAPI+ , llmCodeWeb -- * LLM (Plan) , llmPlanDir , llmPlanMsk@@ -75,7 +79,9 @@ -------------------------------------------------------------------------------- -import Data.Either ( partitionEithers )+import Control.Exception ( SomeException, try )+import Data.Char ( isDigit )+import Data.Either ( fromLeft, partitionEithers ) import Data.List ( isPrefixOf ) import Data.Maybe ( maybeToList ) import qualified System.Environment.Blank as ENV@@ -103,9 +109,9 @@ import Text.Read ( readMaybe ) import qualified Internal.LLM as LLM+import qualified Internal.Utils as UTL import qualified Agent.IO.Effects as EFF-import qualified Agent.Utils.Common as COM -------------------------------------------------------------------------------- @@ -573,7 +579,7 @@ -- NOTE: Other escaped sequences (skip unless positions) aux mln hps hns bcs acs where- ops = map readMaybe $ COM.split '\^[' ps :: [Maybe Position]+ ops = map readMaybe $ UTL.split '\^[' ps :: [Maybe Position] mln = -- NOTE: 0) Print "\^[[6n" position before action (unused) -- NOTE: 1) Print "\^[[6n" end position after action@@ -672,14 +678,12 @@ => LlmConf m where llmPathCWD- -- :: m (Maybe String) :: m (Maybe LLM.Root) instance EFF.LlmConf RIO => LlmConf RIO where- -- llmPathCWD = getEnvVar "LLM_PATH_CWD" llmPathCWD = ( \ case Just rp -> Just $ LLM.Root rp@@ -762,6 +766,69 @@ -------------------------------------------------------------------------------- class+ EFF.LlmCodeTmpl m+ => LlmCodeTmpl m+ where+ llmCodeIns+ :: m [LLM.File]+ llmCodeExa+ :: m [LLM.File]++instance+ ( EFF.LlmConf RIO+ , EFF.LlmCodeTmpl RIO+ )+ => LlmCodeTmpl RIO+ where+ llmCodeIns =+ EFF.llmPathCWD >>= \ ocwd ->+ case ocwd of+ Just (LLM.Root root) ->+ realPath (root ++ "llm") >>= \ efap ->+ case efap of+ -- TODO: DRY+ Right fap ->+ findFiles fap "*code_instructions_*.json" >>= \ eafps ->+ case eafps of+ Right afps ->+ mapM+ ( \ abspath ->+ LLM.File . (,) abspath . lines+ <$> readFileStrict abspath+ ) afps+ Left __ ->+ pure []+ Left ____ ->+ pure []+ Nothing ->+ pure []++ llmCodeExa =+ EFF.llmPathCWD >>= \ ocwd ->+ case ocwd of+ Just (LLM.Root root) ->+ realPath (root ++ "llm") >>= \ efap ->+ case efap of+ -- TODO: DRY+ Right fap ->+ findFiles fap "*code_examples_*.md" >>= \ eafps ->+ case eafps of+ Right afps ->+ mapM+ ( \ abspath ->+ LLM.File . (,) abspath . lines+ <$> readFileStrict abspath+ ) afps+ Left __ ->+ pure []+ Left ____ ->+ pure []+ Nothing ->+ pure []++--------------------------------------------------------------------------------++class ( EFF.LlmConf m , EFF.LlmCodeRoot m , EFF.LlmCodeMask m@@ -772,8 +839,10 @@ :: Maybe LLM.Filter -> m (Either [String] LLM.FilePaths) llmCodeGet- :: String+ :: LLM.AbsoluteFilePath -> m (Either String LLM.File)+ llmCodeGit+ :: m (Either String String) instance ( EFF.LlmConf RIO@@ -786,15 +855,15 @@ EFF.llmPathCWD >>= \ ocwd -> case ocwd of Just (LLM.Root root) ->- EFF.llmCodeDir >>= \ cdir ->- EFF.llmCodeMsk >>= \ cmsk ->- realPath (root ++ "/" ++ cdir) >>= \ efap ->+ EFF.llmCodeDir >>= \ cdir ->+ EFF.llmCodeMsk >>= \ cmsk ->+ realPath (root ++ cdir) >>= \ efap -> case efap of -- TODO: DRY Right fap -> ( \ case- ([], rs) -> Right $ LLM.FilePaths $ concat rs- (ls, __) -> Left ls+ ([], rs) -> Right $ LLM.FilePaths $ mconcat rs+ (ls, __) -> Left ls ) . partitionEithers <$> mapM (\ msk -> findFiles fap ("*" ++ fil ++ "*" ++ msk)) cmsk@@ -808,29 +877,242 @@ Just (LLM.Filter f) -> f Nothing -> [ ] - llmCodeGet abspath =+ llmCodeGet (LLM.AbsoluteFilePath abspath) = EFF.llmPathCWD >>= \ ocwd -> EFF.llmCodeDir >>= \ cdir -> case ocwd of -- TODO: DRY Just (LLM.Root root) ->- realPath (root ++ "/" ++ cdir) >>= \ efap ->+ realPath (root ++ cdir) >>= \ efap -> case efap of Right fap -> if fap `isPrefixOf` abspath then- Right . LLM.File . (,) abspath . lines+ -- NOTE: Root relative path+ Right . LLM.File . (,) (drop len abspath) . lines <$> readFileStrict abspath else pure $ Left $ notPrefixRootAndMode fap abspath LLM.Code Left err -> pure $ Left err+ where+ len = length root Nothing -> pure $ Left noLlmConf + llmCodeGit =+ EFF.llmPathCWD >>= \ ocwd ->+ case ocwd of+ Just (LLM.Root root) ->+ realPath root >>= \ efap ->+ case efap of+ -- TODO: DRY+ Right fap ->+ gitExist fap >>= \ igit ->+ if igit then+ gitBranchesDesc fap+ else+ pure $ Left "No GIT repo initialized"+ Left err ->+ pure $ Left err+ Nothing ->+ pure $ Left noLlmConf+ --------------------------------------------------------------------------------++class+ ( EFF.LlmConf m+ , EFF.LlmCodeRoot m+ )+ => LlmCodeSave m+ where+ llmCodePut+ :: String+ -> LLM.Files+ -> m (Either String String)++instance+ ( EFF.LlmConf RIO+ , EFF.LlmCodeRoot RIO+ )+ => LlmCodeSave RIO+ where+ -- TODO: Needs some refactoring at some point …+ llmCodePut ___ (LLM.Files []) =+ pure $ Left "No files provided, therefore no action performed"+ llmCodePut txt (LLM.Files fs) =+ EFF.llmPathCWD >>= \ ocwd ->+ EFF.llmCodeDir >>= \ cdir ->+ case ocwd of+ Just (LLM.Root root) ->+ realPath root >>= \ erap ->+ case erap of+ Right rp ->+ gitExist rp >>= \ igit ->+ gitignoreTmp rp >>= \ itmp ->+ case (igit, itmp) of+ (True, True) ->+ timestampUTC >>= \ mts ->+ case mts of+ Just ts ->+ realPath (root ++ "tmp") >>= \ etmp ->+ case etmp of+ Right tmp ->+ gitWorktreeAdd root ts tap >>= \ ewta ->+ (+ mapM+ ( \ (LLM.File (fp, fls)) ->+ let+ fil = tap ++ "/" ++ fp+ len = length $ tap ++ "/"+ in+ if dir `isPrefixOf` fil then+ ensureFolderPath tap fil >>= \ eefp ->+ writeFileStrict fil (unlines fls) >>= \ ewfs ->+ case (eefp, ewfs) of+ (Right efp, Right wfs) ->+ pure $ Right $ unlines+ [ "* Ensuring folder exists:"+ , efp+ , "* Writing file to folder:"+ , wfs+ ]+ (_, _) ->+ pure $ Left $ unlines+ [ "* Ensuring folder exists:"+ , fromLeft "No error" eefp+ , "* Writing file to folder:"+ , fromLeft "No error" ewfs+ ]+ else+ pure $ Left $+ ( (drop len fil) +++ " doesn't start with " +++ (drop len dir)+ )+ )+ fs+ ) >>= \ ewfs ->+ gitAddFiles tap >>= \ ewaf ->+ gitCommit tap txt >>= \ ewcf ->+ gitWorktreeRem root tap >>= \ ewrf ->+ case+ ( ewta+ , partitionEithers ewfs+ , ewaf+ , ewcf+ , ewrf+ )+ of+ ( Right wta+ , ( []+ , xs+ )+ , Right ___+ , Right wcf+ , Right wrf+ ) ->+ gitUpdateBranchDesc root ts txt >>= \ ewtb ->+ case ewtb of+ Right _ ->+ pure $ Right $ unlines+ [ "# Temporary worktree branch:"+ , "## Adding:"+ , wta+ , "## Saving files:"+ , concat xs+ , "## Adding files and committing :"+ , wcf+ , "## Removing:"+ , wrf+ ]+ Left e -> pure $ Left e+ ( _, (es,__), _, _, _) ->+ gitUpdateBranchDesc root ts err >>= \ ewtb ->+ case ewtb of+ Right _ ->+ pure $ Left $ unlines+ [ "# Temporary worktree branch error(s):"+ , "## Adding:"+ , fromLeft "No error" ewta+ , "## Adding description:"+ , fromLeft "No error" ewtb+ , "## Saving files:"+ , concat es+ , "## Adding files:"+ , fromLeft "No error" ewaf+ , "## Committing:"+ , fromLeft "No error" ewcf+ , "## Removing:"+ , fromLeft "No error" ewrf+ ]+ Left e -> pure $ Left e+ where+ err = "[ERROR]: " ++ txt+ where+ tap = tmp ++ "/" ++ ts+ dir = tap ++ "/" ++ cdir+ Left e ->+ pure $ Left e+ _______ ->+ pure $ Left "No timestamp"+ (False, ____) ->+ pure $ Left "No GIT repo initialized"+ (____, False) ->+ pure $ Left "No /tmp/ folder added to the .gitignore file"+ Left err ->+ pure $ Left err+ Nothing ->+ pure $ Left noLlmConf+ -------------------------------------------------------------------------------- class+ ( EFF.LlmConf m+ , EFF.LlmCodeConf m+ )+ => LlmCodeConf m+ where+ llmCodeAPI+ :: m (Maybe String)+ llmCodeKey+ :: m (Maybe String)++instance+ ( EFF.LlmConf RIO+ , EFF.LlmCodeConf RIO+ )+ => LlmCodeConf RIO+ where+ llmCodeAPI = getEnvVar "LLM_CODE_LOCALHOST_API"+ llmCodeKey = pure Nothing++--------------------------------------------------------------------------------++class+ ( EFF.LlmConf m+ , EFF.LlmCodeConf m+ )+ => LlmCodePost m+ where+ llmCodeWeb+ :: String+ -> m (Either String String)++instance+ ( EFF.LlmConf RIO+ , EFF.LlmCodeConf RIO+ )+ => LlmCodePost RIO+ where+ llmCodeWeb json =+ EFF.llmCodeAPI >>= \ mapi ->+ EFF.llmCodeKey >>= \ mkey ->+ llmCurl json mapi mkey++--------------------------------------------------------------------------------+--------------------------------------------------------------------------------++class EFF.LlmConf m => LlmPlanRoot m where@@ -895,15 +1177,15 @@ EFF.llmPathCWD >>= \ ocwd -> case ocwd of Just (LLM.Root root) ->- EFF.llmPlanDir >>= \ pdir ->- EFF.llmPlanMsk >>= \ pmsk ->- realPath (root ++ "/" ++ pdir) >>= \ efap ->+ EFF.llmPlanDir >>= \ pdir ->+ EFF.llmPlanMsk >>= \ pmsk ->+ realPath (root ++ pdir) >>= \ efap -> case efap of -- TODO: DRY Right fap -> ( \ case- ([], rs) -> Right $ LLM.FilePaths $ concat rs- (ls, __) -> Left ls+ ([], rs) -> Right $ LLM.FilePaths $ mconcat rs+ (ls, __) -> Left ls ) . partitionEithers <$> mapM (\ msk -> findFiles fap ("*" ++ fil ++ "*" ++ msk)) pmsk@@ -923,16 +1205,19 @@ case ocwd of -- TODO: DRY Just (LLM.Root root) ->- realPath (root ++ "/" ++ pdir) >>= \ efap ->+ realPath (root ++ pdir) >>= \ efap -> case efap of Right fap -> if fap `isPrefixOf` abspath then- Right . LLM.File . (,) abspath . lines+ -- NOTE: Root relative path+ Right . LLM.File . (,) (drop len abspath) . lines <$> readFileStrict abspath else pure $ Left $ notPrefixRootAndMode fap abspath LLM.Plan Left err -> pure $ Left err+ where+ len = length root Nothing -> pure $ Left noLlmConf @@ -977,27 +1262,9 @@ => LlmPlanPost RIO where llmPlanWeb json =- EFF.llmPathCWD >>= \ ocwd ->- case ocwd of- Just (LLM.Root root) ->- realPath root >>= \ efap ->- case efap of- Right rp ->- gitExist rp >>= \ git ->- gitignoreTmp rp >>= \ tmp ->- case (git, tmp) of- (True, True) ->- EFF.llmPlanAPI >>= \ mapi ->- EFF.llmPlanKey >>= \ mkey ->- llmCurl json mapi mkey- (False, ____) ->- pure $ Left "No GIT repo initialized"- (____, False) ->- pure $ Left "No /tmp/ folder added to the .gitignore file"- Left err ->- pure $ Left err- Nothing ->- pure $ Left noLlmConf+ EFF.llmPlanAPI >>= \ mapi ->+ EFF.llmPlanKey >>= \ mkey ->+ llmCurl json mapi mkey -------------------------------------------------------------------------------- --------------------------------------------------------------------------------@@ -1021,15 +1288,29 @@ Left err ) <$> withExitCodeCwd path "find"+ -- NOTE: Use -ipath and -iname (case insensitive) instead? [ path- , "-path"- , mask -- NOTE: Limit to files, excluding symbolic links , "-type", "f"+ -- NOTE: Exclude tmp folder+ , "-not"+ , "-path"+ , path ++ "/tmp/*"+ , "-and"+ -- NOTE: Exclude llm tpl folder+ , "-not"+ , "-ipath"+ , path ++ "/llm/*"+ , "-and" -- NOTE: Exclude dot files and hereby tmp Emacs files (.*#) , "-not" , "-name" , ".*"+ , "-and"+ -- NOTE: Look for filter mask+ , "-path"+ , mask+ , "-print" ] getEnvVar@@ -1075,6 +1356,71 @@ where gi = path ++ "/.gitignore" +gitWorktreeAdd+ :: FilePath+ -> String+ -> FilePath+ -> RIO (Either String String)+gitWorktreeAdd root ts path =+ withExitCodeCwd root "git"+ [ "worktree"+ , "add", "-b", ts+ , path+ ]++gitAddFiles+ :: FilePath+ -> RIO (Either String String)+gitAddFiles path =+ withExitCodeCwd path "git"+ [ "add"+ , "."+ ]++gitCommit+ :: FilePath+ -> String+ -> RIO (Either String String)+gitCommit root mesg =+ withExitCodeCwd root "git"+ [ "commit"+ , "-m"+ , mesg+ ]++gitWorktreeRem+ :: FilePath+ -> FilePath+ -> RIO (Either String String)+gitWorktreeRem root path =+ withExitCodeCwd root "git"+ [ "worktree"+ , "remove"+ , path+ ]++gitBranchesDesc+ :: FilePath+ -> RIO (Either String String)+gitBranchesDesc root =+ withExitCodeCwd root "git"+ [ "config"+ , "--get-regexp"+ , "branch.*.description"+ ]++gitUpdateBranchDesc+ :: FilePath+ -> String+ -> String+ -> RIO (Either String String)+gitUpdateBranchDesc path ts desc =+ withExitCodeCwd path "git"+ [ "config"+ , "branch." ++ ts ++ ".description"+ , desc+ ]+ llmCurl :: String -> Maybe String@@ -1086,7 +1432,7 @@ RestrictedIO $ pure $ Left "No API address was provided" (Just api, Nothing) -> withExitCodeStdIn "curl"- ( [ --"--verbose"+ ( [ -- "--verbose" "--silent" , "--show-error" ]@@ -1102,7 +1448,7 @@ ( [ --"--verbose" "--silent" , "--show-error"- , "--header", "\"Authorization: \"" ++ key ++ "\""+ , "--header", "Authorization: " ++ key ] ++ hs ++ [ -- NOTE: https://curl.se/docs/manpage.html#--json@@ -1113,47 +1459,86 @@ json where hs =- [ "--header" , "\"Accept: application/json\""- , "--header" , "\"Accept-Encoding: gzip, deflate, br\""- , "--header" , "\"Content-Type: application/json; charset=utf-8\""- , "--header" , "\"User-Agent: Λ-gent/0.11\""+ [ "--header" , "Accept: application/json"+ , "--header" , "Accept-Encoding: gzip, deflate, br"+ , "--header" , "Content-Type: application/json; charset=utf-8"+ , "--header" , "User-Agent: Λ-gent/0.11" ] readFileStrict :: FilePath -> RIO String readFileStrict path =- -- NOTE: «Too Many Open Files» error in Linux+ -- TODO: --- -- - http://woshub.com/too-many-open-files-error-linux/+ -- Refactor `RIO String` to `RIO (Either String String)` RestrictedIO $- readFile path >>= \cs ->- length cs `seq` pure cs+ do+ eproc <-+ try+ ( aux+ ) :: IO (Either SomeException String)+ case eproc of+ Right txt -> pure txt+ Left ___ -> pure []+ where+ aux =+ readFile path >>= \cs ->+ length cs `seq` pure cs +writeFileStrict+ :: FilePath+ -> String+ -> RIO (Either String String)+writeFileStrict path txt =+ RestrictedIO $+ do+ eproc <-+ try+ ( writeFile path txt+ ) :: IO (Either SomeException ())+ case eproc of+ Right _ ->+ pure $ Right path+ Left e -> pure $ Left $ show e+ realPath :: FilePath -> RIO (Either String String) realPath path =+ -- NOTE: `realpath` removes any trailing '/' for directories+ -- -- NOTE: Remove any '\NUL` from filepaths -- https://gitlab.haskell.org/ghc/ghc/-/merge_requests/10110 ( \case- Right str -> Right $ filter (/= '\NUL') str+ Right str -> Right $ filter (/= '\n') str Left err -> Left err ) <$> withExitCode "realpath"- [ "--canonicalize-existing"- , "--logical"- , "--physical"- -- NOTE: End each output line with NUL, not newline- , "--zero"- , path+ [ path ] +ensureFolderPath+ :: FilePath+ -> FilePath+ -> RIO (Either String String)+ensureFolderPath root path =+ withExitCodeCwd root "dirname" [ path ] >>= \ eadp ->+ case eadp of+ Right adp ->+ withExitCodeCwd root "mkdir" [ "-p", foo ] >>= \ emeh ->+ case emeh of+ Right _ -> pure $ Right foo+ Left e -> pure $ Left e+ where+ foo = filter (/= '\n') adp+ Left err -> pure $ Left err+ timestampUTC :: RIO (Maybe String) timestampUTC = ( \case- Right str -> Just str- Left ___ -> Nothing+ Right cs -> Just $ filter ( \ c -> c == '-' || isDigit c ) cs+ Left __ -> Nothing ) <$> withExitCode "date" [ "-u"@@ -1167,12 +1552,18 @@ withExitCode cmd args = RestrictedIO $ do- (exitcode, out, err) <- readProcessWithExitCode cmd args []- case exitcode of- ExitSuccess ->- pure $ Right out- ExitFailure _ ->- pure $ Left err+ eproc <-+ try+ ( readProcessWithExitCode cmd args []+ ) :: IO (Either SomeException ((ExitCode, String, String)))+ case eproc of+ Right (exitcode, out, err) ->+ case exitcode of+ ExitSuccess ->+ pure $ Right out+ ExitFailure _ ->+ pure $ Left $ err+ Left e -> pure $ Left $ show e withExitCodeStdIn :: String@@ -1182,17 +1573,20 @@ withExitCodeStdIn cmd args txt = RestrictedIO $ do- (Just hinp, Just hout, Just herr, ph) <- createProcess rcp- hPutStrLn hinp txt- hClose hinp- out <- hGetContents hout- err <- hGetContents herr- exitcode <- waitForProcess ph- case exitcode of- ExitSuccess ->- pure $ Right out- ExitFailure _ ->- pure $ Left err+ eproc <-+ try+ ( aux+ ) :: IO (Either SomeException ((ExitCode, String, String)))+ case eproc of+ Right (exitcode, out, err) ->+ case exitcode of+ ExitSuccess ->+ -- DEBUG: Add --verbose flag which is dispatched to stderr+ -- pure $ Right (" DEBUG: " ++ err ++ out)+ pure $ Right out+ ExitFailure _ ->+ pure $ Left $ err+ Left e -> pure $ Left $ show e where raw = proc cmd args rcp =@@ -1201,21 +1595,36 @@ , std_out = CreatePipe , std_err = CreatePipe }+ aux =+ do+ (Just hinp, Just hout, Just herr, ph) <- createProcess rcp+ hPutStrLn hinp txt+ hClose hinp+ out <- hGetContents hout+ err <- hGetContents herr+ exitcode <- waitForProcess ph+ return (exitcode, out, err) withExitCodeCwd- :: String- -> String+ :: String+ -> String -> [String] -> RIO (Either String String) withExitCodeCwd path cmd args = RestrictedIO $ do- (exitcode, out, err) <- readCreateProcessWithExitCode rcp []- case exitcode of- ExitSuccess ->- pure $ Right out- ExitFailure _ ->- pure $ Left $ err+ eproc <-+ try+ ( readCreateProcessWithExitCode rcp []+ ) :: IO (Either SomeException ((ExitCode, String, String)))+ case eproc of+ Right (exitcode, out, err) ->+ case exitcode of+ ExitSuccess ->+ pure $ Right out+ ExitFailure _ ->+ pure $ Left $ err+ Left e -> pure $ Left $ show e where raw = proc cmd args rcp = raw { cwd = Just path }
+ src/Internal/Utils.hs view
@@ -0,0 +1,133 @@+{-# OPTIONS_GHC -Wall -Werror #-}++{-# LANGUAGE NoGeneralizedNewtypeDeriving #-}+{-# LANGUAGE Safe #-}++{-# LANGUAGE DeriveDataTypeable #-}++--------------------------------------------------------------------------------++-- |+-- Copyright : (c) 2026 SPISE MISU ApS+-- License : SSPL-1.0 OR AGPL-3.0-only+-- Maintainer : SPISE MISU <mail+hackage@spisemisu.com>+-- Stability : experimental++--------------------------------------------------------------------------------++module Internal.Utils+ ( -- * Files+ files+ , file+ -- * History+ , chits+ , chats+ -- * Helpers+ , combine+ , leftpad+ , index+ , split+ , linesOptNums+ )+where++--------------------------------------------------------------------------------++import Prelude hiding ( lines )+import qualified Prelude as Prelude++--------------------------------------------------------------------------------++combine+ :: [ (String, String) ]+ -> [ String ]+combine =+ map+ ( \ (x, y) ->+ if null x then+ y+ else+ x ++ ": " ++ y+ )++leftpad+ :: Char+ -> Int+ -> [Char]+ -> [Char]+leftpad chr rep cs =+ replicate (rep - length xs) chr ++ xs+ where+ xs = take rep cs++index+ :: Int+ -> Int+ -> String+index rep idx =+ leftpad '0' rep $ show idx++split+ :: Char+ -> String+ -> [String]+split sep str =+ case dropWhile (== sep) str of+ [] -> []+ cs ->+ b : split sep bs+ where+ (b, bs) = break (== sep) cs++--------------------------------------------------------------------------------++files+ :: String+ -> Bool+ -> [ String ]+ -> [ (String, String) ]+files prefix nums fs =+ linesOptNums prefix nums 0 fs++file+ :: Bool+ -> String+ -> [ (String, String) ]+file nums f =+ linesOptNums [] nums 1 $ Prelude.lines f++--------------------------------------------------------------------------------++chits+ :: Bool+ -> [ String ]+ -> [ (String, String) ]+chits nums fs =+ linesOptNums [] nums 0 $ reverse fs++chats+ :: Bool+ -> [ String ]+ -> [ (String, String) ]+chats nums fs =+ linesOptNums [] nums 0 $ reverse fs++--------------------------------------------------------------------------------++-- HELPERS (private)++linesOptNums+ :: String+ -> Bool+ -> Int+ -> [ String ]+ -> [ (String, String) ]+linesOptNums prefix nums origo xs =+ zipWith (\ idx rel -> (prefix ++ f idx, rel)) [origo..] xs+ where+ len = length $ show $ length xs+ f i =+ if nums then+ index len i+ else+ []