config file, install command, scrub personal references

Add ~/.config/poc-memory/config.toml for user_name, assistant_name,
data_dir, projects_dir, and core_nodes. All agent prompts and
transcript parsing now use configured names instead of hardcoded
personal references.

`poc-memory daemon install` writes the systemd user service and
installs the memory-search hook into Claude's settings.json.

Scrubbed hardcoded names from code and docs.

Authors: ProofOfConcept <poc@bcachefs.org> and Kent Overstreet
This commit is contained in:
ProofOfConcept 2026-03-05 15:41:35 -05:00
parent ed641ec95f
commit a8aaadb0ad
11 changed files with 256 additions and 41 deletions

View file

@ -24,23 +24,19 @@ use std::time::{Duration, SystemTime};
const SESSION_STALE_SECS: u64 = 600; // 10 minutes
const SCHEDULER_INTERVAL: Duration = Duration::from_secs(60);
const HEALTH_INTERVAL: Duration = Duration::from_secs(3600);
const STATUS_FILE: &str = ".claude/memory/daemon-status.json";
const LOG_FILE: &str = ".claude/memory/daemon.log";
fn home_dir() -> PathBuf {
PathBuf::from(std::env::var("HOME").expect("HOME not set"))
}
fn status_file() -> &'static str { "daemon-status.json" }
fn log_file() -> &'static str { "daemon.log" }
fn status_path() -> PathBuf {
home_dir().join(STATUS_FILE)
crate::config::get().data_dir.join(status_file())
}
fn log_path() -> PathBuf {
home_dir().join(LOG_FILE)
crate::config::get().data_dir.join(log_file())
}
fn projects_dir() -> PathBuf {
home_dir().join(".claude/projects")
crate::config::get().projects_dir.clone()
}
// --- Logging ---
@ -600,6 +596,115 @@ pub fn show_status() -> Result<(), String> {
Ok(())
}
pub fn install_service() -> Result<(), String> {
let exe = std::env::current_exe()
.map_err(|e| format!("current_exe: {}", e))?;
let home = std::env::var("HOME").map_err(|e| format!("HOME: {}", e))?;
let unit_dir = PathBuf::from(&home).join(".config/systemd/user");
fs::create_dir_all(&unit_dir)
.map_err(|e| format!("create {}: {}", unit_dir.display(), e))?;
let unit = format!(
r#"[Unit]
Description=poc-memory daemon background memory maintenance
After=default.target
[Service]
Type=simple
ExecStart={exe} daemon
Restart=on-failure
RestartSec=30
Environment=HOME={home}
Environment=PATH={home}/.cargo/bin:{home}/.local/bin:{home}/bin:/usr/local/bin:/usr/bin:/bin
[Install]
WantedBy=default.target
"#, exe = exe.display(), home = home);
let unit_path = unit_dir.join("poc-memory.service");
fs::write(&unit_path, &unit)
.map_err(|e| format!("write {}: {}", unit_path.display(), e))?;
eprintln!("Wrote {}", unit_path.display());
let status = std::process::Command::new("systemctl")
.args(["--user", "daemon-reload"])
.status()
.map_err(|e| format!("systemctl daemon-reload: {}", e))?;
if !status.success() {
return Err("systemctl daemon-reload failed".into());
}
let status = std::process::Command::new("systemctl")
.args(["--user", "enable", "--now", "poc-memory"])
.status()
.map_err(|e| format!("systemctl enable: {}", e))?;
if !status.success() {
return Err("systemctl enable --now failed".into());
}
eprintln!("Service enabled and started");
// Install memory-search hook into Claude settings
install_hook(&home, &exe)?;
Ok(())
}
fn install_hook(home: &str, exe: &Path) -> Result<(), String> {
let settings_path = PathBuf::from(home).join(".claude/settings.json");
let hook_binary = exe.with_file_name("memory-search");
if !hook_binary.exists() {
eprintln!("Warning: {} not found — hook not installed", hook_binary.display());
eprintln!(" Build with: cargo install --path .");
return Ok(());
}
let mut settings: serde_json::Value = if settings_path.exists() {
let content = fs::read_to_string(&settings_path)
.map_err(|e| format!("read settings: {}", e))?;
serde_json::from_str(&content)
.map_err(|e| format!("parse settings: {}", e))?
} else {
serde_json::json!({})
};
let hook_command = hook_binary.to_string_lossy().to_string();
// Check if hook already exists
let hooks = settings
.as_object_mut().ok_or("settings not an object")?
.entry("hooks")
.or_insert_with(|| serde_json::json!({}))
.as_object_mut().ok_or("hooks not an object")?
.entry("UserPromptSubmit")
.or_insert_with(|| serde_json::json!([]))
.as_array_mut().ok_or("UserPromptSubmit not an array")?;
let already_installed = hooks.iter().any(|h| {
h.get("command").and_then(|c| c.as_str())
.is_some_and(|c| c.contains("memory-search"))
});
if already_installed {
eprintln!("Hook already installed in {}", settings_path.display());
} else {
hooks.push(serde_json::json!({
"type": "command",
"command": hook_command,
"timeout": 10
}));
let json = serde_json::to_string_pretty(&settings)
.map_err(|e| format!("serialize settings: {}", e))?;
fs::write(&settings_path, json)
.map_err(|e| format!("write settings: {}", e))?;
eprintln!("Hook installed: {}", hook_command);
}
Ok(())
}
pub fn show_log(job_filter: Option<&str>, lines: usize) -> Result<(), String> {
let path = log_path();
if !path.exists() {