diff --git a/frontend/src/app/pages/Directory/DirectorySkillsTab.tsx b/frontend/src/app/pages/Directory/DirectorySkillsTab.tsx index adad843d..6cdb37d3 100644 --- a/frontend/src/app/pages/Directory/DirectorySkillsTab.tsx +++ b/frontend/src/app/pages/Directory/DirectorySkillsTab.tsx @@ -14,6 +14,8 @@ import { fetchSkills } from '@/shared/state/skillsSlice'; import { fetchAllRegistrySkills, installCuratedSkill, + INSTALL_COMMAND_RE, + parseInstallCommand, searchCommunitySkills, CommunitySkill, RegistrySkill, @@ -73,7 +75,14 @@ const DirectorySkillsTab: React.FC = ({ onOpenInstalled }) => { const seq = ++searchSeq.current; setCommunityLoading(true); try { - const res = await searchCommunitySkills(query.trim()); + // A pasted README install command ("npx skills add pdf-filler", a skills.sh URL) resolves to + // its skill id server-side first, so the box accepts the grammar people actually copy (ENG-217). + let effective = query.trim(); + if (INSTALL_COMMAND_RE.test(effective)) { + const skillId = await parseInstallCommand(effective).catch(() => null); + if (skillId) effective = skillId; + } + const res = await searchCommunitySkills(effective); if (seq === searchSeq.current) setCommunity(res); } catch { if (seq === searchSeq.current) setCommunity([]); @@ -95,7 +104,9 @@ const DirectorySkillsTab: React.FC = ({ onOpenInstalled }) => { }, [localSkills]); const cards = useMemo((): SkillCardModel[] => { - const q = query.trim().toLowerCase(); + // A pasted install command already resolved server-side into the community results; filtering + // those hits against the raw paste string would hide the exact skill the user asked for. + const q = INSTALL_COMMAND_RE.test(query.trim()) ? '' : query.trim().toLowerCase(); // Join curated skills to their skills.sh twin (source anthropics/skills) so Anthropic cards get real install counts. const communityByName = new Map(community.map((cs) => [cs.name.trim().toLowerCase(), cs])); const out: SkillCardModel[] = []; diff --git a/frontend/src/shared/state/skillRegistrySlice.ts b/frontend/src/shared/state/skillRegistrySlice.ts index e444236b..8bdde524 100644 --- a/frontend/src/shared/state/skillRegistrySlice.ts +++ b/frontend/src/shared/state/skillRegistrySlice.ts @@ -207,6 +207,20 @@ export interface InstallDisclosure { secret_findings: string[]; } +// The grammar people copy out of READMEs; cheap local gate so plain searches never buy a round-trip. +export const INSTALL_COMMAND_RE = /^(npx|npm|pnpm|bunx|yarn)\s|skills\.sh\//i; + +export async function parseInstallCommand(command: string): Promise { + const res = await fetch(`${SKILL_REGISTRY_API}/parse-command`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ command }), + }); + if (!res.ok) return null; + const data = (await res.json()) as { skill_id: string | null }; + return data.skill_id ?? null; +} + export async function searchCommunitySkills(q: string): Promise { const params = new URLSearchParams({ q, limit: '30', source: 'community' }); const res = await fetch(`${SKILL_REGISTRY_API}/search?${params}`);