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 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
76pub struct RecallEcho {
88 entity_root: PathBuf,
89}
90
91impl RecallEcho {
92 #[must_use]
94 pub fn new(entity_root: PathBuf) -> Self {
95 Self { entity_root }
96 }
97
98 pub fn from_default() -> Result<Self, error::RecallError> {
101 Ok(Self::new(paths::entity_root()?))
102 }
103
104 #[must_use]
106 pub fn entity_root(&self) -> &Path {
107 &self.entity_root
108 }
109
110 #[must_use]
112 pub fn memory_dir(&self) -> PathBuf {
113 self.entity_root.join("memory")
114 }
115
116 #[must_use]
118 pub fn memory_file(&self) -> PathBuf {
119 self.memory_dir().join("MEMORY.md")
120 }
121
122 #[must_use]
124 pub fn ephemeral_file(&self) -> PathBuf {
125 self.memory_dir().join("EPHEMERAL.md")
126 }
127
128 #[must_use]
130 pub fn conversations_dir(&self) -> PathBuf {
131 self.memory_dir().join("conversations")
132 }
133
134 #[must_use]
136 pub fn archive_index(&self) -> PathBuf {
137 self.memory_dir().join("ARCHIVE.md")
138 }
139
140 pub fn consume_content(&self) -> Result<Option<String>, error::RecallError> {
145 consume::consume(&self.ephemeral_file())
146 }
147
148 #[must_use]
150 pub fn is_initialized(&self) -> bool {
151 self.memory_dir().exists() && self.conversations_dir().exists()
152 }
153
154 #[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#[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 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;