diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9679d79d1..c0400078d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -328,6 +328,11 @@ license: MIT platforms: [macos, linux] # Optional — restrict to specific OS platforms # Valid: macos, linux, windows # Omit to load on all platforms (default) +prerequisites: # Optional — runtime requirements + env_vars: [MY_API_KEY] # Env vars that must be set + commands: [curl, jq] # CLI binaries that must be on PATH + # Skills with unmet prerequisites are hidden + # from the system prompt and flagged in skill_view. metadata: hermes: tags: [Category, Subcategory, Keywords] @@ -366,6 +371,25 @@ platforms: [windows] # Windows only If the field is omitted or empty, the skill loads on all platforms (backward compatible). See `skills/apple/` for examples of macOS-only skills. +### Skill prerequisites + +Skills can declare runtime prerequisites via the `prerequisites` frontmatter field. Skills with unmet prerequisites are automatically hidden from the system prompt (the agent won't claim it can use them) and show a clear warning in `skill_view()` telling the agent what's missing. + +```yaml +prerequisites: + env_vars: [TENOR_API_KEY] # Env vars checked via os.getenv() + commands: [curl, jq] # CLI binaries checked via shutil.which() +``` + +Both sub-fields are optional — declare only what applies. If the field is omitted entirely, the skill is always available (backward compatible). + +**When to declare prerequisites:** +- The skill uses a CLI tool that isn't universally installed (e.g., `himalaya`, `openhue`, `ddgs`) +- The skill requires an API key in the environment (e.g., `NOTION_API_KEY`, `TENOR_API_KEY`) +- Without these, the skill's commands will fail — not just degrade gracefully + +See `skills/gifs/gif-search/` and `skills/email/himalaya/` for examples. + ### Skill guidelines - **No external dependencies unless absolutely necessary.** Prefer stdlib Python, curl, and existing Hermes tools (`web_extract`, `terminal`, `read_file`). diff --git a/agent/prompt_builder.py b/agent/prompt_builder.py index c933ffe67..09dc6dd43 100644 --- a/agent/prompt_builder.py +++ b/agent/prompt_builder.py @@ -170,6 +170,22 @@ def _skill_is_platform_compatible(skill_file: Path) -> bool: return True # Err on the side of showing the skill +def _skill_prerequisites_met(skill_file: Path) -> bool: + """Check if a SKILL.md's declared prerequisites are satisfied. + + Returns True (show the skill) when prerequisites are met or not declared. + Returns False when the skill explicitly declares prerequisites that are missing. + """ + try: + from tools.skills_tool import _parse_frontmatter, check_skill_prerequisites + raw = skill_file.read_text(encoding="utf-8")[:2000] + frontmatter, _ = _parse_frontmatter(raw) + met, _ = check_skill_prerequisites(frontmatter) + return met + except Exception: + return True + + def build_skills_system_prompt() -> str: """Build a compact skill index for the system prompt. @@ -191,6 +207,9 @@ def build_skills_system_prompt() -> str: # Skip skills incompatible with the current OS platform if not _skill_is_platform_compatible(skill_file): continue + # Skip skills whose prerequisites (env vars, commands) are unmet + if not _skill_prerequisites_met(skill_file): + continue rel_path = skill_file.relative_to(skills_dir) parts = rel_path.parts if len(parts) >= 2: diff --git a/skills/apple/apple-notes/SKILL.md b/skills/apple/apple-notes/SKILL.md index d68c183b5..33fb3ef76 100644 --- a/skills/apple/apple-notes/SKILL.md +++ b/skills/apple/apple-notes/SKILL.md @@ -9,6 +9,8 @@ metadata: hermes: tags: [Notes, Apple, macOS, note-taking] related_skills: [obsidian] +prerequisites: + commands: [memo] --- # Apple Notes diff --git a/skills/apple/apple-reminders/SKILL.md b/skills/apple/apple-reminders/SKILL.md index 872cc3f59..7af393370 100644 --- a/skills/apple/apple-reminders/SKILL.md +++ b/skills/apple/apple-reminders/SKILL.md @@ -8,6 +8,8 @@ platforms: [macos] metadata: hermes: tags: [Reminders, tasks, todo, macOS, Apple] +prerequisites: + commands: [remindctl] --- # Apple Reminders diff --git a/skills/apple/imessage/SKILL.md b/skills/apple/imessage/SKILL.md index 777461d37..82df6a6ec 100644 --- a/skills/apple/imessage/SKILL.md +++ b/skills/apple/imessage/SKILL.md @@ -8,6 +8,8 @@ platforms: [macos] metadata: hermes: tags: [iMessage, SMS, messaging, macOS, Apple] +prerequisites: + commands: [imsg] --- # iMessage diff --git a/skills/email/himalaya/SKILL.md b/skills/email/himalaya/SKILL.md index 08517ebc1..ddbf51aae 100644 --- a/skills/email/himalaya/SKILL.md +++ b/skills/email/himalaya/SKILL.md @@ -8,6 +8,8 @@ metadata: hermes: tags: [Email, IMAP, SMTP, CLI, Communication] homepage: https://github.com/pimalaya/himalaya +prerequisites: + commands: [himalaya] --- # Himalaya Email CLI diff --git a/skills/feeds/blogwatcher/SKILL.md b/skills/feeds/blogwatcher/SKILL.md index 4aadfe943..c1ea4ac24 100644 --- a/skills/feeds/blogwatcher/SKILL.md +++ b/skills/feeds/blogwatcher/SKILL.md @@ -8,6 +8,8 @@ metadata: hermes: tags: [RSS, Blogs, Feed-Reader, Monitoring] homepage: https://github.com/Hyaxia/blogwatcher +prerequisites: + commands: [blogwatcher] --- # Blogwatcher diff --git a/skills/gifs/gif-search/SKILL.md b/skills/gifs/gif-search/SKILL.md index a255b934d..ee55cac88 100644 --- a/skills/gifs/gif-search/SKILL.md +++ b/skills/gifs/gif-search/SKILL.md @@ -1,9 +1,12 @@ --- name: gif-search description: Search and download GIFs from Tenor using curl. No dependencies beyond curl and jq. Useful for finding reaction GIFs, creating visual content, and sending GIFs in chat. -version: 1.0.0 +version: 1.1.0 author: Hermes Agent license: MIT +prerequisites: + env_vars: [TENOR_API_KEY] + commands: [curl, jq] metadata: hermes: tags: [GIF, Media, Search, Tenor, API] @@ -13,32 +16,43 @@ metadata: Search and download GIFs directly via the Tenor API using curl. No extra tools needed. +## Setup + +Set your Tenor API key in your environment (add to `~/.hermes/.env`): + +```bash +TENOR_API_KEY=your_key_here +``` + +Get a free API key at https://developers.google.com/tenor/guides/quickstart — the Google Cloud Console Tenor API key is free and has generous rate limits. + ## Prerequisites -- `curl` and `jq` (both standard on Linux) +- `curl` and `jq` (both standard on macOS/Linux) +- `TENOR_API_KEY` environment variable ## Search for GIFs ```bash # Search and get GIF URLs -curl -s "https://tenor.googleapis.com/v2/search?q=thumbs+up&limit=5&key=AIzaSyAyimkuYQYF_FXVALexPuGQctUWRURdCYQ" | jq -r '.results[].media_formats.gif.url' +curl -s "https://tenor.googleapis.com/v2/search?q=thumbs+up&limit=5&key=${TENOR_API_KEY}" | jq -r '.results[].media_formats.gif.url' # Get smaller/preview versions -curl -s "https://tenor.googleapis.com/v2/search?q=nice+work&limit=3&key=AIzaSyAyimkuYQYF_FXVALexPuGQctUWRURdCYQ" | jq -r '.results[].media_formats.tinygif.url' +curl -s "https://tenor.googleapis.com/v2/search?q=nice+work&limit=3&key=${TENOR_API_KEY}" | jq -r '.results[].media_formats.tinygif.url' ``` ## Download a GIF ```bash # Search and download the top result -URL=$(curl -s "https://tenor.googleapis.com/v2/search?q=celebration&limit=1&key=AIzaSyAyimkuYQYF_FXVALexPuGQctUWRURdCYQ" | jq -r '.results[0].media_formats.gif.url') +URL=$(curl -s "https://tenor.googleapis.com/v2/search?q=celebration&limit=1&key=${TENOR_API_KEY}" | jq -r '.results[0].media_formats.gif.url') curl -sL "$URL" -o celebration.gif ``` ## Get Full Metadata ```bash -curl -s "https://tenor.googleapis.com/v2/search?q=cat&limit=3&key=AIzaSyAyimkuYQYF_FXVALexPuGQctUWRURdCYQ" | jq '.results[] | {title: .title, url: .media_formats.gif.url, preview: .media_formats.tinygif.url, dimensions: .media_formats.gif.dims}' +curl -s "https://tenor.googleapis.com/v2/search?q=cat&limit=3&key=${TENOR_API_KEY}" | jq '.results[] | {title: .title, url: .media_formats.gif.url, preview: .media_formats.tinygif.url, dimensions: .media_formats.gif.dims}' ``` ## API Parameters @@ -47,7 +61,7 @@ curl -s "https://tenor.googleapis.com/v2/search?q=cat&limit=3&key=AIzaSyAyimkuYQ |-----------|-------------| | `q` | Search query (URL-encode spaces as `+`) | | `limit` | Max results (1-50, default 20) | -| `key` | API key (the one above is Tenor's public demo key) | +| `key` | API key (from `$TENOR_API_KEY` env var) | | `media_filter` | Filter formats: `gif`, `tinygif`, `mp4`, `tinymp4`, `webm` | | `contentfilter` | Safety: `off`, `low`, `medium`, `high` | | `locale` | Language: `en_US`, `es`, `fr`, etc. | @@ -67,7 +81,6 @@ Each result has multiple formats under `.media_formats`: ## Notes -- The API key above is Tenor's public demo key — it works but has rate limits - URL-encode the query: spaces as `+`, special chars as `%XX` - For sending in chat, `tinygif` URLs are lighter weight - GIF URLs can be used directly in markdown: `![alt](url)` diff --git a/skills/github/codebase-inspection/SKILL.md b/skills/github/codebase-inspection/SKILL.md index ca71ffdf9..6954ad841 100644 --- a/skills/github/codebase-inspection/SKILL.md +++ b/skills/github/codebase-inspection/SKILL.md @@ -8,6 +8,8 @@ metadata: hermes: tags: [LOC, Code Analysis, pygount, Codebase, Metrics, Repository] related_skills: [github-repo-management] +prerequisites: + commands: [pygount] --- # Codebase Inspection with pygount diff --git a/skills/mcp/mcporter/SKILL.md b/skills/mcp/mcporter/SKILL.md index 0bb08441c..acb6fcfb0 100644 --- a/skills/mcp/mcporter/SKILL.md +++ b/skills/mcp/mcporter/SKILL.md @@ -8,6 +8,8 @@ metadata: hermes: tags: [MCP, Tools, API, Integrations, Interop] homepage: https://mcporter.dev +prerequisites: + commands: [npx] --- # mcporter diff --git a/skills/music-creation/songsee/SKILL.md b/skills/music-creation/songsee/SKILL.md index 4ad4752e3..11bcca0c7 100644 --- a/skills/music-creation/songsee/SKILL.md +++ b/skills/music-creation/songsee/SKILL.md @@ -8,6 +8,8 @@ metadata: hermes: tags: [Audio, Visualization, Spectrogram, Music, Analysis] homepage: https://github.com/steipete/songsee +prerequisites: + commands: [songsee] --- # songsee diff --git a/skills/productivity/notion/SKILL.md b/skills/productivity/notion/SKILL.md index eb6cf1c2b..c74d0df61 100644 --- a/skills/productivity/notion/SKILL.md +++ b/skills/productivity/notion/SKILL.md @@ -8,6 +8,8 @@ metadata: hermes: tags: [Notion, Productivity, Notes, Database, API] homepage: https://developers.notion.com +prerequisites: + env_vars: [NOTION_API_KEY] --- # Notion API diff --git a/skills/research/duckduckgo-search/SKILL.md b/skills/research/duckduckgo-search/SKILL.md index 33742ff18..8066b09cc 100644 --- a/skills/research/duckduckgo-search/SKILL.md +++ b/skills/research/duckduckgo-search/SKILL.md @@ -8,6 +8,8 @@ metadata: hermes: tags: [search, duckduckgo, web-search, free, fallback] related_skills: [arxiv] +prerequisites: + commands: [ddgs] --- # DuckDuckGo Search (Firecrawl Fallback) diff --git a/skills/smart-home/openhue/SKILL.md b/skills/smart-home/openhue/SKILL.md index 9b2252856..b3efd1700 100644 --- a/skills/smart-home/openhue/SKILL.md +++ b/skills/smart-home/openhue/SKILL.md @@ -8,6 +8,8 @@ metadata: hermes: tags: [Smart-Home, Hue, Lights, IoT, Automation] homepage: https://www.openhue.io/cli +prerequisites: + commands: [openhue] --- # OpenHue CLI diff --git a/tests/agent/test_prompt_builder.py b/tests/agent/test_prompt_builder.py index a35983b5f..dbedf184d 100644 --- a/tests/agent/test_prompt_builder.py +++ b/tests/agent/test_prompt_builder.py @@ -8,6 +8,7 @@ from agent.prompt_builder import ( _scan_context_content, _truncate_content, _read_skill_description, + _skill_prerequisites_met, build_skills_system_prompt, build_context_files_prompt, CONTEXT_FILE_MAX_CHARS, @@ -211,6 +212,69 @@ class TestBuildSkillsSystemPrompt: assert "imessage" in result assert "Send iMessages" in result + def test_excludes_skills_with_unmet_prerequisites(self, monkeypatch, tmp_path): + """Skills with missing env var prerequisites should not appear.""" + monkeypatch.setenv("HERMES_HOME", str(tmp_path)) + monkeypatch.delenv("MISSING_API_KEY_XYZ", raising=False) + skills_dir = tmp_path / "skills" / "media" + + gated = skills_dir / "gated-skill" + gated.mkdir(parents=True) + (gated / "SKILL.md").write_text( + "---\nname: gated-skill\ndescription: Needs a key\n" + "prerequisites:\n env_vars: [MISSING_API_KEY_XYZ]\n---\n" + ) + + available = skills_dir / "free-skill" + available.mkdir(parents=True) + (available / "SKILL.md").write_text( + "---\nname: free-skill\ndescription: No prereqs\n---\n" + ) + + result = build_skills_system_prompt() + assert "free-skill" in result + assert "gated-skill" not in result + + def test_includes_skills_with_met_prerequisites(self, monkeypatch, tmp_path): + """Skills with satisfied prerequisites should appear normally.""" + monkeypatch.setenv("HERMES_HOME", str(tmp_path)) + monkeypatch.setenv("MY_API_KEY", "test_value") + skills_dir = tmp_path / "skills" / "media" + + skill = skills_dir / "ready-skill" + skill.mkdir(parents=True) + (skill / "SKILL.md").write_text( + "---\nname: ready-skill\ndescription: Has key\n" + "prerequisites:\n env_vars: [MY_API_KEY]\n---\n" + ) + + result = build_skills_system_prompt() + assert "ready-skill" in result + + +# ========================================================================= +# _skill_prerequisites_met +# ========================================================================= + + +class TestSkillPrerequisitesMet: + def test_met_or_absent(self, tmp_path, monkeypatch): + """No prereqs, met prereqs, and missing file all return True.""" + monkeypatch.setenv("PRESENT_KEY_123", "val") + basic = tmp_path / "basic.md" + basic.write_text("---\nname: basic\ndescription: basic\n---\n") + ready = tmp_path / "ready.md" + ready.write_text("---\nname: ready\ndescription: ready\nprerequisites:\n env_vars: [PRESENT_KEY_123]\n---\n") + assert _skill_prerequisites_met(basic) is True + assert _skill_prerequisites_met(ready) is True + assert _skill_prerequisites_met(tmp_path / "nope.md") is True + + def test_unmet_returns_false(self, tmp_path, monkeypatch): + monkeypatch.delenv("NONEXISTENT_KEY_ABC", raising=False) + skill = tmp_path / "SKILL.md" + skill.write_text("---\nname: gated\ndescription: gated\nprerequisites:\n env_vars: [NONEXISTENT_KEY_ABC]\n---\n") + assert _skill_prerequisites_met(skill) is False + # ========================================================================= # Context files prompt builder diff --git a/tests/tools/test_skills_tool.py b/tests/tools/test_skills_tool.py index 629d3b478..aab9ed10a 100644 --- a/tests/tools/test_skills_tool.py +++ b/tests/tools/test_skills_tool.py @@ -11,6 +11,7 @@ from tools.skills_tool import ( _estimate_tokens, _find_all_skills, _load_category_description, + check_skill_prerequisites, skill_matches_platform, skills_list, skills_categories, @@ -464,3 +465,124 @@ class TestFindAllSkillsPlatformFiltering: assert len(skills_darwin) == 1 assert len(skills_linux) == 1 assert len(skills_win) == 0 + + +# --------------------------------------------------------------------------- +# check_skill_prerequisites +# --------------------------------------------------------------------------- + + +class TestCheckSkillPrerequisites: + def test_no_or_empty_prerequisites(self): + """No field, empty dict, or non-dict all pass.""" + assert check_skill_prerequisites({})[0] is True + assert check_skill_prerequisites({"prerequisites": {}})[0] is True + assert check_skill_prerequisites({"prerequisites": "curl"})[0] is True + + def test_env_var_present_and_missing(self, monkeypatch): + monkeypatch.setenv("MY_TEST_KEY", "val") + monkeypatch.delenv("NONEXISTENT_TEST_VAR_XYZ", raising=False) + assert check_skill_prerequisites({"prerequisites": {"env_vars": ["MY_TEST_KEY"]}})[0] is True + met, missing = check_skill_prerequisites({"prerequisites": {"env_vars": ["NONEXISTENT_TEST_VAR_XYZ"]}}) + assert met is False + assert "env $NONEXISTENT_TEST_VAR_XYZ" in missing + + def test_command_present_and_missing(self): + assert check_skill_prerequisites({"prerequisites": {"commands": ["python3"]}})[0] is True + met, missing = check_skill_prerequisites({"prerequisites": {"commands": ["nonexistent_binary_xyz_123"]}}) + assert met is False + assert "command `nonexistent_binary_xyz_123`" in missing + + def test_mixed_env_and_commands(self, monkeypatch): + monkeypatch.delenv("MISSING_A", raising=False) + met, missing = check_skill_prerequisites({ + "prerequisites": { + "env_vars": ["MISSING_A"], + "commands": ["python3", "nonexistent_cmd_xyz"], + } + }) + assert met is False + assert len(missing) == 2 + + def test_string_instead_of_list(self, monkeypatch): + """YAML scalar (string) should be coerced to a single-element list.""" + monkeypatch.delenv("SOLO_VAR", raising=False) + assert check_skill_prerequisites({"prerequisites": {"env_vars": "SOLO_VAR"}})[0] is False + assert check_skill_prerequisites({"prerequisites": {"commands": "nonexistent_cmd_xyz_solo"}})[0] is False + + +# --------------------------------------------------------------------------- +# _find_all_skills — prerequisites integration +# --------------------------------------------------------------------------- + + +class TestFindAllSkillsPrerequisites: + def test_skills_with_unmet_prereqs_flagged(self, tmp_path, monkeypatch): + monkeypatch.delenv("NONEXISTENT_API_KEY_XYZ", raising=False) + with patch("tools.skills_tool.SKILLS_DIR", tmp_path): + _make_skill( + tmp_path, "needs-key", + frontmatter_extra="prerequisites:\n env_vars: [NONEXISTENT_API_KEY_XYZ]\n", + ) + skills = _find_all_skills() + assert len(skills) == 1 + assert skills[0]["prerequisites_met"] is False + assert any("NONEXISTENT_API_KEY_XYZ" in m for m in skills[0]["prerequisites_missing"]) + + def test_skills_with_met_prereqs_no_flag(self, tmp_path, monkeypatch): + monkeypatch.setenv("MY_PRESENT_KEY", "val") + with patch("tools.skills_tool.SKILLS_DIR", tmp_path): + _make_skill( + tmp_path, "has-key", + frontmatter_extra="prerequisites:\n env_vars: [MY_PRESENT_KEY]\n", + ) + skills = _find_all_skills() + assert len(skills) == 1 + assert "prerequisites_met" not in skills[0] + + def test_skills_without_prereqs_no_flag(self, tmp_path): + with patch("tools.skills_tool.SKILLS_DIR", tmp_path): + _make_skill(tmp_path, "simple-skill") + skills = _find_all_skills() + assert len(skills) == 1 + assert "prerequisites_met" not in skills[0] + + +# --------------------------------------------------------------------------- +# skill_view — prerequisites warnings +# --------------------------------------------------------------------------- + + +class TestSkillViewPrerequisites: + def test_warns_on_unmet_prerequisites(self, tmp_path, monkeypatch): + monkeypatch.delenv("MISSING_KEY_XYZ", raising=False) + with patch("tools.skills_tool.SKILLS_DIR", tmp_path): + _make_skill( + tmp_path, "gated-skill", + frontmatter_extra="prerequisites:\n env_vars: [MISSING_KEY_XYZ]\n", + ) + raw = skill_view("gated-skill") + result = json.loads(raw) + assert result["success"] is True + assert result["prerequisites_met"] is False + assert "MISSING_KEY_XYZ" in result["prerequisites_warning"] + + def test_no_warning_when_prereqs_met(self, tmp_path, monkeypatch): + monkeypatch.setenv("PRESENT_KEY", "value") + with patch("tools.skills_tool.SKILLS_DIR", tmp_path): + _make_skill( + tmp_path, "ready-skill", + frontmatter_extra="prerequisites:\n env_vars: [PRESENT_KEY]\n", + ) + raw = skill_view("ready-skill") + result = json.loads(raw) + assert result["success"] is True + assert "prerequisites_warning" not in result + + def test_no_warning_when_no_prereqs(self, tmp_path): + with patch("tools.skills_tool.SKILLS_DIR", tmp_path): + _make_skill(tmp_path, "plain-skill") + raw = skill_view("plain-skill") + result = json.loads(raw) + assert result["success"] is True + assert "prerequisites_warning" not in result diff --git a/tools/skills_tool.py b/tools/skills_tool.py index e8baa0f59..dce15c449 100644 --- a/tools/skills_tool.py +++ b/tools/skills_tool.py @@ -34,6 +34,11 @@ SKILL.md Format (YAML Frontmatter, agentskills.io compatible): platforms: [macos] # Optional — restrict to specific OS platforms # Valid: macos, linux, windows # Omit to load on all platforms (default) + prerequisites: # Optional — runtime requirements + env_vars: [API_KEY] # Env vars that must be set (checked via os.getenv) + commands: [curl, jq] # CLI binaries that must be on PATH (checked via shutil.which) + # Skills with unmet prerequisites are hidden from the + # system prompt and flagged with a warning in skill_view. compatibility: Requires X # Optional (agentskills.io) metadata: # Optional, arbitrary key-value (agentskills.io) hermes: @@ -65,6 +70,7 @@ Usage: import json import os import re +import shutil import sys from pathlib import Path from typing import Dict, Any, List, Optional, Tuple @@ -118,6 +124,43 @@ def skill_matches_platform(frontmatter: Dict[str, Any]) -> bool: return False +def check_skill_prerequisites(frontmatter: Dict[str, Any]) -> Tuple[bool, List[str]]: + """Check if a skill's declared prerequisites are satisfied. + + Skills declare prerequisites via a top-level ``prerequisites`` dict + in their YAML frontmatter:: + + prerequisites: + env_vars: [TENOR_API_KEY] + commands: [curl, jq] + + Returns: + (all_met, missing) — True + empty list if all met, else False + list + of human-readable descriptions of what's missing. + """ + prereqs = frontmatter.get("prerequisites") + if not prereqs or not isinstance(prereqs, dict): + return True, [] + + missing: List[str] = [] + + env_vars = prereqs.get("env_vars") or [] + if isinstance(env_vars, str): + env_vars = [env_vars] + for var in env_vars: + if not os.getenv(str(var)): + missing.append(f"env ${var}") + + commands = prereqs.get("commands") or [] + if isinstance(commands, str): + commands = [commands] + for cmd in commands: + if not shutil.which(str(cmd)): + missing.append(f"command `{cmd}`") + + return (len(missing) == 0), missing + + def check_skills_requirements() -> bool: """Skills are always available -- the directory is created on first use if needed.""" return True @@ -262,12 +305,19 @@ def _find_all_skills() -> List[Dict[str, Any]]: description = description[:MAX_DESCRIPTION_LENGTH - 3] + "..." category = _get_category_from_path(skill_md) - - skills.append({ + + prereqs_met, prereqs_missing = check_skill_prerequisites(frontmatter) + + entry = { "name": name, "description": description, "category": category, - }) + } + if not prereqs_met: + entry["prerequisites_met"] = False + entry["prerequisites_missing"] = prereqs_missing + + skills.append(entry) except Exception: continue @@ -635,6 +685,17 @@ def skill_view(name: str, file_path: str = None, task_id: str = None) -> str: "usage_hint": "To view linked files, call skill_view(name, file_path) where file_path is e.g. 'references/api.md' or 'assets/config.yaml'" if linked_files else None } + # Prerequisite check — warn the agent if requirements are unmet + prereqs_met, prereqs_missing = check_skill_prerequisites(frontmatter) + if not prereqs_met: + result["prerequisites_met"] = False + result["prerequisites_missing"] = prereqs_missing + result["prerequisites_warning"] = ( + f"This skill requires {', '.join(prereqs_missing)} which " + f"{'is' if len(prereqs_missing) == 1 else 'are'} not available. " + f"Tell the user what's needed before attempting to use this skill." + ) + # Surface agentskills.io optional fields when present if frontmatter.get('compatibility'): result["compatibility"] = frontmatter['compatibility']