lean_ctx/tools/
startup.rs1use 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
9pub fn create_server() -> LeanCtxServer {
11 LeanCtxServer::new()
12}
13
14pub(super) fn has_project_marker(dir: &std::path::Path) -> bool {
15 crate::core::pathutil::has_project_marker(dir)
16}
17
18pub(super) fn is_suspicious_root(dir: &std::path::Path) -> bool {
19 let s = dir.to_string_lossy();
20 s.contains("/.claude")
21 || s.contains("/.codebuddy")
22 || s.contains("/.codex")
23 || s.contains("/.lmstudio")
24 || s.contains("\\.claude")
25 || s.contains("\\.codebuddy")
26 || s.contains("\\.codex")
27 || s.contains("\\.lmstudio")
28}
29
30pub(super) fn canonicalize_path(path: &std::path::Path) -> String {
31 crate::core::pathutil::safe_canonicalize_or_self(path)
32 .to_string_lossy()
33 .to_string()
34}
35
36pub(super) fn detect_startup_context(
37 explicit_project_root: Option<&str>,
38 startup_cwd: Option<&std::path::Path>,
39) -> StartupContext {
40 let shell_cwd = startup_cwd.map(canonicalize_path);
41 let project_root = explicit_project_root
42 .map(|root| canonicalize_path(std::path::Path::new(root)))
43 .or_else(|| {
44 startup_cwd
45 .and_then(maybe_derive_project_root_from_absolute)
46 .map(|p| canonicalize_path(&p))
47 });
48
49 let shell_cwd = match (shell_cwd, project_root.as_ref()) {
50 (Some(cwd), Some(root))
51 if std::path::Path::new(&cwd).starts_with(std::path::Path::new(root)) =>
52 {
53 Some(cwd)
54 }
55 (_, Some(root)) => Some(root.clone()),
56 (cwd, None) => cwd,
57 };
58
59 StartupContext {
60 project_root,
61 shell_cwd,
62 }
63}
64
65pub(super) fn maybe_derive_project_root_from_absolute(
66 abs: &std::path::Path,
67) -> Option<std::path::PathBuf> {
68 let mut cur = if abs.is_dir() {
69 abs.to_path_buf()
70 } else {
71 abs.parent()?.to_path_buf()
72 };
73 loop {
74 if has_project_marker(&cur) {
75 return Some(crate::core::pathutil::safe_canonicalize_or_self(&cur));
76 }
77 if !cur.pop() {
78 break;
79 }
80 }
81 None
82}
83
84pub(crate) fn auto_consolidate_knowledge(project_root: &str) {
85 use crate::core::knowledge::ProjectKnowledge;
86 use crate::core::session::SessionState;
87 use chrono::Utc;
88
89 let Some(mut session) = SessionState::load_latest() else {
90 return;
91 };
92
93 let watermark = session.last_consolidate_ts;
94
95 let new_findings: Vec<_> = session
96 .findings
97 .iter()
98 .filter(|f| match watermark {
99 Some(ts) => f.timestamp > ts,
100 None => true,
101 })
102 .collect();
103
104 let new_decisions: Vec<_> = session
105 .decisions
106 .iter()
107 .filter(|d| match watermark {
108 Some(ts) => d.timestamp > ts,
109 None => true,
110 })
111 .collect();
112
113 if new_findings.is_empty() && new_decisions.is_empty() {
114 return;
115 }
116
117 let Ok(policy) = crate::core::config::Config::load().memory_policy_effective() else {
118 return;
119 };
120 let _ = ProjectKnowledge::mutate_locked(project_root, |knowledge| {
124 for finding in &new_findings {
125 let key = if let Some(ref file) = finding.file {
126 if let Some(line) = finding.line {
127 format!("{file}:{line}")
128 } else {
129 file.clone()
130 }
131 } else {
132 let slug: String = finding
133 .summary
134 .chars()
135 .take(60)
136 .collect::<String>()
137 .replace(' ', "-")
138 .to_lowercase();
139 format!("finding-{slug}")
140 };
141 knowledge.remember("finding", &key, &finding.summary, &session.id, 0.7, &policy);
142 }
143
144 for decision in &new_decisions {
145 let key = decision
146 .summary
147 .chars()
148 .take(50)
149 .collect::<String>()
150 .replace(' ', "-")
151 .to_lowercase();
152 knowledge.remember(
153 "decision",
154 &key,
155 &decision.summary,
156 &session.id,
157 0.85,
158 &policy,
159 );
160 }
161
162 let task_desc = session
163 .task
164 .as_ref()
165 .map(|t| t.description.clone())
166 .unwrap_or_default();
167
168 let summary = format!(
169 "Auto-consolidate session {}: {} — {} findings, {} decisions",
170 session.id,
171 task_desc,
172 new_findings.len(),
173 new_decisions.len()
174 );
175 knowledge.consolidate(&summary, vec![session.id.clone()], &policy);
176 });
177
178 session.last_consolidate_ts = Some(Utc::now());
179 let _ = session.save();
180}