use std::path::Path;
const SKILL_FILE_NAME: &str = "skill.md";
const AGENTS_FILE_NAME: &str = "agents.md";
const CLAUDE_FILE_NAME: &str = "claude.md";
const SYSTEM_FILE_NAME: &str = "system.md";
const PERSONA_FILE_NAME: &str = "persona.md";
const SOUL_FILE_NAME: &str = "soul.md";
const MCP_JSON_FILE_NAME: &str = "mcp.json";
const MCP_YAML_FILE_NAME: &str = "mcp.yaml";
const MCP_YML_FILE_NAME: &str = "mcp.yml";
const SKILL_FILE_SUFFIX: &str = ".skill.md";
const PROMPT_FILE_SUFFIX: &str = ".prompt.md";
pub(super) const MARKDOWN_GLOB_PATTERN: &str = "*.md";
pub(super) const JSON_GLOB_PATTERN: &str = "*.json";
pub(super) const YAML_GLOB_PATTERN: &str = "*.yaml";
pub(super) const YML_GLOB_PATTERN: &str = "*.yml";
pub(super) const SCRIPT_GLOB_PATTERNS: &[&str] = &[
"*.sh", "*.bash", "*.zsh", "*.ksh", "*.fish", "*.py", "*.ps1", "*.js", "*.cjs", "*.mjs",
"*.ts", "*.rb", "*.pl", "*.rs", "*.go", "*.php",
];
pub(super) const DATA_FILE_GLOB_PATTERNS: &[&str] = &[
"*.json", "*.yaml", "*.yml", "*.toml", "*.txt", "*.env", "*.cfg", "*.ini",
];
pub(super) const SCRIPT_DISCOVERY_SUBDIRS: &[&str] = &[
"scripts",
"bin",
"hooks",
"src",
"lib",
"references",
"tools",
"actions",
"commands",
"workflows",
"deploy",
"config",
".github",
];
pub(super) const MAX_DISCOVERED_SCRIPTS: usize = 400;
pub(super) const MAX_DISCOVERED_DATA_FILES: usize = 100;
pub(super) const MAX_DATA_FILE_BYTES: u64 = 512 * 1024;
pub(super) fn is_explicit_skill_file(path: &Path) -> bool {
let Some(file_name) = path.file_name().and_then(|n| n.to_str()) else {
return false;
};
let file_name_lower = file_name.to_ascii_lowercase();
file_name_lower == SKILL_FILE_NAME
|| file_name_lower == AGENTS_FILE_NAME
|| file_name_lower == CLAUDE_FILE_NAME
|| file_name_lower == SYSTEM_FILE_NAME
|| file_name_lower == PERSONA_FILE_NAME
|| file_name_lower == SOUL_FILE_NAME
|| file_name_lower == MCP_JSON_FILE_NAME
|| file_name_lower == MCP_YAML_FILE_NAME
|| file_name_lower == MCP_YML_FILE_NAME
|| file_name_lower.ends_with(SKILL_FILE_SUFFIX)
|| file_name_lower.ends_with(PROMPT_FILE_SUFFIX)
|| is_markdown_under_prompts_dir(path, &file_name_lower)
}
const PROMPT_DIR_MARKDOWN_SUFFIXES: &[&str] = &[".md", ".mdx"];
fn is_markdown_under_prompts_dir(path: &Path, file_name_lower: &str) -> bool {
if !PROMPT_DIR_MARKDOWN_SUFFIXES
.iter()
.any(|suffix| file_name_lower.ends_with(suffix))
{
return false;
}
let Some(parent) = path.parent() else {
return false;
};
let parent_is_prompts = parent
.file_name()
.is_some_and(|name| name.to_string_lossy().eq_ignore_ascii_case("prompts"));
if !parent_is_prompts {
return false;
}
!path_traverses_skip_dir(path)
}
fn path_traverses_skip_dir(path: &Path) -> bool {
let Some(parent) = path.parent() else {
return false;
};
parent.components().any(|component| {
component.as_os_str().to_str().is_some_and(|comp| {
SKIP_DISCOVERY_DIRS
.iter()
.any(|skip| comp.eq_ignore_ascii_case(skip))
})
})
}
pub(super) const SKIP_DISCOVERY_DIRS: &[&str] = &[
"node_modules",
"vendor",
".git",
"dist",
"build",
"target",
".venv",
"venv",
"__pycache__",
".yarn",
".pnpm-store",
".next",
".turbo",
"coverage",
];
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn is_explicit_skill_file_matches_canonical_names_case_insensitive() {
for name in [
"SKILL.md",
"skill.md",
"Skill.MD",
"AGENTS.md",
"CLAUDE.md",
"SYSTEM.md",
"PERSONA.md",
"SOUL.md",
"mcp.json",
"mcp.yaml",
"mcp.yml",
"my-tool.skill.md",
"review.prompt.md",
] {
let p = PathBuf::from(format!("/some/path/{name}"));
assert!(
is_explicit_skill_file(&p),
"{name} MUST be recognised as an explicit skill entrypoint"
);
}
}
#[test]
fn is_explicit_skill_file_accepts_files_under_prompts_directory() {
assert!(is_explicit_skill_file(Path::new("/repo/prompts/review.md")));
assert!(is_explicit_skill_file(Path::new("/repo/Prompts/Plan.md")));
}
#[test]
fn is_explicit_skill_file_rejects_arbitrary_markdown() {
assert!(!is_explicit_skill_file(Path::new("/repo/README.md")));
assert!(!is_explicit_skill_file(Path::new("/repo/docs/notes.md")));
}
#[test]
fn is_explicit_skill_file_rejects_non_markdown_under_prompts() {
assert!(!is_explicit_skill_file(Path::new(
"/repo/prompts/payload.txt"
)));
assert!(!is_explicit_skill_file(Path::new(
"/repo/prompts/data.json"
)));
assert!(!is_explicit_skill_file(Path::new(
"/repo/prompts/script.sh"
)));
}
#[test]
fn is_explicit_skill_file_accepts_mdx_under_prompts() {
assert!(is_explicit_skill_file(Path::new(
"/repo/prompts/Review.mdx"
)));
}
#[test]
fn is_explicit_skill_file_rejects_prompts_under_skipped_directory() {
for path in [
"/repo/node_modules/some-pkg/prompts/payload.md",
"/repo/.venv/lib/site-packages/foo/prompts/cmd.md",
"/repo/target/debug/build/x/prompts/n.md",
"/repo/__pycache__/prompts/n.md",
"/repo/.git/prompts/n.md",
] {
assert!(
!is_explicit_skill_file(Path::new(path)),
"must not classify {path} as explicit; sits under skipped subtree"
);
}
}
#[test]
fn is_explicit_skill_file_accepts_prompts_at_legitimate_depth() {
for path in [
"/repo/prompts/review.md",
"/repo/docs/prompts/review.md",
"/repo/examples/skill-pack/prompts/plan.md",
"/repo/Prompts/Plan.md",
] {
assert!(
is_explicit_skill_file(Path::new(path)),
"must classify {path} as explicit; not under any skipped subtree"
);
}
}
}