spool-memory 0.2.3

Local-first developer memory system — persistent, structured knowledge for AI coding tools
Documentation
use crate::domain::OutputFormat;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AppConfig {
    pub vault: VaultConfig,
    #[serde(default)]
    pub output: OutputConfig,
    #[serde(default)]
    pub developer: DeveloperConfig,
    #[serde(default)]
    pub projects: Vec<ProjectConfig>,
    #[serde(default)]
    pub scenes: Vec<SceneConfig>,
    #[serde(default)]
    pub embedding: EmbeddingConfig,
}

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct DeveloperConfig {
    #[serde(default)]
    pub note_roots: Vec<String>,
}

impl DeveloperConfig {
    pub fn effective_note_roots(&self, project_note_roots: &[String]) -> Vec<String> {
        let mut roots = self.note_roots.clone();
        for root in project_note_roots {
            if !roots.iter().any(|existing| existing == root) {
                roots.push(root.clone());
            }
        }
        roots
    }
}

#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum WakeupProfileConfig {
    Developer,
    #[default]
    Project,
}

impl From<WakeupProfileConfig> for crate::domain::WakeupProfile {
    fn from(value: WakeupProfileConfig) -> Self {
        match value {
            WakeupProfileConfig::Developer => crate::domain::WakeupProfile::Developer,
            WakeupProfileConfig::Project => crate::domain::WakeupProfile::Project,
        }
    }
}

impl From<crate::domain::WakeupProfile> for WakeupProfileConfig {
    fn from(value: crate::domain::WakeupProfile) -> Self {
        match value {
            crate::domain::WakeupProfile::Developer => WakeupProfileConfig::Developer,
            crate::domain::WakeupProfile::Project => WakeupProfileConfig::Project,
        }
    }
}

impl Default for AppConfig {
    fn default() -> Self {
        Self {
            vault: VaultConfig {
                root: PathBuf::new(),
                limits: VaultLimits::default(),
            },
            output: OutputConfig::default(),
            developer: DeveloperConfig::default(),
            projects: Vec::new(),
            scenes: Vec::new(),
            embedding: EmbeddingConfig::default(),
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VaultConfig {
    pub root: PathBuf,
    #[serde(default)]
    pub limits: VaultLimits,
}

#[derive(Debug, Clone, Serialize, Deserialize, ts_rs::TS)]
#[ts(export, export_to = "../frontend/src/lib/types/generated/")]
pub struct VaultLimits {
    #[serde(default = "default_max_files")]
    pub max_files: usize,
    #[serde(default = "default_max_file_bytes")]
    pub max_file_bytes: u64,
    #[serde(default = "default_max_total_bytes")]
    pub max_total_bytes: u64,
    #[serde(default = "default_max_depth")]
    pub max_depth: usize,
}

impl Default for VaultLimits {
    fn default() -> Self {
        Self {
            max_files: default_max_files(),
            max_file_bytes: default_max_file_bytes(),
            max_total_bytes: default_max_total_bytes(),
            max_depth: default_max_depth(),
        }
    }
}

fn default_max_files() -> usize {
    5_000
}

fn default_max_file_bytes() -> u64 {
    512 * 1024
}

fn default_max_total_bytes() -> u64 {
    16 * 1024 * 1024
}

fn default_max_depth() -> usize {
    12
}

fn default_format() -> OutputFormat {
    OutputFormat::Prompt
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OutputConfig {
    #[serde(default = "default_format")]
    pub default_format: OutputFormat,
    #[serde(default = "default_max_chars")]
    pub max_chars: usize,
    #[serde(default = "default_max_notes")]
    pub max_notes: usize,
    #[serde(default = "default_max_lifecycle")]
    pub max_lifecycle: usize,
}

impl Default for OutputConfig {
    fn default() -> Self {
        Self {
            default_format: default_format(),
            max_chars: default_max_chars(),
            max_notes: default_max_notes(),
            max_lifecycle: default_max_lifecycle(),
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProjectConfig {
    pub id: String,
    pub name: String,
    #[serde(default)]
    pub repo_paths: Vec<PathBuf>,
    #[serde(default)]
    pub note_roots: Vec<String>,
    #[serde(default)]
    pub default_tags: Vec<String>,
    #[serde(default)]
    pub modules: Vec<ModuleConfig>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModuleConfig {
    pub id: String,
    #[serde(default)]
    pub path_prefixes: Vec<String>,
    #[serde(default)]
    pub keywords: Vec<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SceneConfig {
    pub id: String,
    #[serde(default)]
    pub keywords: Vec<String>,
    #[serde(default)]
    pub preferred_notes: Vec<String>,
}

fn default_max_chars() -> usize {
    12_000
}

fn default_max_notes() -> usize {
    8
}

fn default_max_lifecycle() -> usize {
    5
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EmbeddingConfig {
    #[serde(default = "default_embedding_enabled")]
    pub enabled: bool,
    #[serde(default)]
    pub model_id: Option<String>,
    #[serde(default)]
    pub model_path: Option<PathBuf>,
    #[serde(default)]
    pub index_path: Option<PathBuf>,
    #[serde(default = "default_auto_index")]
    pub auto_index: bool,
}

impl Default for EmbeddingConfig {
    fn default() -> Self {
        Self {
            enabled: default_embedding_enabled(),
            model_id: None,
            model_path: None,
            index_path: None,
            auto_index: default_auto_index(),
        }
    }
}

impl EmbeddingConfig {
    pub fn resolved_model_path(&self) -> Option<PathBuf> {
        self.model_path.as_ref().map(|p| {
            if p.starts_with("~") {
                if let Some(home) = dirs_home() {
                    home.join(p.strip_prefix("~").unwrap_or(p))
                } else {
                    p.clone()
                }
            } else {
                p.clone()
            }
        })
    }

    pub fn resolved_index_path(&self) -> PathBuf {
        self.index_path.clone().unwrap_or_else(|| {
            dirs_home()
                .unwrap_or_else(|| PathBuf::from("."))
                .join(".spool")
                .join("embedding-index.bin")
        })
    }
}

fn default_embedding_enabled() -> bool {
    true
}

fn default_auto_index() -> bool {
    true
}

fn dirs_home() -> Option<PathBuf> {
    crate::support::home_dir()
}