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 "codebuddy-md" => export_codebuddy_md(&high_confidence, &project_root),
43 _ => {
44 eprintln!(
45 "Unknown format: {format}. Supported: mdc, agents-md, claude-md, codebuddy-md"
46 );
47 std::process::exit(1);
48 }
49 }
50}
51
52fn export_mdc(facts: &[&KnowledgeFact], project_root: &Path) {
53 let output_path = project_root.join(".cursor/rules/lean-ctx-knowledge.mdc");
54 let content = build_mdc_content(facts);
55
56 if let Some(parent) = output_path.parent() {
57 let _ = std::fs::create_dir_all(parent);
58 }
59
60 match std::fs::write(&output_path, &content) {
61 Ok(()) => {
62 println!(
63 "Exported {} rules to {}",
64 facts.len(),
65 output_path.display()
66 );
67 }
68 Err(e) => eprintln!("Error writing {}: {e}", output_path.display()),
69 }
70}
71
72fn export_agents_md(facts: &[&KnowledgeFact], project_root: &Path) {
73 let output_path = project_root.join("AGENTS.md");
74 let section = build_agents_section(facts);
75
76 if output_path.exists() {
77 let existing = std::fs::read_to_string(&output_path).unwrap_or_default();
78 let marker_start = "<!-- lean-ctx-knowledge-start -->";
79 let marker_end = "<!-- lean-ctx-knowledge-end -->";
80
81 let new_content = if existing.contains(marker_start) {
82 let before = existing.split(marker_start).next().unwrap_or("");
83 let after = existing.split(marker_end).nth(1).unwrap_or("");
84 format!("{before}{marker_start}\n{section}\n{marker_end}{after}")
85 } else {
86 format!("{existing}\n\n{marker_start}\n{section}\n{marker_end}\n")
87 };
88
89 match std::fs::write(&output_path, &new_content) {
90 Ok(()) => println!("Updated {} rules in {}", facts.len(), output_path.display()),
91 Err(e) => eprintln!("Error writing {}: {e}", output_path.display()),
92 }
93 } else {
94 let content = format!(
95 "# Project Knowledge (auto-generated by lean-ctx)\n\n\
96 <!-- lean-ctx-knowledge-start -->\n{section}\n<!-- lean-ctx-knowledge-end -->\n"
97 );
98 match std::fs::write(&output_path, &content) {
99 Ok(()) => println!(
100 "Created {} with {} rules",
101 output_path.display(),
102 facts.len()
103 ),
104 Err(e) => eprintln!("Error writing {}: {e}", output_path.display()),
105 }
106 }
107}
108
109fn export_claude_md(facts: &[&KnowledgeFact], project_root: &Path) {
110 let output_path = project_root.join("CLAUDE.md");
111 let section = build_agents_section(facts);
112
113 if output_path.exists() {
114 let existing = std::fs::read_to_string(&output_path).unwrap_or_default();
115 let marker_start = "<!-- lean-ctx-knowledge-start -->";
116 let marker_end = "<!-- lean-ctx-knowledge-end -->";
117
118 let new_content = if existing.contains(marker_start) {
119 let before = existing.split(marker_start).next().unwrap_or("");
120 let after = existing.split(marker_end).nth(1).unwrap_or("");
121 format!("{before}{marker_start}\n{section}\n{marker_end}{after}")
122 } else {
123 format!("{existing}\n\n{marker_start}\n{section}\n{marker_end}\n")
124 };
125
126 match std::fs::write(&output_path, &new_content) {
127 Ok(()) => println!("Updated {} rules in {}", facts.len(), output_path.display()),
128 Err(e) => eprintln!("Error writing {}: {e}", output_path.display()),
129 }
130 } else {
131 let content = format!(
132 "# Project Rules (auto-generated by lean-ctx)\n\n\
133 <!-- lean-ctx-knowledge-start -->\n{section}\n<!-- lean-ctx-knowledge-end -->\n"
134 );
135 match std::fs::write(&output_path, &content) {
136 Ok(()) => println!(
137 "Created {} with {} rules",
138 output_path.display(),
139 facts.len()
140 ),
141 Err(e) => eprintln!("Error writing {}: {e}", output_path.display()),
142 }
143 }
144}
145
146fn export_codebuddy_md(facts: &[&KnowledgeFact], project_root: &Path) {
147 let output_path = project_root.join("CODEBUDDY.md");
148 let section = build_agents_section(facts);
149
150 if output_path.exists() {
151 let existing = std::fs::read_to_string(&output_path).unwrap_or_default();
152 let marker_start = "<!-- lean-ctx-knowledge-start -->";
153 let marker_end = "<!-- lean-ctx-knowledge-end -->";
154
155 let new_content = if existing.contains(marker_start) {
156 let before = existing.split(marker_start).next().unwrap_or("");
157 let after = existing.split(marker_end).nth(1).unwrap_or("");
158 format!("{before}{marker_start}\n{section}\n{marker_end}{after}")
159 } else {
160 format!("{existing}\n\n{marker_start}\n{section}\n{marker_end}\n")
161 };
162
163 match std::fs::write(&output_path, &new_content) {
164 Ok(()) => println!("Updated {} rules in {}", facts.len(), output_path.display()),
165 Err(e) => eprintln!("Error writing {}: {e}", output_path.display()),
166 }
167 } else {
168 let content = format!(
169 "# Project Rules (auto-generated by lean-ctx)\n\n\
170 <!-- lean-ctx-knowledge-start -->\n{section}\n<!-- lean-ctx-knowledge-end -->\n"
171 );
172 match std::fs::write(&output_path, &content) {
173 Ok(()) => println!(
174 "Created {} with {} rules",
175 output_path.display(),
176 facts.len()
177 ),
178 Err(e) => eprintln!("Error writing {}: {e}", output_path.display()),
179 }
180 }
181}
182
183fn build_mdc_content(facts: &[&KnowledgeFact]) -> String {
184 let mut out = String::from(
185 "---\ndescription: \"Project knowledge (auto-generated by lean-ctx export-rules)\"\n\
186 globs: \"**/*\"\nalwaysApply: true\n---\n\n\
187 # Project Knowledge\n\n",
188 );
189
190 let mut by_category: std::collections::BTreeMap<&str, Vec<&&KnowledgeFact>> =
191 std::collections::BTreeMap::new();
192
193 for fact in facts {
194 by_category.entry(&fact.category).or_default().push(fact);
195 }
196
197 for (category, cat_facts) in &by_category {
198 out.push_str(&format!("## {category}\n\n"));
199 for fact in cat_facts {
200 out.push_str(&format!("- **{}**: {}\n", fact.key, fact.value));
201 }
202 out.push('\n');
203 }
204
205 out
206}
207
208fn build_agents_section(facts: &[&KnowledgeFact]) -> String {
209 let mut out = String::from("## Project Knowledge (lean-ctx)\n\n");
210
211 let mut by_category: std::collections::BTreeMap<&str, Vec<&&KnowledgeFact>> =
212 std::collections::BTreeMap::new();
213
214 for fact in facts {
215 by_category.entry(&fact.category).or_default().push(fact);
216 }
217
218 for (category, cat_facts) in &by_category {
219 out.push_str(&format!("### {category}\n\n"));
220 for fact in cat_facts {
221 out.push_str(&format!("- **{}**: {}\n", fact.key, fact.value));
222 }
223 out.push('\n');
224 }
225
226 out.trim_end().to_string()
227}