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