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