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