1use 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
18pub 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
25pub 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 pub db_path: PathBuf,
48
49 pub identity_path: PathBuf,
51
52 #[serde(default = "default_collection_name")]
54 pub collection_name: String,
55
56 #[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 pub fn load() -> Result<Self, ConfigError> {
91 let mut cfg = Self::default();
92
93 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 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 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
134fn 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}