codei_session/store/
mod.rs1mod json;
2mod sqlite;
3
4use std::path::{Path, PathBuf};
5
6use codei_config::{expand_tilde, SessionConfig, SessionStorage};
7
8use crate::error::SessionError;
9use crate::model::Session;
10
11use json::JsonSessionStore;
12use sqlite::SqliteSessionStore;
13
14enum Backend {
15 Sqlite(SqliteSessionStore),
16 Json(JsonSessionStore),
17}
18
19pub struct SessionStore {
21 backend: Backend,
22}
23
24impl SessionStore {
25 pub fn open_for_config(config: &SessionConfig) -> Result<Self, SessionError> {
27 let dir = expand_tilde(&config.dir);
28 let backend = match config.storage {
29 SessionStorage::Sqlite => {
30 Backend::Sqlite(SqliteSessionStore::open(&resolve_sqlite_path(&dir))?)
31 }
32 SessionStorage::Json => Backend::Json(JsonSessionStore::open(&dir)?),
33 };
34 Ok(Self { backend })
35 }
36
37 pub fn open_default() -> Result<Self, SessionError> {
39 Self::open_for_config(&SessionConfig::default())
40 }
41
42 pub fn open(path: &Path) -> Result<Self, SessionError> {
44 Ok(Self {
45 backend: Backend::Sqlite(SqliteSessionStore::open(path)?),
46 })
47 }
48
49 pub fn save(&self, session: &Session) -> Result<(), SessionError> {
50 match &self.backend {
51 Backend::Sqlite(store) => store.save(session),
52 Backend::Json(store) => store.save(session),
53 }
54 }
55
56 pub fn load(&self, id: &str) -> Result<Session, SessionError> {
57 match &self.backend {
58 Backend::Sqlite(store) => store.load(id),
59 Backend::Json(store) => store.load(id),
60 }
61 }
62
63 pub fn list(&self, limit: usize) -> Result<Vec<Session>, SessionError> {
64 match &self.backend {
65 Backend::Sqlite(store) => store.list(limit),
66 Backend::Json(store) => store.list(limit),
67 }
68 }
69
70 pub fn latest(&self) -> Result<Option<Session>, SessionError> {
71 match &self.backend {
72 Backend::Sqlite(store) => store.latest(),
73 Backend::Json(store) => store.latest(),
74 }
75 }
76
77 pub fn delete(&self, id: &str) -> Result<(), SessionError> {
78 match &self.backend {
79 Backend::Sqlite(store) => store.delete(id),
80 Backend::Json(store) => store.delete(id),
81 }
82 }
83
84 pub fn export_jsonl(&self, id: &str) -> Result<String, SessionError> {
85 let session = self.load(id)?;
86 let mut lines = Vec::new();
87 for msg in &session.messages {
88 let line = serde_json::json!({
89 "id": msg.id,
90 "role": msg.role,
91 "content": msg.content,
92 "tool_calls": msg.tool_calls,
93 "tool_call_id": msg.tool_call_id,
94 "created_at": msg.created_at,
95 });
96 lines.push(serde_json::to_string(&line)?);
97 }
98 Ok(lines.join("\n"))
99 }
100}
101
102fn resolve_sqlite_path(dir: &Path) -> PathBuf {
104 let primary = dir.join("sessions.db");
105 if primary.exists() {
106 return primary;
107 }
108 let legacy = expand_tilde("~/.local/share/codei/sessions.db");
109 if legacy.exists() {
110 return legacy;
111 }
112 primary
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118 use crate::model::Session;
119 use codei_config::SessionStorage;
120
121 #[test]
122 fn open_json_backend() {
123 let dir = tempfile::tempdir().unwrap();
124 let config = SessionConfig {
125 storage: SessionStorage::Json,
126 dir: dir.path().to_string_lossy().into_owned(),
127 };
128 let store = SessionStore::open_for_config(&config).unwrap();
129 let mut session = Session::new(dir.path().to_path_buf());
130 session.push_user("hello");
131 store.save(&session).unwrap();
132 assert_eq!(store.load(&session.id).unwrap().messages.len(), 1);
133 }
134}