use crate::config::YamlConfig;
use std::fs;
use std::path::PathBuf;
const MEMORY_INSTRUCTION_PROMPT: &str = "Codebase and user instructions are shown below. Be sure to adhere to these instructions. IMPORTANT: These instructions OVERRIDE any default behavior and you MUST follow them exactly as written.";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum AgentMdType {
User,
Project,
Local,
}
impl AgentMdType {
fn description(self) -> &'static str {
match self {
AgentMdType::User => "user's private global instructions for all projects",
AgentMdType::Project => "project instructions, checked into the codebase",
AgentMdType::Local => "user's private project instructions, not checked in",
}
}
}
struct AgentMdEntry {
path: PathBuf,
md_type: AgentMdType,
content: String,
}
pub fn agent_md_path() -> PathBuf {
YamlConfig::data_dir().join("agent").join("AGENT.md")
}
fn collect_project_entries() -> Vec<AgentMdEntry> {
let mut results: Vec<AgentMdEntry> = Vec::new();
let Ok(cwd) = std::env::current_dir() else {
return results;
};
let mut dirs_upward: Vec<PathBuf> = Vec::new();
let mut current = cwd.as_path();
loop {
dirs_upward.push(current.to_path_buf());
if current.join(".git").exists() {
break;
}
match current.parent() {
Some(parent) => current = parent,
None => break,
}
}
dirs_upward.reverse();
let candidates: &[(&str, AgentMdType)] = &[
("AGENT.md", AgentMdType::Project),
(".jcli/AGENT.md", AgentMdType::Project),
("AGENT.local.md", AgentMdType::Local),
(".jcli/AGENT.local.md", AgentMdType::Local),
];
for dir in &dirs_upward {
for (name, md_type) in candidates {
let path = dir.join(name);
if path.is_file()
&& let Ok(content) = fs::read_to_string(&path)
{
let trimmed = content.trim();
if !trimmed.is_empty() {
results.push(AgentMdEntry {
path,
md_type: *md_type,
content: trimmed.to_string(),
});
}
}
}
}
results
}
pub fn load_agent_md() -> String {
let mut entries: Vec<AgentMdEntry> = Vec::new();
let user_path = agent_md_path();
if user_path.is_file()
&& let Ok(content) = fs::read_to_string(&user_path)
{
let trimmed = content.trim();
if !trimmed.is_empty() {
entries.push(AgentMdEntry {
path: user_path,
md_type: AgentMdType::User,
content: trimmed.to_string(),
});
}
}
entries.extend(collect_project_entries());
if entries.is_empty() {
return String::new();
}
let parts: Vec<String> = entries
.into_iter()
.map(|entry| {
format!(
"Contents of {} ({}):\n\n{}",
entry.path.display(),
entry.md_type.description(),
entry.content
)
})
.collect();
format!("{}\n\n{}", MEMORY_INSTRUCTION_PROMPT, parts.join("\n\n"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_agent_md_type_description() {
assert!(AgentMdType::User.description().contains("global"));
assert!(AgentMdType::Project.description().contains("checked into"));
assert!(AgentMdType::Local.description().contains("not checked in"));
}
#[test]
fn test_load_agent_md_empty_when_no_files() {
let _ = load_agent_md();
}
}