use crate::error::ConfigError;
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
const DEFAULT_PORT: u16 = 7720;
const DEFAULT_BATCH_SIZE: usize = 32;
const DEFAULT_RRF_K: u32 = 60;
const DEFAULT_MAX_FILE_SIZE: u64 = 10 * 1024 * 1024;
const DEFAULT_CONTEXT_LINES: usize = 2;
const DEFAULT_TOP_K: usize = 20;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct SeekrConfig {
pub index_dir: PathBuf,
pub model_dir: PathBuf,
pub embed_model: String,
pub exclude_patterns: Vec<String>,
pub max_file_size: u64,
pub server: ServerConfig,
pub search: SearchConfig,
pub embedding: EmbeddingConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct ServerConfig {
pub host: String,
pub port: u16,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct SearchConfig {
pub context_lines: usize,
pub top_k: usize,
pub rrf_k: u32,
pub score_threshold: f32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct EmbeddingConfig {
pub batch_size: usize,
}
impl Default for SeekrConfig {
fn default() -> Self {
let seekr_dir = default_seekr_dir();
Self {
index_dir: seekr_dir.join("indexes"),
model_dir: seekr_dir.join("models"),
embed_model: "all-MiniLM-L6-v2".to_string(),
exclude_patterns: vec![
"*.min.js".to_string(),
"*.min.css".to_string(),
"*.lock".to_string(),
"package-lock.json".to_string(),
"yarn.lock".to_string(),
],
max_file_size: DEFAULT_MAX_FILE_SIZE,
server: ServerConfig::default(),
search: SearchConfig::default(),
embedding: EmbeddingConfig::default(),
}
}
}
impl Default for ServerConfig {
fn default() -> Self {
Self {
host: "127.0.0.1".to_string(),
port: DEFAULT_PORT,
}
}
}
impl Default for SearchConfig {
fn default() -> Self {
Self {
context_lines: DEFAULT_CONTEXT_LINES,
top_k: DEFAULT_TOP_K,
rrf_k: DEFAULT_RRF_K,
score_threshold: 0.0,
}
}
}
impl Default for EmbeddingConfig {
fn default() -> Self {
Self {
batch_size: DEFAULT_BATCH_SIZE,
}
}
}
impl SeekrConfig {
pub fn load() -> std::result::Result<Self, ConfigError> {
let config_path = default_config_path();
Self::load_from(&config_path)
}
pub fn load_from(path: &Path) -> std::result::Result<Self, ConfigError> {
if !path.exists() {
let config = Self::default();
if let Err(e) = config.save_to(path) {
tracing::warn!("Could not write default config to {}: {}", path.display(), e);
}
return Ok(config);
}
let content = std::fs::read_to_string(path)?;
let config: SeekrConfig =
toml::from_str(&content).map_err(|e| ConfigError::ParseError(e.to_string()))?;
Ok(config)
}
pub fn save_to(&self, path: &Path) -> std::result::Result<(), ConfigError> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let content =
toml::to_string_pretty(self).map_err(|e| ConfigError::ParseError(e.to_string()))?;
std::fs::write(path, content)?;
Ok(())
}
pub fn project_index_dir(&self, project_path: &Path) -> PathBuf {
let canonical = project_path
.canonicalize()
.unwrap_or_else(|_| project_path.to_path_buf());
let hash = blake3::hash(canonical.to_string_lossy().as_bytes());
let hex = hash.to_hex();
let short_hash = &hex.as_str()[..16];
self.index_dir.join(short_hash)
}
}
fn default_seekr_dir() -> PathBuf {
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(".seekr")
}
pub fn default_config_path() -> PathBuf {
default_seekr_dir().join("config.toml")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = SeekrConfig::default();
assert_eq!(config.server.port, 7720);
assert_eq!(config.embed_model, "all-MiniLM-L6-v2");
assert_eq!(config.embedding.batch_size, 32);
assert_eq!(config.search.rrf_k, 60);
}
#[test]
fn test_project_index_dir_isolation() {
let config = SeekrConfig::default();
let dir_a = config.project_index_dir(Path::new("/home/user/project-a"));
let dir_b = config.project_index_dir(Path::new("/home/user/project-b"));
assert_ne!(dir_a, dir_b, "Different projects should have different index dirs");
}
#[test]
fn test_load_nonexistent_returns_default() {
let config = SeekrConfig::load_from(Path::new("/nonexistent/path/config.toml")).unwrap();
assert_eq!(config.server.port, DEFAULT_PORT);
}
}