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