feat: cleaned up skill implementation

This commit is contained in:
2026-06-01 15:13:50 -06:00
parent a4ddc3d65d
commit 1faab15377
11 changed files with 74 additions and 38 deletions
-2
View File
@@ -337,12 +337,10 @@ impl Agent {
&self.config.mcp_servers &self.config.mcp_servers
} }
#[allow(dead_code)]
pub fn skills_enabled(&self) -> Option<bool> { pub fn skills_enabled(&self) -> Option<bool> {
self.config.skills_enabled self.config.skills_enabled
} }
#[allow(dead_code)]
pub fn enabled_skills(&self) -> Option<&[String]> { pub fn enabled_skills(&self) -> Option<&[String]> {
self.config.enabled_skills.as_deref() self.config.enabled_skills.as_deref()
} }
+6 -1
View File
@@ -268,6 +268,7 @@ pub fn install_builtins() -> Result<()> {
Functions::install_builtin_global_tools(false)?; Functions::install_builtin_global_tools(false)?;
Agent::install_builtin_agents(false)?; Agent::install_builtin_agents(false)?;
Macro::install_macros(false)?; Macro::install_macros(false)?;
Skill::install_builtin_skills(false)?;
Ok(()) Ok(())
} }
@@ -276,18 +277,20 @@ pub enum AssetCategory {
Agents, Agents,
Macros, Macros,
Functions, Functions,
Skills,
#[value(name = "mcp_config")] #[value(name = "mcp_config")]
McpConfig, McpConfig,
} }
impl AssetCategory { impl AssetCategory {
pub const NAMES: [&'static str; 4] = ["agents", "macros", "functions", "mcp_config"]; pub const NAMES: [&'static str; 5] = ["agents", "macros", "functions", "skills", "mcp_config"];
pub fn parse(name: &str) -> Option<Self> { pub fn parse(name: &str) -> Option<Self> {
match name { match name {
"agents" => Some(Self::Agents), "agents" => Some(Self::Agents),
"macros" => Some(Self::Macros), "macros" => Some(Self::Macros),
"functions" => Some(Self::Functions), "functions" => Some(Self::Functions),
"skills" => Some(Self::Skills),
"mcp_config" => Some(Self::McpConfig), "mcp_config" => Some(Self::McpConfig),
_ => None, _ => None,
} }
@@ -333,6 +336,7 @@ pub fn install_assets(category: AssetCategory) -> Result<()> {
AssetCategory::Agents => ("agents", paths::agents_data_dir()), AssetCategory::Agents => ("agents", paths::agents_data_dir()),
AssetCategory::Macros => ("macros", paths::macros_dir()), AssetCategory::Macros => ("macros", paths::macros_dir()),
AssetCategory::Functions => ("functions", paths::functions_dir()), AssetCategory::Functions => ("functions", paths::functions_dir()),
AssetCategory::Skills => ("skills", paths::skills_dir()),
AssetCategory::McpConfig => ("MCP config", paths::mcp_config_file()), AssetCategory::McpConfig => ("MCP config", paths::mcp_config_file()),
}; };
@@ -345,6 +349,7 @@ pub fn install_assets(category: AssetCategory) -> Result<()> {
AssetCategory::Agents => Agent::install_builtin_agents(true)?, AssetCategory::Agents => Agent::install_builtin_agents(true)?,
AssetCategory::Macros => Macro::install_macros(true)?, AssetCategory::Macros => Macro::install_macros(true)?,
AssetCategory::Functions => Functions::install_builtin_global_tools(true)?, AssetCategory::Functions => Functions::install_builtin_global_tools(true)?,
AssetCategory::Skills => Skill::install_builtin_skills(true)?,
AssetCategory::McpConfig => Functions::install_mcp_config()?, AssetCategory::McpConfig => Functions::install_mcp_config()?,
} }
-4
View File
@@ -72,12 +72,10 @@ pub fn skills_dir() -> PathBuf {
} }
} }
#[allow(dead_code)]
pub fn skill_dir(name: &str) -> PathBuf { pub fn skill_dir(name: &str) -> PathBuf {
skills_dir().join(name) skills_dir().join(name)
} }
#[allow(dead_code)]
pub fn skill_file(name: &str) -> PathBuf { pub fn skill_file(name: &str) -> PathBuf {
skill_dir(name).join("SKILL.md") skill_dir(name).join("SKILL.md")
} }
@@ -251,7 +249,6 @@ pub fn has_macro(name: &str) -> bool {
names.contains(&name.to_string()) names.contains(&name.to_string())
} }
#[allow(dead_code)]
pub fn list_skills() -> Vec<String> { pub fn list_skills() -> Vec<String> {
let mut names = Vec::new(); let mut names = Vec::new();
if let Ok(rd) = read_dir(skills_dir()) { if let Ok(rd) = read_dir(skills_dir()) {
@@ -270,7 +267,6 @@ pub fn list_skills() -> Vec<String> {
names names
} }
#[allow(dead_code)]
pub fn has_skill(name: &str) -> bool { pub fn has_skill(name: &str) -> bool {
skill_file(name).is_file() skill_file(name).is_file()
} }
+37
View File
@@ -4019,6 +4019,43 @@ mod tests {
); );
} }
#[test]
#[serial]
fn install_builtin_skills_force_overwrites_only_with_force() {
let _guard = TestConfigDirGuard::new();
Skill::install_builtin_skills(false).unwrap();
let file = paths::skill_file("git-master");
assert!(file.exists(), "git-master skill should be installed");
write(&file, "SENTINEL").unwrap();
Skill::install_builtin_skills(false).unwrap();
assert_eq!(
read_to_string(&file).unwrap(),
"SENTINEL",
"non-force install must not overwrite an existing skill"
);
Skill::install_builtin_skills(true).unwrap();
assert_ne!(
read_to_string(&file).unwrap(),
"SENTINEL",
"force install must overwrite the existing skill"
);
}
#[test]
#[serial]
fn install_builtin_skills_installs_all_bundled() {
let _guard = TestConfigDirGuard::new();
Skill::install_builtin_skills(false).unwrap();
assert!(paths::skill_file("git-master").exists());
assert!(paths::skill_file("ai-slop-remover").exists());
assert!(paths::skill_file("code-review").exists());
assert!(paths::skill_file("frontend-ui-ux").exists());
}
#[test] #[test]
#[serial] #[serial]
fn install_functions_force_preserves_user_mcp_json() { fn install_functions_force_preserves_user_mcp_json() {
-2
View File
@@ -285,12 +285,10 @@ impl Role {
self.continuation_prompt.as_deref() self.continuation_prompt.as_deref()
} }
#[allow(dead_code)]
pub fn skills_enabled(&self) -> Option<bool> { pub fn skills_enabled(&self) -> Option<bool> {
self.skills_enabled self.skills_enabled
} }
#[allow(dead_code)]
pub fn enabled_skills(&self) -> Option<&str> { pub fn enabled_skills(&self) -> Option<&str> {
self.enabled_skills.as_deref() self.enabled_skills.as_deref()
} }
-2
View File
@@ -79,12 +79,10 @@ pub struct Session {
} }
impl Session { impl Session {
#[allow(dead_code)]
pub fn skills_enabled(&self) -> Option<bool> { pub fn skills_enabled(&self) -> Option<bool> {
self.skills_enabled self.skills_enabled
} }
#[allow(dead_code)]
pub fn enabled_skills(&self) -> Option<&str> { pub fn enabled_skills(&self) -> Option<&str> {
self.enabled_skills.as_deref() self.enabled_skills.as_deref()
} }
+31 -19
View File
@@ -6,6 +6,7 @@ use rust_embed::Embed;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::Value; use serde_json::Value;
use std::sync::LazyLock; use std::sync::LazyLock;
use log::{debug, info};
#[derive(Embed)] #[derive(Embed)]
#[folder = "assets/skills/"] #[folder = "assets/skills/"]
@@ -39,7 +40,6 @@ pub struct Skill {
auto_unload: Option<bool>, auto_unload: Option<bool>,
} }
#[allow(dead_code)]
impl Skill { impl Skill {
pub fn new(name: &str, content: &str) -> Self { pub fn new(name: &str, content: &str) -> Self {
let mut metadata = ""; let mut metadata = "";
@@ -84,11 +84,36 @@ impl Skill {
skill skill
} }
pub fn builtin(name: &str) -> Result<Self> { pub fn install_builtin_skills(force: bool) -> Result<()> {
let content = SkillsAsset::get(&format!("{name}/SKILL.md")) info!(
.ok_or_else(|| anyhow!("Unknown skill `{name}`"))?; "Installing built-in skills in {}",
let content = unsafe { std::str::from_utf8_unchecked(&content.data) }; paths::skills_dir().display()
Ok(Skill::new(name, content)) );
for file in SkillsAsset::iter() {
debug!("Processing skill file: {}", file.as_ref());
let embedded_file = SkillsAsset::get(&file).ok_or_else(|| {
anyhow!("Failed to load embedded skill file: {}", file.as_ref())
})?;
let content = unsafe { std::str::from_utf8_unchecked(&embedded_file.data) };
let file_path = paths::skills_dir().join(file.as_ref());
if file_path.exists() && !force {
debug!(
"Skill file already exists, skipping: {}",
file_path.display()
);
continue;
}
ensure_parent_exists(&file_path)?;
info!("Creating skill file: {}", file_path.display());
let mut skill_file = File::create(&file_path)?;
Write::write_all(&mut skill_file, content.as_bytes())?;
}
Ok(())
} }
pub fn load(name: &str) -> Result<Self> { pub fn load(name: &str) -> Result<Self> {
@@ -99,12 +124,6 @@ impl Skill {
Ok(Skill::new(name, &content)) Ok(Skill::new(name, &content))
} }
pub fn list_builtin_skill_names() -> Vec<String> {
SkillsAsset::iter()
.filter_map(|v| v.strip_suffix("/SKILL.md").map(|v| v.to_string()))
.collect()
}
pub fn name(&self) -> &str { pub fn name(&self) -> &str {
&self.name &self.name
} }
@@ -307,11 +326,4 @@ mod tests {
assert!(skill.is_compatible(false, false)); assert!(skill.is_compatible(false, false));
} }
#[test]
fn builtin_returns_err_for_unknown_skill() {
let result = Skill::builtin("nonexistent_skill_xyz");
assert!(result.is_err());
}
} }
-5
View File
@@ -7,15 +7,12 @@ use super::session::Session;
use anyhow::{Result, bail}; use anyhow::{Result, bail};
use std::collections::HashSet; use std::collections::HashSet;
#[allow(dead_code)]
#[derive(Debug)] #[derive(Debug)]
pub struct SkillPolicy { pub struct SkillPolicy {
pub skills_enabled: bool, pub skills_enabled: bool,
pub visible: Option<HashSet<String>>,
pub enabled: HashSet<String>, pub enabled: HashSet<String>,
} }
#[allow(dead_code)]
impl SkillPolicy { impl SkillPolicy {
pub fn effective( pub fn effective(
global: &AppConfig, global: &AppConfig,
@@ -101,7 +98,6 @@ impl SkillPolicy {
Ok(Self { Ok(Self {
skills_enabled, skills_enabled,
visible,
enabled, enabled,
}) })
} }
@@ -160,7 +156,6 @@ mod tests {
.unwrap(); .unwrap();
assert!(policy.skills_enabled); assert!(policy.skills_enabled);
assert!(policy.visible.is_none());
assert!(policy.enabled.is_empty()); assert!(policy.enabled.is_empty());
} }
-1
View File
@@ -5,7 +5,6 @@ use anyhow::{Result, bail};
use indexmap::IndexMap; use indexmap::IndexMap;
use std::collections::BTreeSet; use std::collections::BTreeSet;
#[allow(dead_code)]
#[derive(Clone, Default)] #[derive(Clone, Default)]
pub struct SkillRegistry { pub struct SkillRegistry {
loaded: IndexMap<String, Skill>, loaded: IndexMap<String, Skill>,
-1
View File
@@ -355,7 +355,6 @@ impl Functions {
self.declarations.extend(todo::todo_function_declarations()); self.declarations.extend(todo::todo_function_declarations());
} }
#[allow(dead_code)]
pub fn append_skill_functions(&mut self) { pub fn append_skill_functions(&mut self) {
self.declarations self.declarations
.extend(skill::skill_function_declarations()); .extend(skill::skill_function_declarations());
-1
View File
@@ -9,7 +9,6 @@ use serde_json::{Value, json};
pub const SKILL_FUNCTION_PREFIX: &str = "skill__"; pub const SKILL_FUNCTION_PREFIX: &str = "skill__";
#[allow(dead_code)]
pub fn skill_function_declarations() -> Vec<FunctionDeclaration> { pub fn skill_function_declarations() -> Vec<FunctionDeclaration> {
vec![ vec![
FunctionDeclaration { FunctionDeclaration {