1pub mod archive;
21pub mod checkpoint;
22pub mod config;
23pub mod config_cli;
24pub mod consume;
25pub mod conversation;
26pub mod dashboard;
27pub mod distill;
28pub mod ephemeral;
29pub mod error;
30pub mod frontmatter;
31pub mod init;
32pub mod jsonl;
33pub mod paths;
34pub mod search;
35pub mod serve;
36pub mod serve_client;
37mod serve_security;
38pub mod status;
39pub mod summarize;
40pub mod tags;
41
42pub mod graph;
43pub mod graph_bridge;
44pub mod graph_cli;
45#[cfg(feature = "llm")]
46pub mod llm_provider;
47
48#[cfg(feature = "pulse-null")]
49pub mod pulse_null;
50
51#[cfg(feature = "bench")]
52pub mod bench;
53
54use std::fs;
55use std::path::{Path, PathBuf};
56
57pub use archive::SessionMetadata;
58pub use summarize::ConversationSummary;
59
60pub struct RecallEcho {
72 entity_root: PathBuf,
73}
74
75impl RecallEcho {
76 #[must_use]
78 pub fn new(entity_root: PathBuf) -> Self {
79 Self { entity_root }
80 }
81
82 pub fn from_default() -> Result<Self, error::RecallError> {
85 Ok(Self::new(paths::entity_root()?))
86 }
87
88 #[must_use]
90 pub fn entity_root(&self) -> &Path {
91 &self.entity_root
92 }
93
94 #[must_use]
96 pub fn memory_dir(&self) -> PathBuf {
97 self.entity_root.join("memory")
98 }
99
100 #[must_use]
102 pub fn memory_file(&self) -> PathBuf {
103 self.memory_dir().join("MEMORY.md")
104 }
105
106 #[must_use]
108 pub fn ephemeral_file(&self) -> PathBuf {
109 self.memory_dir().join("EPHEMERAL.md")
110 }
111
112 #[must_use]
114 pub fn conversations_dir(&self) -> PathBuf {
115 self.memory_dir().join("conversations")
116 }
117
118 #[must_use]
120 pub fn archive_index(&self) -> PathBuf {
121 self.memory_dir().join("ARCHIVE.md")
122 }
123
124 pub fn consume_content(&self) -> Result<Option<String>, error::RecallError> {
129 consume::consume(&self.ephemeral_file())
130 }
131
132 #[must_use]
134 pub fn is_initialized(&self) -> bool {
135 self.memory_dir().exists() && self.conversations_dir().exists()
136 }
137
138 #[must_use]
140 pub fn memory_line_count(&self) -> usize {
141 let path = self.memory_file();
142 if !path.exists() {
143 return 0;
144 }
145 fs::read_to_string(&path)
146 .unwrap_or_default()
147 .lines()
148 .count()
149 }
150}
151
152#[cfg(feature = "pulse-null")]
157mod plugin_impl {
158 use super::*;
159 use std::any::Any;
160 use std::future::Future;
161 use std::pin::Pin;
162
163 use pulse_system_types::plugin::{Plugin, PluginContext, PluginResult, PluginRole};
164 use pulse_system_types::{HealthStatus, PluginMeta, SetupPrompt};
165
166 impl RecallEcho {
167 fn health_check(&self) -> HealthStatus {
168 if !self.memory_dir().exists() {
169 return HealthStatus::Down("memory directory not found".into());
170 }
171 if !self.memory_file().exists() {
172 return HealthStatus::Degraded("MEMORY.md not found".into());
173 }
174 if !self.conversations_dir().exists() {
175 return HealthStatus::Degraded("conversations directory not found".into());
176 }
177 HealthStatus::Healthy
178 }
179
180 fn get_setup_prompts() -> Vec<SetupPrompt> {
181 vec![SetupPrompt {
182 key: "entity_root".into(),
183 question: "Entity root directory:".into(),
184 required: true,
185 secret: false,
186 default: None,
187 }]
188 }
189 }
190
191 pub async fn create(
193 config: &serde_json::Value,
194 ctx: &PluginContext,
195 ) -> Result<Box<dyn Plugin>, Box<dyn std::error::Error + Send + Sync>> {
196 let entity_root = config
197 .get("entity_root")
198 .and_then(|v| v.as_str())
199 .map(PathBuf::from)
200 .unwrap_or_else(|| ctx.entity_root.clone());
201
202 Ok(Box::new(RecallEcho::new(entity_root)))
203 }
204
205 impl Plugin for RecallEcho {
206 fn meta(&self) -> PluginMeta {
207 PluginMeta {
208 name: "recall-echo".into(),
209 version: env!("CARGO_PKG_VERSION").into(),
210 description: "Persistent memory system with knowledge graph".into(),
211 }
212 }
213
214 fn role(&self) -> PluginRole {
215 PluginRole::Memory
216 }
217
218 fn start(&mut self) -> PluginResult<'_> {
219 Box::pin(async { Ok(()) })
220 }
221
222 fn stop(&mut self) -> PluginResult<'_> {
223 Box::pin(async { Ok(()) })
224 }
225
226 fn health(&self) -> Pin<Box<dyn Future<Output = HealthStatus> + Send + '_>> {
227 Box::pin(async move { self.health_check() })
228 }
229
230 fn setup_prompts(&self) -> Vec<SetupPrompt> {
231 Self::get_setup_prompts()
232 }
233
234 fn as_any(&self) -> &dyn Any {
235 self
236 }
237 }
238}
239
240#[cfg(feature = "pulse-null")]
241pub use plugin_impl::create;