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