Skip to main content

lean_ctx/cli/
export_rules.rs

1use 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}