diff --git a/poc-memory/src/config.rs b/poc-memory/src/config.rs index cb384af..78cc446 100644 --- a/poc-memory/src/config.rs +++ b/poc-memory/src/config.rs @@ -51,6 +51,8 @@ pub struct Config { pub context_groups: Vec, /// Max concurrent LLM calls in the daemon. pub llm_concurrency: usize, + /// Directory containing prompt templates for agents. + pub prompts_dir: PathBuf, /// Separate Claude config dir for background agent work (daemon jobs). /// If set, passed as CLAUDE_CONFIG_DIR so the daemon authenticates /// with different OAuth credentials than the interactive session. @@ -81,6 +83,7 @@ impl Default for Config { }, ], llm_concurrency: 1, + prompts_dir: home.join("poc/memory/prompts"), agent_config_dir: None, } } @@ -138,6 +141,9 @@ impl Config { if let Some(n) = cfg.get("llm_concurrency").and_then(|v| v.as_u64()) { config.llm_concurrency = n.max(1) as usize; } + if let Some(s) = cfg.get("prompts_dir").and_then(|v| v.as_str()) { + config.prompts_dir = expand_home(s); + } if let Some(s) = cfg.get("agent_config_dir").and_then(|v| v.as_str()) { config.agent_config_dir = Some(expand_home(s)); } diff --git a/poc-memory/src/graph.rs b/poc-memory/src/graph.rs index ed2d513..7c9e3f8 100644 --- a/poc-memory/src/graph.rs +++ b/poc-memory/src/graph.rs @@ -519,8 +519,7 @@ pub struct MetricsSnapshot { } fn metrics_log_path() -> std::path::PathBuf { - let home = std::env::var("HOME").unwrap_or_default(); - std::path::PathBuf::from(home).join(".claude/memory/metrics.jsonl") + crate::store::memory_dir().join("metrics.jsonl") } /// Load previous metrics snapshots diff --git a/poc-memory/src/knowledge.rs b/poc-memory/src/knowledge.rs index 9987c97..db53961 100644 --- a/poc-memory/src/knowledge.rs +++ b/poc-memory/src/knowledge.rs @@ -22,8 +22,7 @@ use std::fs; use std::path::{Path, PathBuf}; fn projects_dir() -> PathBuf { - let home = std::env::var("HOME").unwrap_or_else(|_| ".".into()); - PathBuf::from(home).join(".claude/projects") + crate::config::get().projects_dir.clone() } // --------------------------------------------------------------------------- diff --git a/poc-memory/src/main.rs b/poc-memory/src/main.rs index c36bc89..f49dc3d 100644 --- a/poc-memory/src/main.rs +++ b/poc-memory/src/main.rs @@ -19,13 +19,11 @@ use poc_memory::*; use clap::{Parser, Subcommand}; -use std::env; use std::process; /// Find the most recently modified .jsonl transcript in the Claude projects dir. fn find_current_transcript() -> Option { - let home = env::var("HOME").ok()?; - let projects = std::path::Path::new(&home).join(".claude/projects"); + let projects = config::get().projects_dir.clone(); if !projects.exists() { return None; } let mut newest: Option<(std::time::SystemTime, std::path::PathBuf)> = None; @@ -1022,10 +1020,89 @@ fn cmd_link_impact(source: &str, target: &str) -> Result<(), String> { Ok(()) } +/// Apply links from a single agent result JSON file. +/// Returns (links_applied, errors). +fn apply_agent_file( + store: &mut store::Store, + data: &serde_json::Value, +) -> (usize, usize) { + let agent_result = data.get("agent_result").or(Some(data)); + let links = match agent_result.and_then(|r| r.get("links")).and_then(|l| l.as_array()) { + Some(l) => l, + None => return (0, 0), + }; + + let entry_text = data.get("entry_text") + .and_then(|v| v.as_str()) + .unwrap_or(""); + + if let (Some(start), Some(end)) = ( + agent_result.and_then(|r| r.get("source_start")).and_then(|v| v.as_u64()), + agent_result.and_then(|r| r.get("source_end")).and_then(|v| v.as_u64()), + ) { + println!(" Source: L{}-L{}", start, end); + } + + let mut applied = 0; + let mut errors = 0; + + for link in links { + let target = match link.get("target").and_then(|v| v.as_str()) { + Some(t) => t, + None => continue, + }; + let reason = link.get("reason").and_then(|v| v.as_str()).unwrap_or(""); + + if let Some(note) = target.strip_prefix("NOTE:") { + println!(" NOTE: {} — {}", note, reason); + continue; + } + + let resolved = match store.resolve_key(target) { + Ok(r) => r, + Err(_) => { + println!(" SKIP {} (not found in graph)", target); + continue; + } + }; + + let source_key = match store.find_journal_node(entry_text) { + Some(k) => k, + None => { + println!(" SKIP {} (no matching journal node)", target); + continue; + } + }; + + let source_uuid = match store.nodes.get(&source_key) { + Some(n) => n.uuid, + None => continue, + }; + let target_uuid = match store.nodes.get(&resolved) { + Some(n) => n.uuid, + None => continue, + }; + + let rel = store::new_relation( + source_uuid, target_uuid, + store::RelationType::Link, + 0.5, + &source_key, &resolved, + ); + if let Err(e) = store.add_relation(rel) { + eprintln!(" Error adding relation: {}", e); + errors += 1; + } else { + println!(" LINK {} → {} ({})", source_key, resolved, reason); + applied += 1; + } + } + + (applied, errors) +} + fn cmd_apply_agent(process_all: bool) -> Result<(), String> { - let home = env::var("HOME").unwrap_or_default(); - let results_dir = std::path::PathBuf::from(&home) - .join(".claude/memory/agent-results"); + let results_dir = store::memory_dir().join("agent-results"); if !results_dir.exists() { println!("No agent results directory"); @@ -1036,7 +1113,6 @@ fn cmd_apply_agent(process_all: bool) -> Result<(), String> { let mut applied = 0; let mut errors = 0; - // Find .json result files let mut files: Vec<_> = std::fs::read_dir(&results_dir) .map_err(|e| format!("read results dir: {}", e))? .filter_map(|e| e.ok()) @@ -1064,84 +1140,11 @@ fn cmd_apply_agent(process_all: bool) -> Result<(), String> { } }; - // Check for agent_result with links - let agent_result = data.get("agent_result").or(Some(&data)); - let links = match agent_result.and_then(|r| r.get("links")).and_then(|l| l.as_array()) { - Some(l) => l, - None => continue, - }; - - let entry_text = data.get("entry_text") - .and_then(|v| v.as_str()) - .unwrap_or(""); - let source_start = agent_result - .and_then(|r| r.get("source_start")) - .and_then(|v| v.as_u64()); - let source_end = agent_result - .and_then(|r| r.get("source_end")) - .and_then(|v| v.as_u64()); - println!("Processing {}:", path.file_name().unwrap().to_string_lossy()); - if let (Some(start), Some(end)) = (source_start, source_end) { - println!(" Source: L{}-L{}", start, end); - } + let (a, e) = apply_agent_file(&mut store, &data); + applied += a; + errors += e; - for link in links { - let target = match link.get("target").and_then(|v| v.as_str()) { - Some(t) => t, - None => continue, - }; - let reason = link.get("reason").and_then(|v| v.as_str()).unwrap_or(""); - - // Skip NOTE: targets (new topics, not existing nodes) - if let Some(note) = target.strip_prefix("NOTE:") { - println!(" NOTE: {} — {}", note, reason); - continue; - } - - // Try to resolve the target key and link from journal entry - let resolved = match store.resolve_key(target) { - Ok(r) => r, - Err(_) => { - println!(" SKIP {} (not found in graph)", target); - continue; - } - }; - - let source_key = match store.find_journal_node(entry_text) { - Some(k) => k, - None => { - println!(" SKIP {} (no matching journal node)", target); - continue; - } - }; - - // Get UUIDs for both nodes - let source_uuid = match store.nodes.get(&source_key) { - Some(n) => n.uuid, - None => continue, - }; - let target_uuid = match store.nodes.get(&resolved) { - Some(n) => n.uuid, - None => continue, - }; - - let rel = store::new_relation( - source_uuid, target_uuid, - store::RelationType::Link, - 0.5, - &source_key, &resolved, - ); - if let Err(e) = store.add_relation(rel) { - eprintln!(" Error adding relation: {}", e); - errors += 1; - } else { - println!(" LINK {} → {} ({})", source_key, resolved, reason); - applied += 1; - } - } - - // Move processed file to avoid re-processing if !process_all { let done_dir = util::memory_subdir("agent-results/done")?; let dest = done_dir.join(path.file_name().unwrap()); diff --git a/poc-memory/src/neuro/prompts.rs b/poc-memory/src/neuro/prompts.rs index ee3ae56..ac8563b 100644 --- a/poc-memory/src/neuro/prompts.rs +++ b/poc-memory/src/neuro/prompts.rs @@ -11,15 +11,9 @@ use super::scoring::{ replay_queue, replay_queue_with_graph, detect_interference, }; -/// Prompt template directory -pub fn prompts_dir() -> std::path::PathBuf { - let home = std::env::var("HOME").unwrap_or_default(); - std::path::PathBuf::from(home).join("poc/memory/prompts") -} - /// Load a prompt template, replacing {{PLACEHOLDER}} with data pub fn load_prompt(name: &str, replacements: &[(&str, &str)]) -> Result { - let path = prompts_dir().join(format!("{}.md", name)); + let path = crate::config::get().prompts_dir.join(format!("{}.md", name)); let mut content = std::fs::read_to_string(&path) .map_err(|e| format!("load prompt {}: {}", path.display(), e))?; for (placeholder, data) in replacements { diff --git a/poc-memory/src/spectral.rs b/poc-memory/src/spectral.rs index e97eb99..c63e847 100644 --- a/poc-memory/src/spectral.rs +++ b/poc-memory/src/spectral.rs @@ -43,8 +43,7 @@ pub struct SpectralEmbedding { } fn embedding_path() -> PathBuf { - let home = std::env::var("HOME").unwrap_or_default(); - PathBuf::from(home).join(".claude/memory/spectral-embedding.json") + crate::store::memory_dir().join("spectral-embedding.json") } /// Compute spectral decomposition of the memory graph.