diff --git a/src/channels/web/handlers/skills.rs b/src/channels/web/handlers/skills.rs index 6bda411b..400d179a 100644 --- a/src/channels/web/handlers/skills.rs +++ b/src/channels/web/handlers/skills.rs @@ -148,7 +148,14 @@ pub async fn skills_install_handler( .await .map_err(|e| (StatusCode::BAD_REQUEST, e.to_string()))? } else if let Some(ref catalog) = state.skill_catalog { - let url = crate::skills::catalog::skill_download_url(catalog.registry_url(), &req.name); + // Prefer slug (e.g. "owner/skill-name") over display name for the + // download URL, since the registry endpoint expects a slug. + let download_key = req + .slug + .as_deref() + .filter(|s| !s.is_empty()) + .unwrap_or(&req.name); + let url = crate::skills::catalog::skill_download_url(catalog.registry_url(), download_key); crate::tools::builtin::skill_tools::fetch_skill_content(&url) .await .map_err(|e| (StatusCode::BAD_GATEWAY, e.to_string()))? diff --git a/src/channels/web/static/app.js b/src/channels/web/static/app.js index 738f6dde..fd7b16fa 100644 --- a/src/channels/web/static/app.js +++ b/src/channels/web/static/app.js @@ -3580,7 +3580,7 @@ function formatTimeAgo(epochMs) { } function installSkill(nameOrSlug, url, btn) { - var body = { name: nameOrSlug }; + var body = { name: nameOrSlug, slug: nameOrSlug }; if (url) body.url = url; apiFetch('/api/skills/install', { diff --git a/src/channels/web/types.rs b/src/channels/web/types.rs index 41aad382..33c8425e 100644 --- a/src/channels/web/types.rs +++ b/src/channels/web/types.rs @@ -569,6 +569,9 @@ pub struct SkillSearchResponse { #[derive(Debug, Deserialize)] pub struct SkillInstallRequest { pub name: String, + /// Registry slug (e.g. "owner/skill-name"). Preferred over `name` for + /// constructing the download URL when fetching from ClawHub. + pub slug: Option, pub url: Option, pub content: Option, }