1use anyhow::{Context, Result};
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4use std::path::PathBuf;
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct Config {
8 pub default_provider: String,
9 pub default_model: String,
10 pub theme: ThemeConfig,
11 #[serde(default)]
12 pub context: ContextConfig,
13 #[serde(default)]
14 pub mcp: HashMap<String, McpServerConfig>,
15 #[serde(default)]
16 pub agents: HashMap<String, AgentConfig>,
17 #[serde(default)]
18 pub tui: TuiConfig,
19 #[serde(default)]
20 pub permissions: HashMap<String, String>,
21 #[serde(default)]
22 pub providers: HashMap<String, ProviderDefinition>,
23 #[serde(default)]
24 pub custom_tools: HashMap<String, CustomToolConfig>,
25 #[serde(default)]
26 pub commands: HashMap<String, CommandConfig>,
27 #[serde(default)]
28 pub hooks: HashMap<String, HookConfig>,
29 #[serde(default)]
30 pub subagents: SubagentSettings,
31 #[serde(default)]
32 pub memory: MemoryConfig,
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct ContextConfig {
37 #[serde(default = "default_true")]
38 pub auto_load_global: bool,
39 #[serde(default = "default_true")]
40 pub auto_load_project: bool,
41}
42impl Default for ContextConfig {
43 fn default() -> Self {
44 Self {
45 auto_load_global: true,
46 auto_load_project: true,
47 }
48 }
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct ThemeConfig {
53 pub name: String,
54}
55
56#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct McpServerConfig {
58 #[serde(default)]
59 pub command: Vec<String>,
60 pub url: Option<String>,
61 #[serde(default = "default_true")]
62 pub enabled: bool,
63 #[serde(default)]
64 pub env: HashMap<String, String>,
65 #[serde(default = "default_timeout")]
66 pub timeout: u64,
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct AgentConfig {
71 pub description: String,
72 pub model: Option<String>,
73 pub system_prompt: Option<String>,
74 #[serde(default)]
75 pub tools: HashMap<String, bool>,
76 #[serde(default = "default_true")]
77 pub enabled: bool,
78}
79
80#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct TuiConfig {
82 #[serde(default = "default_true")]
83 pub vim_mode: bool,
84 #[serde(default)]
85 pub favorite_models: Vec<String>,
86}
87
88impl Default for TuiConfig {
89 fn default() -> Self {
90 Self {
91 vim_mode: true,
92 favorite_models: Vec::new(),
93 }
94 }
95}
96
97#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct ProviderDefinition {
99 pub api: String,
100 pub base_url: Option<String>,
101 #[serde(default)]
102 pub api_key_env: Option<String>,
103 #[serde(default)]
104 pub models: Vec<String>,
105 pub default_model: Option<String>,
106 #[serde(default = "default_true")]
107 pub enabled: bool,
108}
109
110#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct CustomToolConfig {
112 pub description: String,
113 pub command: String,
114 #[serde(default = "default_schema")]
115 pub schema: serde_json::Value,
116 #[serde(default = "default_timeout")]
117 pub timeout: u64,
118}
119
120#[derive(Debug, Clone, Serialize, Deserialize)]
121pub struct CommandConfig {
122 pub description: String,
123 pub command: String,
124 #[serde(default = "default_timeout")]
125 pub timeout: u64,
126}
127
128#[derive(Debug, Clone, Serialize, Deserialize)]
129pub struct HookConfig {
130 pub command: String,
131 #[serde(default = "default_timeout")]
132 pub timeout: u64,
133}
134
135#[derive(Debug, Clone, Serialize, Deserialize)]
136pub struct SubagentSettings {
137 #[serde(default = "default_true")]
138 pub enabled: bool,
139 #[serde(default = "default_max_subagent_turns")]
140 pub max_turns: usize,
141}
142
143impl Default for SubagentSettings {
144 fn default() -> Self {
145 Self {
146 enabled: true,
147 max_turns: 20,
148 }
149 }
150}
151
152#[derive(Debug, Clone, Serialize, Deserialize)]
153pub struct MemoryConfig {
154 #[serde(default = "default_true")]
155 pub enabled: bool,
156 #[serde(default = "default_true")]
157 pub auto_extract: bool,
158 #[serde(default = "default_inject_count")]
159 pub inject_count: usize,
160 #[serde(default = "default_max_memories")]
161 pub max_memories: usize,
162}
163
164impl Default for MemoryConfig {
165 fn default() -> Self {
166 Self {
167 enabled: true,
168 auto_extract: true,
169 inject_count: 15,
170 max_memories: 2000,
171 }
172 }
173}
174
175fn default_inject_count() -> usize {
176 15
177}
178
179fn default_max_memories() -> usize {
180 2000
181}
182
183fn default_max_subagent_turns() -> usize {
184 20
185}
186
187fn default_true() -> bool {
188 true
189}
190
191fn default_timeout() -> u64 {
192 30
193}
194
195fn default_schema() -> serde_json::Value {
196 serde_json::json!({
197 "type": "object",
198 "properties": {},
199 "required": []
200 })
201}
202
203impl Default for Config {
204 fn default() -> Self {
205 Self {
206 default_provider: "anthropic".to_string(),
207 default_model: "claude-sonnet-4-20250514".to_string(),
208 theme: ThemeConfig {
209 name: "terminal".to_string(),
210 },
211 context: ContextConfig::default(),
212 mcp: HashMap::new(),
213 agents: HashMap::new(),
214 tui: TuiConfig::default(),
215 permissions: HashMap::new(),
216 providers: HashMap::new(),
217 custom_tools: HashMap::new(),
218 commands: HashMap::new(),
219 hooks: HashMap::new(),
220 subagents: SubagentSettings::default(),
221 memory: MemoryConfig::default(),
222 }
223 }
224}
225
226impl Config {
227 pub fn config_dir() -> PathBuf {
228 if let Ok(xdg) = std::env::var("XDG_CONFIG_HOME")
229 && !xdg.is_empty()
230 {
231 return PathBuf::from(xdg).join("dot");
232 }
233 #[cfg(unix)]
234 return dirs::home_dir()
235 .unwrap_or_else(|| PathBuf::from("."))
236 .join(".config")
237 .join("dot");
238 #[cfg(not(unix))]
239 dirs::config_dir()
240 .unwrap_or_else(|| PathBuf::from("."))
241 .join("dot")
242 }
243
244 pub fn config_path() -> PathBuf {
245 Self::config_dir().join("config.toml")
246 }
247
248 pub fn data_dir() -> PathBuf {
249 if let Ok(xdg) = std::env::var("XDG_DATA_HOME")
250 && !xdg.is_empty()
251 {
252 return PathBuf::from(xdg).join("dot");
253 }
254 #[cfg(unix)]
255 return dirs::home_dir()
256 .unwrap_or_else(|| PathBuf::from("."))
257 .join(".local")
258 .join("share")
259 .join("dot");
260 #[cfg(not(unix))]
261 dirs::data_local_dir()
262 .unwrap_or_else(|| PathBuf::from("."))
263 .join("dot")
264 }
265
266 pub fn db_path() -> PathBuf {
267 Self::data_dir().join("dot.db")
268 }
269
270 pub fn load() -> Result<Self> {
271 let path = Self::config_path();
272 if path.exists() {
273 let content = std::fs::read_to_string(&path)
274 .with_context(|| format!("reading config from {}", path.display()))?;
275 toml::from_str(&content).context("parsing config.toml")
276 } else {
277 let config = Self::default();
278 config.save()?;
279 Ok(config)
280 }
281 }
282
283 pub fn save(&self) -> Result<()> {
284 let dir = Self::config_dir();
285 std::fs::create_dir_all(&dir)
286 .with_context(|| format!("creating config dir {}", dir.display()))?;
287 let content = toml::to_string_pretty(self).context("serializing config")?;
288 std::fs::write(Self::config_path(), content).context("writing config.toml")
289 }
290
291 pub fn ensure_dirs() -> Result<()> {
292 std::fs::create_dir_all(Self::config_dir()).context("creating config directory")?;
293 std::fs::create_dir_all(Self::data_dir()).context("creating data directory")?;
294 Ok(())
295 }
296
297 pub fn enabled_mcp_servers(&self) -> Vec<(&str, &McpServerConfig)> {
298 self.mcp
299 .iter()
300 .filter(|(_, cfg)| cfg.enabled && !cfg.command.is_empty())
301 .map(|(name, cfg)| (name.as_str(), cfg))
302 .collect()
303 }
304
305 pub fn enabled_agents(&self) -> Vec<(&str, &AgentConfig)> {
306 self.agents
307 .iter()
308 .filter(|(_, cfg)| cfg.enabled)
309 .map(|(name, cfg)| (name.as_str(), cfg))
310 .collect()
311 }
312
313 pub fn parse_model_spec(spec: &str) -> (Option<&str>, &str) {
316 if let Some((provider, model)) = spec.split_once('/') {
317 (Some(provider), model)
318 } else {
319 (None, spec)
320 }
321 }
322}