cli: convert write/delete/journal-write to use memory_rpc

- 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 <poc@bcachefs.org>
This commit is contained in:
Kent Overstreet 2026-04-12 22:15:53 -04:00
parent 7842b6fc8b
commit 366b17163d
3 changed files with 37 additions and 121 deletions

View file

@ -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<std::sync::Arc<crate::agent::Agent>>, args: &se
Ok(format!("linked {}{} (strength={:.2})", s, t, strength))
}
async fn delete(args: &serde_json::Value) -> Result<String> {
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<String> {
let arc = cached_store().await?;
let mut store = arc.lock().await;

View file

@ -66,30 +66,6 @@ pub fn cmd_tail(n: usize, full: bool, provenance: Option<&str>, dedup: bool) ->
Ok(())
}
pub fn find_current_transcript() -> Option<String> {
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::<String>())
.filter(|s| !s.is_empty())
.collect::<Vec<_>>()
.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(())
}

View file

@ -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<String> {
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::<String>(),
));
}
}
// 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(())
}