1use crate::error::ConfigError;
7use serde::{Deserialize, Serialize};
8use std::path::{Path, PathBuf};
9
10const DEFAULT_PORT: u16 = 7720;
12
13const DEFAULT_BATCH_SIZE: usize = 32;
15
16const DEFAULT_RRF_K: u32 = 60;
18
19const DEFAULT_MAX_FILE_SIZE: u64 = 10 * 1024 * 1024;
21
22const DEFAULT_CONTEXT_LINES: usize = 2;
24
25const DEFAULT_TOP_K: usize = 20;
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
30#[serde(default)]
31pub struct SeekrConfig {
32 pub index_dir: PathBuf,
35
36 pub model_dir: PathBuf,
39
40 pub embed_model: String,
43
44 pub exclude_patterns: Vec<String>,
46
47 pub max_file_size: u64,
49
50 pub server: ServerConfig,
52
53 pub search: SearchConfig,
55
56 pub embedding: EmbeddingConfig,
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize)]
62#[serde(default)]
63pub struct ServerConfig {
64 pub host: String,
66
67 pub port: u16,
69}
70
71#[derive(Debug, Clone, Serialize, Deserialize)]
73#[serde(default)]
74pub struct SearchConfig {
75 pub context_lines: usize,
77
78 pub top_k: usize,
80
81 pub rrf_k: u32,
83
84 pub score_threshold: f32,
86}
87
88#[derive(Debug, Clone, Serialize, Deserialize)]
90#[serde(default)]
91pub struct EmbeddingConfig {
92 pub batch_size: usize,
94}
95
96impl Default for SeekrConfig {
97 fn default() -> Self {
98 let seekr_dir = default_seekr_dir();
99 Self {
100 index_dir: seekr_dir.join("indexes"),
101 model_dir: seekr_dir.join("models"),
102 embed_model: "all-MiniLM-L6-v2".to_string(),
103 exclude_patterns: vec![
104 "*.min.js".to_string(),
105 "*.min.css".to_string(),
106 "*.lock".to_string(),
107 "package-lock.json".to_string(),
108 "yarn.lock".to_string(),
109 ],
110 max_file_size: DEFAULT_MAX_FILE_SIZE,
111 server: ServerConfig::default(),
112 search: SearchConfig::default(),
113 embedding: EmbeddingConfig::default(),
114 }
115 }
116}
117
118impl Default for ServerConfig {
119 fn default() -> Self {
120 Self {
121 host: "127.0.0.1".to_string(),
122 port: DEFAULT_PORT,
123 }
124 }
125}
126
127impl Default for SearchConfig {
128 fn default() -> Self {
129 Self {
130 context_lines: DEFAULT_CONTEXT_LINES,
131 top_k: DEFAULT_TOP_K,
132 rrf_k: DEFAULT_RRF_K,
133 score_threshold: 0.0,
134 }
135 }
136}
137
138impl Default for EmbeddingConfig {
139 fn default() -> Self {
140 Self {
141 batch_size: DEFAULT_BATCH_SIZE,
142 }
143 }
144}
145
146impl SeekrConfig {
147 pub fn load() -> std::result::Result<Self, ConfigError> {
152 let config_path = default_config_path();
153 Self::load_from(&config_path)
154 }
155
156 pub fn load_from(path: &Path) -> std::result::Result<Self, ConfigError> {
158 if !path.exists() {
159 let config = Self::default();
160 if let Err(e) = config.save_to(path) {
162 tracing::warn!(
163 "Could not write default config to {}: {}",
164 path.display(),
165 e
166 );
167 }
168 return Ok(config);
169 }
170
171 let content = std::fs::read_to_string(path)?;
172 let config: SeekrConfig =
173 toml::from_str(&content).map_err(|e| ConfigError::ParseError(e.to_string()))?;
174 Ok(config)
175 }
176
177 pub fn save_to(&self, path: &Path) -> std::result::Result<(), ConfigError> {
179 if let Some(parent) = path.parent() {
180 std::fs::create_dir_all(parent)?;
181 }
182 let content =
183 toml::to_string_pretty(self).map_err(|e| ConfigError::ParseError(e.to_string()))?;
184 std::fs::write(path, content)?;
185 Ok(())
186 }
187
188 pub fn project_index_dir(&self, project_path: &Path) -> PathBuf {
193 let canonical = project_path
194 .canonicalize()
195 .unwrap_or_else(|_| project_path.to_path_buf());
196 let hash = blake3::hash(canonical.to_string_lossy().as_bytes());
197 let hex = hash.to_hex();
199 let short_hash = &hex.as_str()[..16];
200 self.index_dir.join(short_hash)
201 }
202}
203
204fn default_seekr_dir() -> PathBuf {
206 dirs::home_dir()
207 .unwrap_or_else(|| PathBuf::from("."))
208 .join(".seekr")
209}
210
211pub fn default_config_path() -> PathBuf {
213 default_seekr_dir().join("config.toml")
214}
215
216#[cfg(test)]
217mod tests {
218 use super::*;
219
220 #[test]
221 fn test_default_config() {
222 let config = SeekrConfig::default();
223 assert_eq!(config.server.port, 7720);
224 assert_eq!(config.embed_model, "all-MiniLM-L6-v2");
225 assert_eq!(config.embedding.batch_size, 32);
226 assert_eq!(config.search.rrf_k, 60);
227 }
228
229 #[test]
230 fn test_project_index_dir_isolation() {
231 let config = SeekrConfig::default();
232 let dir_a = config.project_index_dir(Path::new("/home/user/project-a"));
233 let dir_b = config.project_index_dir(Path::new("/home/user/project-b"));
234 assert_ne!(
235 dir_a, dir_b,
236 "Different projects should have different index dirs"
237 );
238 }
239
240 #[test]
241 fn test_load_nonexistent_returns_default() {
242 let config = SeekrConfig::load_from(Path::new("/nonexistent/path/config.toml")).unwrap();
243 assert_eq!(config.server.port, DEFAULT_PORT);
244 }
245}