Skip to main content

mneme/config/
settings.rs

1use serde::{Deserialize, Serialize};
2use std::path::PathBuf;
3
4/// Configuración de la base de datos.
5#[derive(Debug, Clone, Serialize, Deserialize)]
6pub struct DatabaseConfig {
7    /// Ruta al archivo SQLite.
8    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/// Configuración del servidor HTTP.
22#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct ServerConfig {
24    /// Host de escucha.
25    pub host: String,
26    /// Puerto de escucha.
27    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/// Configuración del protocolo MCP.
40#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct McpConfig {
42    /// Proyecto por defecto para operaciones MCP.
43    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/// Configuración de la interfaz TUI.
55#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct TuiConfig {
57    /// Tema visual (dark, light).
58    pub theme: String,
59}
60
61impl Default for TuiConfig {
62    fn default() -> Self {
63        Self {
64            theme: "dark".into(),
65        }
66    }
67}
68
69/// Configuración de comportamiento.
70#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct BehaviorConfig {
72    /// Detectar conflictos automáticamente.
73    pub auto_detect_conflicts: bool,
74    /// Habilitar decaimiento de relevancia.
75    pub decay_enabled: bool,
76    /// Factor de decaimiento (0.0 - 1.0).
77    pub decay_factor: f64,
78    /// Máximo de resultados de búsqueda.
79    pub max_search_results: u32,
80    /// Crear sesiones automáticamente.
81    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/// Proveedor de embeddings.
97#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
98#[serde(rename_all = "lowercase")]
99#[derive(Default)]
100pub enum EmbeddingProvider {
101    /// ONNX local via fastembed (default, zero config).
102    #[default]
103    Onnx,
104    /// OpenAI API (requires OPENAI_API_KEY env).
105    OpenAI,
106    /// Ollama local server (requires OLLAMA_HOST env, default http://localhost:11434).
107    Ollama,
108    /// Google Gemini API (requires GOOGLE_API_KEY env).
109    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/// Configuración de embeddings.
141#[derive(Debug, Clone, Serialize, Deserialize)]
142pub struct EmbeddingsConfig {
143    /// Habilitar búsqueda semántica.
144    pub enabled: bool,
145    /// Proveedor de embeddings (onnx, openai, ollama, google).
146    #[serde(default)]
147    pub provider: EmbeddingProvider,
148    /// Modelo de embeddings a utilizar.
149    pub model: String,
150    /// Directorio de caché (usado por ONNX).
151    pub cache_dir: PathBuf,
152    /// Indexar automáticamente nuevas memorias.
153    pub auto_index: bool,
154    /// Peso en la puntuación combinada.
155    pub search_weight: f64,
156    /// Umbral de similitud para considerar relevante.
157    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/// Configuración de sincronización.
178#[derive(Debug, Clone, Serialize, Deserialize)]
179pub struct SyncConfig {
180    /// Habilitar sync.
181    pub enabled: bool,
182    /// ID del peer.
183    pub peer_id: String,
184    /// Nombre del peer.
185    pub peer_name: String,
186    /// Intervalo de auto-sync en segundos (0 = deshabilitado).
187    pub auto_sync_interval: u64,
188    /// Comprimir documentos.
189    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/// Configuración de encriptación.
208#[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/// Configuración global de Mneme.
228#[derive(Debug, Clone, Serialize, Deserialize, Default)]
229pub struct Settings {
230    /// Configuración de base de datos.
231    pub database: DatabaseConfig,
232    /// Configuración de servidor HTTP.
233    pub server: ServerConfig,
234    /// Configuración MCP.
235    pub mcp: McpConfig,
236    /// Configuración TUI.
237    pub tui: TuiConfig,
238    /// Configuración de comportamiento.
239    pub behavior: BehaviorConfig,
240    /// Configuración de embeddings.
241    pub embeddings: EmbeddingsConfig,
242    /// Configuración de sincronización.
243    pub sync: SyncConfig,
244    /// Configuración de encriptación.
245    pub crypto: CryptoConfig,
246}
247
248impl Settings {
249    /// Carga la configuración desde el archivo de configuración.
250    /// Si no existe, crea uno con valores por defecto.
251    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    /// Guarda la configuración actual en el archivo de configuración.
269    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    /// Retorna la ruta al archivo de configuración.
281    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    /// Aplica sobre-escrituras desde variables de entorno.
289    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    /// Infiere el nombre del proyecto actual.
327    /// Intenta obtener el directorio raíz de git; si falla, usa el directorio actual.
328    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    /// Ejecuta `git rev-parse --show-toplevel` para obtener la raíz del repo.
343    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}