Skip to main content

codei_agent/
prompt.rs

1use std::env;
2use std::fs;
3use std::path::Path;
4
5use codei_config::{format_skills_for_prompt, ResolvedConfig};
6
7pub fn build_system_prompt(
8    config: &ResolvedConfig,
9    project_instructions: &str,
10    skills: &[codei_config::Skill],
11) -> String {
12    let cwd = config.cwd.display();
13    let os = env::consts::OS;
14    let language = &config.config.defaults.language;
15    let skills_section = format_skills_for_prompt(skills);
16    let skills_block = if skills_section.is_empty() {
17        String::new()
18    } else {
19        format!("\n\n{skills_section}")
20    };
21
22    format!(
23        r#"You are CodeI, an AI coding assistant running in the user's local terminal.
24
25## Capabilities
26- Read, write, and edit files; search the codebase; run shell commands
27- Working directory: {cwd}
28- Operating system: {os}
29
30## Project instructions
31{project_instructions}
32{skills_block}
33
34## Tool usage
35- Read relevant files before editing code
36- Prefer specialized tools over shell for file operations
37- When using edit, ensure old_string matches exactly once
38- Do not make unrelated changes the user did not ask for
39- Use `read_skill` to load matching skills before specialized work
40
41## Output
42- Communicate with the user in {language}
43- Be concise and actionable"#
44    )
45}
46
47/// Load project instructions from AGENTS.md and `.codei/rules/*.md`.
48pub fn load_project_instructions(config: &ResolvedConfig) -> String {
49    let Some(root) = config.project_root.as_ref() else {
50        return String::new();
51    };
52
53    let mut sections = Vec::new();
54
55    for rel in [".codei/AGENTS.md", "AGENTS.md"] {
56        let path = root.join(rel);
57        if let Some(content) = read_file_if_exists(&path) {
58            sections.push(format!("### From `{rel}`\n{content}"));
59            break;
60        }
61    }
62
63    let rules_dir = root.join(".codei/rules");
64    if rules_dir.is_dir() {
65        let mut rule_files: Vec<_> = fs::read_dir(&rules_dir)
66            .into_iter()
67            .flatten()
68            .filter_map(|e| e.ok())
69            .filter(|e| e.path().extension().is_some_and(|ext| ext == "md"))
70            .collect();
71        rule_files.sort_by_key(|e| e.file_name());
72
73        for entry in rule_files {
74            let path = entry.path();
75            if let Some(content) = read_file_if_exists(&path) {
76                let name = entry.file_name().to_string_lossy().into_owned();
77                sections.push(format!("### Rule `{name}`\n{content}"));
78            }
79        }
80    }
81
82    sections.join("\n\n")
83}
84
85fn read_file_if_exists(path: &Path) -> Option<String> {
86    if path.is_file() {
87        fs::read_to_string(path).ok()
88    } else {
89        None
90    }
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96    use codei_config::ResolvedConfig;
97    use std::path::PathBuf;
98
99    #[test]
100    fn loads_rules_directory() {
101        let dir = tempfile::tempdir().unwrap();
102        let rules = dir.path().join(".codei/rules");
103        fs::create_dir_all(&rules).unwrap();
104        fs::write(rules.join("rust.md"), "Use idiomatic Rust.").unwrap();
105
106        let config = ResolvedConfig {
107            config: Default::default(),
108            cwd: dir.path().to_path_buf(),
109            project_root: Some(dir.path().to_path_buf()),
110            user_config_path: PathBuf::from("/tmp/config.toml"),
111            project_config_path: None,
112        };
113
114        let text = load_project_instructions(&config);
115        assert!(text.contains("Use idiomatic Rust."));
116    }
117}