Skip to main content

lean_ctx/tools/
startup.rs

1use super::server::LeanCtxServer;
2
3#[derive(Clone, Debug, Default)]
4pub(super) struct StartupContext {
5    pub(super) project_root: Option<String>,
6    pub(super) shell_cwd: Option<String>,
7}
8
9/// Creates a new `LeanCtxServer` with default configuration.
10pub fn create_server() -> LeanCtxServer {
11    LeanCtxServer::new()
12}
13
14pub(super) const PROJECT_ROOT_MARKERS: &[&str] = &[
15    ".git",
16    ".lean-ctx.toml",
17    "Cargo.toml",
18    "package.json",
19    "go.mod",
20    "pyproject.toml",
21    "pom.xml",
22    "build.gradle",
23    "Makefile",
24    ".planning",
25];
26
27pub(super) fn has_project_marker(dir: &std::path::Path) -> bool {
28    PROJECT_ROOT_MARKERS.iter().any(|m| dir.join(m).exists())
29}
30
31pub(super) fn is_suspicious_root(dir: &std::path::Path) -> bool {
32    let s = dir.to_string_lossy();
33    s.contains("/.claude")
34        || s.contains("/.codex")
35        || s.contains("\\.claude")
36        || s.contains("\\.codex")
37}
38
39pub(super) fn canonicalize_path(path: &std::path::Path) -> String {
40    crate::core::pathutil::safe_canonicalize_or_self(path)
41        .to_string_lossy()
42        .to_string()
43}
44
45pub(super) fn detect_startup_context(
46    explicit_project_root: Option<&str>,
47    startup_cwd: Option<&std::path::Path>,
48) -> StartupContext {
49    let shell_cwd = startup_cwd.map(canonicalize_path);
50    let project_root = explicit_project_root
51        .map(|root| canonicalize_path(std::path::Path::new(root)))
52        .or_else(|| {
53            startup_cwd
54                .and_then(maybe_derive_project_root_from_absolute)
55                .map(|p| canonicalize_path(&p))
56        });
57
58    let shell_cwd = match (shell_cwd, project_root.as_ref()) {
59        (Some(cwd), Some(root))
60            if std::path::Path::new(&cwd).starts_with(std::path::Path::new(root)) =>
61        {
62            Some(cwd)
63        }
64        (_, Some(root)) => Some(root.clone()),
65        (cwd, None) => cwd,
66    };
67
68    StartupContext {
69        project_root,
70        shell_cwd,
71    }
72}
73
74pub(super) fn maybe_derive_project_root_from_absolute(
75    abs: &std::path::Path,
76) -> Option<std::path::PathBuf> {
77    let mut cur = if abs.is_dir() {
78        abs.to_path_buf()
79    } else {
80        abs.parent()?.to_path_buf()
81    };
82    loop {
83        if has_project_marker(&cur) {
84            return Some(crate::core::pathutil::safe_canonicalize_or_self(&cur));
85        }
86        if !cur.pop() {
87            break;
88        }
89    }
90    None
91}
92
93pub(super) fn auto_consolidate_knowledge(project_root: &str) {
94    use crate::core::knowledge::ProjectKnowledge;
95    use crate::core::session::SessionState;
96
97    let Some(session) = SessionState::load_latest() else {
98        return;
99    };
100
101    if session.findings.is_empty() && session.decisions.is_empty() {
102        return;
103    }
104
105    let Ok(policy) = crate::core::config::Config::load().memory_policy_effective() else {
106        return;
107    };
108    let mut knowledge = ProjectKnowledge::load_or_create(project_root);
109
110    for finding in &session.findings {
111        let key = if let Some(ref file) = finding.file {
112            if let Some(line) = finding.line {
113                format!("{file}:{line}")
114            } else {
115                file.clone()
116            }
117        } else {
118            "finding-auto".to_string()
119        };
120        knowledge.remember("finding", &key, &finding.summary, &session.id, 0.7, &policy);
121    }
122
123    for decision in &session.decisions {
124        let key = decision
125            .summary
126            .chars()
127            .take(50)
128            .collect::<String>()
129            .replace(' ', "-")
130            .to_lowercase();
131        knowledge.remember(
132            "decision",
133            &key,
134            &decision.summary,
135            &session.id,
136            0.85,
137            &policy,
138        );
139    }
140
141    let task_desc = session
142        .task
143        .as_ref()
144        .map(|t| t.description.clone())
145        .unwrap_or_default();
146
147    let summary = format!(
148        "Auto-consolidate session {}: {} — {} findings, {} decisions",
149        session.id,
150        task_desc,
151        session.findings.len(),
152        session.decisions.len()
153    );
154    knowledge.consolidate(&summary, vec![session.id.clone()], &policy);
155    let _ = knowledge.save();
156}