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        _ => {
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}