1use camino::Utf8Path;
2use serde::Deserialize;
3
4use crate::error::Result;
5
6#[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}