forked from kent/consciousness
Replace complex context_groups (with ContextGroup struct, ContextSource enum, labels, keys arrays) with simple string lists: - personality_nodes: loaded into main session context - agent_nodes: loaded into subconscious agent context Removed ~200 lines of code. The distinction between session and agent context is now just which list you're in, not a per-group flag. Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
220 lines
6.7 KiB
Rust
220 lines
6.7 KiB
Rust
// cli/node.rs — node subcommand handlers
|
|
//
|
|
// render, write, node-delete, node-rename, history, list-keys,
|
|
// list-edges, dump-json, lookup-bump, lookups.
|
|
|
|
use anyhow::{bail, Context, Result};
|
|
use crate::hippocampus as memory;
|
|
|
|
pub async fn cmd_weight_set(key: &str, weight: f32) -> Result<()> {
|
|
super::check_dry_run();
|
|
let result = memory::memory_weight_set(None, key, weight).await?;
|
|
println!("{}", result);
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn cmd_node_delete(key: &[String]) -> Result<()> {
|
|
if key.is_empty() {
|
|
bail!("node-delete requires a key");
|
|
}
|
|
super::check_dry_run();
|
|
let key = key.join(" ");
|
|
let result = memory::memory_delete(None, &key).await?;
|
|
println!("{}", result);
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn cmd_node_rename(old_key: &str, new_key: &str) -> Result<()> {
|
|
super::check_dry_run();
|
|
let result = memory::memory_rename(None, old_key, new_key).await?;
|
|
println!("{}", result);
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn cmd_node_restore(key: &[String]) -> Result<()> {
|
|
if key.is_empty() {
|
|
bail!("node-restore requires a key");
|
|
}
|
|
super::check_dry_run();
|
|
let key = key.join(" ");
|
|
let result = memory::memory_restore(None, &key).await?;
|
|
println!("{}", result);
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn cmd_render(key: &[String]) -> Result<()> {
|
|
if key.is_empty() {
|
|
bail!("render requires a key");
|
|
}
|
|
let key = key.join(" ");
|
|
|
|
let rendered = memory::memory_render(None, &key, None).await?;
|
|
print!("{}", rendered);
|
|
|
|
// Mark as seen if we're inside a Claude session (not an agent subprocess —
|
|
// agents read the seen set but shouldn't write to it as a side effect of
|
|
// tool calls; only surface_agent_cycle should mark keys seen)
|
|
if std::env::var("POC_AGENT").is_err()
|
|
&& let Ok(session_id) = std::env::var("POC_SESSION_ID")
|
|
&& !session_id.is_empty()
|
|
{
|
|
let state_dir = crate::store::memory_dir().join("sessions");
|
|
let seen_path = state_dir.join(format!("seen-{}", session_id));
|
|
if let Ok(mut f) = std::fs::OpenOptions::new()
|
|
.create(true).append(true).open(seen_path)
|
|
{
|
|
use std::io::Write;
|
|
let ts = chrono::Local::now().format("%Y-%m-%dT%H:%M:%S");
|
|
let _ = writeln!(f, "{}\t{}", ts, key);
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn cmd_history(key: &[String], full: bool) -> Result<()> {
|
|
if key.is_empty() {
|
|
bail!("history requires a key");
|
|
}
|
|
let key = key.join(" ");
|
|
let result = memory::memory_history(None, &key, Some(full)).await?;
|
|
print!("{}", result);
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn cmd_write(key: &[String]) -> Result<()> {
|
|
if key.is_empty() {
|
|
bail!("write requires a key (reads content from stdin)");
|
|
}
|
|
let key = key.join(" ");
|
|
let mut content = String::new();
|
|
std::io::Read::read_to_string(&mut std::io::stdin(), &mut content)
|
|
.context("read stdin")?;
|
|
|
|
if content.trim().is_empty() {
|
|
bail!("No content on stdin");
|
|
}
|
|
super::check_dry_run();
|
|
|
|
let result = memory::memory_write(None, &key, &content).await?;
|
|
println!("{}", result);
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn cmd_edit(key: &[String]) -> Result<()> {
|
|
if key.is_empty() {
|
|
bail!("edit requires a key");
|
|
}
|
|
let key = key.join(" ");
|
|
|
|
// Get raw content
|
|
let content = memory::memory_render(None, &key, Some(true)).await
|
|
.unwrap_or_default();
|
|
|
|
let tmp = std::env::temp_dir().join(format!("poc-memory-edit-{}.md", key.replace('/', "_")));
|
|
std::fs::write(&tmp, &content)
|
|
.with_context(|| format!("write temp file {}", tmp.display()))?;
|
|
|
|
let editor = std::env::var("EDITOR").unwrap_or_else(|_| "vi".into());
|
|
let status = std::process::Command::new(&editor)
|
|
.arg(&tmp)
|
|
.status()
|
|
.with_context(|| format!("spawn {}", editor))?;
|
|
|
|
if !status.success() {
|
|
let _ = std::fs::remove_file(&tmp);
|
|
bail!("{} exited with {}", editor, status);
|
|
}
|
|
|
|
let new_content = std::fs::read_to_string(&tmp)
|
|
.with_context(|| format!("read temp file {}", tmp.display()))?;
|
|
let _ = std::fs::remove_file(&tmp);
|
|
|
|
if new_content == content {
|
|
println!("No change: '{}'", key);
|
|
return Ok(());
|
|
}
|
|
|
|
if new_content.trim().is_empty() {
|
|
bail!("Content is empty, aborting");
|
|
}
|
|
|
|
super::check_dry_run();
|
|
let result = memory::memory_write(None, &key, &new_content).await?;
|
|
println!("{}", result);
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn cmd_search(keys: &[String]) -> Result<()> {
|
|
if keys.is_empty() {
|
|
bail!("search requires seed keys");
|
|
}
|
|
let result = memory::memory_search(None, keys.to_vec(), None, None, None, None).await?;
|
|
print!("{}", result);
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn cmd_query(expr: &[String]) -> Result<()> {
|
|
if expr.is_empty() {
|
|
bail!("query requires an expression (try: poc-memory query --help)");
|
|
}
|
|
|
|
let query_str = expr.join(" ");
|
|
let result = memory::memory_query(None, &query_str, None).await?;
|
|
print!("{}", result);
|
|
Ok(())
|
|
}
|
|
|
|
/// Load content for a list of node keys.
|
|
async fn load_nodes(keys: &[String]) -> Vec<(String, String)> {
|
|
let mut results = Vec::new();
|
|
for key in keys {
|
|
if let Ok(content) = memory::memory_render(None, key, Some(true)).await {
|
|
if !content.trim().is_empty() {
|
|
results.push((key.clone(), content.trim().to_string()));
|
|
}
|
|
}
|
|
}
|
|
results
|
|
}
|
|
|
|
pub async fn cmd_load_context(stats: bool) -> Result<()> {
|
|
let cfg = crate::config::get();
|
|
|
|
let personality = load_nodes(&cfg.personality_nodes).await;
|
|
let agent = load_nodes(&cfg.agent_nodes).await;
|
|
|
|
if stats {
|
|
let p_words: usize = personality.iter().map(|(_, c)| c.split_whitespace().count()).sum();
|
|
let a_words: usize = agent.iter().map(|(_, c)| c.split_whitespace().count()).sum();
|
|
|
|
println!("{:<25} {:>6} {:>8}", "GROUP", "ITEMS", "WORDS");
|
|
println!("{}", "-".repeat(42));
|
|
println!("{:<25} {:>6} {:>8}", "personality_nodes", personality.len(), p_words);
|
|
println!("{:<25} {:>6} {:>8}", "agent_nodes", agent.len(), a_words);
|
|
println!("{}", "-".repeat(42));
|
|
println!("{:<25} {:>6} {:>8}", "TOTAL", personality.len() + agent.len(), p_words + a_words);
|
|
return Ok(());
|
|
}
|
|
|
|
println!("=== MEMORY SYSTEM ({}) ===", cfg.assistant_name);
|
|
|
|
if !personality.is_empty() {
|
|
println!("--- personality_nodes ({}) ---", personality.len());
|
|
for (key, content) in personality {
|
|
println!("## {}", key);
|
|
println!("{}\n", content);
|
|
}
|
|
}
|
|
|
|
if !agent.is_empty() {
|
|
println!("--- agent_nodes ({}) ---", agent.len());
|
|
for (key, content) in agent {
|
|
println!("## {}", key);
|
|
println!("{}\n", content);
|
|
}
|
|
}
|
|
|
|
println!("=== END MEMORY LOAD ===");
|
|
Ok(())
|
|
}
|