lean_ctx/cli/
export_rules.rs1use std::path::{Path, PathBuf};
2
3use crate::core::knowledge::{KnowledgeFact, ProjectKnowledge};
4
5pub fn run(args: &[String]) {
6 let format = args
7 .iter()
8 .position(|a| a == "--format")
9 .and_then(|i| args.get(i + 1))
10 .map_or("mdc", |s| s.as_str());
11
12 let root_arg = args
13 .iter()
14 .position(|a| a == "--root")
15 .and_then(|i| args.get(i + 1))
16 .map_or(".", |s| s.as_str());
17
18 let project_root = std::fs::canonicalize(root_arg).unwrap_or_else(|_| PathBuf::from(root_arg));
19 let project_root_str = project_root.to_string_lossy();
20
21 let Some(knowledge) = ProjectKnowledge::load(&project_root_str) else {
22 eprintln!("No knowledge found for project: {project_root_str}");
23 eprintln!("Run `lean-ctx` in a session first to accumulate knowledge.");
24 std::process::exit(1);
25 };
26
27 let high_confidence: Vec<&KnowledgeFact> = knowledge
28 .facts
29 .iter()
30 .filter(|f| f.confidence >= 0.6 && f.retrieval_count >= 2)
31 .collect();
32
33 if high_confidence.is_empty() {
34 eprintln!("No high-confidence knowledge facts found (need confidence >= 0.6 and at least 2 retrievals).");
35 std::process::exit(0);
36 }
37
38 match format {
39 "mdc" => export_mdc(&high_confidence, &project_root),
40 "agents-md" => export_agents_md(&high_confidence, &project_root),
41 "claude-md" => export_claude_md(&high_confidence, &project_root),
42 _ => {
43 eprintln!("Unknown format: {format}. Supported: mdc, agents-md, claude-md");
44 std::process::exit(1);
45 }
46 }
47}
48
49fn export_mdc(facts: &[&KnowledgeFact], project_root: &Path) {
50 let output_path = project_root.join(".cursor/rules/lean-ctx-knowledge.mdc");
51 let content = build_mdc_content(facts);
52
53 if let Some(parent) = output_path.parent() {
54 let _ = std::fs::create_dir_all(parent);
55 }
56
57 match std::fs::write(&output_path, &content) {
58 Ok(()) => {
59 println!(
60 "Exported {} rules to {}",
61 facts.len(),
62 output_path.display()
63 );
64 }
65 Err(e) => eprintln!("Error writing {}: {e}", output_path.display()),
66 }
67}
68
69fn export_agents_md(facts: &[&KnowledgeFact], project_root: &Path) {
70 let output_path = project_root.join("AGENTS.md");
71 let section = build_agents_section(facts);
72
73 if output_path.exists() {
74 let existing = std::fs::read_to_string(&output_path).unwrap_or_default();
75 let marker_start = "<!-- lean-ctx-knowledge-start -->";
76 let marker_end = "<!-- lean-ctx-knowledge-end -->";
77
78 let new_content = if existing.contains(marker_start) {
79 let before = existing.split(marker_start).next().unwrap_or("");
80 let after = existing.split(marker_end).nth(1).unwrap_or("");
81 format!("{before}{marker_start}\n{section}\n{marker_end}{after}")
82 } else {
83 format!("{existing}\n\n{marker_start}\n{section}\n{marker_end}\n")
84 };
85
86 match std::fs::write(&output_path, &new_content) {
87 Ok(()) => println!("Updated {} rules in {}", facts.len(), output_path.display()),
88 Err(e) => eprintln!("Error writing {}: {e}", output_path.display()),
89 }
90 } else {
91 let content = format!(
92 "# Project Knowledge (auto-generated by lean-ctx)\n\n\
93 <!-- lean-ctx-knowledge-start -->\n{section}\n<!-- lean-ctx-knowledge-end -->\n"
94 );
95 match std::fs::write(&output_path, &content) {
96 Ok(()) => println!(
97 "Created {} with {} rules",
98 output_path.display(),
99 facts.len()
100 ),
101 Err(e) => eprintln!("Error writing {}: {e}", output_path.display()),
102 }
103 }
104}
105
106fn export_claude_md(facts: &[&KnowledgeFact], project_root: &Path) {
107 let output_path = project_root.join("CLAUDE.md");
108 let section = build_agents_section(facts);
109
110 if output_path.exists() {
111 let existing = std::fs::read_to_string(&output_path).unwrap_or_default();
112 let marker_start = "<!-- lean-ctx-knowledge-start -->";
113 let marker_end = "<!-- lean-ctx-knowledge-end -->";
114
115 let new_content = if existing.contains(marker_start) {
116 let before = existing.split(marker_start).next().unwrap_or("");
117 let after = existing.split(marker_end).nth(1).unwrap_or("");
118 format!("{before}{marker_start}\n{section}\n{marker_end}{after}")
119 } else {
120 format!("{existing}\n\n{marker_start}\n{section}\n{marker_end}\n")
121 };
122
123 match std::fs::write(&output_path, &new_content) {
124 Ok(()) => println!("Updated {} rules in {}", facts.len(), output_path.display()),
125 Err(e) => eprintln!("Error writing {}: {e}", output_path.display()),
126 }
127 } else {
128 let content = format!(
129 "# Project Rules (auto-generated by lean-ctx)\n\n\
130 <!-- lean-ctx-knowledge-start -->\n{section}\n<!-- lean-ctx-knowledge-end -->\n"
131 );
132 match std::fs::write(&output_path, &content) {
133 Ok(()) => println!(
134 "Created {} with {} rules",
135 output_path.display(),
136 facts.len()
137 ),
138 Err(e) => eprintln!("Error writing {}: {e}", output_path.display()),
139 }
140 }
141}
142
143fn build_mdc_content(facts: &[&KnowledgeFact]) -> String {
144 let mut out = String::from(
145 "---\ndescription: \"Project knowledge (auto-generated by lean-ctx export-rules)\"\n\
146 globs: \"**/*\"\nalwaysApply: true\n---\n\n\
147 # Project Knowledge\n\n",
148 );
149
150 let mut by_category: std::collections::BTreeMap<&str, Vec<&&KnowledgeFact>> =
151 std::collections::BTreeMap::new();
152
153 for fact in facts {
154 by_category.entry(&fact.category).or_default().push(fact);
155 }
156
157 for (category, cat_facts) in &by_category {
158 out.push_str(&format!("## {category}\n\n"));
159 for fact in cat_facts {
160 out.push_str(&format!("- **{}**: {}\n", fact.key, fact.value));
161 }
162 out.push('\n');
163 }
164
165 out
166}
167
168fn build_agents_section(facts: &[&KnowledgeFact]) -> String {
169 let mut out = String::from("## Project Knowledge (lean-ctx)\n\n");
170
171 let mut by_category: std::collections::BTreeMap<&str, Vec<&&KnowledgeFact>> =
172 std::collections::BTreeMap::new();
173
174 for fact in facts {
175 by_category.entry(&fact.category).or_default().push(fact);
176 }
177
178 for (category, cat_facts) in &by_category {
179 out.push_str(&format!("### {category}\n\n"));
180 for fact in cat_facts {
181 out.push_str(&format!("- **{}**: {}\n", fact.key, fact.value));
182 }
183 out.push('\n');
184 }
185
186 out.trim_end().to_string()
187}