1use 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
19pub 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
27pub 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 pub db_path: PathBuf,
60
61 pub identity_path: PathBuf,
63
64 #[serde(default = "default_collection_name")]
66 pub collection_name: String,
67
68 #[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 pub fn load() -> Result<Self, ConfigError> {
103 let mut cfg = Self::default();
104
105 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 if let Ok(p) = std::env::var("AIMEM_DB_PATH") {
124 cfg.db_path = resolve_db_override(&p, DEFAULT_DB_FILE_NAME);
126 } else if let Ok(p) = std::env::var("AIMEM_PALACE_PATH") {
127 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 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
150fn 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}