use serde_json::json;
use vta_sdk::prelude::*;
use vta_sdk::protocols::memory::{MemoryItem, MemoryListResponse};
use crate::render::{BOLD, CYAN, DIM, GREEN, RED, RESET, is_json_output, print_json};
pub async fn cmd_memory_plant(
client: &VtaClient,
context: &str,
key: &str,
value: &str,
) -> Result<(), Box<dyn std::error::Error>> {
let resp = client.memory_put(context, key, value).await?;
if is_json_output() {
print_json(&resp)?;
return Ok(());
}
println!("{GREEN}\u{2713}{RESET} Planted {BOLD}{key}{RESET} in context '{context}'.");
println!(" {DIM}{value}{RESET}");
Ok(())
}
pub async fn cmd_memory_recall(
client: &VtaClient,
context: &str,
key: Option<&str>,
) -> Result<(), Box<dyn std::error::Error>> {
let mut items = list_items(client, context).await?;
if let Some(k) = key {
items.retain(|item| item.key == k);
}
if is_json_output() {
print_json(&MemoryListResponse { items })?;
return Ok(());
}
if items.is_empty() {
match key {
Some(k) => println!("No memory under '{k}' in context '{context}'."),
None => println!("Context '{context}' has no memories."),
}
return Ok(());
}
println!();
println!("{BOLD}Memory for context '{context}'{RESET}");
println!();
for item in &items {
println!(" {CYAN}{}{RESET} {}", item.key, item.value);
}
println!();
Ok(())
}
pub async fn cmd_memory_forget(
client: &VtaClient,
context: &str,
key: &str,
) -> Result<(), Box<dyn std::error::Error>> {
let resp = client.memory_delete(context, key).await?;
if is_json_output() {
print_json(&resp)?;
return Ok(());
}
println!("{GREEN}\u{2713}{RESET} Forgot {BOLD}{key}{RESET} in context '{context}'.");
Ok(())
}
pub async fn cmd_memory_wipe(
client: &VtaClient,
context: &str,
assume_yes: bool,
) -> Result<(), Box<dyn std::error::Error>> {
let items = list_items(client, context).await?;
if items.is_empty() {
if is_json_output() {
print_json(&json!({ "wiped": Vec::<String>::new() }))?;
} else {
println!("{DIM}Context '{context}' is already empty — nothing to wipe.{RESET}");
}
return Ok(());
}
match wipe_guard(assume_yes, is_json_output()) {
WipeGuard::RefuseJson => {
return Err("`memory wipe` needs `--yes` in --json mode: there is no \
prompt to confirm to"
.into());
}
WipeGuard::Confirm => {
println!(
"{RED}This wipes all {} in context '{context}'.{RESET}",
count_memories(items.len())
);
let go = dialoguer::Confirm::new()
.with_prompt("Wipe every memory in this context?")
.default(false)
.interact()?;
if !go {
println!("Cancelled — nothing was wiped.");
return Ok(());
}
}
WipeGuard::Proceed => {}
}
let mut wiped: Vec<String> = Vec::with_capacity(items.len());
for item in &items {
if let Err(e) = client.memory_delete(context, &item.key).await {
return Err(format!(
"wiped {} of {} before failing on '{}': {e}",
wiped.len(),
items.len(),
item.key,
)
.into());
}
wiped.push(item.key.clone());
}
if is_json_output() {
print_json(&json!({ "wiped": wiped }))?;
return Ok(());
}
println!(
"{RED}\u{2713}{RESET} Wiped {} from context '{context}'.",
count_memories(wiped.len())
);
Ok(())
}
async fn list_items(
client: &VtaClient,
context: &str,
) -> Result<Vec<MemoryItem>, Box<dyn std::error::Error>> {
let resp = client.memory_list(context).await?;
Ok(decode_items(resp)?)
}
fn decode_items(resp: serde_json::Value) -> Result<Vec<MemoryItem>, serde_json::Error> {
let parsed: MemoryListResponse = serde_json::from_value(resp)?;
Ok(parsed.items)
}
#[derive(Debug, PartialEq, Eq)]
enum WipeGuard {
Proceed,
Confirm,
RefuseJson,
}
fn wipe_guard(assume_yes: bool, json: bool) -> WipeGuard {
match (assume_yes, json) {
(true, _) => WipeGuard::Proceed,
(false, true) => WipeGuard::RefuseJson,
(false, false) => WipeGuard::Confirm,
}
}
fn count_memories(n: usize) -> String {
if n == 1 {
"1 memory".to_string()
} else {
format!("{n} memories")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn wipe_yes_always_proceeds() {
assert_eq!(wipe_guard(true, false), WipeGuard::Proceed);
assert_eq!(wipe_guard(true, true), WipeGuard::Proceed);
}
#[test]
fn wipe_json_without_yes_refuses() {
assert_eq!(wipe_guard(false, true), WipeGuard::RefuseJson);
}
#[test]
fn wipe_interactive_without_yes_confirms() {
assert_eq!(wipe_guard(false, false), WipeGuard::Confirm);
}
#[test]
fn decode_reads_camelcase_items() {
let v = json!({ "items": [
{ "key": "a", "value": "1" },
{ "key": "b", "value": "2" },
] });
let items = decode_items(v).expect("well-formed body decodes");
assert_eq!(items.len(), 2);
assert_eq!(items[0].key, "a");
assert_eq!(items[1].value, "2");
}
#[test]
fn decode_of_a_malformed_body_errors_rather_than_shortening() {
let v = json!({ "items": [ { "key": "a", "value": 7 } ] });
assert!(decode_items(v).is_err());
}
#[test]
fn count_memories_agrees_the_noun() {
assert_eq!(count_memories(0), "0 memories");
assert_eq!(count_memories(1), "1 memory");
assert_eq!(count_memories(2), "2 memories");
}
}