vta_cli_common/commands/
memory.rs1use serde_json::json;
27use vta_sdk::prelude::*;
28use vta_sdk::protocols::memory::{MemoryItem, MemoryListResponse};
29
30use crate::render::{BOLD, CYAN, DIM, GREEN, RED, RESET, is_json_output, print_json};
31
32pub async fn cmd_memory_plant(
35 client: &VtaClient,
36 context: &str,
37 key: &str,
38 value: &str,
39) -> Result<(), Box<dyn std::error::Error>> {
40 let resp = client.memory_put(context, key, value).await?;
41 if is_json_output() {
42 print_json(&resp)?;
43 return Ok(());
44 }
45 println!("{GREEN}\u{2713}{RESET} Planted {BOLD}{key}{RESET} in context '{context}'.");
46 println!(" {DIM}{value}{RESET}");
47 Ok(())
48}
49
50pub async fn cmd_memory_recall(
52 client: &VtaClient,
53 context: &str,
54 key: Option<&str>,
55) -> Result<(), Box<dyn std::error::Error>> {
56 let mut items = list_items(client, context).await?;
57 if let Some(k) = key {
58 items.retain(|item| item.key == k);
59 }
60
61 if is_json_output() {
62 print_json(&MemoryListResponse { items })?;
66 return Ok(());
67 }
68
69 if items.is_empty() {
70 match key {
71 Some(k) => println!("No memory under '{k}' in context '{context}'."),
72 None => println!("Context '{context}' has no memories."),
73 }
74 return Ok(());
75 }
76
77 println!();
78 println!("{BOLD}Memory for context '{context}'{RESET}");
79 println!();
80 for item in &items {
81 println!(" {CYAN}{}{RESET} {}", item.key, item.value);
82 }
83 println!();
84 Ok(())
85}
86
87pub async fn cmd_memory_forget(
89 client: &VtaClient,
90 context: &str,
91 key: &str,
92) -> Result<(), Box<dyn std::error::Error>> {
93 let resp = client.memory_delete(context, key).await?;
94 if is_json_output() {
95 print_json(&resp)?;
96 return Ok(());
97 }
98 println!("{GREEN}\u{2713}{RESET} Deleted memory {BOLD}{key}{RESET} in context '{context}'.");
99 Ok(())
100}
101
102pub async fn cmd_memory_wipe(
111 client: &VtaClient,
112 context: &str,
113 assume_yes: bool,
114) -> Result<(), Box<dyn std::error::Error>> {
115 let items = list_items(client, context).await?;
116
117 if items.is_empty() {
118 if is_json_output() {
119 print_json(&json!({ "wiped": Vec::<String>::new() }))?;
120 } else {
121 println!("{DIM}Context '{context}' is already empty — nothing to wipe.{RESET}");
122 }
123 return Ok(());
124 }
125
126 match wipe_guard(assume_yes, is_json_output()) {
127 WipeGuard::RefuseJson => {
128 return Err("`memory wipe` needs `--yes` in --json mode: there is no \
129 prompt to confirm to"
130 .into());
131 }
132 WipeGuard::Confirm => {
133 println!(
134 "{RED}This wipes all {} in context '{context}'.{RESET}",
135 count_memories(items.len())
136 );
137 let go = dialoguer::Confirm::new()
138 .with_prompt("Wipe every memory in this context?")
139 .default(false)
140 .interact()?;
141 if !go {
142 println!("Cancelled — nothing was wiped.");
143 return Ok(());
144 }
145 }
146 WipeGuard::Proceed => {}
147 }
148
149 let mut wiped: Vec<String> = Vec::with_capacity(items.len());
153 for item in &items {
154 if let Err(e) = client.memory_delete(context, &item.key).await {
155 return Err(format!(
156 "wiped {} of {} before failing on '{}': {e}",
157 wiped.len(),
158 items.len(),
159 item.key,
160 )
161 .into());
162 }
163 wiped.push(item.key.clone());
164 }
165
166 if is_json_output() {
167 print_json(&json!({ "wiped": wiped }))?;
168 return Ok(());
169 }
170 println!(
171 "{RED}\u{2713}{RESET} Wiped {} from context '{context}'.",
172 count_memories(wiped.len())
173 );
174 Ok(())
175}
176
177async fn list_items(
181 client: &VtaClient,
182 context: &str,
183) -> Result<Vec<MemoryItem>, Box<dyn std::error::Error>> {
184 let resp = client.memory_list(context).await?;
185 Ok(decode_items(resp)?)
186}
187
188fn decode_items(resp: serde_json::Value) -> Result<Vec<MemoryItem>, serde_json::Error> {
192 let parsed: MemoryListResponse = serde_json::from_value(resp)?;
193 Ok(parsed.items)
194}
195
196#[derive(Debug, PartialEq, Eq)]
201enum WipeGuard {
202 Proceed,
203 Confirm,
204 RefuseJson,
205}
206
207fn wipe_guard(assume_yes: bool, json: bool) -> WipeGuard {
208 match (assume_yes, json) {
209 (true, _) => WipeGuard::Proceed,
210 (false, true) => WipeGuard::RefuseJson,
211 (false, false) => WipeGuard::Confirm,
212 }
213}
214
215fn count_memories(n: usize) -> String {
217 if n == 1 {
218 "1 memory".to_string()
219 } else {
220 format!("{n} memories")
221 }
222}
223
224#[cfg(test)]
225mod tests {
226 use super::*;
227
228 #[test]
229 fn wipe_yes_always_proceeds() {
230 assert_eq!(wipe_guard(true, false), WipeGuard::Proceed);
232 assert_eq!(wipe_guard(true, true), WipeGuard::Proceed);
233 }
234
235 #[test]
236 fn wipe_json_without_yes_refuses() {
237 assert_eq!(wipe_guard(false, true), WipeGuard::RefuseJson);
240 }
241
242 #[test]
243 fn wipe_interactive_without_yes_confirms() {
244 assert_eq!(wipe_guard(false, false), WipeGuard::Confirm);
245 }
246
247 #[test]
248 fn decode_reads_camelcase_items() {
249 let v = json!({ "items": [
250 { "key": "a", "value": "1" },
251 { "key": "b", "value": "2" },
252 ] });
253 let items = decode_items(v).expect("well-formed body decodes");
254 assert_eq!(items.len(), 2);
255 assert_eq!(items[0].key, "a");
256 assert_eq!(items[1].value, "2");
257 }
258
259 #[test]
260 fn decode_of_a_malformed_body_errors_rather_than_shortening() {
261 let v = json!({ "items": [ { "key": "a", "value": 7 } ] });
264 assert!(decode_items(v).is_err());
265 }
266
267 #[test]
268 fn count_memories_agrees_the_noun() {
269 assert_eq!(count_memories(0), "0 memories");
270 assert_eq!(count_memories(1), "1 memory");
271 assert_eq!(count_memories(2), "2 memories");
272 }
273}