// Configuration for poc-memory // // Loaded from ~/.config/poc-memory/config.toml (or POC_MEMORY_CONFIG env). // Falls back to sensible defaults if no config file exists. use std::path::PathBuf; use std::sync::OnceLock; static CONFIG: OnceLock = OnceLock::new(); #[derive(Debug, Clone)] pub struct ContextGroup { pub label: String, pub keys: Vec, } #[derive(Debug, Clone)] pub struct Config { /// Display name for the human user in transcripts/prompts. pub user_name: String, /// Display name for the AI assistant. pub assistant_name: String, /// Base directory for memory data (store, logs, status). pub data_dir: PathBuf, /// Directory containing Claude session transcripts. pub projects_dir: PathBuf, /// Core node keys that should never be decayed/deleted. pub core_nodes: Vec, /// How many days of journal to include in load-context. pub journal_days: u32, /// Max journal entries to include in load-context. pub journal_max: usize, /// Ordered context groups for session-start loading. pub context_groups: Vec, } impl Default for Config { fn default() -> Self { let home = PathBuf::from(std::env::var("HOME").expect("HOME not set")); Self { user_name: "User".to_string(), assistant_name: "Assistant".to_string(), data_dir: home.join(".claude/memory"), projects_dir: home.join(".claude/projects"), core_nodes: vec!["identity.md".to_string()], journal_days: 7, journal_max: 20, context_groups: vec![ ContextGroup { label: "identity".into(), keys: vec!["identity.md".into()] }, ], } } } impl Config { fn load_from_file() -> Self { let path = std::env::var("POC_MEMORY_CONFIG") .map(PathBuf::from) .unwrap_or_else(|_| { PathBuf::from(std::env::var("HOME").expect("HOME not set")) .join(".config/poc-memory/config.toml") }); let mut config = Config::default(); let Ok(content) = std::fs::read_to_string(&path) else { return config; }; // Simple TOML parser: flat key=value pairs + [context.NAME] sections. let mut context_groups: Vec = Vec::new(); let mut current_section: Option = None; let mut current_label: Option = None; let mut current_keys: Vec = Vec::new(); let mut saw_context = false; for line in content.lines() { let line = line.trim(); if line.is_empty() || line.starts_with('#') { continue; } // Section header: [context.NAME] if line.starts_with('[') && line.ends_with(']') { // Flush previous context section if let Some(name) = current_section.take() { let label = current_label.take() .unwrap_or_else(|| name.replace('_', " ")); context_groups.push(ContextGroup { label, keys: std::mem::take(&mut current_keys) }); } let section = &line[1..line.len()-1]; if let Some(name) = section.strip_prefix("context.") { current_section = Some(name.to_string()); saw_context = true; } continue; } let Some((key, value)) = line.split_once('=') else { continue }; let key = key.trim(); let value = value.trim().trim_matches('"'); // Inside a [context.X] section if current_section.is_some() { match key { "keys" => { current_keys = value.split(',') .map(|s| s.trim().to_string()) .filter(|s| !s.is_empty()) .collect(); } "label" => current_label = Some(value.to_string()), _ => {} } continue; } // Top-level keys match key { "user_name" => config.user_name = value.to_string(), "assistant_name" => config.assistant_name = value.to_string(), "data_dir" => config.data_dir = expand_home(value), "projects_dir" => config.projects_dir = expand_home(value), "core_nodes" => { config.core_nodes = value.split(',') .map(|s| s.trim().to_string()) .filter(|s| !s.is_empty()) .collect(); } "journal_days" => { if let Ok(d) = value.parse() { config.journal_days = d; } } "journal_max" => { if let Ok(m) = value.parse() { config.journal_max = m; } } _ => {} } } // Flush final section if let Some(name) = current_section.take() { let label = current_label.take() .unwrap_or_else(|| name.replace('_', " ")); context_groups.push(ContextGroup { label, keys: current_keys }); } if saw_context { config.context_groups = context_groups; } config } } fn expand_home(path: &str) -> PathBuf { if let Some(rest) = path.strip_prefix("~/") { PathBuf::from(std::env::var("HOME").expect("HOME not set")).join(rest) } else { PathBuf::from(path) } } /// Get the global config (loaded once on first access). pub fn get() -> &'static Config { CONFIG.get_or_init(Config::load_from_file) }