2026-03-05 15:41:35 -05:00
|
|
|
// 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<Config> = OnceLock::new();
|
|
|
|
|
|
2026-03-05 15:54:44 -05:00
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
|
pub struct ContextGroup {
|
|
|
|
|
pub label: String,
|
|
|
|
|
pub keys: Vec<String>,
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-05 15:41:35 -05:00
|
|
|
#[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<String>,
|
2026-03-05 15:54:44 -05:00
|
|
|
/// 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<ContextGroup>,
|
2026-03-05 15:41:35 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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()],
|
2026-03-05 15:54:44 -05:00
|
|
|
journal_days: 7,
|
|
|
|
|
journal_max: 20,
|
|
|
|
|
context_groups: vec![
|
|
|
|
|
ContextGroup { label: "identity".into(), keys: vec!["identity.md".into()] },
|
|
|
|
|
],
|
2026-03-05 15:41:35 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-05 15:54:44 -05:00
|
|
|
// Simple TOML parser: flat key=value pairs + [context.NAME] sections.
|
|
|
|
|
let mut context_groups: Vec<ContextGroup> = Vec::new();
|
|
|
|
|
let mut current_section: Option<String> = None;
|
|
|
|
|
let mut current_label: Option<String> = None;
|
|
|
|
|
let mut current_keys: Vec<String> = Vec::new();
|
|
|
|
|
let mut saw_context = false;
|
|
|
|
|
|
2026-03-05 15:41:35 -05:00
|
|
|
for line in content.lines() {
|
|
|
|
|
let line = line.trim();
|
|
|
|
|
if line.is_empty() || line.starts_with('#') {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2026-03-05 15:54:44 -05:00
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-05 15:41:35 -05:00
|
|
|
let Some((key, value)) = line.split_once('=') else { continue };
|
|
|
|
|
let key = key.trim();
|
|
|
|
|
let value = value.trim().trim_matches('"');
|
|
|
|
|
|
2026-03-05 15:54:44 -05:00
|
|
|
// 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
|
2026-03-05 15:41:35 -05:00
|
|
|
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();
|
|
|
|
|
}
|
2026-03-05 15:54:44 -05:00
|
|
|
"journal_days" => {
|
|
|
|
|
if let Ok(d) = value.parse() { config.journal_days = d; }
|
|
|
|
|
}
|
|
|
|
|
"journal_max" => {
|
|
|
|
|
if let Ok(m) = value.parse() { config.journal_max = m; }
|
|
|
|
|
}
|
2026-03-05 15:41:35 -05:00
|
|
|
_ => {}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-05 15:54:44 -05:00
|
|
|
// 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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-05 15:41:35 -05:00
|
|
|
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)
|
|
|
|
|
}
|