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