Skip to main content

aimem_core/
config.rs

1//! Configuration — mirrors Python's config.py
2//!
3//! Load order: env vars > config file (~/.aimem/config.json) > defaults
4
5use std::path::PathBuf;
6
7use serde::{Deserialize, Serialize};
8use thiserror::Error;
9
10#[derive(Debug, Error)]
11pub enum ConfigError {
12    #[error("I/O error: {0}")]
13    Io(#[from] std::io::Error),
14    #[error("JSON parse error: {0}")]
15    Json(#[from] serde_json::Error),
16}
17
18/// Default path for the Turso DB file (all data in one file).
19pub const DEFAULT_DB_FILE_NAME: &str = "aimem.db";
20
21pub fn default_db_path() -> PathBuf {
22    dirs_next().join(DEFAULT_DB_FILE_NAME)
23}
24
25/// Default path for the identity file.
26pub fn default_identity_path() -> PathBuf {
27    dirs_next().join("identity.txt")
28}
29
30fn dirs_next() -> PathBuf {
31    let home = std::env::var("HOME").unwrap_or_else(|_| ".".to_string());
32    PathBuf::from(home).join(".aimem")
33}
34
35fn resolve_db_override(raw: &str) -> PathBuf {
36    let path = PathBuf::from(shellexpand(raw));
37    if path.extension().is_some() {
38        path
39    } else {
40        path.join(DEFAULT_DB_FILE_NAME)
41    }
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct Config {
46    /// Path to the Turso DB file.
47    pub db_path: PathBuf,
48
49    /// Path to the identity file (L0).
50    pub identity_path: PathBuf,
51
52    /// Drawer collection name (kept for reference; Turso uses a table).
53    #[serde(default = "default_collection_name")]
54    pub collection_name: String,
55
56    /// Wings available by default.
57    #[serde(default = "default_topic_wings")]
58    pub topic_wings: Vec<String>,
59}
60
61fn default_collection_name() -> String {
62    "aimem_drawers".to_string()
63}
64
65fn default_topic_wings() -> Vec<String> {
66    vec![
67        "emotions".into(),
68        "consciousness".into(),
69        "memory".into(),
70        "technical".into(),
71        "identity".into(),
72        "family".into(),
73        "creative".into(),
74    ]
75}
76
77impl Default for Config {
78    fn default() -> Self {
79        Self {
80            db_path: default_db_path(),
81            identity_path: default_identity_path(),
82            collection_name: default_collection_name(),
83            topic_wings: default_topic_wings(),
84        }
85    }
86}
87
88impl Config {
89    /// Load config from env vars > `~/.aimem/config.json` > defaults.
90    pub fn load() -> Result<Self, ConfigError> {
91        let mut cfg = Self::default();
92
93        // Override from config file
94        let config_file = dirs_next().join("config.json");
95        if config_file.exists() {
96            let raw = std::fs::read_to_string(&config_file)?;
97            let file_cfg: serde_json::Value = serde_json::from_str(&raw)?;
98
99            if let Some(p) = file_cfg["db_path"].as_str() {
100                cfg.db_path = PathBuf::from(shellexpand(p));
101            }
102            if let Some(p) = file_cfg["identity_path"].as_str() {
103                cfg.identity_path = PathBuf::from(shellexpand(p));
104            }
105            if let Some(n) = file_cfg["collection_name"].as_str() {
106                cfg.collection_name = n.to_string();
107            }
108        }
109
110        // Override from env vars
111        if let Ok(p) = std::env::var("AIMEM_DB_PATH") {
112            cfg.db_path = resolve_db_override(&p);
113        }
114        if let Ok(p) = std::env::var("AIMEM_IDENTITY_PATH") {
115            cfg.identity_path = PathBuf::from(shellexpand(&p));
116        }
117
118        Ok(cfg)
119    }
120
121    /// Ensure the config directory exists and write a default config.json.
122    pub fn init(&self) -> Result<(), ConfigError> {
123        let dir = dirs_next();
124        std::fs::create_dir_all(&dir)?;
125        let config_file = dir.join("config.json");
126        if !config_file.exists() {
127            let json = serde_json::to_string_pretty(self)?;
128            std::fs::write(&config_file, json)?;
129        }
130        Ok(())
131    }
132}
133
134/// Simple `~` expansion (no full shell expansion needed).
135fn shellexpand(s: &str) -> String {
136    if s.starts_with("~/") {
137        let home = std::env::var("HOME").unwrap_or_else(|_| ".".to_string());
138        format!("{}/{}", home, &s[2..])
139    } else {
140        s.to_string()
141    }
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147
148    #[test]
149    fn default_db_path_uses_aimem_db_name() {
150        let resolved = default_db_path();
151        assert_eq!(
152            resolved.file_name().and_then(|name| name.to_str()),
153            Some(DEFAULT_DB_FILE_NAME)
154        );
155    }
156
157    #[test]
158    fn db_override_uses_aimem_db_for_directory_inputs() {
159        let dir = tempfile::tempdir().expect("failed to create temp dir");
160        let raw = dir.path().to_string_lossy();
161
162        assert_eq!(
163            resolve_db_override(&raw),
164            dir.path().join(DEFAULT_DB_FILE_NAME)
165        );
166    }
167}