consciousness/src/cli/node.rs

193 lines
6.2 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 crate::store;
pub fn cmd_weight_set(key: &str, weight: f32) -> Result<(), String> {
super::check_dry_run();
let result = crate::mcp_server::memory_rpc(
"memory_weight_set",
serde_json::json!({"key": key, "weight": weight}),
).map_err(|e| e.to_string())?;
println!("{}", result);
Ok(())
}
pub fn cmd_list_keys(pattern: Option<&str>) -> Result<(), String> {
let store = store::Store::load()?;
let g = store.build_graph();
if let Some(pat) = pattern {
let pat_lower = pat.to_lowercase();
let (prefix, suffix, middle) = if pat_lower.starts_with('*') && pat_lower.ends_with('*') {
(None, None, Some(pat_lower.trim_matches('*').to_string()))
} else if pat_lower.starts_with('*') {
(None, Some(pat_lower.trim_start_matches('*').to_string()), None)
} else if pat_lower.ends_with('*') {
(Some(pat_lower.trim_end_matches('*').to_string()), None, None)
} else {
(None, None, Some(pat_lower.clone()))
};
let mut keys: Vec<_> = store.nodes.keys()
.filter(|k| {
let kl = k.to_lowercase();
if let Some(ref m) = middle { kl.contains(m.as_str()) }
else if let Some(ref p) = prefix { kl.starts_with(p.as_str()) }
else if let Some(ref s) = suffix { kl.ends_with(s.as_str()) }
else { true }
})
.cloned()
.collect();
keys.sort();
for k in keys { println!("{}", k); }
Ok(())
} else {
crate::query_parser::run_query(&store, &g, "* | sort key asc")
}
}
pub fn cmd_node_delete(key: &[String]) -> Result<(), String> {
if key.is_empty() {
return Err("node-delete requires a key".into());
}
super::check_dry_run();
let key = key.join(" ");
let result = crate::mcp_server::memory_rpc(
"memory_delete",
serde_json::json!({"key": key}),
).map_err(|e| e.to_string())?;
println!("{}", result);
Ok(())
}
pub fn cmd_node_rename(old_key: &str, new_key: &str) -> Result<(), String> {
super::check_dry_run();
let result = crate::mcp_server::memory_rpc(
"memory_rename",
serde_json::json!({"old_key": old_key, "new_key": new_key}),
).map_err(|e| e.to_string())?;
println!("{}", result);
Ok(())
}
pub fn cmd_render(key: &[String]) -> Result<(), String> {
if key.is_empty() {
return Err("render requires a key".into());
}
let key = key.join(" ");
let bare = store::strip_md_suffix(&key);
let rendered = crate::mcp_server::memory_rpc(
"memory_render",
serde_json::json!({"key": bare}),
).map_err(|e| e.to_string())?;
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, bare);
}
}
Ok(())
}
pub fn cmd_history(key: &[String], full: bool) -> Result<(), String> {
if key.is_empty() {
return Err("history requires a key".into());
}
let key = key.join(" ");
let result = crate::mcp_server::memory_rpc(
"memory_history",
serde_json::json!({"key": key, "full": full}),
).map_err(|e| e.to_string())?;
print!("{}", result);
Ok(())
}
pub fn cmd_write(key: &[String]) -> Result<(), String> {
if key.is_empty() {
return Err("write requires a key (reads content from stdin)".into());
}
let key = key.join(" ");
let mut content = String::new();
std::io::Read::read_to_string(&mut std::io::stdin(), &mut content)
.map_err(|e| format!("read stdin: {}", e))?;
if content.trim().is_empty() {
return Err("No content on stdin".into());
}
super::check_dry_run();
let result = crate::mcp_server::memory_rpc(
"memory_write",
serde_json::json!({"key": key, "content": content}),
).map_err(|e| e.to_string())?;
println!("{}", result);
Ok(())
}
pub fn cmd_edit(key: &[String]) -> Result<(), String> {
if key.is_empty() {
return Err("edit requires a key".into());
}
let key = key.join(" ");
// Get raw content via RPC
let content = crate::mcp_server::memory_rpc(
"memory_render",
serde_json::json!({"key": key, "raw": true}),
).unwrap_or_default();
let tmp = std::env::temp_dir().join(format!("poc-memory-edit-{}.md", key.replace('/', "_")));
std::fs::write(&tmp, &content)
.map_err(|e| format!("write temp file: {}", e))?;
let editor = std::env::var("EDITOR").unwrap_or_else(|_| "vi".into());
let status = std::process::Command::new(&editor)
.arg(&tmp)
.status()
.map_err(|e| format!("spawn {}: {}", editor, e))?;
if !status.success() {
let _ = std::fs::remove_file(&tmp);
return Err(format!("{} exited with {}", editor, status));
}
let new_content = std::fs::read_to_string(&tmp)
.map_err(|e| format!("read temp file: {}", e))?;
let _ = std::fs::remove_file(&tmp);
if new_content == content {
println!("No change: '{}'", key);
return Ok(());
}
if new_content.trim().is_empty() {
return Err("Content is empty, aborting".into());
}
super::check_dry_run();
let result = crate::mcp_server::memory_rpc(
"memory_write",
serde_json::json!({"key": key, "content": new_content}),
).map_err(|e| e.to_string())?;
println!("{}", result);
Ok(())
}