never executed always true always false
1 module PureClaw.CLI.Config
2 ( -- * File config
3 FileConfig (..)
4 , emptyFileConfig
5 -- * Loading
6 , loadFileConfig
7 , loadConfig
8 -- * Directory helpers
9 , getPureclawDir
10 ) where
11
12 import Control.Exception
13 import Data.Text (Text)
14 import Data.Text.IO qualified as TIO
15 import System.Directory (getHomeDirectory)
16 import System.FilePath ((</>))
17 import Toml (TomlCodec, (.=))
18 import Toml qualified
19
20 -- | Configuration that can be read from a TOML file.
21 -- All fields are optional — missing fields default to Nothing.
22 data FileConfig = FileConfig
23 { _fc_apiKey :: Maybe Text
24 , _fc_model :: Maybe Text
25 , _fc_provider :: Maybe Text
26 , _fc_system :: Maybe Text
27 , _fc_memory :: Maybe Text
28 , _fc_allow :: Maybe [Text]
29 , _fc_vault_path :: Maybe Text -- ^ vault file path (default: ~/.pureclaw/vault.age)
30 , _fc_vault_recipient :: Maybe Text -- ^ age recipient string (required to enable vault)
31 , _fc_vault_identity :: Maybe Text -- ^ age identity path or plugin string
32 , _fc_vault_unlock :: Maybe Text -- ^ "startup", "on_demand", or "per_access"
33 } deriving stock (Show, Eq)
34
35 emptyFileConfig :: FileConfig
36 emptyFileConfig =
37 FileConfig Nothing Nothing Nothing Nothing Nothing Nothing
38 Nothing Nothing Nothing Nothing
39
40 fileConfigCodec :: TomlCodec FileConfig
41 fileConfigCodec = FileConfig
42 <$> Toml.dioptional (Toml.text "api_key") .= _fc_apiKey
43 <*> Toml.dioptional (Toml.text "model") .= _fc_model
44 <*> Toml.dioptional (Toml.text "provider") .= _fc_provider
45 <*> Toml.dioptional (Toml.text "system") .= _fc_system
46 <*> Toml.dioptional (Toml.text "memory") .= _fc_memory
47 <*> Toml.dioptional (Toml.arrayOf Toml._Text "allow") .= _fc_allow
48 <*> Toml.dioptional (Toml.text "vault_path") .= _fc_vault_path
49 <*> Toml.dioptional (Toml.text "vault_recipient") .= _fc_vault_recipient
50 <*> Toml.dioptional (Toml.text "vault_identity") .= _fc_vault_identity
51 <*> Toml.dioptional (Toml.text "vault_unlock") .= _fc_vault_unlock
52
53 -- | Load config from a single file path.
54 -- Returns 'emptyFileConfig' if the file does not exist or cannot be parsed.
55 loadFileConfig :: FilePath -> IO FileConfig
56 loadFileConfig path = do
57 text <- try @IOError (TIO.readFile path)
58 pure $ case text of
59 Left _ -> emptyFileConfig
60 Right toml -> case Toml.decode fileConfigCodec toml of
61 Left _ -> emptyFileConfig
62 Right c -> c
63
64 -- | The PureClaw home directory: @~\/.pureclaw@.
65 -- This is where config, memory, and vault files are stored by default.
66 getPureclawDir :: IO FilePath
67 getPureclawDir = do
68 home <- getHomeDirectory
69 pure (home </> ".pureclaw")
70
71 -- | Load config from the default locations, trying each in order:
72 --
73 -- 1. @~\/.pureclaw\/config.toml@ (user home)
74 -- 2. @~\/.config\/pureclaw\/config.toml@ (XDG fallback)
75 --
76 -- Returns the first config found, or 'emptyFileConfig' if none exists.
77 loadConfig :: IO FileConfig
78 loadConfig = do
79 home <- try @IOError getHomeDirectory
80 case home of
81 Left _ -> pure emptyFileConfig
82 Right h -> do
83 homeCfg <- loadFileConfig (h </> ".pureclaw" </> "config.toml")
84 if homeCfg /= emptyFileConfig
85 then pure homeCfg
86 else loadFileConfig (h </> ".config" </> "pureclaw" </> "config.toml")