Skip to main content

lean_ctx/core/
journal.rs

1use std::io::Write;
2use std::path::PathBuf;
3
4use chrono::{Local, Utc};
5
6fn journal_path() -> PathBuf {
7    crate::core::paths::state_dir()
8        .unwrap_or_else(|_| PathBuf::from(".lean-ctx"))
9        .join("journal.md")
10}
11
12fn is_enabled() -> bool {
13    if let Ok(v) = std::env::var("LEAN_CTX_JOURNAL") {
14        return !matches!(v.trim(), "0" | "false" | "off");
15    }
16    super::config::Config::load().journal_enabled
17}
18
19/// Append a human-readable entry to the activity journal.
20pub fn log(category: &str, message: &str) {
21    if !is_enabled() {
22        return;
23    }
24    let path = journal_path();
25    let timestamp = Local::now().format("%Y-%m-%d %H:%M");
26
27    let entry = format!("- **{timestamp}** [{category}] {message}\n");
28
29    let needs_header = !path.exists();
30    let file = std::fs::OpenOptions::new()
31        .create(true)
32        .append(true)
33        .open(&path);
34
35    if let Ok(mut f) = file {
36        if needs_header {
37            let date = Utc::now().format("%Y-%m-%d");
38            let _ = writeln!(f, "# lean-ctx Activity Journal\n\n## {date}\n");
39        }
40        let _ = f.write_all(entry.as_bytes());
41    }
42}
43
44/// Insert a day separator if the last entry was on a different date.
45pub fn maybe_day_separator() {
46    if !is_enabled() {
47        return;
48    }
49    let path = journal_path();
50    if !path.exists() {
51        return;
52    }
53
54    let today = Local::now().format("%Y-%m-%d").to_string();
55    let content = std::fs::read_to_string(&path).unwrap_or_default();
56    let header = format!("## {today}");
57    if !content.contains(&header) {
58        let file = std::fs::OpenOptions::new().append(true).open(&path);
59        if let Ok(mut f) = file {
60            let _ = writeln!(f, "\n{header}\n");
61        }
62    }
63}
64
65/// Log a tool call to the journal.
66pub fn log_tool_call(tool_name: &str, summary: &str) {
67    if matches!(
68        tool_name,
69        "ctx_session" | "ctx_knowledge" | "ctx_context" | "ctx_radar"
70    ) {
71        return;
72    }
73    log("tool", &format!("`{tool_name}` — {summary}"));
74}
75
76/// Return the journal content for display.
77pub fn read_journal(tail_lines: usize) -> String {
78    let path = journal_path();
79    if !path.exists() {
80        return "No journal entries yet.".to_string();
81    }
82    let content = std::fs::read_to_string(&path).unwrap_or_default();
83    if tail_lines == 0 {
84        return content;
85    }
86    let lines: Vec<&str> = content.lines().collect();
87    let start = lines.len().saturating_sub(tail_lines);
88    lines[start..].join("\n")
89}
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94
95    #[test]
96    fn journal_log_creates_file() {
97        // `journal.md` is STATE (GH #408); isolated_data_dir collapses all four
98        // category dirs onto one temp dir so the write/read pair stays valid.
99        let iso = crate::core::data_dir::isolated_data_dir();
100        std::env::set_var("LEAN_CTX_JOURNAL", "1");
101
102        log("test", "hello world");
103
104        let path = iso.path().join("journal.md");
105        assert!(path.exists(), "journal.md should be created");
106        let content = std::fs::read_to_string(&path).unwrap();
107        assert!(content.contains("[test] hello world"));
108        assert!(content.contains("# lean-ctx Activity Journal"));
109
110        std::env::remove_var("LEAN_CTX_JOURNAL");
111    }
112
113    #[test]
114    fn read_journal_tail() {
115        let _iso = crate::core::data_dir::isolated_data_dir();
116        std::env::set_var("LEAN_CTX_JOURNAL", "1");
117
118        for i in 0..5 {
119            log("test", &format!("entry {i}"));
120        }
121
122        let tail = read_journal(2);
123        assert!(tail.contains("entry 4"), "should contain last entry");
124        assert!(
125            !tail.contains("Activity Journal"),
126            "should not contain header"
127        );
128
129        std::env::remove_var("LEAN_CTX_JOURNAL");
130    }
131}