CLI: convert node commands to typed async API

- node.rs: use memory::* typed helpers instead of memory_rpc()
- main.rs: make Run trait async, await all command dispatch
- defs.rs: bridge get_group_content async via block_in_place

Co-Authored-By: Proof of Concept <poc@bcachefs.org>
This commit is contained in:
Kent Overstreet 2026-04-13 13:20:04 -04:00
parent 933221f482
commit fa50f1c826
3 changed files with 84 additions and 102 deletions

View file

@ -3,53 +3,46 @@
// render, write, node-delete, node-rename, history, list-keys,
// list-edges, dump-json, lookup-bump, lookups.
use crate::agent::tools::memory;
use crate::store;
pub fn cmd_weight_set(key: &str, weight: f32) -> Result<(), String> {
pub async 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())?;
let result = memory::weight_set(None, key, weight).await
.map_err(|e| e.to_string())?;
println!("{}", result);
Ok(())
}
pub fn cmd_node_delete(key: &[String]) -> Result<(), String> {
pub async 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())?;
let result = memory::delete(None, &key).await
.map_err(|e| e.to_string())?;
println!("{}", result);
Ok(())
}
pub fn cmd_node_rename(old_key: &str, new_key: &str) -> Result<(), String> {
pub async 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())?;
let result = memory::rename(None, old_key, new_key).await
.map_err(|e| e.to_string())?;
println!("{}", result);
Ok(())
}
pub fn cmd_render(key: &[String]) -> Result<(), String> {
pub async 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())?;
let rendered = memory::render(None, &bare, None).await
.map_err(|e| e.to_string())?;
print!("{}", rendered);
// Mark as seen if we're inside a Claude session (not an agent subprocess —
@ -73,20 +66,18 @@ pub fn cmd_render(key: &[String]) -> Result<(), String> {
Ok(())
}
pub fn cmd_history(key: &[String], full: bool) -> Result<(), String> {
pub async 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())?;
let result = memory::history(None, &key, Some(full)).await
.map_err(|e| e.to_string())?;
print!("{}", result);
Ok(())
}
pub fn cmd_write(key: &[String]) -> Result<(), String> {
pub async fn cmd_write(key: &[String]) -> Result<(), String> {
if key.is_empty() {
return Err("write requires a key (reads content from stdin)".into());
}
@ -100,25 +91,21 @@ pub fn cmd_write(key: &[String]) -> Result<(), String> {
}
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())?;
let result = memory::write(None, &key, &content).await
.map_err(|e| e.to_string())?;
println!("{}", result);
Ok(())
}
pub fn cmd_edit(key: &[String]) -> Result<(), String> {
pub async 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();
// Get raw content
let content = 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)
@ -149,42 +136,36 @@ pub fn cmd_edit(key: &[String]) -> Result<(), String> {
}
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())?;
let result = memory::write(None, &key, &new_content).await
.map_err(|e| e.to_string())?;
println!("{}", result);
Ok(())
}
pub fn cmd_search(keys: &[String]) -> Result<(), String> {
pub async fn cmd_search(keys: &[String]) -> Result<(), String> {
if keys.is_empty() {
return Err("search requires seed keys".into());
}
let result = crate::mcp_server::memory_rpc(
"memory_search",
serde_json::json!({"keys": keys}),
).map_err(|e| e.to_string())?;
let result = memory::search(None, keys.to_vec(), None, None, None, None).await
.map_err(|e| e.to_string())?;
print!("{}", result);
Ok(())
}
pub fn cmd_query(expr: &[String]) -> Result<(), String> {
pub async fn cmd_query(expr: &[String]) -> Result<(), String> {
if expr.is_empty() {
return Err("query requires an expression (try: poc-memory query --help)".into());
}
let query_str = expr.join(" ");
let result = crate::mcp_server::memory_rpc(
"memory_query",
serde_json::json!({"query": query_str}),
).map_err(|e| e.to_string())?;
let result = memory::query(None, &query_str, None).await
.map_err(|e| e.to_string())?;
print!("{}", result);
Ok(())
}
/// Get group content via RPC (handles daemon or local fallback)
pub fn get_group_content(group: &crate::config::ContextGroup, cfg: &crate::config::Config) -> Vec<(String, String)> {
/// 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)> {
match group.source {
crate::config::ContextSource::Journal => {
// Query for recent journal entries
@ -192,26 +173,21 @@ pub fn get_group_content(group: &crate::config::ContextGroup, cfg: &crate::confi
let query = format!("all | type:episodic | age:<{} | sort:timestamp | limit:{}",
window, cfg.journal_max);
let keys_str = match crate::mcp_server::memory_rpc(
"memory_query",
serde_json::json!({"query": query}),
) {
let keys_str = match memory::query(None, &query, None).await {
Ok(s) => s,
Err(_) => return vec![],
};
// Parse keys (one per line) and render each
keys_str.lines()
.filter(|k| !k.is_empty() && *k != "no results")
.filter_map(|key| {
let content = crate::mcp_server::memory_rpc(
"memory_render",
serde_json::json!({"key": key, "raw": true}),
).ok()?;
if content.trim().is_empty() { return None; }
Some((key.to_string(), content))
})
.collect()
let mut results = Vec::new();
for key in keys_str.lines().filter(|k| !k.is_empty() && *k != "no results") {
if let Ok(content) = memory::render(None, key, Some(true)).await {
if !content.trim().is_empty() {
results.push((key.to_string(), content));
}
}
}
results
}
crate::config::ContextSource::File => {
group.keys.iter().filter_map(|key| {
@ -221,19 +197,20 @@ pub fn get_group_content(group: &crate::config::ContextGroup, cfg: &crate::confi
}).collect()
}
crate::config::ContextSource::Store => {
group.keys.iter().filter_map(|key| {
let content = crate::mcp_server::memory_rpc(
"memory_render",
serde_json::json!({"key": key, "raw": true}),
).ok()?;
if content.trim().is_empty() { return None; }
Some((key.clone(), content.trim().to_string()))
}).collect()
let mut results = Vec::new();
for key in &group.keys {
if let Ok(content) = memory::render(None, key, Some(true)).await {
if !content.trim().is_empty() {
results.push((key.clone(), content.trim().to_string()));
}
}
}
results
}
}
}
pub fn cmd_load_context(stats: bool) -> Result<(), String> {
pub async fn cmd_load_context(stats: bool) -> Result<(), String> {
let cfg = crate::config::get();
if stats {
@ -243,7 +220,7 @@ pub fn cmd_load_context(stats: bool) -> Result<(), String> {
println!("{}", "-".repeat(42));
for group in &cfg.context_groups {
let entries = get_group_content(group, &cfg);
let entries = get_group_content(group, &cfg).await;
let words: usize = entries.iter()
.map(|(_, c)| c.split_whitespace().count())
.sum();
@ -261,7 +238,7 @@ pub fn cmd_load_context(stats: bool) -> Result<(), String> {
println!("=== MEMORY SYSTEM ({}) ===", cfg.assistant_name);
for group in &cfg.context_groups {
let entries = get_group_content(group, &cfg);
let entries = get_group_content(group, &cfg).await;
if !entries.is_empty() && group.source == crate::config::ContextSource::Journal {
println!("--- recent journal entries ({}/{}) ---",
entries.len(), cfg.journal_max);