Skip to main content

recall_echo/
lib.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5//! recall-echo — Persistent memory system with knowledge graph.
6//!
7//! A general-purpose persistent memory system for any LLM tool — Claude Code,
8//! Ollama, or any provider. Features a four-layer memory architecture with
9//! a knowledge graph (SurrealDB + fastembed) as Layer 0.
10//!
11//! # Architecture
12//!
13//! ```text
14//! Transcript adapters (Claude Code, Codex, Grok, pulse-null Messages)
15//!     → Conversation (universal internal format)
16//!     → Archive pipeline (markdown + index + ephemeral + graph)
17//! ```
18//!
19//! Sessions arrive either because the CLI told us (Claude Code's `SessionEnd`
20//! hook) or because we read what it wrote ([`capture`], over [`transcript`]).
21//!
22//! # Features
23//!
24//! - `pulse-null` — Plugin integration for pulse-null entities
25//! - `llm` — HTTP-based LLM provider for entity extraction
26
27pub mod agent_cli;
28pub mod archive;
29pub mod capture;
30pub mod checkpoint;
31pub mod cli_provider;
32pub mod config;
33pub mod config_cli;
34pub mod consume;
35pub mod conversation;
36pub mod dashboard;
37pub mod distill;
38pub mod ephemeral;
39pub mod error;
40pub mod frontmatter;
41pub mod init;
42pub mod inspect_cli;
43pub mod jsonl;
44pub mod mcp;
45pub mod paths;
46pub mod search;
47pub mod serve;
48pub mod serve_capture;
49pub mod serve_client;
50#[cfg(feature = "llm")]
51pub mod serve_extract;
52mod serve_security;
53pub mod status;
54pub mod summarize;
55pub mod tags;
56pub mod transcript;
57
58pub mod graph;
59pub mod graph_bridge;
60pub mod graph_cli;
61#[cfg(feature = "llm")]
62pub mod llm_provider;
63
64#[cfg(feature = "pulse-null")]
65pub mod pulse_null;
66
67#[cfg(feature = "bench")]
68pub mod bench;
69
70use std::fs;
71use std::path::{Path, PathBuf};
72
73pub use archive::SessionMetadata;
74pub use summarize::ConversationSummary;
75
76/// The recall-echo memory system.
77///
78/// All paths are derived from entity_root:
79/// ```text
80/// {entity_root}/memory/
81/// ├── MEMORY.md
82/// ├── EPHEMERAL.md
83/// ├── ARCHIVE.md
84/// ├── conversations/
85/// └── graph/ (knowledge graph store)
86/// ```
87pub struct RecallEcho {
88    entity_root: PathBuf,
89}
90
91impl RecallEcho {
92    /// Create a new RecallEcho instance with a specific entity root directory.
93    #[must_use]
94    pub fn new(entity_root: PathBuf) -> Self {
95        Self { entity_root }
96    }
97
98    /// Create a RecallEcho using the default path resolution
99    /// (RECALL_ECHO_HOME env var or current working directory).
100    pub fn from_default() -> Result<Self, error::RecallError> {
101        Ok(Self::new(paths::entity_root()?))
102    }
103
104    /// Entity root directory.
105    #[must_use]
106    pub fn entity_root(&self) -> &Path {
107        &self.entity_root
108    }
109
110    /// Memory directory: {entity_root}/memory/
111    #[must_use]
112    pub fn memory_dir(&self) -> PathBuf {
113        self.entity_root.join("memory")
114    }
115
116    /// Path to MEMORY.md.
117    #[must_use]
118    pub fn memory_file(&self) -> PathBuf {
119        self.memory_dir().join("MEMORY.md")
120    }
121
122    /// Path to EPHEMERAL.md.
123    #[must_use]
124    pub fn ephemeral_file(&self) -> PathBuf {
125        self.memory_dir().join("EPHEMERAL.md")
126    }
127
128    /// Path to conversations directory.
129    #[must_use]
130    pub fn conversations_dir(&self) -> PathBuf {
131        self.memory_dir().join("conversations")
132    }
133
134    /// Path to ARCHIVE.md index.
135    #[must_use]
136    pub fn archive_index(&self) -> PathBuf {
137        self.memory_dir().join("ARCHIVE.md")
138    }
139
140    // ── Core operations ──────────────────────────────────────────────
141
142    /// Read EPHEMERAL.md content without clearing it.
143    /// Returns None if the file doesn't exist or is empty.
144    pub fn consume_content(&self) -> Result<Option<String>, error::RecallError> {
145        consume::consume(&self.ephemeral_file())
146    }
147
148    /// Check if the memory system has been initialized.
149    #[must_use]
150    pub fn is_initialized(&self) -> bool {
151        self.memory_dir().exists() && self.conversations_dir().exists()
152    }
153
154    /// Number of lines in MEMORY.md.
155    #[must_use]
156    pub fn memory_line_count(&self) -> usize {
157        let path = self.memory_file();
158        if !path.exists() {
159            return 0;
160        }
161        fs::read_to_string(&path)
162            .unwrap_or_default()
163            .lines()
164            .count()
165    }
166}
167
168// ---------------------------------------------------------------------------
169// Pulse-null plugin implementation — behind feature flag
170// ---------------------------------------------------------------------------
171
172#[cfg(feature = "pulse-null")]
173mod plugin_impl {
174    use super::*;
175    use std::any::Any;
176    use std::future::Future;
177    use std::pin::Pin;
178
179    use pulse_system_types::plugin::{Plugin, PluginContext, PluginResult, PluginRole};
180    use pulse_system_types::{HealthStatus, PluginMeta, SetupPrompt};
181
182    impl RecallEcho {
183        fn health_check(&self) -> HealthStatus {
184            if !self.memory_dir().exists() {
185                return HealthStatus::Down("memory directory not found".into());
186            }
187            if !self.memory_file().exists() {
188                return HealthStatus::Degraded("MEMORY.md not found".into());
189            }
190            if !self.conversations_dir().exists() {
191                return HealthStatus::Degraded("conversations directory not found".into());
192            }
193            HealthStatus::Healthy
194        }
195
196        fn get_setup_prompts() -> Vec<SetupPrompt> {
197            vec![SetupPrompt {
198                key: "entity_root".into(),
199                question: "Entity root directory:".into(),
200                required: true,
201                secret: false,
202                default: None,
203            }]
204        }
205    }
206
207    /// Factory function — creates a fully initialized recall-echo plugin.
208    pub async fn create(
209        config: &serde_json::Value,
210        ctx: &PluginContext,
211    ) -> Result<Box<dyn Plugin>, Box<dyn std::error::Error + Send + Sync>> {
212        let entity_root = config
213            .get("entity_root")
214            .and_then(|v| v.as_str())
215            .map(PathBuf::from)
216            .unwrap_or_else(|| ctx.entity_root.clone());
217
218        Ok(Box::new(RecallEcho::new(entity_root)))
219    }
220
221    impl Plugin for RecallEcho {
222        fn meta(&self) -> PluginMeta {
223            PluginMeta {
224                name: "recall-echo".into(),
225                version: env!("CARGO_PKG_VERSION").into(),
226                description: "Persistent memory system with knowledge graph".into(),
227            }
228        }
229
230        fn role(&self) -> PluginRole {
231            PluginRole::Memory
232        }
233
234        fn start(&mut self) -> PluginResult<'_> {
235            Box::pin(async { Ok(()) })
236        }
237
238        fn stop(&mut self) -> PluginResult<'_> {
239            Box::pin(async { Ok(()) })
240        }
241
242        fn health(&self) -> Pin<Box<dyn Future<Output = HealthStatus> + Send + '_>> {
243            Box::pin(async move { self.health_check() })
244        }
245
246        fn setup_prompts(&self) -> Vec<SetupPrompt> {
247            Self::get_setup_prompts()
248        }
249
250        fn as_any(&self) -> &dyn Any {
251            self
252        }
253    }
254}
255
256#[cfg(feature = "pulse-null")]
257pub use plugin_impl::create;