1pub 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 jsonl;
43pub mod mcp;
44pub mod paths;
45pub mod search;
46pub mod serve;
47pub mod serve_capture;
48pub mod serve_client;
49#[cfg(feature = "llm")]
50pub mod serve_extract;
51mod serve_security;
52pub mod status;
53pub mod summarize;
54pub mod tags;
55pub mod transcript;
56
57pub mod graph;
58pub mod graph_bridge;
59pub mod graph_cli;
60#[cfg(feature = "llm")]
61pub mod llm_provider;
62
63#[cfg(feature = "pulse-null")]
64pub mod pulse_null;
65
66#[cfg(feature = "bench")]
67pub mod bench;
68
69use std::fs;
70use std::path::{Path, PathBuf};
71
72pub use archive::SessionMetadata;
73pub use summarize::ConversationSummary;
74
75pub struct RecallEcho {
87 entity_root: PathBuf,
88}
89
90impl RecallEcho {
91 #[must_use]
93 pub fn new(entity_root: PathBuf) -> Self {
94 Self { entity_root }
95 }
96
97 pub fn from_default() -> Result<Self, error::RecallError> {
100 Ok(Self::new(paths::entity_root()?))
101 }
102
103 #[must_use]
105 pub fn entity_root(&self) -> &Path {
106 &self.entity_root
107 }
108
109 #[must_use]
111 pub fn memory_dir(&self) -> PathBuf {
112 self.entity_root.join("memory")
113 }
114
115 #[must_use]
117 pub fn memory_file(&self) -> PathBuf {
118 self.memory_dir().join("MEMORY.md")
119 }
120
121 #[must_use]
123 pub fn ephemeral_file(&self) -> PathBuf {
124 self.memory_dir().join("EPHEMERAL.md")
125 }
126
127 #[must_use]
129 pub fn conversations_dir(&self) -> PathBuf {
130 self.memory_dir().join("conversations")
131 }
132
133 #[must_use]
135 pub fn archive_index(&self) -> PathBuf {
136 self.memory_dir().join("ARCHIVE.md")
137 }
138
139 pub fn consume_content(&self) -> Result<Option<String>, error::RecallError> {
144 consume::consume(&self.ephemeral_file())
145 }
146
147 #[must_use]
149 pub fn is_initialized(&self) -> bool {
150 self.memory_dir().exists() && self.conversations_dir().exists()
151 }
152
153 #[must_use]
155 pub fn memory_line_count(&self) -> usize {
156 let path = self.memory_file();
157 if !path.exists() {
158 return 0;
159 }
160 fs::read_to_string(&path)
161 .unwrap_or_default()
162 .lines()
163 .count()
164 }
165}
166
167#[cfg(feature = "pulse-null")]
172mod plugin_impl {
173 use super::*;
174 use std::any::Any;
175 use std::future::Future;
176 use std::pin::Pin;
177
178 use pulse_system_types::plugin::{Plugin, PluginContext, PluginResult, PluginRole};
179 use pulse_system_types::{HealthStatus, PluginMeta, SetupPrompt};
180
181 impl RecallEcho {
182 fn health_check(&self) -> HealthStatus {
183 if !self.memory_dir().exists() {
184 return HealthStatus::Down("memory directory not found".into());
185 }
186 if !self.memory_file().exists() {
187 return HealthStatus::Degraded("MEMORY.md not found".into());
188 }
189 if !self.conversations_dir().exists() {
190 return HealthStatus::Degraded("conversations directory not found".into());
191 }
192 HealthStatus::Healthy
193 }
194
195 fn get_setup_prompts() -> Vec<SetupPrompt> {
196 vec![SetupPrompt {
197 key: "entity_root".into(),
198 question: "Entity root directory:".into(),
199 required: true,
200 secret: false,
201 default: None,
202 }]
203 }
204 }
205
206 pub async fn create(
208 config: &serde_json::Value,
209 ctx: &PluginContext,
210 ) -> Result<Box<dyn Plugin>, Box<dyn std::error::Error + Send + Sync>> {
211 let entity_root = config
212 .get("entity_root")
213 .and_then(|v| v.as_str())
214 .map(PathBuf::from)
215 .unwrap_or_else(|| ctx.entity_root.clone());
216
217 Ok(Box::new(RecallEcho::new(entity_root)))
218 }
219
220 impl Plugin for RecallEcho {
221 fn meta(&self) -> PluginMeta {
222 PluginMeta {
223 name: "recall-echo".into(),
224 version: env!("CARGO_PKG_VERSION").into(),
225 description: "Persistent memory system with knowledge graph".into(),
226 }
227 }
228
229 fn role(&self) -> PluginRole {
230 PluginRole::Memory
231 }
232
233 fn start(&mut self) -> PluginResult<'_> {
234 Box::pin(async { Ok(()) })
235 }
236
237 fn stop(&mut self) -> PluginResult<'_> {
238 Box::pin(async { Ok(()) })
239 }
240
241 fn health(&self) -> Pin<Box<dyn Future<Output = HealthStatus> + Send + '_>> {
242 Box::pin(async move { self.health_check() })
243 }
244
245 fn setup_prompts(&self) -> Vec<SetupPrompt> {
246 Self::get_setup_prompts()
247 }
248
249 fn as_any(&self) -> &dyn Any {
250 self
251 }
252 }
253}
254
255#[cfg(feature = "pulse-null")]
256pub use plugin_impl::create;