1use serde::{Deserialize, Serialize};
2use std::path::PathBuf;
3
4#[derive(Debug, Clone, Serialize, Deserialize)]
6pub struct DatabaseConfig {
7 pub path: PathBuf,
9}
10
11impl Default for DatabaseConfig {
12 fn default() -> Self {
13 let mut path = dirs::data_dir().unwrap_or_else(|| PathBuf::from("."));
14 path.push("mneme");
15 std::fs::create_dir_all(&path).ok();
16 path.push("mneme.db");
17 Self { path }
18 }
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct ServerConfig {
24 pub host: String,
26 pub port: u16,
28}
29
30impl Default for ServerConfig {
31 fn default() -> Self {
32 Self {
33 host: "127.0.0.1".into(),
34 port: 8080,
35 }
36 }
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct McpConfig {
42 pub default_project: String,
44}
45
46impl Default for McpConfig {
47 fn default() -> Self {
48 Self {
49 default_project: "default".into(),
50 }
51 }
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct TuiConfig {
57 pub theme: String,
59}
60
61impl Default for TuiConfig {
62 fn default() -> Self {
63 Self {
64 theme: "dark".into(),
65 }
66 }
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct BehaviorConfig {
72 pub auto_detect_conflicts: bool,
74 pub decay_enabled: bool,
76 pub decay_factor: f64,
78 pub max_search_results: u32,
80 pub auto_session: bool,
82}
83
84impl Default for BehaviorConfig {
85 fn default() -> Self {
86 Self {
87 auto_detect_conflicts: true,
88 decay_enabled: true,
89 decay_factor: 0.95,
90 max_search_results: 20,
91 auto_session: true,
92 }
93 }
94}
95
96#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
98#[serde(rename_all = "lowercase")]
99#[derive(Default)]
100pub enum EmbeddingProvider {
101 #[default]
103 Onnx,
104 OpenAI,
106 Ollama,
108 Google,
110}
111
112impl std::fmt::Display for EmbeddingProvider {
113 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
114 let s = match self {
115 EmbeddingProvider::Onnx => "onnx",
116 EmbeddingProvider::OpenAI => "openai",
117 EmbeddingProvider::Ollama => "ollama",
118 EmbeddingProvider::Google => "google",
119 };
120 write!(f, "{}", s)
121 }
122}
123
124impl std::str::FromStr for EmbeddingProvider {
125 type Err = crate::error::MnemeError;
126 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
127 match s.to_lowercase().as_str() {
128 "onnx" => Ok(EmbeddingProvider::Onnx),
129 "openai" => Ok(EmbeddingProvider::OpenAI),
130 "ollama" => Ok(EmbeddingProvider::Ollama),
131 "google" => Ok(EmbeddingProvider::Google),
132 other => Err(crate::error::MnemeError::Config(format!(
133 "Unknown embedding provider: {}",
134 other
135 ))),
136 }
137 }
138}
139
140#[derive(Debug, Clone, Serialize, Deserialize)]
142pub struct EmbeddingsConfig {
143 pub enabled: bool,
145 #[serde(default)]
147 pub provider: EmbeddingProvider,
148 pub model: String,
150 pub cache_dir: PathBuf,
152 pub auto_index: bool,
154 pub search_weight: f64,
156 pub similarity_threshold: f32,
158}
159
160impl Default for EmbeddingsConfig {
161 fn default() -> Self {
162 let mut cache_dir = dirs::cache_dir().unwrap_or_else(|| PathBuf::from("."));
163 cache_dir.push("mneme");
164 std::fs::create_dir_all(&cache_dir).ok();
165 Self {
166 enabled: true,
167 provider: EmbeddingProvider::Onnx,
168 model: "BAAI/bge-small-en-v1.5".into(),
169 cache_dir,
170 auto_index: true,
171 search_weight: 0.3,
172 similarity_threshold: 0.75,
173 }
174 }
175}
176
177#[derive(Debug, Clone, Serialize, Deserialize)]
179pub struct SyncConfig {
180 pub enabled: bool,
182 pub peer_id: String,
184 pub peer_name: String,
186 pub auto_sync_interval: u64,
188 pub compress: bool,
190}
191
192impl Default for SyncConfig {
193 fn default() -> Self {
194 Self {
195 enabled: true,
196 peer_id: String::new(),
197 peer_name: hostname::get()
198 .ok()
199 .and_then(|h| h.into_string().ok())
200 .unwrap_or_else(|| "mneme-peer".to_string()),
201 auto_sync_interval: 0,
202 compress: true,
203 }
204 }
205}
206
207#[derive(Debug, Clone, Serialize, Deserialize)]
209pub struct CryptoConfig {
210 pub enabled: bool,
211 pub auto_load_identity: bool,
212 pub identity_path: Option<PathBuf>,
213 pub always_encrypt_projects: Vec<String>,
214}
215
216impl Default for CryptoConfig {
217 fn default() -> Self {
218 Self {
219 enabled: false,
220 auto_load_identity: true,
221 identity_path: None,
222 always_encrypt_projects: vec![],
223 }
224 }
225}
226
227#[derive(Debug, Clone, Serialize, Deserialize, Default)]
229pub struct Settings {
230 pub database: DatabaseConfig,
232 pub server: ServerConfig,
234 pub mcp: McpConfig,
236 pub tui: TuiConfig,
238 pub behavior: BehaviorConfig,
240 pub embeddings: EmbeddingsConfig,
242 pub sync: SyncConfig,
244 pub crypto: CryptoConfig,
246}
247
248impl Settings {
249 pub fn load() -> crate::error::Result<Self> {
252 let path = Self::config_path();
253
254 if !path.exists() {
255 let settings = Self::default();
256 settings.save()?;
257 return Ok(settings);
258 }
259
260 let content = std::fs::read_to_string(&path)?;
261 let mut settings: Settings = toml::from_str(&content)
262 .map_err(|e| crate::error::MnemeError::Config(e.to_string()))?;
263
264 settings.apply_env_overrides();
265 Ok(settings)
266 }
267
268 pub fn save(&self) -> crate::error::Result<()> {
270 let path = Self::config_path();
271 if let Some(parent) = path.parent() {
272 std::fs::create_dir_all(parent)?;
273 }
274 let content = toml::to_string_pretty(self)
275 .map_err(|e| crate::error::MnemeError::Config(e.to_string()))?;
276 std::fs::write(&path, content)?;
277 Ok(())
278 }
279
280 pub fn config_path() -> PathBuf {
282 let mut path = dirs::config_dir().unwrap_or_else(|| PathBuf::from("."));
283 path.push("mneme");
284 path.push("config.toml");
285 path
286 }
287
288 pub fn apply_env_overrides(&mut self) {
290 if let Ok(val) = std::env::var("MNEME_DB_PATH") {
291 self.database.path = PathBuf::from(val);
292 }
293 if let Ok(val) = std::env::var("MNEME_PORT") {
294 if let Ok(port) = val.parse::<u16>() {
295 self.server.port = port;
296 }
297 }
298 if let Ok(val) = std::env::var("MNEME_PROJECT") {
299 self.mcp.default_project = val;
300 }
301 if let Ok(val) = std::env::var("MNEME_HOST") {
302 self.server.host = val;
303 }
304 if let Ok(val) = std::env::var("MNEME_EMBEDDINGS_ENABLED") {
305 self.embeddings.enabled = val.parse::<bool>().unwrap_or(self.embeddings.enabled);
306 }
307 if let Ok(val) = std::env::var("MNEME_EMBEDDING_PROVIDER") {
308 if let Ok(provider) = val.parse() {
309 self.embeddings.provider = provider;
310 }
311 }
312 if let Ok(val) = std::env::var("MNEME_CACHE_DIR") {
313 self.embeddings.cache_dir = PathBuf::from(val);
314 }
315 if let Ok(val) = std::env::var("MNEME_EMBEDDINGS_MODEL") {
316 self.embeddings.model = val;
317 }
318 if let Ok(val) = std::env::var("MNEME_CRYPTO_ENABLED") {
319 self.crypto.enabled = val.parse::<bool>().unwrap_or(self.crypto.enabled);
320 }
321 if let Ok(val) = std::env::var("MNEME_IDENTITY") {
322 self.crypto.identity_path = Some(PathBuf::from(val));
323 }
324 }
325
326 pub fn infer_project() -> String {
329 match Self::git_toplevel() {
330 Some(path) => path
331 .file_name()
332 .and_then(|n| n.to_str())
333 .unwrap_or("unknown")
334 .to_string(),
335 None => std::env::current_dir()
336 .ok()
337 .and_then(|p| p.file_name().map(|n| n.to_string_lossy().into_owned()))
338 .unwrap_or_else(|| "unknown".into()),
339 }
340 }
341
342 pub fn git_toplevel() -> Option<PathBuf> {
344 let output = std::process::Command::new("git")
345 .args(["rev-parse", "--show-toplevel"])
346 .output()
347 .ok()?;
348 if output.status.success() {
349 let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
350 Some(PathBuf::from(path))
351 } else {
352 None
353 }
354 }
355}