mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
Revert "feat: skill prerequisites — hide skills with unmet runtime dependencies"
This commit is contained in:
parent
0df7df52f3
commit
b8120df860
17 changed files with 11 additions and 336 deletions
|
|
@ -328,11 +328,6 @@ 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]
|
||||
|
|
@ -371,25 +366,6 @@ 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`).
|
||||
|
|
|
|||
|
|
@ -170,22 +170,6 @@ 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.
|
||||
|
||||
|
|
@ -207,9 +191,6 @@ 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:
|
||||
|
|
|
|||
|
|
@ -9,8 +9,6 @@ metadata:
|
|||
hermes:
|
||||
tags: [Notes, Apple, macOS, note-taking]
|
||||
related_skills: [obsidian]
|
||||
prerequisites:
|
||||
commands: [memo]
|
||||
---
|
||||
|
||||
# Apple Notes
|
||||
|
|
|
|||
|
|
@ -8,8 +8,6 @@ platforms: [macos]
|
|||
metadata:
|
||||
hermes:
|
||||
tags: [Reminders, tasks, todo, macOS, Apple]
|
||||
prerequisites:
|
||||
commands: [remindctl]
|
||||
---
|
||||
|
||||
# Apple Reminders
|
||||
|
|
|
|||
|
|
@ -8,8 +8,6 @@ platforms: [macos]
|
|||
metadata:
|
||||
hermes:
|
||||
tags: [iMessage, SMS, messaging, macOS, Apple]
|
||||
prerequisites:
|
||||
commands: [imsg]
|
||||
---
|
||||
|
||||
# iMessage
|
||||
|
|
|
|||
|
|
@ -8,8 +8,6 @@ metadata:
|
|||
hermes:
|
||||
tags: [Email, IMAP, SMTP, CLI, Communication]
|
||||
homepage: https://github.com/pimalaya/himalaya
|
||||
prerequisites:
|
||||
commands: [himalaya]
|
||||
---
|
||||
|
||||
# Himalaya Email CLI
|
||||
|
|
|
|||
|
|
@ -8,8 +8,6 @@ metadata:
|
|||
hermes:
|
||||
tags: [RSS, Blogs, Feed-Reader, Monitoring]
|
||||
homepage: https://github.com/Hyaxia/blogwatcher
|
||||
prerequisites:
|
||||
commands: [blogwatcher]
|
||||
---
|
||||
|
||||
# Blogwatcher
|
||||
|
|
|
|||
|
|
@ -1,12 +1,9 @@
|
|||
---
|
||||
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.1.0
|
||||
version: 1.0.0
|
||||
author: Hermes Agent
|
||||
license: MIT
|
||||
prerequisites:
|
||||
env_vars: [TENOR_API_KEY]
|
||||
commands: [curl, jq]
|
||||
metadata:
|
||||
hermes:
|
||||
tags: [GIF, Media, Search, Tenor, API]
|
||||
|
|
@ -16,43 +13,32 @@ 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 macOS/Linux)
|
||||
- `TENOR_API_KEY` environment variable
|
||||
- `curl` and `jq` (both standard on Linux)
|
||||
|
||||
## Search for GIFs
|
||||
|
||||
```bash
|
||||
# Search and get GIF URLs
|
||||
curl -s "https://tenor.googleapis.com/v2/search?q=thumbs+up&limit=5&key=${TENOR_API_KEY}" | jq -r '.results[].media_formats.gif.url'
|
||||
curl -s "https://tenor.googleapis.com/v2/search?q=thumbs+up&limit=5&key=AIzaSyAyimkuYQYF_FXVALexPuGQctUWRURdCYQ" | 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=${TENOR_API_KEY}" | jq -r '.results[].media_formats.tinygif.url'
|
||||
curl -s "https://tenor.googleapis.com/v2/search?q=nice+work&limit=3&key=AIzaSyAyimkuYQYF_FXVALexPuGQctUWRURdCYQ" | 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=${TENOR_API_KEY}" | jq -r '.results[0].media_formats.gif.url')
|
||||
URL=$(curl -s "https://tenor.googleapis.com/v2/search?q=celebration&limit=1&key=AIzaSyAyimkuYQYF_FXVALexPuGQctUWRURdCYQ" | 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=${TENOR_API_KEY}" | 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=AIzaSyAyimkuYQYF_FXVALexPuGQctUWRURdCYQ" | jq '.results[] | {title: .title, url: .media_formats.gif.url, preview: .media_formats.tinygif.url, dimensions: .media_formats.gif.dims}'
|
||||
```
|
||||
|
||||
## API Parameters
|
||||
|
|
@ -61,7 +47,7 @@ curl -s "https://tenor.googleapis.com/v2/search?q=cat&limit=3&key=${TENOR_API_KE
|
|||
|-----------|-------------|
|
||||
| `q` | Search query (URL-encode spaces as `+`) |
|
||||
| `limit` | Max results (1-50, default 20) |
|
||||
| `key` | API key (from `$TENOR_API_KEY` env var) |
|
||||
| `key` | API key (the one above is Tenor's public demo key) |
|
||||
| `media_filter` | Filter formats: `gif`, `tinygif`, `mp4`, `tinymp4`, `webm` |
|
||||
| `contentfilter` | Safety: `off`, `low`, `medium`, `high` |
|
||||
| `locale` | Language: `en_US`, `es`, `fr`, etc. |
|
||||
|
|
@ -81,6 +67,7 @@ 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: ``
|
||||
|
|
|
|||
|
|
@ -8,8 +8,6 @@ metadata:
|
|||
hermes:
|
||||
tags: [LOC, Code Analysis, pygount, Codebase, Metrics, Repository]
|
||||
related_skills: [github-repo-management]
|
||||
prerequisites:
|
||||
commands: [pygount]
|
||||
---
|
||||
|
||||
# Codebase Inspection with pygount
|
||||
|
|
|
|||
|
|
@ -8,8 +8,6 @@ metadata:
|
|||
hermes:
|
||||
tags: [MCP, Tools, API, Integrations, Interop]
|
||||
homepage: https://mcporter.dev
|
||||
prerequisites:
|
||||
commands: [npx]
|
||||
---
|
||||
|
||||
# mcporter
|
||||
|
|
|
|||
|
|
@ -8,8 +8,6 @@ metadata:
|
|||
hermes:
|
||||
tags: [Audio, Visualization, Spectrogram, Music, Analysis]
|
||||
homepage: https://github.com/steipete/songsee
|
||||
prerequisites:
|
||||
commands: [songsee]
|
||||
---
|
||||
|
||||
# songsee
|
||||
|
|
|
|||
|
|
@ -8,8 +8,6 @@ metadata:
|
|||
hermes:
|
||||
tags: [Notion, Productivity, Notes, Database, API]
|
||||
homepage: https://developers.notion.com
|
||||
prerequisites:
|
||||
env_vars: [NOTION_API_KEY]
|
||||
---
|
||||
|
||||
# Notion API
|
||||
|
|
|
|||
|
|
@ -8,8 +8,6 @@ metadata:
|
|||
hermes:
|
||||
tags: [search, duckduckgo, web-search, free, fallback]
|
||||
related_skills: [arxiv]
|
||||
prerequisites:
|
||||
commands: [ddgs]
|
||||
---
|
||||
|
||||
# DuckDuckGo Search (Firecrawl Fallback)
|
||||
|
|
|
|||
|
|
@ -8,8 +8,6 @@ metadata:
|
|||
hermes:
|
||||
tags: [Smart-Home, Hue, Lights, IoT, Automation]
|
||||
homepage: https://www.openhue.io/cli
|
||||
prerequisites:
|
||||
commands: [openhue]
|
||||
---
|
||||
|
||||
# OpenHue CLI
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ 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,
|
||||
|
|
@ -212,69 +211,6 @@ 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
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ from tools.skills_tool import (
|
|||
_estimate_tokens,
|
||||
_find_all_skills,
|
||||
_load_category_description,
|
||||
check_skill_prerequisites,
|
||||
skill_matches_platform,
|
||||
skills_list,
|
||||
skills_categories,
|
||||
|
|
@ -465,124 +464,3 @@ 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
|
||||
|
|
|
|||
|
|
@ -34,11 +34,6 @@ 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:
|
||||
|
|
@ -70,7 +65,6 @@ Usage:
|
|||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, List, Optional, Tuple
|
||||
|
|
@ -124,43 +118,6 @@ 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
|
||||
|
|
@ -305,19 +262,12 @@ def _find_all_skills() -> List[Dict[str, Any]]:
|
|||
description = description[:MAX_DESCRIPTION_LENGTH - 3] + "..."
|
||||
|
||||
category = _get_category_from_path(skill_md)
|
||||
|
||||
prereqs_met, prereqs_missing = check_skill_prerequisites(frontmatter)
|
||||
|
||||
entry = {
|
||||
|
||||
skills.append({
|
||||
"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
|
||||
|
|
@ -685,17 +635,6 @@ 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']
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue