Skip to main content

codei_agent/
prompt.rs

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