From 45fdff8173a8b86c5a07d40ca084fb4eecb92c90 Mon Sep 17 00:00:00 2001 From: ciregenz Date: Fri, 7 Aug 2026 01:16:35 -0700 Subject: [PATCH] [eric] skills: the Add box accepts the install commands people paste from READMEs (npx/npm/pnpm/bunx, URLs, bare names) --- .../skill_registry/parse_install_command.py | 55 +++++++++++++++++++ backend/apps/skill_registry/skill_registry.py | 14 +++++ backend/tests/test_parse_install_command.py | 43 +++++++++++++++ 3 files changed, 112 insertions(+) create mode 100644 backend/apps/skill_registry/parse_install_command.py create mode 100644 backend/tests/test_parse_install_command.py diff --git a/backend/apps/skill_registry/parse_install_command.py b/backend/apps/skill_registry/parse_install_command.py new file mode 100644 index 00000000..988f25bf --- /dev/null +++ b/backend/apps/skill_registry/parse_install_command.py @@ -0,0 +1,55 @@ +"""Turn the install commands people already paste from READMEs into a skill id we can install. + +The ecosystem's grammar is `npx skills add `, and every neighbouring form (npm/pnpm/bunx, +`install` instead of `add`, a bare `@scope/name`, a skills.sh URL, or just the name) means the same +thing to the person pasting it. Accepting only our own button was the friction.""" + +import re +from typing import Optional + +from typeguard import typechecked + +# The runners people actually have in their muscle memory. +P_RUNNERS = ("npx", "npm", "pnpm", "pnpx", "yarn", "bunx", "bun", "deno") +P_VERBS = ("add", "install", "i") +P_SKILL_ID = re.compile(r"^[A-Za-z0-9@._/-]+$") + + +@typechecked +def parse_install_command(raw: str) -> Optional[str]: + """Return the skill id a pasted command refers to, or None when it is not an install command. + + None means "I could not read this", never a guess: installing the wrong skill because a paste + was ambiguous is worse than asking the user to pick from the list.""" + text = (raw or "").strip() + if not text: + return None + # A skills.sh (or GitHub) URL carries the id in its last meaningful path segment. + if text.startswith(("http://", "https://")): + parts = [p for p in text.split("?")[0].split("#")[0].rstrip("/").split("/") if p] + tail = parts[-1] if parts else "" + return tail if tail and P_SKILL_ID.match(tail) else None + + # Strip a leading shell prompt or copy artifact ("$ npx ..."). + text = re.sub(r"^[$>#]\s*", "", text) + tokens = text.split() + if not tokens: + return None + + if tokens[0].lower() in P_RUNNERS: + # npx skills add | npm i skills | bunx skills add + rest = [t for t in tokens[1:] if not t.startswith("-")] + # The verb and the package name arrive in either order ("npx skills add x", "npm i skills x"), + # so strip both, in whichever order they appear. + for _ in range(2): + if rest and rest[0].lower() in ("skills", "skill", "@skills/cli", "openswarm"): + rest = rest[1:] + elif rest and rest[0].lower() in P_VERBS: + rest = rest[1:] + candidate = rest[0] if rest else "" + return candidate if candidate and P_SKILL_ID.match(candidate) else None + + # A bare id or scoped package pasted on its own. + if len(tokens) == 1 and P_SKILL_ID.match(tokens[0]) and "." not in tokens[0].split("/")[-1][:1]: + return tokens[0] + return None diff --git a/backend/apps/skill_registry/skill_registry.py b/backend/apps/skill_registry/skill_registry.py index 5822b64b..9a5e447b 100644 --- a/backend/apps/skill_registry/skill_registry.py +++ b/backend/apps/skill_registry/skill_registry.py @@ -115,6 +115,20 @@ class p_InstallRequest(BaseModel): confirm: bool = False +class p_ParseCommandRequest(BaseModel): + # What the user pasted: "npx skills add pdf-filler", a skills.sh URL, or a bare name. + command: str + + +@skill_registry.router.post("/parse-command") +def registry_parse_command(req: p_ParseCommandRequest) -> dict: + """Resolve a pasted install command to a skill id so the Marketplace's Add box accepts the + grammar people already copy out of READMEs. Resolution only; nothing is installed here, and an + unreadable paste returns null rather than a guess.""" + from backend.apps.skill_registry.parse_install_command import parse_install_command + return {"skill_id": parse_install_command(req.command)} + + @skill_registry.router.post("/install") async def registry_install(req: p_InstallRequest): """Install a community (skills.sh) skill, in two honest steps. diff --git a/backend/tests/test_parse_install_command.py b/backend/tests/test_parse_install_command.py new file mode 100644 index 00000000..42563728 --- /dev/null +++ b/backend/tests/test_parse_install_command.py @@ -0,0 +1,43 @@ +"""Pasting the command from a README must install the skill. Guessing wrong is worse than asking, +so anything unreadable returns None rather than a best effort.""" + +import pytest + +from backend.apps.skill_registry.parse_install_command import parse_install_command + + +@pytest.mark.parametrize("cmd", [ + "npx skills add pdf-filler", + "npx skills install pdf-filler", + "npm i skills pdf-filler", + "pnpm skills add pdf-filler", + "bunx skills add pdf-filler", + "$ npx skills add pdf-filler", + "npx --yes skills add pdf-filler", + "npx skills add pdf-filler ", +]) +def test_every_common_runner_and_verb_resolves_the_same_skill(cmd): + assert parse_install_command(cmd) == "pdf-filler" + + +def test_scoped_names_survive(): + assert parse_install_command("npx skills add @anthropic/docx") == "@anthropic/docx" + assert parse_install_command("@anthropic/docx") == "@anthropic/docx" + + +def test_a_bare_name_is_accepted(): + assert parse_install_command("pdf-filler") == "pdf-filler" + + +def test_urls_carry_their_id_in_the_last_segment(): + assert parse_install_command("https://skills.sh/s/pdf-filler") == "pdf-filler" + assert parse_install_command("https://skills.sh/s/pdf-filler/") == "pdf-filler" + assert parse_install_command("https://skills.sh/s/pdf-filler?ref=x") == "pdf-filler" + + +@pytest.mark.parametrize("junk", [ + "", " ", "npx skills add", "npm install", "how do i install a skill", + "rm -rf /", "npx skills add ; rm -rf /", +]) +def test_unreadable_input_returns_none_rather_than_a_guess(junk): + assert parse_install_command(junk) is None