Skip to main content

innate_core/
paths.rs

1//! Single source of truth for the `~/.innate` directory layout.
2//!
3//! ```text
4//! ~/.innate/
5//!   settings.json            ← user config (root)
6//!   settings.schema.jsonc    ← schema for settings.json (root)
7//!   data/                    ← databases + runtime state
8//!     personal.db (+ -shm, -wal)
9//!     daemon_state.sqlite
10//!     daemon.pid
11//!   logs/                    ← operational logs
12//!     daemon.log
13//!     mcp.log
14//!   sessions/                ← agent session traces (watched by the daemon)
15//!     session.log
16//! ```
17//!
18//! All callers must go through these helpers rather than re-deriving paths, so
19//! the layout stays consistent. `ensure_layout()` creates the subdirectories and
20//! relocates files from the older flat layout on first run.
21
22use std::path::PathBuf;
23
24/// Root `~/.innate` directory (falls back to `./.innate` if home is unknown).
25pub fn innate_home() -> PathBuf {
26    dirs_next::home_dir()
27        .unwrap_or_else(|| PathBuf::from("."))
28        .join(".innate")
29}
30
31/// `~/.innate/data` — databases and runtime state.
32pub fn data_dir() -> PathBuf {
33    innate_home().join("data")
34}
35
36/// `~/.innate/logs` — operational logs.
37pub fn logs_dir() -> PathBuf {
38    innate_home().join("logs")
39}
40
41/// `~/.innate/sessions` — agent session traces watched by the daemon.
42pub fn sessions_dir() -> PathBuf {
43    innate_home().join("sessions")
44}
45
46pub fn settings_path() -> PathBuf {
47    innate_home().join("settings.json")
48}
49
50pub fn default_db_path() -> PathBuf {
51    data_dir().join("personal.db")
52}
53
54pub fn daemon_pid_path() -> PathBuf {
55    data_dir().join("daemon.pid")
56}
57
58pub fn daemon_state_path() -> PathBuf {
59    data_dir().join("daemon_state.sqlite")
60}
61
62pub fn daemon_log_path() -> PathBuf {
63    logs_dir().join("daemon.log")
64}
65
66pub fn mcp_log_path() -> PathBuf {
67    logs_dir().join("mcp.log")
68}
69
70/// `~/.innate/logs/llm_trace.log` — JSONL trace of every LLM/embedding HTTP call
71/// (request/response previews, latency, retries, errors) for agent debugging.
72pub fn llm_trace_path() -> PathBuf {
73    logs_dir().join("llm_trace.log")
74}
75
76pub fn session_log_path() -> PathBuf {
77    sessions_dir().join("session.log")
78}
79
80/// Local cache of the last backup time.
81pub fn backup_state_path() -> PathBuf {
82    data_dir().join("backup_state.json")
83}
84
85/// Ephemeral working directory (e.g. backup VACUUM temp copies).
86pub fn tmp_dir() -> PathBuf {
87    data_dir().join("tmp")
88}
89
90/// Create the standard subdirectories and migrate any files left over from the
91/// older flat layout (`~/.innate/<file>`) into their new homes. Idempotent and
92/// best-effort: a single failed move never aborts startup.
93pub fn ensure_layout() {
94    ensure_layout_at(&innate_home());
95}
96
97/// Layout logic against an explicit base directory (testable without touching
98/// the real `$HOME`). Database sidecars (`-shm`/`-wal`) ride along with the main
99/// db so SQLite stays consistent.
100pub fn ensure_layout_at(home: &std::path::Path) {
101    let data = home.join("data");
102    let logs = home.join("logs");
103    for dir in [&data, &logs, &home.join("sessions")] {
104        let _ = std::fs::create_dir_all(dir);
105    }
106
107    let moves: [(PathBuf, PathBuf); 8] = [
108        (home.join("personal.db"), data.join("personal.db")),
109        (home.join("personal.db-shm"), data.join("personal.db-shm")),
110        (home.join("personal.db-wal"), data.join("personal.db-wal")),
111        (
112            home.join("daemon_state.sqlite"),
113            data.join("daemon_state.sqlite"),
114        ),
115        (home.join("daemon.pid"), data.join("daemon.pid")),
116        (
117            home.join("backup_state.json"),
118            data.join("backup_state.json"),
119        ),
120        (home.join("daemon.log"), logs.join("daemon.log")),
121        (home.join("mcp.log"), logs.join("mcp.log")),
122    ];
123    for (legacy, target) in moves {
124        if legacy.exists() && !target.exists() {
125            let _ = std::fs::rename(&legacy, &target);
126        }
127    }
128}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133
134    #[test]
135    fn ensure_layout_migrates_legacy_flat_files() {
136        let tmp = tempfile::tempdir().unwrap();
137        let home = tmp.path();
138        // Seed the old flat layout.
139        for f in [
140            "personal.db",
141            "personal.db-wal",
142            "daemon_state.sqlite",
143            "daemon.pid",
144            "daemon.log",
145            "mcp.log",
146        ] {
147            std::fs::write(home.join(f), b"x").unwrap();
148        }
149
150        ensure_layout_at(home);
151
152        // Subdirectories created.
153        assert!(home.join("data").is_dir());
154        assert!(home.join("logs").is_dir());
155        assert!(home.join("sessions").is_dir());
156        // Files relocated, originals gone.
157        assert!(home.join("data/personal.db").exists());
158        assert!(home.join("data/personal.db-wal").exists());
159        assert!(home.join("data/daemon_state.sqlite").exists());
160        assert!(home.join("data/daemon.pid").exists());
161        assert!(home.join("logs/daemon.log").exists());
162        assert!(home.join("logs/mcp.log").exists());
163        assert!(!home.join("personal.db").exists());
164        assert!(!home.join("mcp.log").exists());
165
166        // Idempotent: a second run is a no-op and does not error.
167        ensure_layout_at(home);
168        assert!(home.join("data/personal.db").exists());
169    }
170}