use std::path::{Path, PathBuf};
use zeph_memory::sqlite::SourceKind;
use zeph_skills::SkillSource;
use zeph_skills::manager::SkillManager;
use super::error::AgentError;
use super::{Agent, Channel};
impl<C: Channel> Agent<C> {
pub(super) async fn handle_skill_install(
&mut self,
source: Option<&str>,
) -> Result<(), AgentError> {
let Some(source) = source else {
self.channel
.send("Usage: /skill install <url|path>")
.await?;
return Ok(());
};
let Some(managed_dir) = self.skill_state.managed_dir.clone() else {
self.channel
.send("Skill management directory not configured.")
.await?;
return Ok(());
};
let mgr = SkillManager::new(managed_dir.clone());
let source_owned = source.to_owned();
let result = tokio::task::spawn_blocking(move || {
if source_owned.starts_with("http://")
|| source_owned.starts_with("https://")
|| source_owned.starts_with("git@")
{
mgr.install_from_url(&source_owned)
} else {
mgr.install_from_path(Path::new(&source_owned))
}
})
.await
.map_err(|e| AgentError::Other(format!("spawn_blocking failed: {e}")))?;
match result {
Ok(installed) => {
if let Some(memory) = &self.memory_state.memory {
let (source_kind, source_url, source_path) = match &installed.source {
SkillSource::Hub { url } => (SourceKind::Hub, Some(url.as_str()), None),
SkillSource::File { path } => (
SourceKind::File,
None,
Some(path.to_string_lossy().into_owned()),
),
SkillSource::Local => (SourceKind::Local, None, None),
};
if let Err(e) = memory
.sqlite()
.upsert_skill_trust(
&installed.name,
"quarantined",
source_kind,
source_url,
source_path.as_deref(),
&installed.blake3_hash,
)
.await
{
tracing::warn!("failed to record trust for '{}': {e:#}", installed.name);
}
}
self.reload_skills().await;
let skill_md = managed_dir.join(&installed.name).join("SKILL.md");
let missing_secrets: Vec<String> =
if let Ok(meta) = zeph_skills::loader::load_skill_meta(&skill_md) {
meta.requires_secrets
.iter()
.filter(|s| {
!self
.skill_state
.available_custom_secrets
.contains_key(s.as_str())
})
.cloned()
.collect()
} else {
Vec::new()
};
let mut msg = format!(
"Skill \"{}\" installed (trust: quarantined). Use `/skill trust {} trusted` to promote.",
installed.name, installed.name,
);
if !missing_secrets.is_empty() {
use std::fmt::Write;
let _ = write!(
msg,
"\n⚠ Missing secrets: {}. Run `zeph vault set ZEPH_SECRET_<NAME> <value>` for each.",
missing_secrets.join(", ")
);
}
self.channel.send(&msg).await?;
}
Err(e) => {
self.channel.send(&format!("Install failed: {e}")).await?;
}
}
Ok(())
}
pub(super) async fn handle_skill_remove(
&mut self,
name: Option<&str>,
) -> Result<(), AgentError> {
let Some(name) = name else {
self.channel.send("Usage: /skill remove <name>").await?;
return Ok(());
};
let Some(managed_dir) = &self.skill_state.managed_dir else {
self.channel
.send("Skill management directory not configured.")
.await?;
return Ok(());
};
let mgr = SkillManager::new(managed_dir.clone());
let name_owned = name.to_owned();
let remove_result = tokio::task::spawn_blocking(move || mgr.remove(&name_owned))
.await
.map_err(|e| AgentError::Other(format!("spawn_blocking failed: {e}")))?;
match remove_result {
Ok(()) => {
if let Some(memory) = &self.memory_state.memory
&& let Err(e) = memory.sqlite().delete_skill_trust(name).await
{
tracing::warn!("failed to remove trust record for '{name}': {e:#}");
}
self.reload_skills().await;
self.channel
.send(&format!("Skill \"{name}\" removed."))
.await?;
}
Err(e) => {
self.channel.send(&format!("Remove failed: {e}")).await?;
}
}
Ok(())
}
}
const _: fn() = || {
let _: PathBuf = PathBuf::new();
};