use std::fs;
use std::path::PathBuf;
#[derive(Debug, Clone)]
pub enum PromptSource {
FileSystem { root_dir: PathBuf },
Embedded { name: String, content: &'static str },
EmbeddedMap {
prompts: &'static [(&'static str, &'static str)],
},
}
#[derive(Debug, Clone)]
pub struct PromptLoader {
source: PromptSource,
}
impl PromptLoader {
pub fn from_fs(root_dir: impl Into<PathBuf>) -> Self {
Self {
source: PromptSource::FileSystem {
root_dir: root_dir.into(),
},
}
}
pub fn from_embedded(name: impl Into<String>, content: &'static str) -> Self {
Self {
source: PromptSource::Embedded {
name: name.into(),
content,
},
}
}
pub fn from_embedded_map(prompts: &'static [(&'static str, &'static str)]) -> Self {
Self {
source: PromptSource::EmbeddedMap { prompts },
}
}
pub fn load(&self, agent_name: &str) -> String {
match &self.source {
PromptSource::FileSystem { root_dir } => {
let path = root_dir.join(format!("{agent_name}.md"));
match fs::read_to_string(&path) {
Ok(content) => strip_yaml_frontmatter(&content),
Err(_) => fallback_prompt(agent_name),
}
}
PromptSource::Embedded { name, content } => {
if name == agent_name {
strip_yaml_frontmatter(content)
} else {
fallback_prompt(agent_name)
}
}
PromptSource::EmbeddedMap { prompts } => prompts
.iter()
.find(|(name, _)| *name == agent_name)
.map(|(_, content)| strip_yaml_frontmatter(content))
.unwrap_or_else(|| fallback_prompt(agent_name)),
}
}
}
#[macro_export]
macro_rules! include_agent_prompts {
($($name:literal => $path:literal),+ $(,)?) => {
&[
$(($name, include_str!($path))),+
]
};
}
pub fn default_agents_dir() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../..")
.join("packages/uira/claude-plugin/agents")
}
pub fn strip_yaml_frontmatter(content: &str) -> String {
let s = content.trim();
if !s.starts_with("---") {
return s.to_string();
}
let mut lines = s.lines();
let first = lines.next().unwrap_or_default();
if first.trim() != "---" {
return s.to_string();
}
for line in &mut lines {
if line.trim() == "---" {
let rest: String = lines.collect::<Vec<_>>().join("\n");
return rest.trim().to_string();
}
}
s.to_string()
}
pub fn fallback_prompt(agent_name: &str) -> String {
format!(
"Agent: {agent_name}\n\nPrompt file not found. Please ensure packages/uira/claude-plugin/agents/{agent_name}.md exists.",
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn strip_frontmatter_when_present() {
let md = "---\nname: test\n---\n\nHello\nWorld\n";
assert_eq!(strip_yaml_frontmatter(md), "Hello\nWorld");
}
#[test]
fn strip_frontmatter_is_noop_without_frontmatter() {
let md = "Hello\nWorld\n";
assert_eq!(strip_yaml_frontmatter(md), "Hello\nWorld");
}
#[test]
fn strip_frontmatter_is_noop_when_unclosed() {
let md = "---\nname: test\nHello\n";
assert_eq!(strip_yaml_frontmatter(md), "---\nname: test\nHello");
}
#[test]
fn fs_loader_falls_back_when_missing() {
let tmp = tempfile::tempdir().unwrap();
let loader = PromptLoader::from_fs(tmp.path());
let prompt = loader.load("missing");
assert!(prompt.contains("Prompt file not found"));
}
}