feat: cleaned up skill implementation
This commit is contained in:
@@ -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
@@ -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()?,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>,
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user