vtcode_core/skills/
loader.rs1use crate::skills::cli_bridge::discover_cli_tools;
2use crate::skills::model::{SkillLoadOutcome, SkillScope};
3use hashbrown::HashSet;
4use std::path::{Path, PathBuf};
5
6#[derive(Debug, Clone)]
8pub struct SkillLoaderConfig {
9 pub codex_home: PathBuf,
10 pub cwd: PathBuf,
11 pub project_root: Option<PathBuf>,
12 pub include_bundled_system_skills: bool,
13}
14
15pub struct SkillRoot {
16 pub path: PathBuf,
17 pub scope: SkillScope,
18 pub is_tool_root: bool,
19 pub is_plugin_root: bool,
20}
21
22mod cache;
23mod codex;
24mod discovery;
25mod enhanced;
26mod mentions;
27mod resources;
28
29pub use self::enhanced::{EnhancedSkill, EnhancedSkillLoader};
30
31#[cfg(test)]
32pub(crate) use self::discovery::find_git_root;
33use self::discovery::tool_config_to_metadata;
34use self::discovery::{discover_metadata_under_root, discover_skills_under_root, skill_roots_with_home_dir};
35#[cfg(test)]
36pub(crate) use self::enhanced::{discovery_config_for_codex_home, select_traditional_skill_ctx};
37
38pub use self::resources::load_skill_resources;
39
40use self::codex::filter_disabled_skills;
41
42pub use self::mentions::{SkillMentionDetectionOptions, detect_skill_mentions, detect_skill_mentions_with_options};
43
44pub(crate) use self::cache::clear_lightweight_skill_metadata_cache;
45use self::cache::{LightweightSkillCacheKey, cache_lightweight_skill_outcome, get_cached_lightweight_skill_outcome};
46
47pub fn load_skills(config: &SkillLoaderConfig) -> SkillLoadOutcome {
48 let home_dir = dirs::home_dir();
49 load_skills_with_home_dir(config, home_dir.as_deref())
50}
51
52pub fn discover_skill_metadata_lightweight(config: &SkillLoaderConfig) -> SkillLoadOutcome {
56 let home_dir = dirs::home_dir();
57 discover_skill_metadata_lightweight_with_home_dir(config, home_dir.as_deref())
58}
59
60pub(crate) fn load_skills_with_home_dir(config: &SkillLoaderConfig, home_dir: Option<&Path>) -> SkillLoadOutcome {
63 let mut outcome = SkillLoadOutcome::default();
64 let roots = skill_roots_with_home_dir(config, home_dir);
65
66 for root in roots {
67 discover_skills_under_root(&root, &mut outcome);
68 }
69
70 add_system_cli_tools(&mut outcome);
71 dedup_and_sort(&mut outcome);
72 filter_disabled_skills(&mut outcome, home_dir);
73 outcome
74}
75
76fn discover_skill_metadata_lightweight_with_home_dir(
79 config: &SkillLoaderConfig,
80 home_dir: Option<&Path>,
81) -> SkillLoadOutcome {
82 let cache_key = LightweightSkillCacheKey::new(config, home_dir);
83 if let Some(cached) = get_cached_lightweight_skill_outcome(&cache_key) {
84 return cached;
85 }
86
87 let outcome = discover_skill_metadata_lightweight_uncached(config, home_dir);
88 cache_lightweight_skill_outcome(cache_key, &outcome);
89 outcome
90}
91
92fn discover_skill_metadata_lightweight_uncached(
93 config: &SkillLoaderConfig,
94 home_dir: Option<&Path>,
95) -> SkillLoadOutcome {
96 let mut outcome = SkillLoadOutcome::default();
97 let roots = skill_roots_with_home_dir(config, home_dir);
98
99 for root in roots {
100 discover_metadata_under_root(&root, &mut outcome);
101 }
102
103 add_system_cli_tools(&mut outcome);
104 dedup_and_sort(&mut outcome);
105 filter_disabled_skills(&mut outcome, home_dir);
106 outcome
107}
108
109fn add_system_cli_tools(outcome: &mut SkillLoadOutcome) {
110 if let Ok(system_tools) = discover_cli_tools() {
111 for tool in system_tools {
112 if let Ok(skill) = tool_config_to_metadata(&tool, SkillScope::System) {
113 outcome.skills.push(skill);
114 }
115 }
116 }
117}
118
119fn dedup_and_sort(outcome: &mut SkillLoadOutcome) {
120 let mut seen: HashSet<String> = HashSet::new();
121 let mut shadowed: Vec<String> = Vec::new();
122 outcome.skills.retain(|skill| {
123 if seen.insert(skill.name.clone()) {
124 return true;
125 }
126 shadowed.push(format!("{} ({})", skill.name, skill.path.display()));
130 false
131 });
132 if !shadowed.is_empty() {
133 tracing::warn!(
134 shadowed = ?shadowed,
135 "duplicate skill names shadowed by earlier discovery; only the first occurrence is loaded"
136 );
137 }
138 outcome.skills.sort_by(|a, b| a.name.cmp(&b.name));
139}
140
141#[cfg(test)]
144pub fn load_skills_hermetic(config: &SkillLoaderConfig) -> SkillLoadOutcome {
145 load_skills_with_home_dir(config, None)
146}
147
148#[cfg(test)]
150pub fn discover_skill_metadata_lightweight_hermetic(config: &SkillLoaderConfig) -> SkillLoadOutcome {
151 discover_skill_metadata_lightweight_with_home_dir(config, None)
152}
153
154#[cfg(test)]
155mod tests;