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 -- * Writing
9 , updateVaultConfig
10 -- * Directory helpers
11 , getPureclawDir
12 ) where
13
14 import Control.Applicative ((<|>))
15 import Control.Exception
16 import Data.Text (Text)
17 import Data.Text.IO qualified as TIO
18 import System.Directory (getHomeDirectory)
19 import System.FilePath ((</>))
20 import Toml (TomlCodec, (.=))
21 import Toml qualified
22
23 -- | Configuration that can be read from a TOML file.
24 -- All fields are optional — missing fields default to Nothing.
25 data FileConfig = FileConfig
26 { _fc_apiKey :: Maybe Text
27 , _fc_model :: Maybe Text
28 , _fc_provider :: Maybe Text
29 , _fc_system :: Maybe Text
30 , _fc_memory :: Maybe Text
31 , _fc_allow :: Maybe [Text]
32 , _fc_vault_path :: Maybe Text -- ^ vault file path (default: ~/.pureclaw/vault.age)
33 , _fc_vault_recipient :: Maybe Text -- ^ age recipient string (required to enable vault)
34 , _fc_vault_identity :: Maybe Text -- ^ age identity path or plugin string
35 , _fc_vault_unlock :: Maybe Text -- ^ "startup", "on_demand", or "per_access"
36 } deriving stock (Show, Eq)
37
38 emptyFileConfig :: FileConfig
39 emptyFileConfig =
40 FileConfig Nothing Nothing Nothing Nothing Nothing Nothing
41 Nothing Nothing Nothing Nothing
42
43 fileConfigCodec :: TomlCodec FileConfig
44 fileConfigCodec = FileConfig
45 <$> Toml.dioptional (Toml.text "api_key") .= _fc_apiKey
46 <*> Toml.dioptional (Toml.text "model") .= _fc_model
47 <*> Toml.dioptional (Toml.text "provider") .= _fc_provider
48 <*> Toml.dioptional (Toml.text "system") .= _fc_system
49 <*> Toml.dioptional (Toml.text "memory") .= _fc_memory
50 <*> Toml.dioptional (Toml.arrayOf Toml._Text "allow") .= _fc_allow
51 <*> Toml.dioptional (Toml.text "vault_path") .= _fc_vault_path
52 <*> Toml.dioptional (Toml.text "vault_recipient") .= _fc_vault_recipient
53 <*> Toml.dioptional (Toml.text "vault_identity") .= _fc_vault_identity
54 <*> Toml.dioptional (Toml.text "vault_unlock") .= _fc_vault_unlock
55
56 -- | Load config from a single file path.
57 -- Returns 'emptyFileConfig' if the file does not exist or cannot be parsed.
58 loadFileConfig :: FilePath -> IO FileConfig
59 loadFileConfig path = do
60 text <- try @IOError (TIO.readFile path)
61 pure $ case text of
62 Left _ -> emptyFileConfig
63 Right toml -> case Toml.decode fileConfigCodec toml of
64 Left _ -> emptyFileConfig
65 Right c -> c
66
67 -- | The PureClaw home directory: @~\/.pureclaw@.
68 -- This is where config, memory, and vault files are stored by default.
69 getPureclawDir :: IO FilePath
70 getPureclawDir = do
71 home <- getHomeDirectory
72 pure (home </> ".pureclaw")
73
74 -- | Load config from the default locations, trying each in order:
75 --
76 -- 1. @~\/.pureclaw\/config.toml@ (user home)
77 -- 2. @~\/.config\/pureclaw\/config.toml@ (XDG fallback)
78 --
79 -- Returns the first config found, or 'emptyFileConfig' if none exists.
80 loadConfig :: IO FileConfig
81 loadConfig = do
82 home <- try @IOError getHomeDirectory
83 case home of
84 Left _ -> pure emptyFileConfig
85 Right h -> do
86 homeCfg <- loadFileConfig (h </> ".pureclaw" </> "config.toml")
87 if homeCfg /= emptyFileConfig
88 then pure homeCfg
89 else loadFileConfig (h </> ".config" </> "pureclaw" </> "config.toml")
90
91 -- | Update vault-related fields in a config file, preserving all other settings.
92 -- 'Nothing' means "leave this field unchanged". If all four arguments are
93 -- 'Nothing', this is a no-op (no file write occurs).
94 updateVaultConfig
95 :: FilePath -- ^ Config file path
96 -> Maybe Text -- ^ vault_path
97 -> Maybe Text -- ^ vault_recipient
98 -> Maybe Text -- ^ vault_identity
99 -> Maybe Text -- ^ vault_unlock
100 -> IO ()
101 updateVaultConfig _ Nothing Nothing Nothing Nothing = pure ()
102 updateVaultConfig path vaultPath vaultRecipient vaultIdentity vaultUnlock = do
103 existing <- loadFileConfig path
104 let updated = existing
105 { _fc_vault_path = vaultPath <|> _fc_vault_path existing
106 , _fc_vault_recipient = vaultRecipient -- Direct: Nothing clears stale age creds
107 , _fc_vault_identity = vaultIdentity -- Direct: Nothing clears stale age creds
108 , _fc_vault_unlock = vaultUnlock <|> _fc_vault_unlock existing
109 }
110 TIO.writeFile path (Toml.encode fileConfigCodec updated)