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(super) fn auto_consolidate_knowledge(project_root: &str) {
94 use crate::core::knowledge::ProjectKnowledge;
95 use crate::core::session::SessionState;
96
97 let Some(session) = SessionState::load_latest() else {
98 return;
99 };
100
101 if session.findings.is_empty() && session.decisions.is_empty() {
102 return;
103 }
104
105 let Ok(policy) = crate::core::config::Config::load().memory_policy_effective() else {
106 return;
107 };
108 let mut knowledge = ProjectKnowledge::load_or_create(project_root);
109
110 for finding in &session.findings {
111 let key = if let Some(ref file) = finding.file {
112 if let Some(line) = finding.line {
113 format!("{file}:{line}")
114 } else {
115 file.clone()
116 }
117 } else {
118 "finding-auto".to_string()
119 };
120 knowledge.remember("finding", &key, &finding.summary, &session.id, 0.7, &policy);
121 }
122
123 for decision in &session.decisions {
124 let key = decision
125 .summary
126 .chars()
127 .take(50)
128 .collect::<String>()
129 .replace(' ', "-")
130 .to_lowercase();
131 knowledge.remember(
132 "decision",
133 &key,
134 &decision.summary,
135 &session.id,
136 0.85,
137 &policy,
138 );
139 }
140
141 let task_desc = session
142 .task
143 .as_ref()
144 .map(|t| t.description.clone())
145 .unwrap_or_default();
146
147 let summary = format!(
148 "Auto-consolidate session {}: {} — {} findings, {} decisions",
149 session.id,
150 task_desc,
151 session.findings.len(),
152 session.decisions.len()
153 );
154 knowledge.consolidate(&summary, vec![session.id.clone()], &policy);
155 let _ = knowledge.save();
156}