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