11use anyhow:: { Context , Result } ;
2- use serde:: Deserialize ;
3- use std:: path:: PathBuf ;
2+ use serde:: { Deserialize , Serialize } ;
3+ use std:: path:: { Path , PathBuf } ;
4+
5+ /// Model override configuration.
6+ #[ derive( Debug , Clone , Default , Serialize , Deserialize ) ]
7+ #[ serde( default ) ]
8+ pub struct ModelConfig {
9+ /// Override embedding model URI (e.g., "hf:repo/file.gguf").
10+ pub embed : Option < String > ,
11+ /// Override reranker model URI.
12+ pub rerank : Option < String > ,
13+ /// Override expansion/orchestrator model URI.
14+ pub expand : Option < String > ,
15+ }
416
517/// Application configuration, loaded from `~/.engraph/config.toml` with CLI overrides.
6- #[ derive( Debug , Clone , Deserialize ) ]
18+ #[ derive( Debug , Clone , Serialize , Deserialize ) ]
719#[ serde( default ) ]
820pub struct Config {
921 /// Path to the Obsidian vault to index.
@@ -14,6 +26,10 @@ pub struct Config {
1426 pub exclude : Vec < String > ,
1527 /// Number of files to process per embedding batch.
1628 pub batch_size : usize ,
29+ /// Whether intelligence features are enabled. None = not yet configured.
30+ pub intelligence : Option < bool > ,
31+ /// Model override URIs.
32+ pub models : ModelConfig ,
1733}
1834
1935impl Default for Config {
@@ -23,6 +39,8 @@ impl Default for Config {
2339 top_n : 5 ,
2440 exclude : vec ! [ ".obsidian/" . to_string( ) ] ,
2541 batch_size : 64 ,
42+ intelligence : None ,
43+ models : ModelConfig :: default ( ) ,
2644 }
2745 }
2846}
@@ -68,6 +86,34 @@ impl Config {
6886 let dir = Self :: data_dir ( ) ?;
6987 crate :: profile:: load_vault_toml ( & dir)
7088 }
89+
90+ /// Whether intelligence is enabled (defaults to false if not configured).
91+ pub fn intelligence_enabled ( & self ) -> bool {
92+ self . intelligence . unwrap_or ( false )
93+ }
94+
95+ /// Save config to a specific path.
96+ pub fn save_to ( & self , path : & Path ) -> Result < ( ) > {
97+ let content = toml:: to_string_pretty ( self ) . context ( "serializing config" ) ?;
98+ std:: fs:: write ( path, content) . with_context ( || format ! ( "writing {}" , path. display( ) ) ) ?;
99+ Ok ( ( ) )
100+ }
101+
102+ /// Load config from a specific path.
103+ pub fn load_from ( path : & Path ) -> Result < Self > {
104+ let contents =
105+ std:: fs:: read_to_string ( path) . with_context ( || format ! ( "reading {}" , path. display( ) ) ) ?;
106+ let config: Config =
107+ toml:: from_str ( & contents) . with_context ( || format ! ( "parsing {}" , path. display( ) ) ) ?;
108+ Ok ( config)
109+ }
110+
111+ /// Save to the default config path (`~/.engraph/config.toml`).
112+ pub fn save ( & self ) -> Result < ( ) > {
113+ let path = Self :: data_dir ( ) ?. join ( "config.toml" ) ;
114+ std:: fs:: create_dir_all ( path. parent ( ) . unwrap ( ) ) ?;
115+ self . save_to ( & path)
116+ }
71117}
72118
73119#[ cfg( test) ]
@@ -138,4 +184,54 @@ batch_size = 128
138184 let cfg = Config :: load ( ) . unwrap ( ) ;
139185 assert_eq ! ( cfg. batch_size, 64 ) ;
140186 }
187+
188+ #[ test]
189+ fn parse_intelligence_config ( ) {
190+ let toml_str = r#"
191+ intelligence = true
192+
193+ [models]
194+ embed = "hf:ggml-org/embeddinggemma-300M-GGUF/embeddinggemma-300M-Q8_0.gguf"
195+ rerank = "hf:ggml-org/Qwen3-Reranker-0.6B-Q8_0-GGUF/qwen3-reranker-0.6b-q8_0.gguf"
196+ "# ;
197+ let cfg: Config = toml:: from_str ( toml_str) . unwrap ( ) ;
198+ assert_eq ! ( cfg. intelligence, Some ( true ) ) ;
199+ assert ! ( cfg. models. embed. is_some( ) ) ;
200+ assert ! ( cfg. models. rerank. is_some( ) ) ;
201+ assert ! ( cfg. models. expand. is_none( ) ) ;
202+ }
203+
204+ #[ test]
205+ fn intelligence_defaults_to_none ( ) {
206+ let cfg = Config :: default ( ) ;
207+ assert ! ( cfg. intelligence. is_none( ) ) ;
208+ assert ! ( cfg. models. embed. is_none( ) ) ;
209+ }
210+
211+ #[ test]
212+ fn intelligence_false_disables_features ( ) {
213+ let toml_str = r#"intelligence = false"# ;
214+ let cfg: Config = toml:: from_str ( toml_str) . unwrap ( ) ;
215+ assert_eq ! ( cfg. intelligence, Some ( false ) ) ;
216+ assert ! ( !cfg. intelligence_enabled( ) ) ;
217+ }
218+
219+ #[ test]
220+ fn test_config_roundtrip_with_intelligence ( ) {
221+ let dir = tempfile:: tempdir ( ) . unwrap ( ) ;
222+ let config_path = dir. path ( ) . join ( "config.toml" ) ;
223+
224+ let mut cfg = Config :: default ( ) ;
225+ cfg. intelligence = Some ( true ) ;
226+ cfg. models . embed = Some ( "hf:custom/model/embed.gguf" . into ( ) ) ;
227+
228+ cfg. save_to ( & config_path) . unwrap ( ) ;
229+
230+ let loaded = Config :: load_from ( & config_path) . unwrap ( ) ;
231+ assert_eq ! ( loaded. intelligence, Some ( true ) ) ;
232+ assert_eq ! (
233+ loaded. models. embed,
234+ Some ( "hf:custom/model/embed.gguf" . into( ) )
235+ ) ;
236+ }
141237}
0 commit comments