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