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