consciousness/src/cli/node.rs

253 lines
8.1 KiB
Rust
Raw Normal View History

// 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(())
}
2026-04-12 23:01:39 -04:00
pub async fn cmd_search(keys: &[String]) -> Result<()> {
2026-04-12 23:01:39 -04:00
if keys.is_empty() {
bail!("search requires seed keys");
2026-04-12 23:01:39 -04:00
}
let result = memory::memory_search(None, keys.to_vec(), None, None, None, None).await?;
2026-04-12 23:01:39 -04:00
print!("{}", result);
Ok(())
}
pub async fn cmd_query(expr: &[String]) -> Result<()> {
2026-04-12 23:01:39 -04:00
if expr.is_empty() {
bail!("query requires an expression (try: poc-memory query --help)");
2026-04-12 23:01:39 -04:00
}
let query_str = expr.join(" ");
let result = memory::memory_query(None, &query_str, None).await?;
2026-04-12 23:01:39 -04:00
print!("{}", result);
Ok(())
}
/// Get group content (handles daemon or local fallback)
pub async fn get_group_content(group: &crate::config::ContextGroup, cfg: &crate::config::Config) -> Vec<(String, String)> {
2026-04-12 23:01:39 -04:00
match group.source {
crate::config::ContextSource::Journal => {
// Query for recent journal entries
let window: i64 = cfg.journal_days as i64 * 24 * 3600;
let query = format!("all | type:episodic | age:<{} | sort:timestamp | limit:{}",
window, cfg.journal_max);
let keys_str = match memory::memory_query(None, &query, None).await {
2026-04-12 23:01:39 -04:00
Ok(s) => s,
Err(_) => return vec![],
};
// Parse keys (one per line) and render each
let mut results = Vec::new();
for key in keys_str.lines().filter(|k| !k.is_empty() && *k != "no results") {
if let Ok(content) = memory::memory_render(None, key, Some(true)).await {
if !content.trim().is_empty() {
results.push((key.to_string(), content));
}
}
}
results
2026-04-12 23:01:39 -04:00
}
crate::config::ContextSource::Store => {
let mut results = Vec::new();
for key in &group.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
2026-04-12 23:01:39 -04:00
}
}
}
pub async fn cmd_load_context(stats: bool) -> Result<()> {
2026-04-12 23:01:39 -04:00
let cfg = crate::config::get();
if stats {
let mut total_words = 0;
let mut total_entries = 0;
println!("{:<25} {:>6} {:>8}", "GROUP", "ITEMS", "WORDS");
println!("{}", "-".repeat(42));
for group in &cfg.context_groups {
let entries = get_group_content(group, &cfg).await;
2026-04-12 23:01:39 -04:00
let words: usize = entries.iter()
.map(|(_, c)| c.split_whitespace().count())
.sum();
let count = entries.len();
println!("{:<25} {:>6} {:>8}", group.label, count, words);
total_words += words;
total_entries += count;
}
println!("{}", "-".repeat(42));
println!("{:<25} {:>6} {:>8}", "TOTAL", total_entries, total_words);
return Ok(());
}
println!("=== MEMORY SYSTEM ({}) ===", cfg.assistant_name);
for group in &cfg.context_groups {
let entries = get_group_content(group, &cfg).await;
2026-04-12 23:01:39 -04:00
if !entries.is_empty() && group.source == crate::config::ContextSource::Journal {
println!("--- recent journal entries ({}/{}) ---",
entries.len(), cfg.journal_max);
}
for (key, content) in entries {
if group.source == crate::config::ContextSource::Journal {
println!("## {}", key);
} else {
println!("--- {} ({}) ---", key, group.label);
}
println!("{}\n", content);
}
}
println!("=== END MEMORY LOAD ===");
Ok(())
}