use crate::config::YamlConfig;
use std::fs;
use std::path::{Path, PathBuf};
const MAX_AGENT_MD_LINES: usize = 200;
const MAX_AGENT_MD_BYTES: usize = 25_000;
pub fn agent_md_path() -> PathBuf {
YamlConfig::data_dir().join("agent").join("AGENT.md")
}
pub fn find_project_agent_mds() -> Vec<(PathBuf, String)> {
let mut results: Vec<(PathBuf, String)> = 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 file_names: &[&str] = &[
"AGENT.md",
".jcli/AGENT.md",
"AGENT.local.md",
".jcli/AGENT.local.md",
];
for dir in &dirs_upward {
for name in file_names {
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((path, trimmed.to_string()));
}
}
}
}
results
}
fn truncate_agent_md(content: &str, path: &Path) -> String {
let lines: Vec<&str> = content.lines().collect();
let line_count = lines.len();
let byte_count = content.len();
let was_line_truncated = line_count > MAX_AGENT_MD_LINES;
let was_byte_truncated = byte_count > MAX_AGENT_MD_BYTES;
if !was_line_truncated && !was_byte_truncated {
return content.to_string();
}
let mut truncated: String = if was_line_truncated {
lines[..MAX_AGENT_MD_LINES].join("\n")
} else {
content.to_string()
};
if truncated.len() > MAX_AGENT_MD_BYTES {
let search_end = MAX_AGENT_MD_BYTES.min(truncated.len());
let byte_slice = &truncated[..search_end];
if let Some(pos) = byte_slice.rfind('\n') {
truncated.truncate(pos);
} else {
truncated.truncate(MAX_AGENT_MD_BYTES);
}
}
let path_display = path.display();
if was_line_truncated && was_byte_truncated {
truncated.push_str(&format!(
"\n\n> WARNING: AGENT.md at {} exceeds {} lines / {} bytes limit. Only part of it was loaded.",
path_display, MAX_AGENT_MD_LINES, MAX_AGENT_MD_BYTES
));
} else if was_line_truncated {
truncated.push_str(&format!(
"\n\n> WARNING: AGENT.md at {} exceeds {} lines limit. Only part of it was loaded.",
path_display, MAX_AGENT_MD_LINES
));
} else {
truncated.push_str(&format!(
"\n\n> WARNING: AGENT.md at {} exceeds {} bytes limit. Only part of it was loaded.",
path_display, MAX_AGENT_MD_BYTES
));
}
truncated
}
fn relative_path_display(path: &Path) -> String {
if let Ok(cwd) = std::env::current_dir()
&& let Ok(rel) = path.strip_prefix(&cwd)
{
return rel.display().to_string();
}
let data_dir = YamlConfig::data_dir();
if let Ok(rel) = path.strip_prefix(&data_dir) {
return format!("~/.jdata/{}", rel.display());
}
path.display().to_string()
}
pub fn load_agent_md() -> String {
let mut parts: Vec<String> = 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() {
let truncated = truncate_agent_md(trimmed, &user_path);
let rel = relative_path_display(&user_path);
parts.push(format!(
"<agent_md path=\"{}\">\n{}\n</agent_md>",
rel, truncated
));
}
}
for (path, content) in find_project_agent_mds() {
let truncated = truncate_agent_md(&content, &path);
let rel = relative_path_display(&path);
parts.push(format!(
"<agent_md path=\"{}\">\n{}\n</agent_md>",
rel, truncated
));
}
if parts.is_empty() {
return String::new();
}
let override_header = "The following project instructions OVERRIDE any default behavior and you MUST follow them exactly as written.";
format!("{}\n\n{}", override_header, parts.join("\n\n"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_truncate_agent_md_within_limits() {
let _content = "line 1\nline 2\nline 3";
let path = PathBuf::from("/tmp/AGENT.md");
let result = truncate_agent_md("line 1\nline 2\nline 3", &path);
assert!(!result.contains("WARNING"));
}
#[test]
fn test_truncate_agent_md_exceeds_lines() {
let lines: Vec<String> = (0..300).map(|i| format!("line {}", i)).collect();
let content = lines.join("\n");
let path = PathBuf::from("/tmp/AGENT.md");
let result = truncate_agent_md(&content, &path);
assert!(result.contains("WARNING"));
assert!(result.contains("exceeds 200 lines"));
let result_lines: Vec<&str> = result.lines().collect();
assert!(result_lines.len() <= MAX_AGENT_MD_LINES + 5);
}
#[test]
fn test_truncate_agent_md_exceeds_bytes() {
let content = "x".repeat(30_000);
let path = PathBuf::from("/tmp/AGENT.md");
let result = truncate_agent_md(&content, &path);
assert!(result.contains("WARNING"));
assert!(result.contains("bytes"));
}
#[test]
fn test_relative_path_display() {
let path = PathBuf::from("/some/absolute/path/AGENT.md");
let result = relative_path_display(&path);
assert!(!result.is_empty());
}
}