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