Skip to main content

argyph_core/
config.rs

1use camino::Utf8Path;
2use serde::Deserialize;
3
4use crate::error::Result;
5
6/// Layered configuration (env > repo file > defaults).
7///
8/// Priority: `ARGYPH_*` env vars > `.argyph/config.toml` > built-in defaults.
9#[derive(Debug, Clone, Deserialize)]
10pub struct Config {
11    #[serde(default)]
12    pub index: IndexConfig,
13
14    #[serde(default)]
15    pub search: SearchConfig,
16
17    #[serde(default)]
18    pub pack: PackConfig,
19
20    #[serde(default)]
21    pub locate: LocateConfig,
22
23    #[serde(default)]
24    pub locate_smart: LocateSmartConfig,
25}
26
27#[derive(Debug, Clone, Deserialize, Default)]
28pub struct IndexConfig {
29    #[serde(default = "default_exclude")]
30    pub exclude: Vec<String>,
31
32    #[serde(default = "default_languages")]
33    pub languages: Vec<String>,
34}
35
36#[derive(Debug, Clone, Deserialize, Default)]
37pub struct SearchConfig {
38    #[serde(default = "default_hybrid_alpha")]
39    pub hybrid_alpha: f64,
40}
41
42#[derive(Debug, Clone, Deserialize, Default)]
43pub struct PackConfig {
44    #[serde(default = "default_token_budget")]
45    pub default_token_budget: u64,
46}
47
48#[derive(Debug, Clone, Deserialize)]
49pub struct LocateConfig {
50    #[serde(default = "default_locate_max_file_bytes")]
51    pub max_file_bytes: u64,
52}
53
54impl Default for LocateConfig {
55    fn default() -> Self {
56        Self {
57            max_file_bytes: default_locate_max_file_bytes(),
58        }
59    }
60}
61
62fn default_locate_max_file_bytes() -> u64 {
63    10_485_760
64}
65
66#[derive(Debug, Clone, Default, serde::Deserialize)]
67pub struct LocateSmartConfig {
68    #[serde(default)]
69    pub enabled: bool,
70    #[serde(default)]
71    pub provider: Option<String>,
72    #[serde(default)]
73    pub model: Option<String>,
74    #[serde(default)]
75    pub endpoint: Option<String>,
76    #[serde(default = "default_max_steps")]
77    pub max_steps: u8,
78    #[serde(default = "default_max_output_tokens")]
79    pub max_output_tokens: u32,
80}
81
82fn default_max_steps() -> u8 {
83    4
84}
85
86fn default_max_output_tokens() -> u32 {
87    1024
88}
89
90fn default_exclude() -> Vec<String> {
91    vec!["docs/generated/**".into(), "**/*.min.js".into()]
92}
93
94fn default_languages() -> Vec<String> {
95    vec!["rust".into(), "typescript".into(), "python".into()]
96}
97
98fn default_hybrid_alpha() -> f64 {
99    0.5
100}
101
102fn default_token_budget() -> u64 {
103    50000
104}
105
106impl Default for Config {
107    fn default() -> Self {
108        Self {
109            index: IndexConfig {
110                exclude: default_exclude(),
111                languages: default_languages(),
112            },
113            search: SearchConfig {
114                hybrid_alpha: default_hybrid_alpha(),
115            },
116            pack: PackConfig {
117                default_token_budget: default_token_budget(),
118            },
119            locate: LocateConfig::default(),
120            locate_smart: LocateSmartConfig::default(),
121        }
122    }
123}
124
125impl Config {
126    pub fn load(root: &Utf8Path) -> Result<Self> {
127        let config_path = root.join(".argyph").join("config.toml");
128        let mut config = if config_path.exists() {
129            match std::fs::read_to_string(config_path.as_str()) {
130                Ok(content) => match toml::from_str(&content) {
131                    Ok(config) => config,
132                    Err(e) => {
133                        tracing::warn!("invalid .argyph/config.toml: {e}, using defaults");
134                        Self::default()
135                    }
136                },
137                Err(e) => {
138                    tracing::warn!("cannot read .argyph/config.toml: {e}, using defaults");
139                    Self::default()
140                }
141            }
142        } else {
143            Self::default()
144        };
145        config.apply_env_overrides();
146        Ok(config)
147    }
148
149    pub fn apply_env_overrides(&mut self) {
150        if let Ok(v) = std::env::var("ARGYPH_LOCATE_SMART_ENABLED") {
151            self.locate_smart.enabled = v == "true" || v == "1";
152        }
153        if let Ok(v) = std::env::var("ARGYPH_LOCATE_SMART_PROVIDER") {
154            self.locate_smart.provider = Some(v);
155        }
156        if let Ok(v) = std::env::var("ARGYPH_LOCATE_SMART_MODEL") {
157            self.locate_smart.model = Some(v);
158        }
159    }
160}
161
162#[cfg(test)]
163#[allow(clippy::unwrap_used)]
164mod tests {
165    use super::*;
166
167    #[test]
168    fn default_config_is_sane() {
169        let config = Config::default();
170        assert_eq!(config.index.languages.len(), 3);
171        assert_eq!(config.search.hybrid_alpha, 0.5);
172        assert_eq!(config.pack.default_token_budget, 50000);
173    }
174
175    #[test]
176    fn load_non_existent_config_returns_defaults() {
177        let dir = tempfile::tempdir().unwrap();
178        let root = camino::Utf8PathBuf::from_path_buf(dir.path().to_path_buf()).unwrap();
179        let config = Config::load(&root).unwrap();
180        assert_eq!(config.index.languages.len(), 3);
181    }
182}