Skip to main content

sediment/
lib.rs

1//! Sediment: Semantic memory for AI agents
2//!
3//! A local-first, MCP-native vector database for AI agent memory.
4//!
5//! ## Features
6//!
7//! - **Embedded storage** - LanceDB-powered, directory-based, no server required
8//! - **Local embeddings** - Uses `all-MiniLM-L6-v2` locally, no API keys needed
9//! - **MCP-native** - 4 tools for seamless LLM integration
10//! - **Project-aware** - Scoped memories with automatic project detection
11//! - **Auto-chunking** - Long content is automatically chunked for better search
12
13use serde::{Deserialize, Serialize};
14use std::path::{Path, PathBuf};
15use uuid::Uuid;
16
17pub mod access;
18pub mod chunker;
19pub mod consolidation;
20pub mod db;
21pub mod document;
22pub mod embedder;
23pub mod error;
24pub mod graph;
25pub mod item;
26pub mod mcp;
27pub mod retry;
28
29pub use chunker::{ChunkResult, ChunkingConfig, chunk_content};
30pub use db::Database;
31pub use document::ContentType;
32pub use embedder::{EMBEDDING_DIM, Embedder};
33pub use error::{Result, SedimentError};
34pub use item::{Chunk, ConflictInfo, Item, ItemFilters, SearchResult, StoreResult};
35pub use retry::{RetryConfig, with_retry};
36
37/// Scope for storing items
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
39#[serde(rename_all = "lowercase")]
40pub enum StoreScope {
41    /// Store in project-local scope (with project_id)
42    #[default]
43    Project,
44    /// Store in global scope (no project_id)
45    Global,
46}
47
48impl std::str::FromStr for StoreScope {
49    type Err = String;
50
51    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
52        match s.to_lowercase().as_str() {
53            "project" => Ok(StoreScope::Project),
54            "global" => Ok(StoreScope::Global),
55            _ => Err(format!(
56                "Invalid store scope: {}. Use 'project' or 'global'",
57                s
58            )),
59        }
60    }
61}
62
63impl std::fmt::Display for StoreScope {
64    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
65        match self {
66            StoreScope::Project => write!(f, "project"),
67            StoreScope::Global => write!(f, "global"),
68        }
69    }
70}
71
72/// Scope for listing items (recall always searches all with boosting)
73#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
74#[serde(rename_all = "lowercase")]
75pub enum ListScope {
76    /// List only project-local items
77    Project,
78    /// List only global items
79    Global,
80    /// List all items
81    #[default]
82    All,
83}
84
85impl std::str::FromStr for ListScope {
86    type Err = String;
87
88    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
89        match s.to_lowercase().as_str() {
90            "project" => Ok(ListScope::Project),
91            "global" => Ok(ListScope::Global),
92            "all" => Ok(ListScope::All),
93            _ => Err(format!(
94                "Invalid list scope: {}. Use 'project', 'global', or 'all'",
95                s
96            )),
97        }
98    }
99}
100
101impl std::fmt::Display for ListScope {
102    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
103        match self {
104            ListScope::Project => write!(f, "project"),
105            ListScope::Global => write!(f, "global"),
106            ListScope::All => write!(f, "all"),
107        }
108    }
109}
110
111/// Get the central database path.
112///
113/// Returns `~/.sediment/data` or the path specified in `SEDIMENT_DB` environment variable.
114/// Note: LanceDB uses a directory, not a single file.
115pub fn central_db_path() -> PathBuf {
116    if let Ok(path) = std::env::var("SEDIMENT_DB") {
117        return PathBuf::from(path);
118    }
119
120    dirs::home_dir()
121        .unwrap_or_else(|| PathBuf::from("."))
122        .join(".sediment")
123        .join("data")
124}
125
126/// Get the default global database path (alias for central_db_path for backwards compatibility)
127pub fn default_db_path() -> PathBuf {
128    central_db_path()
129}
130
131/// Project configuration stored in `.sediment/config`
132#[derive(Debug, Clone, Serialize, Deserialize)]
133pub struct ProjectConfig {
134    /// Unique project identifier (UUID)
135    pub project_id: String,
136}
137
138impl Default for ProjectConfig {
139    fn default() -> Self {
140        Self {
141            project_id: Uuid::new_v4().to_string(),
142        }
143    }
144}
145
146/// Get or create the project ID for a given project root.
147///
148/// The project ID is stored in `<project_root>/.sediment/config`.
149/// If no config exists, a new UUID is generated and saved.
150pub fn get_or_create_project_id(project_root: &Path) -> std::io::Result<String> {
151    let config_path = project_root.join(".sediment").join("config");
152
153    // Try to read existing config
154    if config_path.exists() {
155        let content = std::fs::read_to_string(&config_path)?;
156        if let Ok(config) = serde_json::from_str::<ProjectConfig>(&content) {
157            return Ok(config.project_id);
158        }
159    }
160
161    // Create new config with generated UUID
162    let config = ProjectConfig::default();
163
164    // Ensure .sediment directory exists
165    let sediment_dir = project_root.join(".sediment");
166    std::fs::create_dir_all(&sediment_dir)?;
167
168    // Save config
169    let content =
170        serde_json::to_string_pretty(&config).map_err(|e| std::io::Error::other(e.to_string()))?;
171    std::fs::write(&config_path, content)?;
172
173    Ok(config.project_id)
174}
175
176/// Apply similarity boosting based on project context.
177///
178/// - Same project: 1.15x boost (capped at 1.0)
179/// - Different project: 0.95x penalty
180/// - Global or no context: no change
181pub fn boost_similarity(
182    base: f32,
183    mem_project: Option<&str>,
184    current_project: Option<&str>,
185) -> f32 {
186    match (mem_project, current_project) {
187        (Some(m), Some(c)) if m == c => (base * 1.15).min(1.0), // Same project: boost
188        (Some(_), Some(_)) => base * 0.95,                      // Different project: slight penalty
189        _ => base,                                              // Global or no context
190    }
191}
192
193/// Find the project root by walking up from the given path.
194///
195/// Looks for directories containing `.sediment/` or `.git/` markers.
196/// Returns `None` if no project root is found.
197pub fn find_project_root(start: &Path) -> Option<PathBuf> {
198    let mut current = start.to_path_buf();
199
200    // If start is a file, use its parent directory
201    if current.is_file() {
202        current = current.parent()?.to_path_buf();
203    }
204
205    loop {
206        // Check for .sediment directory first (explicit project marker)
207        if current.join(".sediment").is_dir() {
208            return Some(current);
209        }
210
211        // Check for .git directory as fallback
212        if current.join(".git").exists() {
213            return Some(current);
214        }
215
216        // Move to parent directory
217        match current.parent() {
218            Some(parent) => current = parent.to_path_buf(),
219            None => return None,
220        }
221    }
222}
223
224/// Initialize a project directory for Sediment.
225///
226/// Creates the `.sediment/` directory in the specified path and generates a project ID.
227pub fn init_project(project_root: &Path) -> std::io::Result<PathBuf> {
228    let sediment_dir = project_root.join(".sediment");
229    std::fs::create_dir_all(&sediment_dir)?;
230
231    // Generate project ID
232    get_or_create_project_id(project_root)?;
233
234    Ok(sediment_dir)
235}