1mod activate;
40mod discover;
41mod error;
42mod miss;
43mod parse;
44mod sections;
45mod skill;
46mod skip;
47mod source;
48mod why;
49
50pub use activate::{
51 DEFAULT_ACTIVATE_HINT, FormatOptions, ListFormat, LoadView, ProgressiveBudgets, filter_skills,
52 format_available_skills_xml, format_catalog, format_load_message, format_load_view,
53 format_package_envelope, parse_list_format, progressive_budgets, rank_skills_for_catalog,
54 skill_relevance_score, trigger_matches, unknown_list_format,
55};
56pub use discover::{
57 CURSOR_VENDOR_DENYLIST, DiscoveryOptions, ValidationReport, discover, find_skill_by_name,
58 format_watch_dirs, validate_path, validate_path_with_options, walk_cwd_to_git_root, watch_dirs,
59 with_home_override,
60};
61pub use error::{Error, ParseError, sanitize_error_token};
62pub use miss::{
63 SkillMiss, UNKNOWN_SKILL_KIND, unknown_or_skipped_skill, unknown_or_skipped_skill_message,
64 unknown_or_skipped_skill_named,
65};
66pub use parse::{
67 normalize_skill_name, parse_skill, skill_name_is_ascii_policy, skill_name_matches_directory,
68 skill_names_equal, validate_skill_name,
69};
70pub use sections::{
71 CHARS_PER_TOKEN, SkillOutline, SkillSection, SkillSectionMeta, WHOLE_BODY_CHEAPER_TOKENS,
72 estimate_tokens, outline_of, skill_section, split_sections, unknown_section_message,
73};
74pub use skill::{
75 SKILL_BODY_LINE_SOFT_WARN, SKILL_COMPATIBILITY_MAX_CHARS, SKILL_DESCRIPTION_MAX_CHARS,
76 SKILL_MD_MAX_BYTES, SKILL_NAME_MAX_CHARS, Skill,
77};
78pub use skip::{
79 DiscoveryReport, HostTokenField, SkillSkip, SkipKind, format_list_tsv, format_skip_tsv,
80};
81pub use source::SkillSource;
82pub use why::{
83 ActivationDecision, ActivationReason, SkillSummary, WhyReport, format_why_text, why,
84};
85
86pub fn version() -> &'static str {
88 env!("CARGO_PKG_VERSION")
89}
90
91#[cfg(test)]
92mod tests {
93 #[test]
94 fn version_is_nonzero() {
95 assert!(!super::version().is_empty());
96 }
97
98 #[test]
101 fn crate_root_docs_attach_agents_to_cwd_to_git() {
102 let docs: String = include_str!("lib.rs")
103 .lines()
104 .filter(|line| line.starts_with("//!"))
105 .collect::<Vec<_>>()
106 .join("\n");
107 assert!(
108 docs.contains("cwd-to-git `.agents`"),
109 "crate-root rustdoc must attach .agents to cwd-to-git, not walk the whole tree: {docs}"
110 );
111 assert!(
112 super::DiscoveryOptions::default().implicit_roots,
113 "documented default must stay true"
114 );
115 }
116
117 #[test]
118 fn unknown_skill_miss_omits_path() {
119 let unknown = super::unknown_or_skipped_skill("no-such-skill", &[]);
120 assert_eq!(unknown.error_kind, super::UNKNOWN_SKILL_KIND);
121 assert!(unknown.path.is_none(), "unknown_skill miss must omit path");
122 let json = serde_json::to_value(&unknown).expect("miss serde");
123 assert!(
124 json.get("path").is_none(),
125 "unknown_skill JSON omits path: {json}"
126 );
127 assert!(
128 json.get("winner_path").is_none(),
129 "unknown_skill JSON omits winner_path: {json}"
130 );
131 }
132
133 #[test]
134 fn name_collision_miss_peels_winner_path() {
135 use std::path::PathBuf;
136
137 use super::{SkillSkip, SkipKind, unknown_or_skipped_skill};
138
139 let skip = SkillSkip {
140 path: PathBuf::from("/tmp/b/foo/SKILL.md"),
141 name: Some("foo".to_owned()),
142 kind: SkipKind::NameCollision,
143 detail: "lost to /tmp/a/foo/SKILL.md".to_owned(),
144 winner_path: Some(PathBuf::from("/tmp/a/foo/SKILL.md")),
145 ..SkillSkip::default()
146 };
147 let miss = unknown_or_skipped_skill("foo", std::slice::from_ref(&skip));
148 assert_eq!(miss.error_kind, "name_collision");
149 assert_eq!(miss.path.as_deref(), Some(skip.path.as_path()));
150 assert_eq!(
151 miss.winner_path.as_deref(),
152 Some(std::path::Path::new("/tmp/a/foo/SKILL.md"))
153 );
154 let json = serde_json::to_value(&miss).expect("miss serde");
155 assert_eq!(json["error_kind"], "name_collision");
156 assert_eq!(json["winner_path"], "/tmp/a/foo/SKILL.md");
157 }
158
159 #[test]
160 fn validation_report_success_has_no_error_kind() {
161 let dir = tempfile::tempdir().expect("pkg");
162 let pkg = dir.path().join("demo");
163 std::fs::create_dir_all(&pkg).expect("dir");
164 std::fs::write(
165 pkg.join("SKILL.md"),
166 "---\nname: demo\ndescription: ok\n---\nbody\n",
167 )
168 .expect("skill");
169 let report = super::validate_path(&pkg);
170 assert!(report.ok, "package directory must validate: {report:?}");
171 assert!(report.miss().is_none(), "ok report has no miss");
172 let json = serde_json::to_value(&report).expect("report serde");
173 assert!(
174 json.get("error_kind").is_none(),
175 "success ValidationReport must not grow error_kind: {json}"
176 );
177 }
178}