Skip to main content

sparrow/memory/
cli.rs

1//! Memory CLI — visible memory management for Sparrow.
2//!
3//! Commands: show, edit, search, list facts stored in SQLite.
4
5use std::path::PathBuf;
6
7/// A single memory fact stored in the knowledge base.
8#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
9pub struct MemoryFact {
10    pub id: i64,
11    pub key: String,
12    pub value: String,
13    pub category: String,
14    pub created_at: String,
15    pub updated_at: String,
16}
17
18/// Memory store backed by SQLite.
19pub struct MemoryStore {
20    db_path: PathBuf,
21}
22
23impl MemoryStore {
24    /// Open or create the memory database.
25    pub fn open(db_path: PathBuf) -> anyhow::Result<Self> {
26        let conn = rusqlite::Connection::open(&db_path)?;
27        conn.execute_batch(
28            "CREATE TABLE IF NOT EXISTS memory (
29                id INTEGER PRIMARY KEY AUTOINCREMENT,
30                key TEXT NOT NULL UNIQUE,
31                value TEXT NOT NULL,
32                category TEXT NOT NULL DEFAULT 'general',
33                created_at TEXT NOT NULL DEFAULT (datetime('now')),
34                updated_at TEXT NOT NULL DEFAULT (datetime('now'))
35            );
36            CREATE INDEX IF NOT EXISTS idx_memory_key ON memory(key);
37            CREATE INDEX IF NOT EXISTS idx_memory_category ON memory(category);"
38        )?;
39        Ok(Self { db_path })
40    }
41
42    /// Upsert a memory fact.
43    pub fn set(&self, key: &str, value: &str, category: &str) -> anyhow::Result<()> {
44        let conn = rusqlite::Connection::open(&self.db_path)?;
45        conn.execute(
46            "INSERT INTO memory (key, value, category) VALUES (?1, ?2, ?3)
47             ON CONFLICT(key) DO UPDATE SET value = ?2, category = ?3, updated_at = datetime('now')",
48            rusqlite::params![key, value, category],
49        )?;
50        Ok(())
51    }
52
53    /// Get a memory fact by key.
54    pub fn get(&self, key: &str) -> anyhow::Result<Option<MemoryFact>> {
55        let conn = rusqlite::Connection::open(&self.db_path)?;
56        let mut stmt = conn.prepare(
57            "SELECT id, key, value, category, created_at, updated_at FROM memory WHERE key = ?1"
58        )?;
59        let result = stmt.query_row(rusqlite::params![key], |row| {
60            Ok(MemoryFact {
61                id: row.get(0)?,
62                key: row.get(1)?,
63                value: row.get(2)?,
64                category: row.get(3)?,
65                created_at: row.get(4)?,
66                updated_at: row.get(5)?,
67            })
68        }).ok();
69        Ok(result)
70    }
71
72    /// Search memory facts by keyword.
73    pub fn search(&self, query: &str) -> anyhow::Result<Vec<MemoryFact>> {
74        let conn = rusqlite::Connection::open(&self.db_path)?;
75        let pattern = format!("%{}%", query);
76        let mut stmt = conn.prepare(
77            "SELECT id, key, value, category, created_at, updated_at FROM memory
78             WHERE key LIKE ?1 OR value LIKE ?1 OR category LIKE ?1
79             ORDER BY updated_at DESC LIMIT 50"
80        )?;
81        let facts = stmt.query_map(rusqlite::params![pattern], |row| {
82            Ok(MemoryFact {
83                id: row.get(0)?,
84                key: row.get(1)?,
85                value: row.get(2)?,
86                category: row.get(3)?,
87                created_at: row.get(4)?,
88                updated_at: row.get(5)?,
89            })
90        })?.filter_map(|r| r.ok()).collect();
91        Ok(facts)
92    }
93
94    /// List all memory facts.
95    pub fn list_all(&self) -> anyhow::Result<Vec<MemoryFact>> {
96        let conn = rusqlite::Connection::open(&self.db_path)?;
97        let mut stmt = conn.prepare(
98            "SELECT id, key, value, category, created_at, updated_at FROM memory
99             ORDER BY updated_at DESC"
100        )?;
101        let facts = stmt.query_map([], |row| {
102            Ok(MemoryFact {
103                id: row.get(0)?,
104                key: row.get(1)?,
105                value: row.get(2)?,
106                category: row.get(3)?,
107                created_at: row.get(4)?,
108                updated_at: row.get(5)?,
109            })
110        })?.filter_map(|r| r.ok()).collect();
111        Ok(facts)
112    }
113
114    /// List facts by category.
115    pub fn list_by_category(&self, category: &str) -> anyhow::Result<Vec<MemoryFact>> {
116        let conn = rusqlite::Connection::open(&self.db_path)?;
117        let mut stmt = conn.prepare(
118            "SELECT id, key, value, category, created_at, updated_at FROM memory
119             WHERE category = ?1 ORDER BY updated_at DESC"
120        )?;
121        let facts = stmt.query_map(rusqlite::params![category], |row| {
122            Ok(MemoryFact {
123                id: row.get(0)?,
124                key: row.get(1)?,
125                value: row.get(2)?,
126                category: row.get(3)?,
127                created_at: row.get(4)?,
128                updated_at: row.get(5)?,
129            })
130        })?.filter_map(|r| r.ok()).collect();
131        Ok(facts)
132    }
133
134    /// Delete a memory fact by key.
135    pub fn delete(&self, key: &str) -> anyhow::Result<bool> {
136        let conn = rusqlite::Connection::open(&self.db_path)?;
137        let count = conn.execute("DELETE FROM memory WHERE key = ?1", rusqlite::params![key])?;
138        Ok(count > 0)
139    }
140
141    /// Get the total count of facts.
142    pub fn count(&self) -> anyhow::Result<usize> {
143        let conn = rusqlite::Connection::open(&self.db_path)?;
144        let count: usize = conn.query_row("SELECT COUNT(*) FROM memory", [], |row| row.get(0))?;
145        Ok(count)
146    }
147
148    /// Get all categories with counts.
149    pub fn categories(&self) -> anyhow::Result<Vec<(String, usize)>> {
150        let conn = rusqlite::Connection::open(&self.db_path)?;
151        let mut stmt = conn.prepare(
152            "SELECT category, COUNT(*) as cnt FROM memory GROUP BY category ORDER BY cnt DESC"
153        )?;
154        let cats = stmt.query_map([], |row| {
155            Ok((row.get::<_, String>(0)?, row.get::<_, usize>(1)?))
156        })?.filter_map(|r| r.ok()).collect();
157        Ok(cats)
158    }
159}