spool-memory 0.1.0

Local-first developer memory system — persistent, structured knowledge for AI coding tools
Documentation
use crate::app;
use crate::enhancement_trace::{PromptOptimizeTrace, read_latest_prompt_optimize_trace};
use crate::memory_gateway::load_config;
use serde::Serialize;
use std::fs;
use std::path::Path;
use ts_rs::TS;

#[derive(Debug, Clone, Serialize, TS)]
#[ts(export, export_to = "../frontend/src/lib/types/generated/")]
pub struct DesktopStatusResponse {
    pub config_exists: bool,
    pub vault_available: bool,
    pub vault_root: Option<String>,
    pub cwd_exists: bool,
    pub cwd: String,
    pub session_sources_available: bool,
    pub claude_mcp_registered: bool,
    pub codex_mcp_registered: bool,
    pub mcp_config_detected: bool,
    pub spool_mcp_command: String,
    pub claude_mcp_snippet: String,
    pub codex_mcp_snippet: String,
    pub recent_enhancement: Option<PromptOptimizeTrace>,
}

pub fn collect_status(
    config_path: &Path,
    cwd: &Path,
    vault_override: Option<&Path>,
    provider_session_count: usize,
) -> DesktopStatusResponse {
    let config_exists = config_path.exists() && config_path.is_file();
    let cwd_exists = cwd.exists() && cwd.is_dir();

    let resolved_vault = if let Some(override_path) = vault_override {
        app::resolve_override_path(override_path, config_path)
            .ok()
            .map(|path| path.display().to_string())
    } else if config_exists {
        load_config(config_path)
            .ok()
            .map(|cfg| cfg.vault.root.display().to_string())
    } else {
        None
    };

    let vault_available = resolved_vault
        .as_deref()
        .map(|path| fs::metadata(path).map(|m| m.is_dir()).unwrap_or(false))
        .unwrap_or(false);
    let claude_mcp_registered = detect_claude_spool_mcp();
    let codex_mcp_registered = detect_codex_spool_mcp();
    let spool_mcp_command = suggested_spool_mcp_command();
    let recent_enhancement = read_latest_prompt_optimize_trace(config_path)
        .ok()
        .flatten();
    let claude_mcp_snippet = format!(
        r#""spool": {{
  "type": "stdio",
  "command": "{}",
  "args": ["--config", "{}"]
}}"#,
        spool_mcp_command,
        config_path.display()
    );
    let codex_mcp_snippet = format!(
        r#"[mcp_servers.spool]
type = "stdio"
command = "{}"
args = ["--config", "{}"]"#,
        spool_mcp_command,
        config_path.display()
    );

    DesktopStatusResponse {
        config_exists,
        vault_available,
        vault_root: resolved_vault,
        cwd_exists,
        cwd: cwd.display().to_string(),
        session_sources_available: provider_session_count > 0,
        claude_mcp_registered,
        codex_mcp_registered,
        mcp_config_detected: claude_mcp_registered || codex_mcp_registered,
        spool_mcp_command,
        claude_mcp_snippet,
        codex_mcp_snippet,
        recent_enhancement,
    }
}

fn suggested_spool_mcp_command() -> String {
    Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("target/debug/spool-mcp")
        .display()
        .to_string()
}

fn detect_claude_spool_mcp() -> bool {
    let Some(home) = crate::support::home_dir() else {
        return false;
    };
    let path = home.join(".claude.json");
    let content = match fs::read_to_string(path) {
        Ok(content) => content,
        Err(_) => return false,
    };
    let value: serde_json::Value = match serde_json::from_str(&content) {
        Ok(value) => value,
        Err(_) => return false,
    };
    value
        .get("mcpServers")
        .and_then(|servers| servers.get("spool"))
        .is_some()
}

fn detect_codex_spool_mcp() -> bool {
    let Some(home) = crate::support::home_dir() else {
        return false;
    };
    let path = home.join(".codex/config.toml");
    let content = match fs::read_to_string(path) {
        Ok(content) => content,
        Err(_) => return false,
    };
    let value: toml::Value = match toml::from_str(&content) {
        Ok(value) => value,
        Err(_) => return false,
    };
    value
        .get("mcp_servers")
        .and_then(|servers| servers.get("spool"))
        .is_some()
}