Skip to main content

recall_echo/
paths.rs

1//! Path resolution utilities for recall-echo.
2//!
3//! Supports two modes:
4//! 1. **Entity mode** (pulse-null) — entity_root/memory/ layout
5//! 2. **Claude mode** (standalone) — ~/.claude/ layout for Claude Code hooks
6
7use std::path::PathBuf;
8
9use crate::error::RecallError;
10
11/// Returns the default entity root directory.
12///
13/// Resolution order:
14/// 1. RECALL_ECHO_HOME env var (explicit override)
15/// 2. Current working directory (for pulse-null entities)
16pub fn entity_root() -> Result<PathBuf, RecallError> {
17    if let Ok(p) = std::env::var("RECALL_ECHO_HOME") {
18        return Ok(PathBuf::from(p));
19    }
20    std::env::current_dir().map_err(RecallError::from)
21}
22
23/// Returns the memory directory: {entity_root}/memory/
24pub fn memory_dir() -> Result<PathBuf, RecallError> {
25    Ok(entity_root()?.join("memory"))
26}
27
28pub fn memory_file() -> Result<PathBuf, RecallError> {
29    Ok(memory_dir()?.join("MEMORY.md"))
30}
31
32pub fn ephemeral_file() -> Result<PathBuf, RecallError> {
33    Ok(memory_dir()?.join("EPHEMERAL.md"))
34}
35
36pub fn archive_index() -> Result<PathBuf, RecallError> {
37    Ok(memory_dir()?.join("ARCHIVE.md"))
38}
39
40pub fn conversations_dir() -> Result<PathBuf, RecallError> {
41    Ok(memory_dir()?.join("conversations"))
42}
43
44pub fn config_file() -> Result<PathBuf, RecallError> {
45    Ok(memory_dir()?.join(".recall-echo.toml"))
46}
47
48/// Returns the Claude Code base directory (~/.claude/).
49///
50/// Used when recall-echo is invoked as a Claude Code hook (archive-session,
51/// checkpoint). The memory layout inside ~/.claude/ mirrors the entity layout:
52/// ~/.claude/conversations/, ~/.claude/ARCHIVE.md, ~/.claude/EPHEMERAL.md, etc.
53pub fn claude_dir() -> Result<PathBuf, RecallError> {
54    let home = dirs::home_dir()
55        .ok_or_else(|| RecallError::Other("Could not determine home directory".into()))?;
56    Ok(home.join(".claude"))
57}
58
59/// Expand a leading `~/` to the home directory. Other paths pass through.
60#[must_use]
61pub fn expand_tilde(path: &str) -> String {
62    if let Some(rest) = path.strip_prefix("~/") {
63        if let Some(home) = dirs::home_dir() {
64            return home.join(rest).to_string_lossy().to_string();
65        }
66    }
67    path.to_string()
68}
69
70/// Detect Claude Code installation.
71/// Returns Some(~/.claude/) if it exists, None otherwise.
72#[must_use]
73pub fn detect_claude_code() -> Option<PathBuf> {
74    let home = dirs::home_dir()?;
75    let claude = home.join(".claude");
76    if claude.exists() {
77        Some(claude)
78    } else {
79        None
80    }
81}