Source code for smarter.apps.plugin.manifest.models.skill_plugin.source

"""
Resolution of the remote locations from which a SkillPlugin can source its skill.

A skill can be sourced from a public GitHub repository, such as Anthropic's
https://github.com/anthropics/skills, by referring to either the skill's directory
or its ``SKILL.md`` file. Any other ``https`` URL of a ``SKILL.md`` file is supported too.

.. code-block:: text

    # a skill directory in a GitHub repository
    https://github.com/anthropics/skills/tree/main/skills/pdf

    # a SKILL.md file in a GitHub repository
    https://github.com/anthropics/skills/blob/main/skills/pdf/SKILL.md
    https://raw.githubusercontent.com/anthropics/skills/main/skills/pdf/SKILL.md

    # a GitHub repository whose root is a skill. HEAD is its default branch.
    https://github.com/example-org/my-skill

    # any other SKILL.md file
    https://example.com/skills/my-skill/SKILL.md

.. note::

    GitHub branch names may contain slashes, which makes a URL such as
    ``/tree/feature/x/skills/pdf`` ambiguous. The first path segment after ``tree`` or
    ``blob`` is always taken to be the ref, so refer to such branches by commit SHA instead.

.. note::

    **Experimental.** The SkillPlugin was designed and coded by Claude Code (Anthropic's
    Claude Opus 5.5), with Lawrence McDaniel as co-author. It is experimental, and will
    be documented.
"""

import posixpath
from dataclasses import dataclass
from typing import Optional
from urllib.parse import quote, urlparse

from smarter.common.exceptions import SmarterValueError

from .document import SKILL_FILENAME

GITHUB_HOSTS = ("github.com", "www.github.com")
GITHUB_RAW_HOST = "raw.githubusercontent.com"
GITHUB_DEFAULT_REF = "HEAD"
MAX_SOURCE_URL_LENGTH = 2048


[docs] class SkillSourceError(SmarterValueError): """Raised when a skill source URL is invalid, or a skill cannot be retrieved from it."""
[docs] @dataclass(frozen=True) class SkillSourceLocation: """ The resolved location of a remote skill. :ivar url: The URL as written in the manifest. :ivar skill_url: The URL from which to download SKILL.md. :ivar base_url: The URL of the skill root directory, ending in ``/``. Bundled files are resolved relative to it. :ivar owner: The GitHub repository owner, for GitHub sources. :ivar repo: The GitHub repository name, for GitHub sources. :ivar ref: The GitHub branch, tag or commit, for GitHub sources. :ivar path: The path of the skill root directory within the GitHub repository, for GitHub sources. Empty if the skill is the repository root. """ url: str skill_url: str base_url: str owner: Optional[str] = None repo: Optional[str] = None ref: Optional[str] = None path: Optional[str] = None @property def is_github(self) -> bool: """True if the skill is hosted in a GitHub repository.""" return self.owner is not None @property def skill_directory_name(self) -> Optional[str]: """The name of the skill root directory, which the specification requires to match the skill's name.""" if self.is_github: return posixpath.basename(self.path) if self.path else self.repo parent = posixpath.dirname(urlparse(self.skill_url).path.rstrip("/")) return posixpath.basename(parent) or None
[docs] def resource_url(self, path: str) -> str: """Return the download URL of a bundled file, given its normalized path relative to the skill root.""" return self.base_url + quote(path)
def _github_location(url: str, owner: str, repo: str, ref: str, path: str) -> SkillSourceLocation: path = path.strip("/") if path.split("/")[-1] == SKILL_FILENAME: path = posixpath.dirname(path) base = f"https://{GITHUB_RAW_HOST}/{quote(owner)}/{quote(repo)}/{quote(ref)}/" base_url = base + (quote(path) + "/" if path else "") return SkillSourceLocation( url=url, skill_url=base_url + SKILL_FILENAME, base_url=base_url, owner=owner, repo=repo, ref=ref, path=path, ) # pylint: disable=too-many-branches
[docs] def parse_source_url(url: str) -> SkillSourceLocation: """ Resolve a skill source URL to the location of its SKILL.md file and bundled files. :param url: A GitHub URL of a skill directory or SKILL.md file, or an https URL of a SKILL.md file. :return: The resolved location. :raises SkillSourceError: If the URL is not a supported https URL. **Example:** .. code-block:: python location = parse_source_url("https://github.com/anthropics/skills/tree/main/skills/pdf") location.skill_url # 'https://raw.githubusercontent.com/anthropics/skills/main/skills/pdf/SKILL.md' """ if not isinstance(url, str) or not url.strip(): raise SkillSourceError("skill source url must not be empty.") url = url.strip() if len(url) > MAX_SOURCE_URL_LENGTH: raise SkillSourceError(f"skill source url must be at most {MAX_SOURCE_URL_LENGTH} characters.") parsed = urlparse(url) if parsed.scheme != "https": raise SkillSourceError(f"skill source url must be an https url: {url}") if not parsed.hostname: raise SkillSourceError(f"skill source url has no host: {url}") if parsed.username or parsed.password: raise SkillSourceError("skill source url must not contain credentials.") host = parsed.hostname.lower() segments = [segment for segment in parsed.path.split("/") if segment] if host in GITHUB_HOSTS: if len(segments) < 2: raise SkillSourceError(f"GitHub skill source url must refer to a repository: {url}") owner, repo = segments[0], segments[1].removesuffix(".git") if len(segments) == 2: return _github_location(url, owner, repo, GITHUB_DEFAULT_REF, "") if segments[2] not in ("tree", "blob") or len(segments) < 4: raise SkillSourceError( f"GitHub skill source url must refer to a skill directory (/tree/<ref>/<path>) or a " f"{SKILL_FILENAME} file (/blob/<ref>/<path>/{SKILL_FILENAME}): {url}" ) if segments[2] == "blob" and segments[-1] != SKILL_FILENAME: raise SkillSourceError(f"GitHub skill source file url must refer to a {SKILL_FILENAME} file: {url}") return _github_location(url, owner, repo, segments[3], "/".join(segments[4:])) if host == GITHUB_RAW_HOST: if len(segments) < 4 or segments[-1] != SKILL_FILENAME: raise SkillSourceError( f"{GITHUB_RAW_HOST} skill source url must refer to a {SKILL_FILENAME} file " f"(/<owner>/<repo>/<ref>/<path>/{SKILL_FILENAME}): {url}" ) return _github_location(url, segments[0], segments[1], segments[2], "/".join(segments[3:])) if not segments or segments[-1] != SKILL_FILENAME: raise SkillSourceError(f"skill source url must refer to a GitHub repository or a {SKILL_FILENAME} file: {url}") skill_url = parsed._replace(query="", fragment="").geturl() return SkillSourceLocation(url=url, skill_url=skill_url, base_url=skill_url[: -len(SKILL_FILENAME)])