[eric] skills: the Add box resolves pasted README install commands (npx/pnpm/bunx, skills.sh URLs) through the parser that shipped without a client

This commit is contained in:
ciregenz
2026-08-09 12:51:02 -07:00
parent 14d4a71e15
commit e4454a0b9e
2 changed files with 27 additions and 2 deletions
@@ -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<Props> = ({ 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<Props> = ({ 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[] = [];
@@ -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<string | null> {
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<CommunitySkill[]> {
const params = new URLSearchParams({ q, limit: '30', source: 'community' });
const res = await fetch(`${SKILL_REGISTRY_API}/search?${params}`);