spool-memory 0.2.3

Local-first developer memory system — persistent, structured knowledge for AI coding tools
Documentation
use serde_json::{Value, json};

pub(super) const RESOURCE_SESSION_HANDOFF_URI: &str = "spool://docs/session-handoff";
pub(super) const RESOURCE_CURRENT_PLAN_URI: &str = "spool://docs/mcp-prompts-round-8";
pub(super) const RESOURCE_RESTART_GUIDE_URI: &str = "spool://docs/restart-guide";

pub(super) fn tool_definitions() -> Vec<Value> {
    vec![
        json!({
            "name": "prompt_optimize",
            "description": "Generate a combined wakeup + context prompt block for terminal AI clients from cwd, task, and optional files.",
            "inputSchema": prompt_optimize_schema()
        }),
        json!({
            "name": "memory_search",
            "description": "Build a routed context bundle for a task using the existing spool retrieval core.",
            "inputSchema": route_schema(false)
        }),
        json!({
            "name": "memory_explain",
            "description": "Explain why spool matched the current project, notes, and candidates.",
            "inputSchema": route_schema(false)
        }),
        json!({
            "name": "memory_wakeup",
            "description": "Build a wakeup packet for a developer or project profile.",
            "inputSchema": wakeup_schema()
        }),
        json!({
            "name": "memory_review_queue",
            "description": "Return the current pending review queue from the lifecycle ledger.",
            "inputSchema": { "type": "object", "properties": {} }
        }),
        json!({
            "name": "memory_wakeup_ready",
            "description": "Return memories that are currently wakeup-ready.",
            "inputSchema": { "type": "object", "properties": {} }
        }),
        json!({
            "name": "memory_get",
            "description": "Get a single lifecycle record by record_id.",
            "inputSchema": record_id_schema()
        }),
        json!({
            "name": "memory_history",
            "description": "Get the full event history for a lifecycle record.",
            "inputSchema": record_id_schema()
        }),
        json!({
            "name": "memory_record_manual",
            "description": "Create an accepted manual memory record.",
            "inputSchema": memory_write_schema()
        }),
        json!({
            "name": "memory_propose",
            "description": "Create a candidate AI-proposed memory record.",
            "inputSchema": memory_write_schema()
        }),
        json!({
            "name": "memory_accept",
            "description": "Accept a candidate lifecycle record.",
            "inputSchema": record_id_schema()
        }),
        json!({
            "name": "memory_promote",
            "description": "Promote an accepted lifecycle record to canonical.",
            "inputSchema": record_id_schema()
        }),
        json!({
            "name": "memory_archive",
            "description": "Archive a lifecycle record.",
            "inputSchema": record_id_schema()
        }),
        json!({
            "name": "memory_import_session",
            "description": "Heuristically extract candidate memories from a Claude Code / Codex session transcript. Default dry-run; set apply=true to write them into the ledger as AI proposals.",
            "inputSchema": import_session_schema()
        }),
        json!({
            "name": "memory_sync_vault",
            "description": "Sync accepted/canonical lifecycle records to vault as canonical notes in 50-Memory-Ledger/Extracted/. Honors body-hash-based user edit protection. Default live write; set dry_run=true to preview.",
            "inputSchema": sync_vault_schema()
        }),
        json!({
            "name": "memory_distill_pending",
            "description": "Drain the post-tool-use queue and scan the session transcript with Tier 1 heuristics (self-tag → accepted, incident extraction → candidate). Use after a long working session to flush pending signals into the ledger without waiting for the Stop hook. R4a: Tier 1 only; R4b will prefer MCP sampling.",
            "inputSchema": distill_pending_schema()
        }),
        json!({
            "name": "memory_check_contradictions",
            "description": "Check a lifecycle record against existing wakeup-ready memories for contradictions. Returns a list of conflicting record IDs with signal type (negation/replacement).",
            "inputSchema": record_id_schema()
        }),
        json!({
            "name": "memory_staleness_report",
            "description": "Report which wakeup-ready memories are stale (not recently retrieved). Shows days since last reference and the staleness penalty applied. Use to identify memories that may need review or archival.",
            "inputSchema": { "type": "object", "properties": {} }
        }),
        json!({
            "name": "memory_import_git",
            "description": "Scan recent git commits in the current repo and extract decision/incident/pattern candidates from conventional commit messages. Candidates are written as AI proposals for user review. Set dry_run=true to preview without writing.",
            "inputSchema": {
                "type": "object",
                "properties": {
                    "cwd": { "type": "string", "description": "Repository path (defaults to current directory)" },
                    "limit": { "type": "integer", "description": "Number of recent commits to scan (default 30)" },
                    "dry_run": { "type": "boolean", "description": "Preview only, don't write to ledger" }
                }
            }
        }),
        json!({
            "name": "memory_dedup_suggestions",
            "description": "Find duplicate or highly similar memories that could be merged. Returns pairs of records with similarity scores above 50%.",
            "inputSchema": { "type": "object", "properties": {} }
        }),
        json!({
            "name": "memory_consolidate",
            "description": "Detect clusters of related fragmented memories (3+ records sharing entities/tags) and optionally merge them. Default dry_run=true returns suggestions only.",
            "inputSchema": {
                "type": "object",
                "properties": {
                    "dry_run": { "type": "boolean", "default": true, "description": "When true (default), only return suggestions. When false, execute the merge." }
                }
            }
        }),
        json!({
            "name": "memory_prune",
            "description": "Detect stale, expired, or superseded memories and optionally archive them. Default dry_run=true returns suggestions only.",
            "inputSchema": {
                "type": "object",
                "properties": {
                    "dry_run": { "type": "boolean", "default": true, "description": "When true (default), only return suggestions. When false, execute the archival." }
                }
            }
        }),
        json!({
            "name": "memory_crystallize",
            "description": "Detect clusters of related memory fragments and synthesize them into structured knowledge pages. Uses LLM synthesis when sampling is available, falls back to template-based synthesis otherwise. Knowledge pages are persisted as candidate records for user review.",
            "inputSchema": {
                "type": "object",
                "properties": {
                    "topic": { "type": "string", "description": "Optional topic filter — only crystallize clusters matching this topic (case-insensitive substring match on entities/tags/title)." },
                    "dry_run": { "type": "boolean", "default": false, "description": "When true, detect clusters and show what would be synthesized without persisting." }
                }
            }
        }),
        json!({
            "name": "memory_lint",
            "description": "Run a wiki-style lint pass over the ledger and Obsidian vault. Returns prune suggestions (stale / superseded / expired), broken cross-references, and orphan notes (vault files without matching ledger records). Pure read — no mutations.",
            "inputSchema": {
                "type": "object",
                "properties": {}
            }
        }),
    ]
}

pub(super) fn prompt_definitions() -> Vec<Value> {
    vec![
        json!({
            "name": "review_lifecycle_queue",
            "description": "Review pending lifecycle memories and decide which tools to call next.",
            "arguments": [{
                "name": "focus",
                "description": "Optional reviewer focus, such as preferences, constraints, or decisions.",
                "required": false
            }]
        }),
        json!({
            "name": "generate_project_wakeup",
            "description": "Build and summarize a project wakeup packet.",
            "arguments": [
                { "name": "cwd", "description": "Repository working directory.", "required": false },
                { "name": "task", "description": "Optional wakeup task wording.", "required": false }
            ]
        }),
        json!({
            "name": "retrieve_project_context",
            "description": "Retrieve routed project context and explain why it matched.",
            "arguments": [
                { "name": "cwd", "description": "Repository working directory.", "required": false },
                { "name": "task", "description": "Context retrieval task wording.", "required": false }
            ]
        }),
    ]
}

pub(super) fn resource_definitions() -> Vec<Value> {
    vec![
        json!({
            "uri": RESOURCE_SESSION_HANDOFF_URI,
            "name": "Session Handoff",
            "description": "Current spool handoff and restart status.",
            "mimeType": "text/markdown"
        }),
        json!({
            "uri": RESOURCE_CURRENT_PLAN_URI,
            "name": "Current Plan",
            "description": "Current MCP prompts/resources implementation plan.",
            "mimeType": "text/markdown"
        }),
        json!({
            "uri": RESOURCE_RESTART_GUIDE_URI,
            "name": "Restart Guide",
            "description": "Restart entry points and current next steps.",
            "mimeType": "text/markdown"
        }),
    ]
}

pub(super) fn record_id_schema() -> Value {
    json!({
        "type": "object",
        "properties": {
            "record_id": { "type": "string" },
            "actor": { "type": "string" },
            "reason": { "type": "string" },
            "evidence_refs": {
                "type": "array",
                "items": { "type": "string" }
            }
        },
        "required": ["record_id"]
    })
}

pub(super) fn import_session_schema() -> Value {
    json!({
        "type": "object",
        "properties": {
            "provider": {
                "type": "string",
                "enum": ["claude", "codex"],
                "description": "Source provider for the session transcript."
            },
            "session_id": {
                "type": "string",
                "description": "Raw session id (no provider prefix)."
            },
            "apply": {
                "type": "boolean",
                "default": false,
                "description": "When false (default) only returns candidates. When true writes them as AI proposals."
            },
            "actor": {
                "type": "string",
                "description": "Optional provenance actor recorded on each written proposal."
            }
        },
        "required": ["provider", "session_id"]
    })
}

pub(super) fn sync_vault_schema() -> Value {
    json!({
        "type": "object",
        "properties": {
            "dry_run": {
                "type": "boolean",
                "default": false,
                "description": "Preview actions without writing to vault."
            },
            "enrich": {
                "type": "boolean",
                "default": false,
                "description": "Backfill entities/tags/triggers on records that lack them using heuristic extraction."
            }
        }
    })
}

pub(super) fn distill_pending_schema() -> Value {
    json!({
        "type": "object",
        "properties": {
            "cwd": {
                "type": "string",
                "description": "Absolute project root. Required: heuristics resolve the .spool/ runtime dir + Claude Code transcript dir relative to this."
            },
            "transcript_path": {
                "type": "string",
                "description": "Optional explicit path to a Claude Code session jsonl. If absent, the tool falls back to scanning ~/.claude/projects/<sanitized-cwd>/ for the most recently modified jsonl."
            }
        },
        "required": ["cwd"]
    })
}

pub(super) fn memory_write_schema() -> Value {
    json!({
        "type": "object",
        "properties": {
            "title": { "type": "string" },
            "summary": { "type": "string" },
            "memory_type": { "type": "string" },
            "scope": {
                "type": "string",
                "enum": ["user", "project", "workspace", "team", "agent"]
            },
            "source_ref": { "type": "string" },
            "project_id": { "type": "string" },
            "user_id": { "type": "string" },
            "sensitivity": { "type": "string" },
            "actor": { "type": "string" },
            "reason": { "type": "string" },
            "evidence_refs": {
                "type": "array",
                "items": { "type": "string" }
            }
        },
        "required": ["title", "summary", "memory_type", "scope", "source_ref"]
    })
}

pub(super) fn route_schema(include_profile: bool) -> Value {
    let mut properties = serde_json::Map::from_iter([
        ("task".to_string(), json!({ "type": "string" })),
        ("cwd".to_string(), json!({ "type": "string" })),
        (
            "files".to_string(),
            json!({
                "type": "array",
                "items": { "type": "string" }
            }),
        ),
        (
            "target".to_string(),
            json!({
                "type": "string",
                "enum": ["claude", "codex", "opencode"]
            }),
        ),
        (
            "format".to_string(),
            json!({
                "type": "string",
                "enum": ["prompt", "markdown", "json"]
            }),
        ),
    ]);
    if include_profile {
        properties.insert(
            "profile".to_string(),
            json!({
                "type": "string",
                "enum": ["developer", "project"]
            }),
        );
    }

    json!({
        "type": "object",
        "properties": properties,
        "required": ["task", "cwd"]
    })
}

pub(super) fn wakeup_schema() -> Value {
    route_schema(true)
}

pub(super) fn prompt_optimize_schema() -> Value {
    json!({
        "type": "object",
        "properties": {
            "task": { "type": "string" },
            "cwd": { "type": "string" },
            "files": {
                "type": "array",
                "items": { "type": "string" }
            },
            "target": {
                "type": "string",
                "enum": ["claude", "codex", "opencode"]
            },
            "profile": {
                "type": "string",
                "enum": ["developer", "project"]
            },
            "provider": { "type": "string" },
            "session_id": { "type": "string" }
        },
        "required": ["task", "cwd"]
    })
}