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
47pub 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}