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
60            .query_row(rusqlite::params![key], |row| {
61                Ok(MemoryFact {
62                    id: row.get(0)?,
63                    key: row.get(1)?,
64                    value: row.get(2)?,
65                    category: row.get(3)?,
66                    created_at: row.get(4)?,
67                    updated_at: row.get(5)?,
68                })
69            })
70            .ok();
71        Ok(result)
72    }
73
74    /// Search memory facts by keyword.
75    pub fn search(&self, query: &str) -> anyhow::Result<Vec<MemoryFact>> {
76        let conn = rusqlite::Connection::open(&self.db_path)?;
77        let pattern = format!("%{}%", query);
78        let mut stmt = conn.prepare(
79            "SELECT id, key, value, category, created_at, updated_at FROM memory
80             WHERE key LIKE ?1 OR value LIKE ?1 OR category LIKE ?1
81             ORDER BY updated_at DESC LIMIT 50",
82        )?;
83        let facts = stmt
84            .query_map(rusqlite::params![pattern], |row| {
85                Ok(MemoryFact {
86                    id: row.get(0)?,
87                    key: row.get(1)?,
88                    value: row.get(2)?,
89                    category: row.get(3)?,
90                    created_at: row.get(4)?,
91                    updated_at: row.get(5)?,
92                })
93            })?
94            .filter_map(|r| r.ok())
95            .collect();
96        Ok(facts)
97    }
98
99    /// List all memory facts.
100    pub fn list_all(&self) -> anyhow::Result<Vec<MemoryFact>> {
101        let conn = rusqlite::Connection::open(&self.db_path)?;
102        let mut stmt = conn.prepare(
103            "SELECT id, key, value, category, created_at, updated_at FROM memory
104             ORDER BY updated_at DESC",
105        )?;
106        let facts = stmt
107            .query_map([], |row| {
108                Ok(MemoryFact {
109                    id: row.get(0)?,
110                    key: row.get(1)?,
111                    value: row.get(2)?,
112                    category: row.get(3)?,
113                    created_at: row.get(4)?,
114                    updated_at: row.get(5)?,
115                })
116            })?
117            .filter_map(|r| r.ok())
118            .collect();
119        Ok(facts)
120    }
121
122    /// List facts by category.
123    pub fn list_by_category(&self, category: &str) -> anyhow::Result<Vec<MemoryFact>> {
124        let conn = rusqlite::Connection::open(&self.db_path)?;
125        let mut stmt = conn.prepare(
126            "SELECT id, key, value, category, created_at, updated_at FROM memory
127             WHERE category = ?1 ORDER BY updated_at DESC",
128        )?;
129        let facts = stmt
130            .query_map(rusqlite::params![category], |row| {
131                Ok(MemoryFact {
132                    id: row.get(0)?,
133                    key: row.get(1)?,
134                    value: row.get(2)?,
135                    category: row.get(3)?,
136                    created_at: row.get(4)?,
137                    updated_at: row.get(5)?,
138                })
139            })?
140            .filter_map(|r| r.ok())
141            .collect();
142        Ok(facts)
143    }
144
145    /// Delete a memory fact by key.
146    pub fn delete(&self, key: &str) -> anyhow::Result<bool> {
147        let conn = rusqlite::Connection::open(&self.db_path)?;
148        let count = conn.execute("DELETE FROM memory WHERE key = ?1", rusqlite::params![key])?;
149        Ok(count > 0)
150    }
151
152    /// Get the total count of facts.
153    pub fn count(&self) -> anyhow::Result<usize> {
154        let conn = rusqlite::Connection::open(&self.db_path)?;
155        let count: usize = conn.query_row("SELECT COUNT(*) FROM memory", [], |row| row.get(0))?;
156        Ok(count)
157    }
158
159    /// Get all categories with counts.
160    pub fn categories(&self) -> anyhow::Result<Vec<(String, usize)>> {
161        let conn = rusqlite::Connection::open(&self.db_path)?;
162        let mut stmt = conn.prepare(
163            "SELECT category, COUNT(*) as cnt FROM memory GROUP BY category ORDER BY cnt DESC",
164        )?;
165        let cats = stmt
166            .query_map([], |row| {
167                Ok((row.get::<_, String>(0)?, row.get::<_, usize>(1)?))
168            })?
169            .filter_map(|r| r.ok())
170            .collect();
171        Ok(cats)
172    }
173}