Source code for pynydus.api.skill_format
"""Agent Skills format: SKILL.md parse / render.
Implements the agentskills.io directory convention:
skills/<slug>/SKILL.md
Each SKILL.md has YAML front-matter (between ``---`` fences) followed by a
Markdown body. The frontmatter fields follow the Agent Skills spec:
name (required) Slug-style identifier, max 64 chars
description (required) Human-readable summary
version (optional) SemVer string, defaults to "1.0"
license (optional) SPDX identifier
compatibility (optional) List of compatible runtimes
allowed-tools (optional) List of permitted tool names
metadata (optional) Arbitrary key-value pairs
Non-spec fields (e.g. ``agent_type``, ``tags``) are stored inside the
``metadata`` map so the frontmatter remains spec-compliant.
"""
from __future__ import annotations
import re
from typing import Any
import yaml
from pydantic import BaseModel, Field
# ---------------------------------------------------------------------------
# AgentSkill model
# ---------------------------------------------------------------------------
[docs]
class AgentSkill(BaseModel):
"""Spec-compliant representation of a single Agent Skill (agentskills.io)."""
name: str
description: str = ""
version: str = "1.0"
license: str = ""
compatibility: list[str] = Field(default_factory=list)
allowed_tools: list[str] = Field(default_factory=list)
metadata: dict[str, Any] = Field(default_factory=dict)
body: str = ""
"""Markdown body: the main skill content (instructions, code, etc.)."""
# ---------------------------------------------------------------------------
# Parse / render
# ---------------------------------------------------------------------------
_FRONTMATTER_RE = re.compile(
r"\A\s*---\s*\n(.*?)---\s*\n?(.*)",
re.DOTALL,
)
[docs]
def parse_skill_md(text: str) -> AgentSkill:
"""Parse a SKILL.md string into ``AgentSkill``.
Args:
text: Full file contents. optional YAML front-matter (``---`` fences)
then Markdown body. Spec fields are parsed. unknown keys go into
``metadata``.
Returns:
Parsed skill model.
Raises:
ValueError: If there is no name and no body.
"""
m = _FRONTMATTER_RE.match(text)
if m:
raw_yaml = m.group(1)
body = m.group(2).strip()
meta: dict[str, Any] = yaml.safe_load(raw_yaml) or {}
else:
meta = {}
body = text.strip()
if "name" not in meta and not body:
raise ValueError("SKILL.md must contain at least a name in front-matter or a body")
_SPEC_KEYS = {
"name",
"description",
"version",
"license",
"compatibility",
"allowed-tools",
"metadata",
}
extra: dict[str, Any] = {}
for key in list(meta):
if key not in _SPEC_KEYS:
extra[key] = meta.pop(key)
user_metadata: dict[str, Any] = meta.get("metadata", {})
if isinstance(user_metadata, dict):
extra.update(user_metadata)
merged_metadata = extra
return AgentSkill(
name=meta.get("name", ""),
description=meta.get("description", ""),
version=str(meta.get("version", "1.0")),
license=meta.get("license", ""),
compatibility=meta.get("compatibility", []),
allowed_tools=meta.get("allowed-tools", []),
metadata=merged_metadata,
body=body,
)
[docs]
def render_skill_md(skill: AgentSkill) -> str:
"""Serialize ``AgentSkill`` to spec-compliant SKILL.md text.
Args:
skill: Parsed or constructed skill model.
Returns:
Front-matter plus body string.
Note:
Only spec keys are top-level YAML fields. extra data stays under
``metadata``.
"""
meta: dict[str, Any] = {"name": skill.name}
meta["description"] = skill.description or skill.name
if skill.version and skill.version != "1.0":
meta["version"] = skill.version
if skill.license:
meta["license"] = skill.license
if skill.compatibility:
meta["compatibility"] = skill.compatibility
if skill.allowed_tools:
meta["allowed-tools"] = skill.allowed_tools
if skill.metadata:
meta["metadata"] = dict(skill.metadata)
yaml_block = yaml.dump(meta, default_flow_style=False, sort_keys=False).strip()
parts = [f"---\n{yaml_block}\n---"]
if skill.body:
parts.append(skill.body)
return "\n\n".join(parts) + "\n"
# ---------------------------------------------------------------------------
# Slug helper
# ---------------------------------------------------------------------------
_SLUG_RE = re.compile(r"[^a-z0-9]+")
_CONSECUTIVE_HYPHENS = re.compile(r"-{2,}")
[docs]
def skill_slug(name: str) -> str:
"""Convert a display name to a filesystem-safe slug.
Args:
name: Human-readable skill name.
Returns:
Lowercase slug (max 64 chars, no repeated or edge hyphens).
Examples:
>>> skill_slug("My Cool Skill!")
'my-cool-skill'
"""
slug = _SLUG_RE.sub("-", name.lower()).strip("-")
slug = _CONSECUTIVE_HYPHENS.sub("-", slug)
slug = slug[:64].rstrip("-")
return slug or "unnamed"