Skip to main content

recall_echo/
paths.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5//! Path resolution utilities for recall-echo.
6//!
7//! Supports two modes:
8//! 1. **Entity mode** (pulse-null) — entity_root/memory/ layout
9//! 2. **Claude mode** (standalone) — ~/.claude/ layout for Claude Code hooks
10
11use std::path::PathBuf;
12
13use crate::error::RecallError;
14
15/// Returns the default entity root directory.
16///
17/// Resolution order:
18/// 1. RECALL_ECHO_HOME env var (explicit override)
19/// 2. Current working directory (for pulse-null entities)
20pub fn entity_root() -> Result<PathBuf, RecallError> {
21    if let Ok(p) = std::env::var("RECALL_ECHO_HOME") {
22        return Ok(PathBuf::from(p));
23    }
24    std::env::current_dir().map_err(RecallError::from)
25}
26
27/// Returns the memory directory: {entity_root}/memory/
28pub fn memory_dir() -> Result<PathBuf, RecallError> {
29    Ok(entity_root()?.join("memory"))
30}
31
32pub fn memory_file() -> Result<PathBuf, RecallError> {
33    Ok(memory_dir()?.join("MEMORY.md"))
34}
35
36pub fn ephemeral_file() -> Result<PathBuf, RecallError> {
37    Ok(memory_dir()?.join("EPHEMERAL.md"))
38}
39
40pub fn archive_index() -> Result<PathBuf, RecallError> {
41    Ok(memory_dir()?.join("ARCHIVE.md"))
42}
43
44pub fn conversations_dir() -> Result<PathBuf, RecallError> {
45    Ok(memory_dir()?.join("conversations"))
46}
47
48pub fn config_file() -> Result<PathBuf, RecallError> {
49    Ok(memory_dir()?.join(".recall-echo.toml"))
50}
51
52/// Returns the Claude Code base directory (~/.claude/).
53///
54/// Used when recall-echo is invoked as a Claude Code hook (archive-session,
55/// checkpoint). The memory layout inside ~/.claude/ mirrors the entity layout:
56/// ~/.claude/conversations/, ~/.claude/ARCHIVE.md, ~/.claude/EPHEMERAL.md, etc.
57pub fn claude_dir() -> Result<PathBuf, RecallError> {
58    let home = dirs::home_dir()
59        .ok_or_else(|| RecallError::Other("Could not determine home directory".into()))?;
60    Ok(home.join(".claude"))
61}
62
63/// Expand a leading `~/` to the home directory. Other paths pass through.
64#[must_use]
65pub fn expand_tilde(path: &str) -> String {
66    if let Some(rest) = path.strip_prefix("~/") {
67        if let Some(home) = dirs::home_dir() {
68            return home.join(rest).to_string_lossy().to_string();
69        }
70    }
71    path.to_string()
72}
73
74/// Detect Claude Code installation.
75/// Returns Some(~/.claude/) if it exists, None otherwise.
76#[must_use]
77pub fn detect_claude_code() -> Option<PathBuf> {
78    // Overridable so tests (and sandboxed runs) never touch the real
79    // ~/.claude — hook installation writes settings.json unconditionally,
80    // and a test that installs hooks would otherwise repoint the user's
81    // live hooks at the test binary.
82    if let Some(dir) = std::env::var_os(CLAUDE_DIR_ENV) {
83        let claude = PathBuf::from(dir);
84        return claude.exists().then_some(claude);
85    }
86    let home = dirs::home_dir()?;
87    let claude = home.join(".claude");
88    if claude.exists() {
89        Some(claude)
90    } else {
91        None
92    }
93}
94
95/// Overrides the Claude Code configuration directory (`~/.claude`).
96pub const CLAUDE_DIR_ENV: &str = "RECALL_ECHO_CLAUDE_DIR";