From 366b17163d264bff9d7c14c792f65791c15781ba Mon Sep 17 00:00:00 2001 From: Kent Overstreet Date: Sun, 12 Apr 2026 22:15:53 -0400 Subject: [PATCH] cli: convert write/delete/journal-write to use memory_rpc MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - cmd_write → memory_write RPC - cmd_node_delete → new memory_delete MCP tool + RPC - cmd_journal_write → journal_new RPC Removes validate_inline_refs and find_current_transcript (now handled server-side or not needed). Co-Authored-By: Proof of Concept --- src/agent/tools/memory.rs | 16 +++++++- src/cli/journal.rs | 64 ++++++-------------------------- src/cli/node.rs | 78 ++++++--------------------------------- 3 files changed, 37 insertions(+), 121 deletions(-) diff --git a/src/agent/tools/memory.rs b/src/agent/tools/memory.rs index 72c230e..5873dc6 100644 --- a/src/agent/tools/memory.rs +++ b/src/agent/tools/memory.rs @@ -115,6 +115,7 @@ async fn dispatch( "memory_links" => links(&args).await, "memory_link_set" => link_set(&args).await, "memory_link_add" => link_add(agent, &args).await, + "memory_delete" => delete(&args).await, "memory_weight_set" => weight_set(&args).await, "memory_rename" => rename(&args).await, "memory_supersede" => supersede(agent, &args).await, @@ -130,7 +131,7 @@ async fn dispatch( // ── Definitions ──────────────────────────────────────────────── -pub fn memory_tools() -> [super::Tool; 12] { +pub fn memory_tools() -> [super::Tool; 13] { use super::Tool; [ Tool { name: "memory_render", description: "Read a memory node's content and links.", @@ -151,6 +152,9 @@ pub fn memory_tools() -> [super::Tool; 12] { Tool { name: "memory_link_add", description: "Add a new link between two nodes.", parameters_json: r#"{"type":"object","properties":{"source":{"type":"string"},"target":{"type":"string"}},"required":["source","target"]}"#, handler: Arc::new(|a, v| Box::pin(async move { dispatch("memory_link_add", &a, v).await })) }, + Tool { name: "memory_delete", description: "Delete a memory node.", + parameters_json: r#"{"type":"object","properties":{"key":{"type":"string","description":"Node key"}},"required":["key"]}"#, + handler: Arc::new(|a, v| Box::pin(async move { dispatch("memory_delete", &a, v).await })) }, Tool { name: "memory_weight_set", description: "Set a node's weight directly (0.01 to 1.0).", parameters_json: r#"{"type":"object","properties":{"key":{"type":"string"},"weight":{"type":"number","description":"0.01 to 1.0"}},"required":["key","weight"]}"#, handler: Arc::new(|a, v| Box::pin(async move { dispatch("memory_weight_set", &a, v).await })) }, @@ -311,6 +315,16 @@ async fn link_add(agent: &Option>, args: &se Ok(format!("linked {} → {} (strength={:.2})", s, t, strength)) } +async fn delete(args: &serde_json::Value) -> Result { + let key = get_str(args, "key")?; + let arc = cached_store().await?; + let mut store = arc.lock().await; + let resolved = store.resolve_key(key).map_err(|e| anyhow::anyhow!("{}", e))?; + store.delete_node(&resolved).map_err(|e| anyhow::anyhow!("{}", e))?; + store.save().map_err(|e| anyhow::anyhow!("{}", e))?; + Ok(format!("deleted {}", resolved)) +} + async fn weight_set(args: &serde_json::Value) -> Result { let arc = cached_store().await?; let mut store = arc.lock().await; diff --git a/src/cli/journal.rs b/src/cli/journal.rs index 4a413ee..f8772c6 100644 --- a/src/cli/journal.rs +++ b/src/cli/journal.rs @@ -66,30 +66,6 @@ pub fn cmd_tail(n: usize, full: bool, provenance: Option<&str>, dedup: bool) -> Ok(()) } -pub fn find_current_transcript() -> Option { - let projects = crate::config::get().projects_dir.clone(); - if !projects.exists() { return None; } - - let mut newest: Option<(std::time::SystemTime, std::path::PathBuf)> = None; - if let Ok(dirs) = std::fs::read_dir(&projects) { - for dir_entry in dirs.filter_map(|e| e.ok()) { - if !dir_entry.path().is_dir() { continue; } - if let Ok(files) = std::fs::read_dir(dir_entry.path()) { - for f in files.filter_map(|e| e.ok()) { - let p = f.path(); - if p.extension().map(|x| x == "jsonl").unwrap_or(false) - && let Ok(meta) = p.metadata() - && let Ok(mtime) = meta.modified() - && newest.as_ref().is_none_or(|(t, _)| mtime > *t) { - newest = Some((mtime, p)); - } - } - } - } - } - newest.map(|(_, p)| p.to_string_lossy().to_string()) -} - pub fn cmd_journal_tail(n: usize, full: bool, level: u8) -> Result<(), String> { let format = if full { "full" } else { "compact" }; let result = crate::mcp_server::memory_rpc( @@ -105,36 +81,18 @@ pub fn cmd_journal_write(name: &str, text: &[String]) -> Result<(), String> { return Err("journal write requires text".into()); } super::check_dry_run(); - let text = text.join(" "); - - let timestamp = crate::store::format_datetime(crate::store::now_epoch()); - let content = format!("## {} — {}\n\n{}", timestamp, name, text); - - let key: String = name.split_whitespace() - .map(|w| w.to_lowercase() - .chars().filter(|c| c.is_alphanumeric() || *c == '-') - .collect::()) - .filter(|s| !s.is_empty()) - .collect::>() - .join("-"); - - let source_ref = find_current_transcript(); - - let mut store = crate::store::Store::load()?; - - let mut node = crate::store::new_node(&key, &content); - node.node_type = crate::store::NodeType::EpisodicSession; - node.provenance = "journal".to_string(); - if let Some(src) = source_ref { - node.source_ref = src; - } - - store.upsert_node(node)?; - store.save()?; - - let word_count = text.split_whitespace().count(); - println!("Appended entry at {} ({} words)", timestamp, word_count); + let body = text.join(" "); + let result = crate::mcp_server::memory_rpc( + "journal_new", + serde_json::json!({ + "name": name, + "title": name, + "body": body, + "level": 0 + }), + ).map_err(|e| e.to_string())?; + println!("{}", result); Ok(()) } diff --git a/src/cli/node.rs b/src/cli/node.rs index fdf90e7..35677a9 100644 --- a/src/cli/node.rs +++ b/src/cli/node.rs @@ -71,11 +71,11 @@ pub fn cmd_node_delete(key: &[String]) -> Result<(), String> { } super::check_dry_run(); let key = key.join(" "); - let mut store = store::Store::load()?; - let resolved = store.resolve_key(&key)?; - store.delete_node(&resolved)?; - store.save()?; - println!("Deleted '{}'", resolved); + let result = crate::mcp_server::memory_rpc( + "memory_delete", + serde_json::json!({"key": key}), + ).map_err(|e| e.to_string())?; + println!("{}", result); Ok(()) } @@ -130,48 +130,6 @@ pub fn cmd_render(key: &[String]) -> Result<(), String> { Ok(()) } -/// Check content for common inline reference problems: -/// - `poc-memory render key` embedded in content (render artifact, should be just `key`) -/// - `→ something` where something doesn't parse as a valid key -/// - `key` referencing a node that doesn't exist -fn validate_inline_refs(content: &str, store: &store::Store) -> Vec { - let mut warnings = Vec::new(); - - for line in content.lines() { - // Check for render commands embedded in content - if line.contains("poc-memory render ") && !line.starts_with(" ") { - // Skip lines that look like CLI documentation/examples - if !line.contains("CLI") && !line.contains("equivalent") && !line.contains("tool") { - warnings.push(format!( - "render command in content (should be just `key`): {}", - line.chars().take(80).collect::(), - )); - } - } - - // Check → references - if let Some(rest) = line.trim().strip_prefix("→ ") { - // Extract the key (may be backtick-quoted) - let key = rest.trim().trim_matches('`').trim(); - if !key.is_empty() && !store.nodes.contains_key(key) { - // Might be a poc-memory render artifact - if let Some(k) = key.strip_prefix("poc-memory render ") { - warnings.push(format!( - "render artifact in → reference (use `{}` not `poc-memory render {}`)", k, k, - )); - } else if key.contains(' ') { - warnings.push(format!( - "→ reference doesn't look like a key: → {}", key, - )); - } - // Don't warn about missing keys — the target might be created later - } - } - } - - warnings -} - pub fn cmd_history(key: &[String], full: bool) -> Result<(), String> { if key.is_empty() { return Err("history requires a key".into()); @@ -245,7 +203,7 @@ pub fn cmd_write(key: &[String]) -> Result<(), String> { if key.is_empty() { return Err("write requires a key (reads content from stdin)".into()); } - let raw_key = key.join(" "); + 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))?; @@ -255,25 +213,11 @@ pub fn cmd_write(key: &[String]) -> Result<(), String> { } super::check_dry_run(); - let mut store = store::Store::load()?; - let key = store.resolve_key(&raw_key).unwrap_or(raw_key); - - // Validate inline references: warn about render commands embedded - // in content (should be just `key`) and broken references. - let warnings = validate_inline_refs(&content, &store); - for w in &warnings { - eprintln!("warning: {}", w); - } - - let result = store.upsert(&key, &content)?; - match result { - "unchanged" => println!("No change: '{}'", key), - "updated" => println!("Updated '{}' (v{})", key, store.nodes[&key].version), - _ => println!("Created '{}'", key), - } - if result != "unchanged" { - store.save()?; - } + 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(()) }