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" => handle_remember(project_root, category, key, value, session_id, confidence),
19 "recall" => handle_recall(project_root, category, query),
20 "pattern" => handle_pattern(project_root, pattern_type, value, examples, session_id),
21 "status" => handle_status(project_root),
22 "remove" => handle_remove(project_root, category, key),
23 "export" => handle_export(project_root),
24 "consolidate" => handle_consolidate(project_root),
25 "timeline" => handle_timeline(project_root, category),
26 "rooms" => handle_rooms(project_root),
27 "search" => handle_search(query),
28 "wakeup" => handle_wakeup(project_root),
29 _ => format!(
30 "Unknown action: {action}. Use: remember, recall, pattern, status, remove, export, consolidate, timeline, rooms, search, wakeup"
31 ),
32 }
33}
34
35fn handle_remember(
36 project_root: &str,
37 category: Option<&str>,
38 key: Option<&str>,
39 value: Option<&str>,
40 session_id: &str,
41 confidence: Option<f32>,
42) -> String {
43 let cat = match category {
44 Some(c) => c,
45 None => return "Error: category is required for remember".to_string(),
46 };
47 let k = match key {
48 Some(k) => k,
49 None => return "Error: key is required for remember".to_string(),
50 };
51 let v = match value {
52 Some(v) => v,
53 None => return "Error: value is required for remember".to_string(),
54 };
55 let conf = confidence.unwrap_or(0.8);
56 let mut knowledge = ProjectKnowledge::load_or_create(project_root);
57 let contradiction = knowledge.remember(cat, k, v, session_id, conf);
58
59 let mut result = format!(
60 "Remembered [{cat}] {k}: {v} (confidence: {:.0}%)",
61 conf * 100.0
62 );
63
64 if let Some(c) = contradiction {
65 result.push_str(&format!("\nā CONTRADICTION DETECTED: {}", c.resolution));
66 }
67
68 match knowledge.save() {
69 Ok(()) => result,
70 Err(e) => format!("{result}\n(save failed: {e})"),
71 }
72}
73
74fn handle_recall(project_root: &str, category: Option<&str>, query: Option<&str>) -> String {
75 let knowledge = match ProjectKnowledge::load(project_root) {
76 Some(k) => k,
77 None => return "No knowledge stored for this project yet.".to_string(),
78 };
79
80 if let Some(cat) = category {
81 let facts = knowledge.recall_by_category(cat);
82 if facts.is_empty() {
83 return format!("No facts in category '{cat}'.");
84 }
85 return format_facts(&facts, Some(cat));
86 }
87
88 if let Some(q) = query {
89 let facts = knowledge.recall(q);
90 if facts.is_empty() {
91 return format!("No facts matching '{q}'.");
92 }
93 return format_facts(&facts, None);
94 }
95
96 "Error: provide query or category for recall".to_string()
97}
98
99fn handle_pattern(
100 project_root: &str,
101 pattern_type: Option<&str>,
102 value: Option<&str>,
103 examples: Option<Vec<String>>,
104 session_id: &str,
105) -> String {
106 let pt = match pattern_type {
107 Some(p) => p,
108 None => return "Error: pattern_type is required".to_string(),
109 };
110 let desc = match value {
111 Some(v) => v,
112 None => return "Error: value (description) is required for pattern".to_string(),
113 };
114 let exs = examples.unwrap_or_default();
115 let mut knowledge = ProjectKnowledge::load_or_create(project_root);
116 knowledge.add_pattern(pt, desc, exs, session_id);
117 match knowledge.save() {
118 Ok(()) => format!("Pattern [{pt}] added: {desc}"),
119 Err(e) => format!("Pattern added but save failed: {e}"),
120 }
121}
122
123fn handle_status(project_root: &str) -> String {
124 let knowledge = match ProjectKnowledge::load(project_root) {
125 Some(k) => k,
126 None => {
127 return "No knowledge stored for this project yet. Use ctx_knowledge(action=\"remember\") to start.".to_string();
128 }
129 };
130
131 let current_facts = knowledge.facts.iter().filter(|f| f.is_current()).count();
132 let archived_facts = knowledge.facts.len() - current_facts;
133
134 let mut out = format!(
135 "Project Knowledge: {} active facts ({} archived), {} patterns, {} history entries\n",
136 current_facts,
137 archived_facts,
138 knowledge.patterns.len(),
139 knowledge.history.len()
140 );
141 out.push_str(&format!(
142 "Last updated: {}\n",
143 knowledge.updated_at.format("%Y-%m-%d %H:%M UTC")
144 ));
145
146 let rooms = knowledge.list_rooms();
147 if !rooms.is_empty() {
148 out.push_str("Rooms: ");
149 let room_strs: Vec<String> = rooms.iter().map(|(c, n)| format!("{c}({n})")).collect();
150 out.push_str(&room_strs.join(", "));
151 out.push('\n');
152 }
153
154 out.push_str(&knowledge.format_summary());
155 out
156}
157
158fn handle_remove(project_root: &str, category: Option<&str>, key: Option<&str>) -> String {
159 let cat = match category {
160 Some(c) => c,
161 None => return "Error: category is required for remove".to_string(),
162 };
163 let k = match key {
164 Some(k) => k,
165 None => return "Error: key is required for remove".to_string(),
166 };
167 let mut knowledge = ProjectKnowledge::load_or_create(project_root);
168 if knowledge.remove_fact(cat, k) {
169 match knowledge.save() {
170 Ok(()) => format!("Removed [{cat}] {k}"),
171 Err(e) => format!("Removed but save failed: {e}"),
172 }
173 } else {
174 format!("No fact found: [{cat}] {k}")
175 }
176}
177
178fn handle_export(project_root: &str) -> String {
179 let knowledge = match ProjectKnowledge::load(project_root) {
180 Some(k) => k,
181 None => return "No knowledge to export.".to_string(),
182 };
183 match serde_json::to_string_pretty(&knowledge) {
184 Ok(json) => json,
185 Err(e) => format!("Export failed: {e}"),
186 }
187}
188
189fn handle_consolidate(project_root: &str) -> String {
190 let session = match SessionState::load_latest() {
191 Some(s) => s,
192 None => return "No active session to consolidate.".to_string(),
193 };
194
195 let mut knowledge = ProjectKnowledge::load_or_create(project_root);
196 let mut consolidated = 0u32;
197
198 for finding in &session.findings {
199 let key_text = if let Some(ref file) = finding.file {
200 if let Some(line) = finding.line {
201 format!("{file}:{line}")
202 } else {
203 file.clone()
204 }
205 } else {
206 format!("finding-{consolidated}")
207 };
208
209 knowledge.remember("finding", &key_text, &finding.summary, &session.id, 0.7);
210 consolidated += 1;
211 }
212
213 for decision in &session.decisions {
214 let key_text = decision
215 .summary
216 .chars()
217 .take(50)
218 .collect::<String>()
219 .replace(' ', "-")
220 .to_lowercase();
221
222 knowledge.remember("decision", &key_text, &decision.summary, &session.id, 0.85);
223 consolidated += 1;
224 }
225
226 let task_desc = session
227 .task
228 .as_ref()
229 .map(|t| t.description.clone())
230 .unwrap_or_else(|| "(no task)".into());
231
232 let summary = format!(
233 "Session {}: {} ā {} findings, {} decisions consolidated",
234 session.id,
235 task_desc,
236 session.findings.len(),
237 session.decisions.len()
238 );
239 knowledge.consolidate(&summary, vec![session.id.clone()]);
240
241 match knowledge.save() {
242 Ok(()) => format!(
243 "Consolidated {consolidated} items from session {} into project knowledge.\n\
244 Facts: {}, Patterns: {}, History: {}",
245 session.id,
246 knowledge.facts.len(),
247 knowledge.patterns.len(),
248 knowledge.history.len()
249 ),
250 Err(e) => format!("Consolidation done but save failed: {e}"),
251 }
252}
253
254fn handle_timeline(project_root: &str, category: Option<&str>) -> String {
255 let knowledge = match ProjectKnowledge::load(project_root) {
256 Some(k) => k,
257 None => return "No knowledge stored yet.".to_string(),
258 };
259
260 let cat = match category {
261 Some(c) => c,
262 None => return "Error: category is required for timeline".to_string(),
263 };
264
265 let facts = knowledge.timeline(cat);
266 if facts.is_empty() {
267 return format!("No history for category '{cat}'.");
268 }
269
270 let mut out = format!("Timeline [{cat}] ({} entries):\n", facts.len());
271 for f in &facts {
272 let status = if f.is_current() {
273 "CURRENT"
274 } else {
275 "archived"
276 };
277 let valid_range = match (f.valid_from, f.valid_until) {
278 (Some(from), Some(until)) => format!(
279 "{} ā {}",
280 from.format("%Y-%m-%d %H:%M"),
281 until.format("%Y-%m-%d %H:%M")
282 ),
283 (Some(from), None) => format!("{} ā now", from.format("%Y-%m-%d %H:%M")),
284 _ => "unknown".to_string(),
285 };
286 out.push_str(&format!(
287 " {} = {} [{status}] ({valid_range}) conf={:.0}% x{}\n",
288 f.key,
289 f.value,
290 f.confidence * 100.0,
291 f.confirmation_count
292 ));
293 }
294 out
295}
296
297fn handle_rooms(project_root: &str) -> String {
298 let knowledge = match ProjectKnowledge::load(project_root) {
299 Some(k) => k,
300 None => return "No knowledge stored yet.".to_string(),
301 };
302
303 let rooms = knowledge.list_rooms();
304 if rooms.is_empty() {
305 return "No knowledge rooms yet. Use ctx_knowledge(action=\"remember\", category=\"...\") to create rooms.".to_string();
306 }
307
308 let mut out = format!(
309 "Knowledge Rooms ({} rooms, project: {}):\n",
310 rooms.len(),
311 short_hash(&knowledge.project_hash)
312 );
313 for (cat, count) in &rooms {
314 out.push_str(&format!(" [{cat}] {count} fact(s)\n"));
315 }
316 out
317}
318
319fn handle_search(query: Option<&str>) -> String {
320 let q = match query {
321 Some(q) => q,
322 None => return "Error: query is required for search".to_string(),
323 };
324
325 let sessions_dir = match dirs::home_dir() {
326 Some(h) => h.join(".lean-ctx").join("sessions"),
327 None => return "Cannot determine home directory.".to_string(),
328 };
329
330 if !sessions_dir.exists() {
331 return "No sessions found.".to_string();
332 }
333
334 let knowledge_dir = match dirs::home_dir() {
335 Some(h) => h.join(".lean-ctx").join("knowledge"),
336 None => return "Cannot determine home directory.".to_string(),
337 };
338
339 let q_lower = q.to_lowercase();
340 let terms: Vec<&str> = q_lower.split_whitespace().collect();
341 let mut results = Vec::new();
342
343 if knowledge_dir.exists() {
344 if let Ok(entries) = std::fs::read_dir(&knowledge_dir) {
345 for entry in entries.flatten() {
346 let knowledge_file = entry.path().join("knowledge.json");
347 if let Ok(content) = std::fs::read_to_string(&knowledge_file) {
348 if let Ok(knowledge) = serde_json::from_str::<ProjectKnowledge>(&content) {
349 for fact in &knowledge.facts {
350 let searchable = format!(
351 "{} {} {}",
352 fact.category.to_lowercase(),
353 fact.key.to_lowercase(),
354 fact.value.to_lowercase()
355 );
356 let match_count =
357 terms.iter().filter(|t| searchable.contains(**t)).count();
358 if match_count > 0 {
359 results.push((
360 knowledge.project_root.clone(),
361 fact.category.clone(),
362 fact.key.clone(),
363 fact.value.clone(),
364 fact.confidence,
365 match_count as f32 / terms.len() as f32,
366 ));
367 }
368 }
369 }
370 }
371 }
372 }
373 }
374
375 if let Ok(entries) = std::fs::read_dir(&sessions_dir) {
376 for entry in entries.flatten() {
377 let path = entry.path();
378 if path.extension().and_then(|e| e.to_str()) != Some("json") {
379 continue;
380 }
381 if path.file_name().and_then(|n| n.to_str()) == Some("latest.json") {
382 continue;
383 }
384 if let Ok(json) = std::fs::read_to_string(&path) {
385 if let Ok(session) = serde_json::from_str::<SessionState>(&json) {
386 for finding in &session.findings {
387 let searchable = finding.summary.to_lowercase();
388 let match_count = terms.iter().filter(|t| searchable.contains(**t)).count();
389 if match_count > 0 {
390 let project = session
391 .project_root
392 .clone()
393 .unwrap_or_else(|| "unknown".to_string());
394 results.push((
395 project,
396 "session-finding".to_string(),
397 session.id.clone(),
398 finding.summary.clone(),
399 0.6,
400 match_count as f32 / terms.len() as f32,
401 ));
402 }
403 }
404 for decision in &session.decisions {
405 let searchable = decision.summary.to_lowercase();
406 let match_count = terms.iter().filter(|t| searchable.contains(**t)).count();
407 if match_count > 0 {
408 let project = session
409 .project_root
410 .clone()
411 .unwrap_or_else(|| "unknown".to_string());
412 results.push((
413 project,
414 "session-decision".to_string(),
415 session.id.clone(),
416 decision.summary.clone(),
417 0.7,
418 match_count as f32 / terms.len() as f32,
419 ));
420 }
421 }
422 }
423 }
424 }
425 }
426
427 if results.is_empty() {
428 return format!("No results found for '{q}' across all sessions and projects.");
429 }
430
431 results.sort_by(|a, b| b.5.partial_cmp(&a.5).unwrap_or(std::cmp::Ordering::Equal));
432 results.truncate(20);
433
434 let mut out = format!("Cross-session search '{q}' ({} results):\n", results.len());
435 for (project, cat, key, value, conf, _relevance) in &results {
436 let project_short = short_path(project);
437 out.push_str(&format!(
438 " [{cat}/{key}] {value} (project: {project_short}, conf: {:.0}%)\n",
439 conf * 100.0
440 ));
441 }
442 out
443}
444
445fn handle_wakeup(project_root: &str) -> String {
446 let knowledge = match ProjectKnowledge::load(project_root) {
447 Some(k) => k,
448 None => return "No knowledge for wake-up briefing.".to_string(),
449 };
450 let aaak = knowledge.format_aaak();
451 if aaak.is_empty() {
452 return "No knowledge yet. Start using ctx_knowledge(action=\"remember\") to build project memory.".to_string();
453 }
454 format!("WAKE-UP BRIEFING:\n{aaak}")
455}
456
457fn format_facts(
458 facts: &[&crate::core::knowledge::KnowledgeFact],
459 category: Option<&str>,
460) -> String {
461 let mut out = String::new();
462 if let Some(cat) = category {
463 out.push_str(&format!("Facts [{cat}] ({}):\n", facts.len()));
464 } else {
465 out.push_str(&format!("Matching facts ({}):\n", facts.len()));
466 }
467 for f in facts {
468 let temporal = if !f.is_current() { " [archived]" } else { "" };
469 out.push_str(&format!(
470 " [{}/{}]: {} (confidence: {:.0}%, confirmed: {} x{}){temporal}\n",
471 f.category,
472 f.key,
473 f.value,
474 f.confidence * 100.0,
475 f.last_confirmed.format("%Y-%m-%d"),
476 f.confirmation_count
477 ));
478 }
479 out
480}
481
482fn short_path(path: &str) -> String {
483 let parts: Vec<&str> = path.split('/').collect();
484 if parts.len() <= 2 {
485 return path.to_string();
486 }
487 parts[parts.len() - 2..].join("/")
488}
489
490fn short_hash(hash: &str) -> &str {
491 if hash.len() > 8 {
492 &hash[..8]
493 } else {
494 hash
495 }
496}