Skip to main content

lean_ctx/tools/
ctx_knowledge.rs

1use crate::core::knowledge::ProjectKnowledge;
2use crate::core::session::SessionState;
3
4#[allow(clippy::too_many_arguments)]
5pub fn handle(
6    project_root: &str,
7    action: &str,
8    category: Option<&str>,
9    key: Option<&str>,
10    value: Option<&str>,
11    query: Option<&str>,
12    session_id: &str,
13    pattern_type: Option<&str>,
14    examples: Option<Vec<String>>,
15    confidence: Option<f32>,
16) -> String {
17    match action {
18        "remember" => {
19            let cat = match category {
20                Some(c) => c,
21                None => return "Error: category is required for remember".to_string(),
22            };
23            let k = match key {
24                Some(k) => k,
25                None => return "Error: key is required for remember".to_string(),
26            };
27            let v = match value {
28                Some(v) => v,
29                None => return "Error: value is required for remember".to_string(),
30            };
31            let conf = confidence.unwrap_or(0.8);
32            let mut knowledge = ProjectKnowledge::load_or_create(project_root);
33            knowledge.remember(cat, k, v, session_id, conf);
34            match knowledge.save() {
35                Ok(()) => format!("Remembered [{cat}] {k}: {v} (confidence: {:.0}%)", conf * 100.0),
36                Err(e) => format!("Remembered but save failed: {e}"),
37            }
38        }
39
40        "recall" => {
41            let knowledge = match ProjectKnowledge::load(project_root) {
42                Some(k) => k,
43                None => return "No knowledge stored for this project yet.".to_string(),
44            };
45
46            if let Some(cat) = category {
47                let facts = knowledge.recall_by_category(cat);
48                if facts.is_empty() {
49                    return format!("No facts in category '{cat}'.");
50                }
51                return format_facts(&facts, Some(cat));
52            }
53
54            if let Some(q) = query {
55                let facts = knowledge.recall(q);
56                if facts.is_empty() {
57                    return format!("No facts matching '{q}'.");
58                }
59                return format_facts(&facts, None);
60            }
61
62            "Error: provide query or category for recall".to_string()
63        }
64
65        "pattern" => {
66            let pt = match pattern_type {
67                Some(p) => p,
68                None => return "Error: pattern_type is required".to_string(),
69            };
70            let desc = match value {
71                Some(v) => v,
72                None => return "Error: value (description) is required for pattern".to_string(),
73            };
74            let exs = examples.unwrap_or_default();
75            let mut knowledge = ProjectKnowledge::load_or_create(project_root);
76            knowledge.add_pattern(pt, desc, exs, session_id);
77            match knowledge.save() {
78                Ok(()) => format!("Pattern [{pt}] added: {desc}"),
79                Err(e) => format!("Pattern added but save failed: {e}"),
80            }
81        }
82
83        "status" => {
84            let knowledge = match ProjectKnowledge::load(project_root) {
85                Some(k) => k,
86                None => return "No knowledge stored for this project yet. Use ctx_knowledge(action=\"remember\") to start.".to_string(),
87            };
88
89            let mut out = format!(
90                "Project Knowledge: {} facts, {} patterns, {} history entries\n",
91                knowledge.facts.len(),
92                knowledge.patterns.len(),
93                knowledge.history.len()
94            );
95            out.push_str(&format!("Last updated: {}\n", knowledge.updated_at.format("%Y-%m-%d %H:%M UTC")));
96            out.push_str(&knowledge.format_summary());
97            out
98        }
99
100        "remove" => {
101            let cat = match category {
102                Some(c) => c,
103                None => return "Error: category is required for remove".to_string(),
104            };
105            let k = match key {
106                Some(k) => k,
107                None => return "Error: key is required for remove".to_string(),
108            };
109            let mut knowledge = ProjectKnowledge::load_or_create(project_root);
110            if knowledge.remove_fact(cat, k) {
111                match knowledge.save() {
112                    Ok(()) => format!("Removed [{cat}] {k}"),
113                    Err(e) => format!("Removed but save failed: {e}"),
114                }
115            } else {
116                format!("No fact found: [{cat}] {k}")
117            }
118        }
119
120        "export" => {
121            let knowledge = match ProjectKnowledge::load(project_root) {
122                Some(k) => k,
123                None => return "No knowledge to export.".to_string(),
124            };
125            match serde_json::to_string_pretty(&knowledge) {
126                Ok(json) => json,
127                Err(e) => format!("Export failed: {e}"),
128            }
129        }
130
131        "consolidate" => {
132            let session = match SessionState::load_latest() {
133                Some(s) => s,
134                None => return "No active session to consolidate.".to_string(),
135            };
136
137            let mut knowledge = ProjectKnowledge::load_or_create(project_root);
138            let mut consolidated = 0u32;
139
140            for finding in &session.findings {
141                let key_text = if let Some(ref file) = finding.file {
142                    if let Some(line) = finding.line {
143                        format!("{file}:{line}")
144                    } else {
145                        file.clone()
146                    }
147                } else {
148                    format!("finding-{consolidated}")
149                };
150
151                knowledge.remember(
152                    "finding",
153                    &key_text,
154                    &finding.summary,
155                    &session.id,
156                    0.7,
157                );
158                consolidated += 1;
159            }
160
161            for decision in &session.decisions {
162                let key_text = decision
163                    .summary
164                    .chars()
165                    .take(50)
166                    .collect::<String>()
167                    .replace(' ', "-")
168                    .to_lowercase();
169
170                knowledge.remember(
171                    "decision",
172                    &key_text,
173                    &decision.summary,
174                    &session.id,
175                    0.85,
176                );
177                consolidated += 1;
178            }
179
180            let task_desc = session
181                .task
182                .as_ref()
183                .map(|t| t.description.clone())
184                .unwrap_or_else(|| "(no task)".into());
185
186            let summary = format!(
187                "Session {}: {} — {} findings, {} decisions consolidated",
188                session.id,
189                task_desc,
190                session.findings.len(),
191                session.decisions.len()
192            );
193            knowledge.consolidate(&summary, vec![session.id.clone()]);
194
195            match knowledge.save() {
196                Ok(()) => format!(
197                    "Consolidated {consolidated} items from session {} into project knowledge.\n\
198                     Facts: {}, Patterns: {}, History: {}",
199                    session.id,
200                    knowledge.facts.len(),
201                    knowledge.patterns.len(),
202                    knowledge.history.len()
203                ),
204                Err(e) => format!("Consolidation done but save failed: {e}"),
205            }
206        }
207
208        _ => format!(
209            "Unknown action: {action}. Use: remember, recall, pattern, status, remove, export, consolidate"
210        ),
211    }
212}
213
214fn format_facts(
215    facts: &[&crate::core::knowledge::KnowledgeFact],
216    category: Option<&str>,
217) -> String {
218    let mut out = String::new();
219    if let Some(cat) = category {
220        out.push_str(&format!("Facts [{cat}] ({}):\n", facts.len()));
221    } else {
222        out.push_str(&format!("Matching facts ({}):\n", facts.len()));
223    }
224    for f in facts {
225        out.push_str(&format!(
226            "  [{}/{}]: {} (confidence: {:.0}%, confirmed: {})\n",
227            f.category,
228            f.key,
229            f.value,
230            f.confidence * 100.0,
231            f.last_confirmed.format("%Y-%m-%d")
232        ));
233    }
234    out
235}