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!("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}