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) 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(crate) fn auto_consolidate_knowledge(project_root: &str) {
94 use crate::core::knowledge::ProjectKnowledge;
95 use crate::core::session::SessionState;
96 use chrono::Utc;
97
98 let Some(mut session) = SessionState::load_latest() else {
99 return;
100 };
101
102 let watermark = session.last_consolidate_ts;
103
104 let new_findings: Vec<_> = session
105 .findings
106 .iter()
107 .filter(|f| match watermark {
108 Some(ts) => f.timestamp > ts,
109 None => true,
110 })
111 .collect();
112
113 let new_decisions: Vec<_> = session
114 .decisions
115 .iter()
116 .filter(|d| match watermark {
117 Some(ts) => d.timestamp > ts,
118 None => true,
119 })
120 .collect();
121
122 if new_findings.is_empty() && new_decisions.is_empty() {
123 return;
124 }
125
126 let Ok(policy) = crate::core::config::Config::load().memory_policy_effective() else {
127 return;
128 };
129 let mut knowledge = ProjectKnowledge::load_or_create(project_root);
130
131 for finding in &new_findings {
132 let key = if let Some(ref file) = finding.file {
133 if let Some(line) = finding.line {
134 format!("{file}:{line}")
135 } else {
136 file.clone()
137 }
138 } else {
139 let slug: String = finding
140 .summary
141 .chars()
142 .take(60)
143 .collect::<String>()
144 .replace(' ', "-")
145 .to_lowercase();
146 format!("finding-{slug}")
147 };
148 knowledge.remember("finding", &key, &finding.summary, &session.id, 0.7, &policy);
149 }
150
151 for decision in &new_decisions {
152 let key = decision
153 .summary
154 .chars()
155 .take(50)
156 .collect::<String>()
157 .replace(' ', "-")
158 .to_lowercase();
159 knowledge.remember(
160 "decision",
161 &key,
162 &decision.summary,
163 &session.id,
164 0.85,
165 &policy,
166 );
167 }
168
169 let task_desc = session
170 .task
171 .as_ref()
172 .map(|t| t.description.clone())
173 .unwrap_or_default();
174
175 let summary = format!(
176 "Auto-consolidate session {}: {} — {} findings, {} decisions",
177 session.id,
178 task_desc,
179 new_findings.len(),
180 new_decisions.len()
181 );
182 knowledge.consolidate(&summary, vec![session.id.clone()], &policy);
183 let _ = knowledge.save();
184
185 session.last_consolidate_ts = Some(Utc::now());
186 let _ = session.save();
187}