codei-agent 0.0.12

终端优先的 AI 编程 Agent,用自然语言在本地仓库中读代码、改代码、跑命令、调试问题。
Documentation
use std::env;
use std::fs;
use std::path::Path;

use codei_config::{format_skills_for_prompt, ResolvedConfig};

pub fn build_system_prompt(
    config: &ResolvedConfig,
    project_instructions: &str,
    skills: &[codei_config::Skill],
) -> String {
    let cwd = config.cwd.display();
    let os = env::consts::OS;
    let language = &config.config.defaults.language;
    let skills_section = format_skills_for_prompt(skills);
    let skills_block = if skills_section.is_empty() {
        String::new()
    } else {
        format!("\n\n{skills_section}")
    };

    format!(
        r#"You are CodeI, an AI coding assistant running in the user's local terminal.

## Capabilities
- Read, write, and edit files; search the codebase; run shell commands
- Working directory: {cwd}
- Operating system: {os}

## Project instructions
{project_instructions}
{skills_block}

## Tool usage
- Read relevant files before editing code
- Prefer specialized tools over shell for file operations
- When using edit, ensure old_string matches exactly once
- Do not make unrelated changes the user did not ask for
- Use `read_skill` to load matching skills before specialized work

## Output
- Communicate with the user in {language}
- Be concise and actionable"#
    )
}

/// Load project instructions from AGENTS.md and `.codei/rules/*.md`.
pub fn load_project_instructions(config: &ResolvedConfig) -> String {
    let Some(root) = config.project_root.as_ref() else {
        return String::new();
    };

    let mut sections = Vec::new();

    for rel in [".codei/AGENTS.md", "AGENTS.md"] {
        let path = root.join(rel);
        if let Some(content) = read_file_if_exists(&path) {
            sections.push(format!("### From `{rel}`\n{content}"));
            break;
        }
    }

    let rules_dir = root.join(".codei/rules");
    if rules_dir.is_dir() {
        let mut rule_files: Vec<_> = fs::read_dir(&rules_dir)
            .into_iter()
            .flatten()
            .filter_map(|e| e.ok())
            .filter(|e| e.path().extension().is_some_and(|ext| ext == "md"))
            .collect();
        rule_files.sort_by_key(|e| e.file_name());

        for entry in rule_files {
            let path = entry.path();
            if let Some(content) = read_file_if_exists(&path) {
                let name = entry.file_name().to_string_lossy().into_owned();
                sections.push(format!("### Rule `{name}`\n{content}"));
            }
        }
    }

    sections.join("\n\n")
}

fn read_file_if_exists(path: &Path) -> Option<String> {
    if path.is_file() {
        fs::read_to_string(path).ok()
    } else {
        None
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use codei_config::ResolvedConfig;
    use std::path::PathBuf;

    #[test]
    fn loads_rules_directory() {
        let dir = tempfile::tempdir().unwrap();
        let rules = dir.path().join(".codei/rules");
        fs::create_dir_all(&rules).unwrap();
        fs::write(rules.join("rust.md"), "Use idiomatic Rust.").unwrap();

        let config = ResolvedConfig {
            config: Default::default(),
            cwd: dir.path().to_path_buf(),
            project_root: Some(dir.path().to_path_buf()),
            user_config_path: PathBuf::from("/tmp/config.toml"),
            project_config_path: None,
        };

        let text = load_project_instructions(&config);
        assert!(text.contains("Use idiomatic Rust."));
    }
}