Skip to main content

lean_ctx/rules_inject/
content.rs

1//! The rules/skill payloads lean-ctx injects: shared + dedicated markdown,
2//! Cursor MDC frontmatter, and per-agent rule paths.
3//!
4//! Rule CONTENT is delegated to `core::rules_canonical` — this module only
5//! handles format dispatch and the Cursor-specific frontmatter.
6
7use std::path::{Path, PathBuf};
8
9use super::RulesFormat;
10use crate::core::config::CompressionLevel;
11use crate::core::rules_canonical::{self as rc, Wrapper};
12
13/// The wrapper a Cursor mdc at `mdc_path` should carry (GL #1153): the
14/// honest `HookCovered` profile when the sibling `hooks.json` (always under
15/// the same `.cursor/` dir) shows lean-ctx compressing the native tools,
16/// otherwise the full `Dedicated` mapping. Path-derived so isolated test
17/// homes and the real `~/.cursor` behave identically.
18pub(super) fn cursor_wrapper_for_mdc(mdc_path: &Path) -> Wrapper {
19    let covered = mdc_path
20        .ancestors()
21        .find(|path| {
22            path.file_name()
23                .is_some_and(|name| name == std::ffi::OsStr::new(".cursor"))
24        })
25        .is_some_and(|cursor_dir| {
26            crate::core::rules_channel::cursor_hooks_json_covers(&cursor_dir.join("hooks.json"))
27        });
28    if covered {
29        Wrapper::HookCovered
30    } else {
31        Wrapper::Dedicated
32    }
33}
34
35/// Wrap a rendered rules body in the Cursor mdc frontmatter. The description
36/// stays profile-neutral: whether native tools are replaced (Dedicated) or
37/// hook-covered (GL #1153) is stated by the body itself.
38pub(crate) fn cursor_mdc_document(body: &str) -> String {
39    format!(
40        "---\n\
41         description: \"lean-ctx: context compression layer — \
42         tool guidance in rule body.\"\n\
43         globs: **/*\n\
44         alwaysApply: true\n\
45         ---\n\n\
46         {body}"
47    )
48}
49
50pub(super) fn rules_content(
51    format: &RulesFormat,
52    level: CompressionLevel,
53    wrapper: Wrapper,
54    tool_profile: &crate::core::tool_profiles::ToolProfile,
55) -> String {
56    let shadow = crate::core::config::Config::load().shadow_mode;
57    match format {
58        RulesFormat::SharedMarkdown => rc::render(shadow, Wrapper::Shared, level, tool_profile),
59        RulesFormat::DedicatedMarkdown => {
60            rc::render(shadow, Wrapper::Dedicated, level, tool_profile)
61        }
62        RulesFormat::CursorMdc => {
63            cursor_mdc_document(&rc::render(shadow, wrapper, level, tool_profile))
64        }
65    }
66}
67
68pub fn opencode_dedicated_rules_path(home: &std::path::Path) -> PathBuf {
69    home.join(".config/opencode/rules/lean-ctx.md")
70}
71
72pub fn gemini_dedicated_rules_path(home: &std::path::Path) -> PathBuf {
73    home.join(".gemini").join(GEMINI_DEDICATED_CONTEXT_FILENAME)
74}
75
76/// The `context.fileName` entry registered for Gemini in dedicated mode.
77pub const GEMINI_DEDICATED_CONTEXT_FILENAME: &str = "LEANCTX.md";