pub mod archive;
pub mod checkpoint;
pub mod config;
pub mod config_cli;
pub mod consume;
pub mod conversation;
pub mod dashboard;
pub mod distill;
pub mod ephemeral;
pub mod error;
pub mod frontmatter;
pub mod init;
pub mod jsonl;
pub mod paths;
pub mod search;
pub mod status;
pub mod summarize;
pub mod tags;
pub mod graph;
pub mod graph_bridge;
pub mod graph_cli;
#[cfg(feature = "llm")]
pub mod llm_provider;
#[cfg(feature = "pulse-null")]
pub mod pulse_null;
use std::fs;
use std::path::{Path, PathBuf};
pub use archive::SessionMetadata;
pub use summarize::ConversationSummary;
pub struct RecallEcho {
entity_root: PathBuf,
}
impl RecallEcho {
#[must_use]
pub fn new(entity_root: PathBuf) -> Self {
Self { entity_root }
}
pub fn from_default() -> Result<Self, error::RecallError> {
Ok(Self::new(paths::entity_root()?))
}
#[must_use]
pub fn entity_root(&self) -> &Path {
&self.entity_root
}
#[must_use]
pub fn memory_dir(&self) -> PathBuf {
self.entity_root.join("memory")
}
#[must_use]
pub fn memory_file(&self) -> PathBuf {
self.memory_dir().join("MEMORY.md")
}
#[must_use]
pub fn ephemeral_file(&self) -> PathBuf {
self.memory_dir().join("EPHEMERAL.md")
}
#[must_use]
pub fn conversations_dir(&self) -> PathBuf {
self.memory_dir().join("conversations")
}
#[must_use]
pub fn archive_index(&self) -> PathBuf {
self.memory_dir().join("ARCHIVE.md")
}
pub fn consume_content(&self) -> Result<Option<String>, error::RecallError> {
consume::consume(&self.ephemeral_file())
}
#[must_use]
pub fn is_initialized(&self) -> bool {
self.memory_dir().exists() && self.conversations_dir().exists()
}
#[must_use]
pub fn memory_line_count(&self) -> usize {
let path = self.memory_file();
if !path.exists() {
return 0;
}
fs::read_to_string(&path)
.unwrap_or_default()
.lines()
.count()
}
}
#[cfg(feature = "pulse-null")]
mod plugin_impl {
use super::*;
use std::any::Any;
use std::future::Future;
use std::pin::Pin;
use pulse_system_types::plugin::{Plugin, PluginContext, PluginResult, PluginRole};
use pulse_system_types::{HealthStatus, PluginMeta, SetupPrompt};
impl RecallEcho {
fn health_check(&self) -> HealthStatus {
if !self.memory_dir().exists() {
return HealthStatus::Down("memory directory not found".into());
}
if !self.memory_file().exists() {
return HealthStatus::Degraded("MEMORY.md not found".into());
}
if !self.conversations_dir().exists() {
return HealthStatus::Degraded("conversations directory not found".into());
}
HealthStatus::Healthy
}
fn get_setup_prompts() -> Vec<SetupPrompt> {
vec![SetupPrompt {
key: "entity_root".into(),
question: "Entity root directory:".into(),
required: true,
secret: false,
default: None,
}]
}
}
pub async fn create(
config: &serde_json::Value,
ctx: &PluginContext,
) -> Result<Box<dyn Plugin>, Box<dyn std::error::Error + Send + Sync>> {
let entity_root = config
.get("entity_root")
.and_then(|v| v.as_str())
.map(PathBuf::from)
.unwrap_or_else(|| ctx.entity_root.clone());
Ok(Box::new(RecallEcho::new(entity_root)))
}
impl Plugin for RecallEcho {
fn meta(&self) -> PluginMeta {
PluginMeta {
name: "recall-echo".into(),
version: env!("CARGO_PKG_VERSION").into(),
description: "Persistent memory system with knowledge graph".into(),
}
}
fn role(&self) -> PluginRole {
PluginRole::Memory
}
fn start(&mut self) -> PluginResult<'_> {
Box::pin(async { Ok(()) })
}
fn stop(&mut self) -> PluginResult<'_> {
Box::pin(async { Ok(()) })
}
fn health(&self) -> Pin<Box<dyn Future<Output = HealthStatus> + Send + '_>> {
Box::pin(async move { self.health_check() })
}
fn setup_prompts(&self) -> Vec<SetupPrompt> {
Self::get_setup_prompts()
}
fn as_any(&self) -> &dyn Any {
self
}
}
}
#[cfg(feature = "pulse-null")]
pub use plugin_impl::create;