agent_code_lib/config/
mod.rs1mod schema;
13
14pub use schema::*;
15
16use crate::error::ConfigError;
17use std::path::{Path, PathBuf};
18
19static LOADING: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
21
22impl Config {
23 pub fn load() -> Result<Config, ConfigError> {
25 if LOADING.swap(true, std::sync::atomic::Ordering::SeqCst) {
27 return Ok(Config::default());
28 }
29 let result = Self::load_inner();
30 LOADING.store(false, std::sync::atomic::Ordering::SeqCst);
31 result
32 }
33
34 fn load_inner() -> Result<Config, ConfigError> {
35 let mut config = Config::default();
36
37 if let Some(path) = user_config_path()
39 && path.exists()
40 {
41 let content = std::fs::read_to_string(&path)
42 .map_err(|e| ConfigError::FileError(format!("{path:?}: {e}")))?;
43 let user_config: Config = toml::from_str(&content)?;
44 config.merge(user_config);
45 }
46
47 if let Some(path) = find_project_config() {
49 let content = std::fs::read_to_string(&path)
50 .map_err(|e| ConfigError::FileError(format!("{path:?}: {e}")))?;
51 let project_config: Config = toml::from_str(&content)?;
52 config.merge(project_config);
53 }
54
55 let env_api_key = resolve_api_key_from_env();
60 if env_api_key.is_some() {
61 config.api.api_key = env_api_key;
62 }
63
64 if let Ok(url) = std::env::var("AGENT_CODE_API_BASE_URL") {
66 config.api.base_url = url;
67 }
68
69 if let Ok(model) = std::env::var("AGENT_CODE_MODEL") {
71 config.api.model = model;
72 }
73
74 Ok(config)
75 }
76
77 fn merge(&mut self, other: Config) {
80 if !other.api.base_url.is_empty() {
81 self.api.base_url = other.api.base_url;
82 }
83 if !other.api.model.is_empty() {
84 self.api.model = other.api.model;
85 }
86 if other.api.api_key.is_some() {
87 self.api.api_key = other.api.api_key;
88 }
89 if other.api.max_output_tokens.is_some() {
90 self.api.max_output_tokens = other.api.max_output_tokens;
91 }
92 if other.permissions.default_mode != PermissionMode::Ask {
93 self.permissions.default_mode = other.permissions.default_mode;
94 }
95 if !other.permissions.rules.is_empty() {
96 self.permissions.rules.extend(other.permissions.rules);
97 }
98 for (name, entry) in other.mcp_servers {
100 self.mcp_servers.insert(name, entry);
101 }
102 }
103}
104
105fn resolve_api_key_from_env() -> Option<String> {
110 std::env::var("AGENT_CODE_API_KEY")
111 .or_else(|_| std::env::var("ANTHROPIC_API_KEY"))
112 .or_else(|_| std::env::var("OPENAI_API_KEY"))
113 .or_else(|_| std::env::var("XAI_API_KEY"))
114 .or_else(|_| std::env::var("GOOGLE_API_KEY"))
115 .or_else(|_| std::env::var("DEEPSEEK_API_KEY"))
116 .or_else(|_| std::env::var("GROQ_API_KEY"))
117 .or_else(|_| std::env::var("MISTRAL_API_KEY"))
118 .or_else(|_| std::env::var("ZHIPU_API_KEY"))
119 .or_else(|_| std::env::var("TOGETHER_API_KEY"))
120 .or_else(|_| std::env::var("OPENROUTER_API_KEY"))
121 .or_else(|_| std::env::var("COHERE_API_KEY"))
122 .or_else(|_| std::env::var("PERPLEXITY_API_KEY"))
123 .ok()
124}
125
126fn user_config_path() -> Option<PathBuf> {
128 dirs::config_dir().map(|d| d.join("agent-code").join("config.toml"))
129}
130
131fn find_project_config() -> Option<PathBuf> {
133 let cwd = std::env::current_dir().ok()?;
134 find_config_in_ancestors(&cwd)
135}
136
137pub fn watch_config(
140 on_reload: impl Fn(Config) + Send + 'static,
141) -> Option<std::thread::JoinHandle<()>> {
142 let user_path = user_config_path()?;
143 let project_path = find_project_config();
144
145 let user_mtime = std::fs::metadata(&user_path)
147 .ok()
148 .and_then(|m| m.modified().ok());
149 let project_mtime = project_path
150 .as_ref()
151 .and_then(|p| std::fs::metadata(p).ok())
152 .and_then(|m| m.modified().ok());
153
154 Some(std::thread::spawn(move || {
155 let mut last_user = user_mtime;
156 let mut last_project = project_mtime;
157
158 loop {
159 std::thread::sleep(std::time::Duration::from_secs(5));
160
161 let cur_user = std::fs::metadata(&user_path)
162 .ok()
163 .and_then(|m| m.modified().ok());
164 let cur_project = project_path
165 .as_ref()
166 .and_then(|p| std::fs::metadata(p).ok())
167 .and_then(|m| m.modified().ok());
168
169 let changed = cur_user != last_user || cur_project != last_project;
170
171 if changed {
172 if let Ok(config) = Config::load() {
173 tracing::info!("Config reloaded (file change detected)");
174 on_reload(config);
175 }
176 last_user = cur_user;
177 last_project = cur_project;
178 }
179 }
180 }))
181}
182
183fn find_config_in_ancestors(start: &Path) -> Option<PathBuf> {
184 let mut dir = start.to_path_buf();
185 loop {
186 let candidate = dir.join(".agent").join("settings.toml");
187 if candidate.exists() {
188 return Some(candidate);
189 }
190 if !dir.pop() {
191 return None;
192 }
193 }
194}