Skip to main content

sc/cli/commands/
memory.rs

1//! Memory command implementations (project-level persistent storage).
2
3use crate::cli::MemoryCommands;
4use crate::config::{current_project_path, default_actor, resolve_db_path};
5use crate::error::{Error, Result};
6use crate::storage::SqliteStorage;
7use serde::Serialize;
8use std::path::PathBuf;
9
10/// Output for memory save.
11#[derive(Serialize)]
12struct MemorySaveOutput {
13    key: String,
14    category: String,
15    project_path: String,
16}
17
18/// Output for memory get.
19#[derive(Serialize)]
20struct MemoryGetOutput {
21    key: String,
22    value: String,
23    category: String,
24}
25
26/// Output for memory list.
27#[derive(Serialize)]
28struct MemoryListOutput {
29    items: Vec<MemoryItem>,
30    count: usize,
31}
32
33#[derive(Serialize)]
34struct MemoryItem {
35    key: String,
36    value: String,
37    category: String,
38}
39
40/// Execute memory commands.
41pub fn execute(
42    command: &MemoryCommands,
43    db_path: Option<&PathBuf>,
44    actor: Option<&str>,
45    json: bool,
46) -> Result<()> {
47    match command {
48        MemoryCommands::Save {
49            key,
50            value,
51            category,
52        } => save(key, value, category, db_path, actor, json),
53        MemoryCommands::Get { key } => get(key, db_path, json),
54        MemoryCommands::List { category } => list(category.as_deref(), db_path, json),
55        MemoryCommands::Delete { key } => delete(key, db_path, actor, json),
56    }
57}
58
59fn save(
60    key: &str,
61    value: &str,
62    category: &str,
63    db_path: Option<&PathBuf>,
64    actor: Option<&str>,
65    json: bool,
66) -> Result<()> {
67    let db_path = resolve_db_path(db_path.map(|p| p.as_path()))
68        .ok_or(Error::NotInitialized)?;
69
70    if !db_path.exists() {
71        return Err(Error::NotInitialized);
72    }
73
74    let mut storage = SqliteStorage::open(&db_path)?;
75    let actor = actor.map(ToString::to_string).unwrap_or_else(default_actor);
76    let project_path = current_project_path()
77        .map(|p| p.to_string_lossy().to_string())
78        .ok_or_else(|| Error::Other("Could not determine project path".to_string()))?;
79
80    // Generate ID
81    let id = format!("mem_{}", &uuid::Uuid::new_v4().to_string()[..12]);
82
83    storage.save_memory(&id, &project_path, key, value, category, &actor)?;
84
85    if crate::is_silent() {
86        println!("{key}");
87        return Ok(());
88    }
89
90    if json {
91        let output = MemorySaveOutput {
92            key: key.to_string(),
93            category: category.to_string(),
94            project_path,
95        };
96        println!("{}", serde_json::to_string(&output)?);
97    } else {
98        println!("Saved memory: {key} [{category}]");
99    }
100
101    Ok(())
102}
103
104fn get(key: &str, db_path: Option<&PathBuf>, json: bool) -> Result<()> {
105    let db_path = resolve_db_path(db_path.map(|p| p.as_path()))
106        .ok_or(Error::NotInitialized)?;
107
108    if !db_path.exists() {
109        return Err(Error::NotInitialized);
110    }
111
112    let storage = SqliteStorage::open(&db_path)?;
113    let project_path = current_project_path()
114        .map(|p| p.to_string_lossy().to_string())
115        .ok_or_else(|| Error::Other("Could not determine project path".to_string()))?;
116
117    let memory = storage
118        .get_memory(&project_path, key)?
119        .ok_or_else(|| Error::Other(format!("Memory not found: {key}")))?;
120
121    if json {
122        let output = MemoryGetOutput {
123            key: memory.key,
124            value: memory.value,
125            category: memory.category,
126        };
127        println!("{}", serde_json::to_string(&output)?);
128    } else {
129        println!("{}: {}", memory.key, memory.value);
130    }
131
132    Ok(())
133}
134
135fn list(category: Option<&str>, db_path: Option<&PathBuf>, json: bool) -> Result<()> {
136    let db_path = resolve_db_path(db_path.map(|p| p.as_path()))
137        .ok_or(Error::NotInitialized)?;
138
139    if !db_path.exists() {
140        return Err(Error::NotInitialized);
141    }
142
143    let storage = SqliteStorage::open(&db_path)?;
144    let project_path = current_project_path()
145        .map(|p| p.to_string_lossy().to_string())
146        .ok_or_else(|| Error::Other("Could not determine project path".to_string()))?;
147
148    let memories = storage.list_memory(&project_path, category)?;
149
150    if crate::is_csv() {
151        println!("key,category,value");
152        for m in &memories {
153            println!("{},{},{}", m.key, m.category, crate::csv_escape(&m.value));
154        }
155    } else if json {
156        let items: Vec<MemoryItem> = memories
157            .iter()
158            .map(|m| MemoryItem {
159                key: m.key.clone(),
160                value: m.value.clone(),
161                category: m.category.clone(),
162            })
163            .collect();
164        let output = MemoryListOutput {
165            count: items.len(),
166            items,
167        };
168        println!("{}", serde_json::to_string(&output)?);
169    } else if memories.is_empty() {
170        println!("No memory items found.");
171    } else {
172        println!("Memory items ({} found):", memories.len());
173        println!();
174        for mem in &memories {
175            let cat_icon = match mem.category.as_str() {
176                "command" => "$",
177                "config" => "⚙",
178                "note" => "📝",
179                _ => "•",
180            };
181            println!("{} {} [{}]", cat_icon, mem.key, mem.category);
182            // Truncate long values
183            let display_value = if mem.value.len() > 80 {
184                format!("{}...", &mem.value[..80])
185            } else {
186                mem.value.clone()
187            };
188            println!("  {display_value}");
189            println!();
190        }
191    }
192
193    Ok(())
194}
195
196fn delete(key: &str, db_path: Option<&PathBuf>, actor: Option<&str>, json: bool) -> Result<()> {
197    let db_path = resolve_db_path(db_path.map(|p| p.as_path()))
198        .ok_or(Error::NotInitialized)?;
199
200    if !db_path.exists() {
201        return Err(Error::NotInitialized);
202    }
203
204    let mut storage = SqliteStorage::open(&db_path)?;
205    let actor = actor.map(ToString::to_string).unwrap_or_else(default_actor);
206    let project_path = current_project_path()
207        .map(|p| p.to_string_lossy().to_string())
208        .ok_or_else(|| Error::Other("Could not determine project path".to_string()))?;
209
210    storage.delete_memory(&project_path, key, &actor)?;
211
212    if json {
213        let output = serde_json::json!({
214            "key": key,
215            "deleted": true
216        });
217        println!("{output}");
218    } else {
219        println!("Deleted memory: {key}");
220    }
221
222    Ok(())
223}