Skip to main content

lean_ctx/core/terse/
rules_inject.rs

1//! Injects compression prompts into agent rules files for all integration modes.
2//!
3//! Called from:
4//! - `lean-ctx compression <level>` (CLI command)
5//! - `lean-ctx setup` (initial setup)
6//! - MCP server startup (ensures consistency after manual config edits)
7
8use crate::core::config::CompressionLevel;
9
10const COMPRESSION_BLOCK_START: &str = "<!-- lean-ctx-compression -->";
11const COMPRESSION_BLOCK_END: &str = "<!-- /lean-ctx-compression -->";
12
13/// Updates all detected agent rules files with the compression prompt for `level`.
14/// Idempotent — safe to call repeatedly. Returns the number of files updated.
15pub fn inject(level: &CompressionLevel) -> usize {
16    let prompt = super::agent_prompts::build_prompt_block(level);
17    let prompt_ascii = super::agent_prompts::build_prompt_block_for_client(level, "cursor");
18    let block = |p: &str| {
19        if p.is_empty() {
20            String::new()
21        } else {
22            format!("{COMPRESSION_BLOCK_START}\n{p}\n{COMPRESSION_BLOCK_END}")
23        }
24    };
25
26    let home = crate::core::home::resolve_home_dir().unwrap_or_default();
27    let cwd = std::env::current_dir().unwrap_or_default();
28    let mut updated = 0;
29
30    let cursor_paths: Vec<std::path::PathBuf> = vec![
31        home.join(".cursor/rules/lean-ctx.mdc"),
32        cwd.join(".cursorrules"),
33    ];
34    let other_paths: Vec<std::path::PathBuf> = vec![
35        cwd.join("AGENTS.md"),
36        cwd.join(".claude/rules/lean-ctx.md"),
37        cwd.join(".kiro/steering/lean-ctx.md"),
38        home.join(".config/crush/rules/lean-ctx.md"),
39        home.join(".qoder/rules/lean-ctx.md"),
40    ];
41
42    for path in cursor_paths {
43        if path.exists() {
44            if let Ok(content) = std::fs::read_to_string(&path) {
45                let new_content = upsert_block(&content, &block(&prompt_ascii));
46                if new_content != content {
47                    let _ = std::fs::write(&path, &new_content);
48                    updated += 1;
49                }
50            }
51        }
52    }
53
54    for path in other_paths {
55        if path.exists() {
56            if let Ok(content) = std::fs::read_to_string(&path) {
57                let new_content = upsert_block(&content, &block(&prompt));
58                if new_content != content {
59                    let _ = std::fs::write(&path, &new_content);
60                    updated += 1;
61                }
62            }
63        }
64    }
65
66    updated
67}
68
69fn upsert_block(content: &str, block: &str) -> String {
70    if content.contains(COMPRESSION_BLOCK_START) {
71        crate::marked_block::replace_marked_block(
72            content,
73            COMPRESSION_BLOCK_START,
74            COMPRESSION_BLOCK_END,
75            block,
76        )
77    } else if block.is_empty() {
78        content.to_string()
79    } else {
80        let mut out = content.trim_end().to_string();
81        out.push_str("\n\n");
82        out.push_str(block);
83        out.push('\n');
84        out
85    }
86}