feat: platform-conditional skill loading + Apple/macOS skills
Add a 'platforms' field to SKILL.md frontmatter that restricts skills to specific operating systems. Skills with platforms: [macos] only appear in the system prompt, skills_list(), and slash commands on macOS. Skills without the field load everywhere (backward compatible). Implementation: - skill_matches_platform() in tools/skills_tool.py — core filter - Wired into all 3 discovery paths: prompt_builder.py, skills_tool.py, skill_commands.py - 28 new tests across 3 test files New bundled Apple/macOS skills (all platforms: [macos]): - imessage — Send/receive iMessages via imsg CLI - apple-reminders — Manage Reminders via remindctl CLI - apple-notes — Manage Notes via memo CLI - findmy — Track devices/AirTags via AppleScript + screen capture Docs updated: CONTRIBUTING.md, AGENTS.md, creating-skills.md, skills.md (user guide)
This commit is contained in:
parent
74fe1e2254
commit
f668e9fc75
15 changed files with 803 additions and 2 deletions
|
|
@ -204,7 +204,7 @@ Every installed skill in `~/.hermes/skills/` is automatically registered as a sl
|
||||||
The skill name (from frontmatter or folder name) becomes the command: `axolotl` → `/axolotl`.
|
The skill name (from frontmatter or folder name) becomes the command: `axolotl` → `/axolotl`.
|
||||||
|
|
||||||
Implementation (`agent/skill_commands.py`, shared between CLI and gateway):
|
Implementation (`agent/skill_commands.py`, shared between CLI and gateway):
|
||||||
1. `scan_skill_commands()` scans all SKILL.md files at startup
|
1. `scan_skill_commands()` scans all SKILL.md files at startup, filtering out skills incompatible with the current OS platform (via the `platforms` frontmatter field)
|
||||||
2. `build_skill_invocation_message()` loads the SKILL.md content and builds a user-turn message
|
2. `build_skill_invocation_message()` loads the SKILL.md content and builds a user-turn message
|
||||||
3. The message includes the full skill content, a list of supporting files (not loaded), and the user's instruction
|
3. The message includes the full skill content, a list of supporting files (not loaded), and the user's instruction
|
||||||
4. Supporting files can be loaded on demand via the `skill_view` tool
|
4. Supporting files can be loaded on demand via the `skill_view` tool
|
||||||
|
|
@ -657,6 +657,7 @@ SKILL.md files use YAML frontmatter (agentskills.io format):
|
||||||
name: skill-name
|
name: skill-name
|
||||||
description: Brief description for listing
|
description: Brief description for listing
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
|
platforms: [macos] # Optional — restrict to specific OS (macos/linux/windows)
|
||||||
metadata:
|
metadata:
|
||||||
hermes:
|
hermes:
|
||||||
tags: [tag1, tag2]
|
tags: [tag1, tag2]
|
||||||
|
|
@ -665,6 +666,8 @@ metadata:
|
||||||
# Skill Content...
|
# Skill Content...
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Platform filtering** — Skills with a `platforms` field are automatically excluded from the system prompt index, `skills_list()`, and slash commands on incompatible platforms. Skills without the field load everywhere (backward compatible). See `skills/apple/` for macOS-only examples (iMessage, Reminders, Notes, FindMy).
|
||||||
|
|
||||||
**Skills Hub** — user-driven skill search/install from online registries and official optional skills. Sources: official optional skills (shipped with repo, labeled "official"), GitHub (openai/skills, anthropics/skills, custom taps), ClawHub, Claude marketplace, LobeHub. Not exposed as an agent tool — the model cannot search for or install skills. Users manage skills via `hermes skills browse/search/install` CLI commands or the `/skills` slash command in chat.
|
**Skills Hub** — user-driven skill search/install from online registries and official optional skills. Sources: official optional skills (shipped with repo, labeled "official"), GitHub (openai/skills, anthropics/skills, custom taps), ClawHub, Claude marketplace, LobeHub. Not exposed as an agent tool — the model cannot search for or install skills. Users manage skills via `hermes skills browse/search/install` CLI commands or the `/skills` slash command in chat.
|
||||||
|
|
||||||
Key files:
|
Key files:
|
||||||
|
|
|
||||||
|
|
@ -325,6 +325,9 @@ description: Brief description (shown in skill search results)
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
author: Your Name
|
author: Your Name
|
||||||
license: MIT
|
license: MIT
|
||||||
|
platforms: [macos, linux] # Optional — restrict to specific OS platforms
|
||||||
|
# Valid: macos, linux, windows
|
||||||
|
# Omit to load on all platforms (default)
|
||||||
metadata:
|
metadata:
|
||||||
hermes:
|
hermes:
|
||||||
tags: [Category, Subcategory, Keywords]
|
tags: [Category, Subcategory, Keywords]
|
||||||
|
|
@ -351,6 +354,18 @@ Known failure modes and how to handle them.
|
||||||
How the agent confirms it worked.
|
How the agent confirms it worked.
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Platform-specific skills
|
||||||
|
|
||||||
|
Skills can declare which OS platforms they support via the `platforms` frontmatter field. Skills with this field are automatically hidden from the system prompt, `skills_list()`, and slash commands on incompatible platforms.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
platforms: [macos] # macOS only (e.g., iMessage, Apple Reminders)
|
||||||
|
platforms: [macos, linux] # macOS and Linux
|
||||||
|
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 guidelines
|
### Skill guidelines
|
||||||
|
|
||||||
- **No external dependencies unless absolutely necessary.** Prefer stdlib Python, curl, and existing Hermes tools (`web_extract`, `terminal`, `read_file`).
|
- **No external dependencies unless absolutely necessary.** Prefer stdlib Python, curl, and existing Hermes tools (`web_extract`, `terminal`, `read_file`).
|
||||||
|
|
|
||||||
|
|
@ -142,12 +142,28 @@ def _read_skill_description(skill_file: Path, max_chars: int = 60) -> str:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def _skill_is_platform_compatible(skill_file: Path) -> bool:
|
||||||
|
"""Quick check if a SKILL.md is compatible with the current OS platform.
|
||||||
|
|
||||||
|
Reads just enough to parse the ``platforms`` frontmatter field.
|
||||||
|
Skills without the field (the vast majority) are always compatible.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
from tools.skills_tool import _parse_frontmatter, skill_matches_platform
|
||||||
|
raw = skill_file.read_text(encoding="utf-8")[:2000]
|
||||||
|
frontmatter, _ = _parse_frontmatter(raw)
|
||||||
|
return skill_matches_platform(frontmatter)
|
||||||
|
except Exception:
|
||||||
|
return True # Err on the side of showing the skill
|
||||||
|
|
||||||
|
|
||||||
def build_skills_system_prompt() -> str:
|
def build_skills_system_prompt() -> str:
|
||||||
"""Build a compact skill index for the system prompt.
|
"""Build a compact skill index for the system prompt.
|
||||||
|
|
||||||
Scans ~/.hermes/skills/ for SKILL.md files grouped by category.
|
Scans ~/.hermes/skills/ for SKILL.md files grouped by category.
|
||||||
Includes per-skill descriptions from frontmatter so the model can
|
Includes per-skill descriptions from frontmatter so the model can
|
||||||
match skills by meaning, not just name.
|
match skills by meaning, not just name.
|
||||||
|
Filters out skills incompatible with the current OS platform.
|
||||||
"""
|
"""
|
||||||
hermes_home = Path(os.getenv("HERMES_HOME", Path.home() / ".hermes"))
|
hermes_home = Path(os.getenv("HERMES_HOME", Path.home() / ".hermes"))
|
||||||
skills_dir = hermes_home / "skills"
|
skills_dir = hermes_home / "skills"
|
||||||
|
|
@ -159,6 +175,9 @@ def build_skills_system_prompt() -> str:
|
||||||
# Each entry: (skill_name, description)
|
# Each entry: (skill_name, description)
|
||||||
skills_by_category: dict[str, list[tuple[str, str]]] = {}
|
skills_by_category: dict[str, list[tuple[str, str]]] = {}
|
||||||
for skill_file in skills_dir.rglob("SKILL.md"):
|
for skill_file in skills_dir.rglob("SKILL.md"):
|
||||||
|
# Skip skills incompatible with the current OS platform
|
||||||
|
if not _skill_is_platform_compatible(skill_file):
|
||||||
|
continue
|
||||||
rel_path = skill_file.relative_to(skills_dir)
|
rel_path = skill_file.relative_to(skills_dir)
|
||||||
parts = rel_path.parts
|
parts = rel_path.parts
|
||||||
if len(parts) >= 2:
|
if len(parts) >= 2:
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ def scan_skill_commands() -> Dict[str, Dict[str, Any]]:
|
||||||
global _skill_commands
|
global _skill_commands
|
||||||
_skill_commands = {}
|
_skill_commands = {}
|
||||||
try:
|
try:
|
||||||
from tools.skills_tool import SKILLS_DIR, _parse_frontmatter
|
from tools.skills_tool import SKILLS_DIR, _parse_frontmatter, skill_matches_platform
|
||||||
if not SKILLS_DIR.exists():
|
if not SKILLS_DIR.exists():
|
||||||
return _skill_commands
|
return _skill_commands
|
||||||
for skill_md in SKILLS_DIR.rglob("SKILL.md"):
|
for skill_md in SKILLS_DIR.rglob("SKILL.md"):
|
||||||
|
|
@ -31,6 +31,9 @@ def scan_skill_commands() -> Dict[str, Dict[str, Any]]:
|
||||||
try:
|
try:
|
||||||
content = skill_md.read_text(encoding='utf-8')
|
content = skill_md.read_text(encoding='utf-8')
|
||||||
frontmatter, body = _parse_frontmatter(content)
|
frontmatter, body = _parse_frontmatter(content)
|
||||||
|
# Skip skills incompatible with the current OS platform
|
||||||
|
if not skill_matches_platform(frontmatter):
|
||||||
|
continue
|
||||||
name = frontmatter.get('name', skill_md.parent.name)
|
name = frontmatter.get('name', skill_md.parent.name)
|
||||||
description = frontmatter.get('description', '')
|
description = frontmatter.get('description', '')
|
||||||
if not description:
|
if not description:
|
||||||
|
|
|
||||||
3
skills/apple/DESCRIPTION.md
Normal file
3
skills/apple/DESCRIPTION.md
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
---
|
||||||
|
description: Apple/macOS-specific skills — iMessage, Reminders, Notes, FindMy, and macOS automation. These skills only load on macOS systems.
|
||||||
|
---
|
||||||
88
skills/apple/apple-notes/SKILL.md
Normal file
88
skills/apple/apple-notes/SKILL.md
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
---
|
||||||
|
name: apple-notes
|
||||||
|
description: Manage Apple Notes via the memo CLI on macOS (create, view, search, edit).
|
||||||
|
version: 1.0.0
|
||||||
|
author: Hermes Agent
|
||||||
|
license: MIT
|
||||||
|
platforms: [macos]
|
||||||
|
metadata:
|
||||||
|
hermes:
|
||||||
|
tags: [Notes, Apple, macOS, note-taking]
|
||||||
|
related_skills: [obsidian]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Apple Notes
|
||||||
|
|
||||||
|
Use `memo` to manage Apple Notes directly from the terminal. Notes sync across all Apple devices via iCloud.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- **macOS** with Notes.app
|
||||||
|
- Install: `brew tap antoniorodr/memo && brew install antoniorodr/memo/memo`
|
||||||
|
- Grant Automation access to Notes.app when prompted (System Settings → Privacy → Automation)
|
||||||
|
|
||||||
|
## When to Use
|
||||||
|
|
||||||
|
- User asks to create, view, or search Apple Notes
|
||||||
|
- Saving information to Notes.app for cross-device access
|
||||||
|
- Organizing notes into folders
|
||||||
|
- Exporting notes to Markdown/HTML
|
||||||
|
|
||||||
|
## When NOT to Use
|
||||||
|
|
||||||
|
- Obsidian vault management → use the `obsidian` skill
|
||||||
|
- Bear Notes → separate app (not supported here)
|
||||||
|
- Quick agent-only notes → use the `memory` tool instead
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
### View Notes
|
||||||
|
|
||||||
|
```bash
|
||||||
|
memo notes # List all notes
|
||||||
|
memo notes -f "Folder Name" # Filter by folder
|
||||||
|
memo notes -s "query" # Search notes (fuzzy)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Create Notes
|
||||||
|
|
||||||
|
```bash
|
||||||
|
memo notes -a # Interactive editor
|
||||||
|
memo notes -a "Note Title" # Quick add with title
|
||||||
|
```
|
||||||
|
|
||||||
|
### Edit Notes
|
||||||
|
|
||||||
|
```bash
|
||||||
|
memo notes -e # Interactive selection to edit
|
||||||
|
```
|
||||||
|
|
||||||
|
### Delete Notes
|
||||||
|
|
||||||
|
```bash
|
||||||
|
memo notes -d # Interactive selection to delete
|
||||||
|
```
|
||||||
|
|
||||||
|
### Move Notes
|
||||||
|
|
||||||
|
```bash
|
||||||
|
memo notes -m # Move note to folder (interactive)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Export Notes
|
||||||
|
|
||||||
|
```bash
|
||||||
|
memo notes -ex # Export to HTML/Markdown
|
||||||
|
```
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
|
||||||
|
- Cannot edit notes containing images or attachments
|
||||||
|
- Interactive prompts require terminal access (use pty=true if needed)
|
||||||
|
- macOS only — requires Apple Notes.app
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
1. Prefer Apple Notes when user wants cross-device sync (iPhone/iPad/Mac)
|
||||||
|
2. Use the `memory` tool for agent-internal notes that don't need to sync
|
||||||
|
3. Use the `obsidian` skill for Markdown-native knowledge management
|
||||||
96
skills/apple/apple-reminders/SKILL.md
Normal file
96
skills/apple/apple-reminders/SKILL.md
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
---
|
||||||
|
name: apple-reminders
|
||||||
|
description: Manage Apple Reminders via remindctl CLI (list, add, complete, delete).
|
||||||
|
version: 1.0.0
|
||||||
|
author: Hermes Agent
|
||||||
|
license: MIT
|
||||||
|
platforms: [macos]
|
||||||
|
metadata:
|
||||||
|
hermes:
|
||||||
|
tags: [Reminders, tasks, todo, macOS, Apple]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Apple Reminders
|
||||||
|
|
||||||
|
Use `remindctl` to manage Apple Reminders directly from the terminal. Tasks sync across all Apple devices via iCloud.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- **macOS** with Reminders.app
|
||||||
|
- Install: `brew install steipete/tap/remindctl`
|
||||||
|
- Grant Reminders permission when prompted
|
||||||
|
- Check: `remindctl status` / Request: `remindctl authorize`
|
||||||
|
|
||||||
|
## When to Use
|
||||||
|
|
||||||
|
- User mentions "reminder" or "Reminders app"
|
||||||
|
- Creating personal to-dos with due dates that sync to iOS
|
||||||
|
- Managing Apple Reminders lists
|
||||||
|
- User wants tasks to appear on their iPhone/iPad
|
||||||
|
|
||||||
|
## When NOT to Use
|
||||||
|
|
||||||
|
- Scheduling agent alerts → use the cronjob tool instead
|
||||||
|
- Calendar events → use Apple Calendar or Google Calendar
|
||||||
|
- Project task management → use GitHub Issues, Notion, etc.
|
||||||
|
- If user says "remind me" but means an agent alert → clarify first
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
### View Reminders
|
||||||
|
|
||||||
|
```bash
|
||||||
|
remindctl # Today's reminders
|
||||||
|
remindctl today # Today
|
||||||
|
remindctl tomorrow # Tomorrow
|
||||||
|
remindctl week # This week
|
||||||
|
remindctl overdue # Past due
|
||||||
|
remindctl all # Everything
|
||||||
|
remindctl 2026-01-04 # Specific date
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manage Lists
|
||||||
|
|
||||||
|
```bash
|
||||||
|
remindctl list # List all lists
|
||||||
|
remindctl list Work # Show specific list
|
||||||
|
remindctl list Projects --create # Create list
|
||||||
|
remindctl list Work --delete # Delete list
|
||||||
|
```
|
||||||
|
|
||||||
|
### Create Reminders
|
||||||
|
|
||||||
|
```bash
|
||||||
|
remindctl add "Buy milk"
|
||||||
|
remindctl add --title "Call mom" --list Personal --due tomorrow
|
||||||
|
remindctl add --title "Meeting prep" --due "2026-02-15 09:00"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Complete / Delete
|
||||||
|
|
||||||
|
```bash
|
||||||
|
remindctl complete 1 2 3 # Complete by ID
|
||||||
|
remindctl delete 4A83 --force # Delete by ID
|
||||||
|
```
|
||||||
|
|
||||||
|
### Output Formats
|
||||||
|
|
||||||
|
```bash
|
||||||
|
remindctl today --json # JSON for scripting
|
||||||
|
remindctl today --plain # TSV format
|
||||||
|
remindctl today --quiet # Counts only
|
||||||
|
```
|
||||||
|
|
||||||
|
## Date Formats
|
||||||
|
|
||||||
|
Accepted by `--due` and date filters:
|
||||||
|
- `today`, `tomorrow`, `yesterday`
|
||||||
|
- `YYYY-MM-DD`
|
||||||
|
- `YYYY-MM-DD HH:mm`
|
||||||
|
- ISO 8601 (`2026-01-04T12:34:56Z`)
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
1. When user says "remind me", clarify: Apple Reminders (syncs to phone) vs agent cronjob alert
|
||||||
|
2. Always confirm reminder content and due date before creating
|
||||||
|
3. Use `--json` for programmatic parsing
|
||||||
131
skills/apple/findmy/SKILL.md
Normal file
131
skills/apple/findmy/SKILL.md
Normal file
|
|
@ -0,0 +1,131 @@
|
||||||
|
---
|
||||||
|
name: findmy
|
||||||
|
description: Track Apple devices and AirTags via FindMy.app on macOS using AppleScript and screen capture.
|
||||||
|
version: 1.0.0
|
||||||
|
author: Hermes Agent
|
||||||
|
license: MIT
|
||||||
|
platforms: [macos]
|
||||||
|
metadata:
|
||||||
|
hermes:
|
||||||
|
tags: [FindMy, AirTag, location, tracking, macOS, Apple]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Find My (Apple)
|
||||||
|
|
||||||
|
Track Apple devices and AirTags via the FindMy.app on macOS. Since Apple doesn't
|
||||||
|
provide a CLI for FindMy, this skill uses AppleScript to open the app and
|
||||||
|
screen capture to read device locations.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- **macOS** with Find My app and iCloud signed in
|
||||||
|
- Devices/AirTags already registered in Find My
|
||||||
|
- Screen Recording permission for terminal (System Settings → Privacy → Screen Recording)
|
||||||
|
- **Optional but recommended**: Install `peekaboo` for better UI automation:
|
||||||
|
`brew install steipete/tap/peekaboo`
|
||||||
|
|
||||||
|
## When to Use
|
||||||
|
|
||||||
|
- User asks "where is my [device/cat/keys/bag]?"
|
||||||
|
- Tracking AirTag locations
|
||||||
|
- Checking device locations (iPhone, iPad, Mac, AirPods)
|
||||||
|
- Monitoring pet or item movement over time (AirTag patrol routes)
|
||||||
|
|
||||||
|
## Method 1: AppleScript + Screenshot (Basic)
|
||||||
|
|
||||||
|
### Open FindMy and Navigate
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Open Find My app
|
||||||
|
osascript -e 'tell application "FindMy" to activate'
|
||||||
|
|
||||||
|
# Wait for it to load
|
||||||
|
sleep 3
|
||||||
|
|
||||||
|
# Take a screenshot of the Find My window
|
||||||
|
screencapture -w -o /tmp/findmy.png
|
||||||
|
```
|
||||||
|
|
||||||
|
Then use `vision_analyze` to read the screenshot:
|
||||||
|
```
|
||||||
|
vision_analyze(image_url="/tmp/findmy.png", question="What devices/items are shown and what are their locations?")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Switch Between Tabs
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Switch to Devices tab
|
||||||
|
osascript -e '
|
||||||
|
tell application "System Events"
|
||||||
|
tell process "FindMy"
|
||||||
|
click button "Devices" of toolbar 1 of window 1
|
||||||
|
end tell
|
||||||
|
end tell'
|
||||||
|
|
||||||
|
# Switch to Items tab (AirTags)
|
||||||
|
osascript -e '
|
||||||
|
tell application "System Events"
|
||||||
|
tell process "FindMy"
|
||||||
|
click button "Items" of toolbar 1 of window 1
|
||||||
|
end tell
|
||||||
|
end tell'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Method 2: Peekaboo UI Automation (Recommended)
|
||||||
|
|
||||||
|
If `peekaboo` is installed, use it for more reliable UI interaction:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Open Find My
|
||||||
|
osascript -e 'tell application "FindMy" to activate'
|
||||||
|
sleep 3
|
||||||
|
|
||||||
|
# Capture and annotate the UI
|
||||||
|
peekaboo see --app "FindMy" --annotate --path /tmp/findmy-ui.png
|
||||||
|
|
||||||
|
# Click on a specific device/item by element ID
|
||||||
|
peekaboo click --on B3 --app "FindMy"
|
||||||
|
|
||||||
|
# Capture the detail view
|
||||||
|
peekaboo image --app "FindMy" --path /tmp/findmy-detail.png
|
||||||
|
```
|
||||||
|
|
||||||
|
Then analyze with vision:
|
||||||
|
```
|
||||||
|
vision_analyze(image_url="/tmp/findmy-detail.png", question="What is the location shown for this device/item? Include address and coordinates if visible.")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Workflow: Track AirTag Location Over Time
|
||||||
|
|
||||||
|
For monitoring an AirTag (e.g., tracking a cat's patrol route):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Open FindMy to Items tab
|
||||||
|
osascript -e 'tell application "FindMy" to activate'
|
||||||
|
sleep 3
|
||||||
|
|
||||||
|
# 2. Click on the AirTag item (stay on page — AirTag only updates when page is open)
|
||||||
|
|
||||||
|
# 3. Periodically capture location
|
||||||
|
while true; do
|
||||||
|
screencapture -w -o /tmp/findmy-$(date +%H%M%S).png
|
||||||
|
sleep 300 # Every 5 minutes
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
Analyze each screenshot with vision to extract coordinates, then compile a route.
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
|
||||||
|
- FindMy has **no CLI or API** — must use UI automation
|
||||||
|
- AirTags only update location while the FindMy page is actively displayed
|
||||||
|
- Location accuracy depends on nearby Apple devices in the FindMy network
|
||||||
|
- Screen Recording permission required for screenshots
|
||||||
|
- AppleScript UI automation may break across macOS versions
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
1. Keep FindMy app in the foreground when tracking AirTags (updates stop when minimized)
|
||||||
|
2. Use `vision_analyze` to read screenshot content — don't try to parse pixels
|
||||||
|
3. For ongoing tracking, use a cronjob to periodically capture and log locations
|
||||||
|
4. Respect privacy — only track devices/items the user owns
|
||||||
100
skills/apple/imessage/SKILL.md
Normal file
100
skills/apple/imessage/SKILL.md
Normal file
|
|
@ -0,0 +1,100 @@
|
||||||
|
---
|
||||||
|
name: imessage
|
||||||
|
description: Send and receive iMessages/SMS via the imsg CLI on macOS.
|
||||||
|
version: 1.0.0
|
||||||
|
author: Hermes Agent
|
||||||
|
license: MIT
|
||||||
|
platforms: [macos]
|
||||||
|
metadata:
|
||||||
|
hermes:
|
||||||
|
tags: [iMessage, SMS, messaging, macOS, Apple]
|
||||||
|
---
|
||||||
|
|
||||||
|
# iMessage
|
||||||
|
|
||||||
|
Use `imsg` to read and send iMessage/SMS via macOS Messages.app.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- **macOS** with Messages.app signed in
|
||||||
|
- Install: `brew install steipete/tap/imsg`
|
||||||
|
- Grant Full Disk Access for terminal (System Settings → Privacy → Full Disk Access)
|
||||||
|
- Grant Automation permission for Messages.app when prompted
|
||||||
|
|
||||||
|
## When to Use
|
||||||
|
|
||||||
|
- User asks to send an iMessage or text message
|
||||||
|
- Reading iMessage conversation history
|
||||||
|
- Checking recent Messages.app chats
|
||||||
|
- Sending to phone numbers or Apple IDs
|
||||||
|
|
||||||
|
## When NOT to Use
|
||||||
|
|
||||||
|
- Telegram/Discord/Slack/WhatsApp messages → use the appropriate gateway channel
|
||||||
|
- Group chat management (adding/removing members) → not supported
|
||||||
|
- Bulk/mass messaging → always confirm with user first
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
### List Chats
|
||||||
|
|
||||||
|
```bash
|
||||||
|
imsg chats --limit 10 --json
|
||||||
|
```
|
||||||
|
|
||||||
|
### View History
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# By chat ID
|
||||||
|
imsg history --chat-id 1 --limit 20 --json
|
||||||
|
|
||||||
|
# With attachments info
|
||||||
|
imsg history --chat-id 1 --limit 20 --attachments --json
|
||||||
|
```
|
||||||
|
|
||||||
|
### Send Messages
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Text only
|
||||||
|
imsg send --to "+14155551212" --text "Hello!"
|
||||||
|
|
||||||
|
# With attachment
|
||||||
|
imsg send --to "+14155551212" --text "Check this out" --file /path/to/image.jpg
|
||||||
|
|
||||||
|
# Force iMessage or SMS
|
||||||
|
imsg send --to "+14155551212" --text "Hi" --service imessage
|
||||||
|
imsg send --to "+14155551212" --text "Hi" --service sms
|
||||||
|
```
|
||||||
|
|
||||||
|
### Watch for New Messages
|
||||||
|
|
||||||
|
```bash
|
||||||
|
imsg watch --chat-id 1 --attachments
|
||||||
|
```
|
||||||
|
|
||||||
|
## Service Options
|
||||||
|
|
||||||
|
- `--service imessage` — Force iMessage (requires recipient has iMessage)
|
||||||
|
- `--service sms` — Force SMS (green bubble)
|
||||||
|
- `--service auto` — Let Messages.app decide (default)
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
1. **Always confirm recipient and message content** before sending
|
||||||
|
2. **Never send to unknown numbers** without explicit user approval
|
||||||
|
3. **Verify file paths** exist before attaching
|
||||||
|
4. **Don't spam** — rate-limit yourself
|
||||||
|
|
||||||
|
## Example Workflow
|
||||||
|
|
||||||
|
User: "Text mom that I'll be late"
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Find mom's chat
|
||||||
|
imsg chats --limit 20 --json | jq '.[] | select(.displayName | contains("Mom"))'
|
||||||
|
|
||||||
|
# 2. Confirm with user: "Found Mom at +1555123456. Send 'I'll be late' via iMessage?"
|
||||||
|
|
||||||
|
# 3. Send after confirmation
|
||||||
|
imsg send --to "+1555123456" --text "I'll be late"
|
||||||
|
```
|
||||||
|
|
@ -165,6 +165,52 @@ class TestBuildSkillsSystemPrompt:
|
||||||
# "search" should appear only once per category
|
# "search" should appear only once per category
|
||||||
assert result.count("- search") == 1
|
assert result.count("- search") == 1
|
||||||
|
|
||||||
|
def test_excludes_incompatible_platform_skills(self, monkeypatch, tmp_path):
|
||||||
|
"""Skills with platforms: [macos] should not appear on Linux."""
|
||||||
|
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
||||||
|
skills_dir = tmp_path / "skills" / "apple"
|
||||||
|
skills_dir.mkdir(parents=True)
|
||||||
|
|
||||||
|
# macOS-only skill
|
||||||
|
mac_skill = skills_dir / "imessage"
|
||||||
|
mac_skill.mkdir()
|
||||||
|
(mac_skill / "SKILL.md").write_text(
|
||||||
|
"---\nname: imessage\ndescription: Send iMessages\nplatforms: [macos]\n---\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Universal skill
|
||||||
|
uni_skill = skills_dir / "web-search"
|
||||||
|
uni_skill.mkdir()
|
||||||
|
(uni_skill / "SKILL.md").write_text(
|
||||||
|
"---\nname: web-search\ndescription: Search the web\n---\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
from unittest.mock import patch
|
||||||
|
with patch("tools.skills_tool.sys") as mock_sys:
|
||||||
|
mock_sys.platform = "linux"
|
||||||
|
result = build_skills_system_prompt()
|
||||||
|
|
||||||
|
assert "web-search" in result
|
||||||
|
assert "imessage" not in result
|
||||||
|
|
||||||
|
def test_includes_matching_platform_skills(self, monkeypatch, tmp_path):
|
||||||
|
"""Skills with platforms: [macos] should appear on macOS."""
|
||||||
|
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
||||||
|
skills_dir = tmp_path / "skills" / "apple"
|
||||||
|
mac_skill = skills_dir / "imessage"
|
||||||
|
mac_skill.mkdir(parents=True)
|
||||||
|
(mac_skill / "SKILL.md").write_text(
|
||||||
|
"---\nname: imessage\ndescription: Send iMessages\nplatforms: [macos]\n---\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
from unittest.mock import patch
|
||||||
|
with patch("tools.skills_tool.sys") as mock_sys:
|
||||||
|
mock_sys.platform = "darwin"
|
||||||
|
result = build_skills_system_prompt()
|
||||||
|
|
||||||
|
assert "imessage" in result
|
||||||
|
assert "Send iMessages" in result
|
||||||
|
|
||||||
|
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
# Context files prompt builder
|
# Context files prompt builder
|
||||||
|
|
|
||||||
87
tests/agent/test_skill_commands.py
Normal file
87
tests/agent/test_skill_commands.py
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
"""Tests for agent/skill_commands.py — skill slash command scanning and platform filtering."""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from agent.skill_commands import scan_skill_commands, build_skill_invocation_message
|
||||||
|
|
||||||
|
|
||||||
|
def _make_skill(skills_dir, name, frontmatter_extra="", body="Do the thing.", category=None):
|
||||||
|
"""Helper to create a minimal skill directory with SKILL.md."""
|
||||||
|
if category:
|
||||||
|
skill_dir = skills_dir / category / name
|
||||||
|
else:
|
||||||
|
skill_dir = skills_dir / name
|
||||||
|
skill_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
content = f"""\
|
||||||
|
---
|
||||||
|
name: {name}
|
||||||
|
description: Description for {name}.
|
||||||
|
{frontmatter_extra}---
|
||||||
|
|
||||||
|
# {name}
|
||||||
|
|
||||||
|
{body}
|
||||||
|
"""
|
||||||
|
(skill_dir / "SKILL.md").write_text(content)
|
||||||
|
return skill_dir
|
||||||
|
|
||||||
|
|
||||||
|
class TestScanSkillCommands:
|
||||||
|
def test_finds_skills(self, tmp_path):
|
||||||
|
with patch("tools.skills_tool.SKILLS_DIR", tmp_path):
|
||||||
|
_make_skill(tmp_path, "my-skill")
|
||||||
|
result = scan_skill_commands()
|
||||||
|
assert "/my-skill" in result
|
||||||
|
assert result["/my-skill"]["name"] == "my-skill"
|
||||||
|
|
||||||
|
def test_empty_dir(self, tmp_path):
|
||||||
|
with patch("tools.skills_tool.SKILLS_DIR", tmp_path):
|
||||||
|
result = scan_skill_commands()
|
||||||
|
assert result == {}
|
||||||
|
|
||||||
|
def test_excludes_incompatible_platform(self, tmp_path):
|
||||||
|
"""macOS-only skills should not register slash commands on Linux."""
|
||||||
|
with patch("tools.skills_tool.SKILLS_DIR", tmp_path), \
|
||||||
|
patch("tools.skills_tool.sys") as mock_sys:
|
||||||
|
mock_sys.platform = "linux"
|
||||||
|
_make_skill(tmp_path, "imessage", frontmatter_extra="platforms: [macos]\n")
|
||||||
|
_make_skill(tmp_path, "web-search")
|
||||||
|
result = scan_skill_commands()
|
||||||
|
assert "/web-search" in result
|
||||||
|
assert "/imessage" not in result
|
||||||
|
|
||||||
|
def test_includes_matching_platform(self, tmp_path):
|
||||||
|
"""macOS-only skills should register slash commands on macOS."""
|
||||||
|
with patch("tools.skills_tool.SKILLS_DIR", tmp_path), \
|
||||||
|
patch("tools.skills_tool.sys") as mock_sys:
|
||||||
|
mock_sys.platform = "darwin"
|
||||||
|
_make_skill(tmp_path, "imessage", frontmatter_extra="platforms: [macos]\n")
|
||||||
|
result = scan_skill_commands()
|
||||||
|
assert "/imessage" in result
|
||||||
|
|
||||||
|
def test_universal_skill_on_any_platform(self, tmp_path):
|
||||||
|
"""Skills without platforms field should register on any platform."""
|
||||||
|
with patch("tools.skills_tool.SKILLS_DIR", tmp_path), \
|
||||||
|
patch("tools.skills_tool.sys") as mock_sys:
|
||||||
|
mock_sys.platform = "win32"
|
||||||
|
_make_skill(tmp_path, "generic-tool")
|
||||||
|
result = scan_skill_commands()
|
||||||
|
assert "/generic-tool" in result
|
||||||
|
|
||||||
|
|
||||||
|
class TestBuildSkillInvocationMessage:
|
||||||
|
def test_builds_message(self, tmp_path):
|
||||||
|
with patch("tools.skills_tool.SKILLS_DIR", tmp_path):
|
||||||
|
_make_skill(tmp_path, "test-skill")
|
||||||
|
scan_skill_commands()
|
||||||
|
msg = build_skill_invocation_message("/test-skill", "do stuff")
|
||||||
|
assert msg is not None
|
||||||
|
assert "test-skill" in msg
|
||||||
|
assert "do stuff" in msg
|
||||||
|
|
||||||
|
def test_returns_none_for_unknown(self, tmp_path):
|
||||||
|
with patch("tools.skills_tool.SKILLS_DIR", tmp_path):
|
||||||
|
scan_skill_commands()
|
||||||
|
msg = build_skill_invocation_message("/nonexistent")
|
||||||
|
assert msg is None
|
||||||
|
|
@ -11,6 +11,7 @@ from tools.skills_tool import (
|
||||||
_estimate_tokens,
|
_estimate_tokens,
|
||||||
_find_all_skills,
|
_find_all_skills,
|
||||||
_load_category_description,
|
_load_category_description,
|
||||||
|
skill_matches_platform,
|
||||||
skills_list,
|
skills_list,
|
||||||
skills_categories,
|
skills_categories,
|
||||||
skill_view,
|
skill_view,
|
||||||
|
|
@ -332,3 +333,134 @@ class TestSkillsCategories:
|
||||||
result = json.loads(raw)
|
result = json.loads(raw)
|
||||||
assert result["success"] is True
|
assert result["success"] is True
|
||||||
assert result["categories"] == []
|
assert result["categories"] == []
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# skill_matches_platform
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
class TestSkillMatchesPlatform:
|
||||||
|
"""Tests for the platforms frontmatter field filtering."""
|
||||||
|
|
||||||
|
def test_no_platforms_field_matches_everything(self):
|
||||||
|
"""Skills without a platforms field should load on any OS."""
|
||||||
|
assert skill_matches_platform({}) is True
|
||||||
|
assert skill_matches_platform({"name": "foo"}) is True
|
||||||
|
|
||||||
|
def test_empty_platforms_matches_everything(self):
|
||||||
|
"""Empty platforms list should load on any OS."""
|
||||||
|
assert skill_matches_platform({"platforms": []}) is True
|
||||||
|
assert skill_matches_platform({"platforms": None}) is True
|
||||||
|
|
||||||
|
def test_macos_on_darwin(self):
|
||||||
|
with patch("tools.skills_tool.sys") as mock_sys:
|
||||||
|
mock_sys.platform = "darwin"
|
||||||
|
assert skill_matches_platform({"platforms": ["macos"]}) is True
|
||||||
|
|
||||||
|
def test_macos_on_linux(self):
|
||||||
|
with patch("tools.skills_tool.sys") as mock_sys:
|
||||||
|
mock_sys.platform = "linux"
|
||||||
|
assert skill_matches_platform({"platforms": ["macos"]}) is False
|
||||||
|
|
||||||
|
def test_linux_on_linux(self):
|
||||||
|
with patch("tools.skills_tool.sys") as mock_sys:
|
||||||
|
mock_sys.platform = "linux"
|
||||||
|
assert skill_matches_platform({"platforms": ["linux"]}) is True
|
||||||
|
|
||||||
|
def test_linux_on_darwin(self):
|
||||||
|
with patch("tools.skills_tool.sys") as mock_sys:
|
||||||
|
mock_sys.platform = "darwin"
|
||||||
|
assert skill_matches_platform({"platforms": ["linux"]}) is False
|
||||||
|
|
||||||
|
def test_windows_on_win32(self):
|
||||||
|
with patch("tools.skills_tool.sys") as mock_sys:
|
||||||
|
mock_sys.platform = "win32"
|
||||||
|
assert skill_matches_platform({"platforms": ["windows"]}) is True
|
||||||
|
|
||||||
|
def test_windows_on_linux(self):
|
||||||
|
with patch("tools.skills_tool.sys") as mock_sys:
|
||||||
|
mock_sys.platform = "linux"
|
||||||
|
assert skill_matches_platform({"platforms": ["windows"]}) is False
|
||||||
|
|
||||||
|
def test_multi_platform_match(self):
|
||||||
|
"""Skills listing multiple platforms should match any of them."""
|
||||||
|
with patch("tools.skills_tool.sys") as mock_sys:
|
||||||
|
mock_sys.platform = "darwin"
|
||||||
|
assert skill_matches_platform({"platforms": ["macos", "linux"]}) is True
|
||||||
|
mock_sys.platform = "linux"
|
||||||
|
assert skill_matches_platform({"platforms": ["macos", "linux"]}) is True
|
||||||
|
mock_sys.platform = "win32"
|
||||||
|
assert skill_matches_platform({"platforms": ["macos", "linux"]}) is False
|
||||||
|
|
||||||
|
def test_string_instead_of_list(self):
|
||||||
|
"""A single string value should be treated as a one-element list."""
|
||||||
|
with patch("tools.skills_tool.sys") as mock_sys:
|
||||||
|
mock_sys.platform = "darwin"
|
||||||
|
assert skill_matches_platform({"platforms": "macos"}) is True
|
||||||
|
mock_sys.platform = "linux"
|
||||||
|
assert skill_matches_platform({"platforms": "macos"}) is False
|
||||||
|
|
||||||
|
def test_case_insensitive(self):
|
||||||
|
with patch("tools.skills_tool.sys") as mock_sys:
|
||||||
|
mock_sys.platform = "darwin"
|
||||||
|
assert skill_matches_platform({"platforms": ["MacOS"]}) is True
|
||||||
|
assert skill_matches_platform({"platforms": ["MACOS"]}) is True
|
||||||
|
|
||||||
|
def test_unknown_platform_no_match(self):
|
||||||
|
with patch("tools.skills_tool.sys") as mock_sys:
|
||||||
|
mock_sys.platform = "linux"
|
||||||
|
assert skill_matches_platform({"platforms": ["freebsd"]}) is False
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# _find_all_skills — platform filtering integration
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
class TestFindAllSkillsPlatformFiltering:
|
||||||
|
"""Test that _find_all_skills respects the platforms field."""
|
||||||
|
|
||||||
|
def test_excludes_incompatible_platform(self, tmp_path):
|
||||||
|
with patch("tools.skills_tool.SKILLS_DIR", tmp_path), \
|
||||||
|
patch("tools.skills_tool.sys") as mock_sys:
|
||||||
|
mock_sys.platform = "linux"
|
||||||
|
_make_skill(tmp_path, "universal-skill")
|
||||||
|
_make_skill(tmp_path, "mac-only", frontmatter_extra="platforms: [macos]\n")
|
||||||
|
skills = _find_all_skills()
|
||||||
|
names = {s["name"] for s in skills}
|
||||||
|
assert "universal-skill" in names
|
||||||
|
assert "mac-only" not in names
|
||||||
|
|
||||||
|
def test_includes_matching_platform(self, tmp_path):
|
||||||
|
with patch("tools.skills_tool.SKILLS_DIR", tmp_path), \
|
||||||
|
patch("tools.skills_tool.sys") as mock_sys:
|
||||||
|
mock_sys.platform = "darwin"
|
||||||
|
_make_skill(tmp_path, "mac-only", frontmatter_extra="platforms: [macos]\n")
|
||||||
|
skills = _find_all_skills()
|
||||||
|
names = {s["name"] for s in skills}
|
||||||
|
assert "mac-only" in names
|
||||||
|
|
||||||
|
def test_no_platforms_always_included(self, tmp_path):
|
||||||
|
"""Skills without platforms field should appear on any platform."""
|
||||||
|
with patch("tools.skills_tool.SKILLS_DIR", tmp_path), \
|
||||||
|
patch("tools.skills_tool.sys") as mock_sys:
|
||||||
|
mock_sys.platform = "win32"
|
||||||
|
_make_skill(tmp_path, "generic-skill")
|
||||||
|
skills = _find_all_skills()
|
||||||
|
assert len(skills) == 1
|
||||||
|
assert skills[0]["name"] == "generic-skill"
|
||||||
|
|
||||||
|
def test_multi_platform_skill(self, tmp_path):
|
||||||
|
with patch("tools.skills_tool.SKILLS_DIR", tmp_path), \
|
||||||
|
patch("tools.skills_tool.sys") as mock_sys:
|
||||||
|
_make_skill(tmp_path, "cross-plat", frontmatter_extra="platforms: [macos, linux]\n")
|
||||||
|
mock_sys.platform = "darwin"
|
||||||
|
skills_darwin = _find_all_skills()
|
||||||
|
mock_sys.platform = "linux"
|
||||||
|
skills_linux = _find_all_skills()
|
||||||
|
mock_sys.platform = "win32"
|
||||||
|
skills_win = _find_all_skills()
|
||||||
|
assert len(skills_darwin) == 1
|
||||||
|
assert len(skills_linux) == 1
|
||||||
|
assert len(skills_win) == 0
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,9 @@ SKILL.md Format (YAML Frontmatter, agentskills.io compatible):
|
||||||
description: Brief description # Required, max 1024 chars
|
description: Brief description # Required, max 1024 chars
|
||||||
version: 1.0.0 # Optional
|
version: 1.0.0 # Optional
|
||||||
license: MIT # Optional (agentskills.io)
|
license: MIT # Optional (agentskills.io)
|
||||||
|
platforms: [macos] # Optional — restrict to specific OS platforms
|
||||||
|
# Valid: macos, linux, windows
|
||||||
|
# Omit to load on all platforms (default)
|
||||||
compatibility: Requires X # Optional (agentskills.io)
|
compatibility: Requires X # Optional (agentskills.io)
|
||||||
metadata: # Optional, arbitrary key-value (agentskills.io)
|
metadata: # Optional, arbitrary key-value (agentskills.io)
|
||||||
hermes:
|
hermes:
|
||||||
|
|
@ -62,6 +65,7 @@ Usage:
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, Any, List, Optional, Tuple
|
from typing import Dict, Any, List, Optional, Tuple
|
||||||
|
|
||||||
|
|
@ -78,6 +82,41 @@ SKILLS_DIR = HERMES_HOME / "skills"
|
||||||
MAX_NAME_LENGTH = 64
|
MAX_NAME_LENGTH = 64
|
||||||
MAX_DESCRIPTION_LENGTH = 1024
|
MAX_DESCRIPTION_LENGTH = 1024
|
||||||
|
|
||||||
|
# Platform identifiers for the 'platforms' frontmatter field.
|
||||||
|
# Maps user-friendly names to sys.platform prefixes.
|
||||||
|
_PLATFORM_MAP = {
|
||||||
|
"macos": "darwin",
|
||||||
|
"linux": "linux",
|
||||||
|
"windows": "win32",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def skill_matches_platform(frontmatter: Dict[str, Any]) -> bool:
|
||||||
|
"""Check if a skill is compatible with the current OS platform.
|
||||||
|
|
||||||
|
Skills declare platform requirements via a top-level ``platforms`` list
|
||||||
|
in their YAML frontmatter::
|
||||||
|
|
||||||
|
platforms: [macos] # macOS only
|
||||||
|
platforms: [macos, linux] # macOS and Linux
|
||||||
|
|
||||||
|
Valid values: ``macos``, ``linux``, ``windows``.
|
||||||
|
|
||||||
|
If the field is absent or empty the skill is compatible with **all**
|
||||||
|
platforms (backward-compatible default).
|
||||||
|
"""
|
||||||
|
platforms = frontmatter.get("platforms")
|
||||||
|
if not platforms:
|
||||||
|
return True # No restriction → loads everywhere
|
||||||
|
if not isinstance(platforms, list):
|
||||||
|
platforms = [platforms]
|
||||||
|
current = sys.platform
|
||||||
|
for p in platforms:
|
||||||
|
mapped = _PLATFORM_MAP.get(str(p).lower().strip(), str(p).lower().strip())
|
||||||
|
if current.startswith(mapped):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def check_skills_requirements() -> bool:
|
def check_skills_requirements() -> bool:
|
||||||
"""Skills are always available -- the directory is created on first use if needed."""
|
"""Skills are always available -- the directory is created on first use if needed."""
|
||||||
|
|
@ -204,6 +243,10 @@ def _find_all_skills() -> List[Dict[str, Any]]:
|
||||||
try:
|
try:
|
||||||
content = skill_md.read_text(encoding='utf-8')
|
content = skill_md.read_text(encoding='utf-8')
|
||||||
frontmatter, body = _parse_frontmatter(content)
|
frontmatter, body = _parse_frontmatter(content)
|
||||||
|
|
||||||
|
# Skip skills incompatible with the current OS platform
|
||||||
|
if not skill_matches_platform(frontmatter):
|
||||||
|
continue
|
||||||
|
|
||||||
name = frontmatter.get('name', skill_dir.name)[:MAX_NAME_LENGTH]
|
name = frontmatter.get('name', skill_dir.name)[:MAX_NAME_LENGTH]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,9 @@ description: Brief description (shown in skill search results)
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
author: Your Name
|
author: Your Name
|
||||||
license: MIT
|
license: MIT
|
||||||
|
platforms: [macos, linux] # Optional — restrict to specific OS platforms
|
||||||
|
# Valid: macos, linux, windows
|
||||||
|
# Omit to load on all platforms (default)
|
||||||
metadata:
|
metadata:
|
||||||
hermes:
|
hermes:
|
||||||
tags: [Category, Subcategory, Keywords]
|
tags: [Category, Subcategory, Keywords]
|
||||||
|
|
@ -76,6 +79,20 @@ Known failure modes and how to handle them.
|
||||||
How the agent confirms it worked.
|
How the agent confirms it worked.
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Platform-Specific Skills
|
||||||
|
|
||||||
|
Skills can restrict themselves to specific operating systems using the `platforms` field:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
platforms: [macos] # macOS only (e.g., iMessage, Apple Reminders)
|
||||||
|
platforms: [macos, linux] # macOS and Linux
|
||||||
|
platforms: [windows] # Windows only
|
||||||
|
```
|
||||||
|
|
||||||
|
When set, the skill is automatically hidden from the system prompt, `skills_list()`, and slash commands on incompatible platforms. If omitted or empty, the skill loads on all platforms (backward compatible).
|
||||||
|
|
||||||
|
See `skills/apple/` for examples of macOS-only skills.
|
||||||
|
|
||||||
## Skill Guidelines
|
## Skill Guidelines
|
||||||
|
|
||||||
### No External Dependencies
|
### No External Dependencies
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,7 @@ The agent only loads the full skill content when it actually needs it.
|
||||||
name: my-skill
|
name: my-skill
|
||||||
description: Brief description of what this skill does
|
description: Brief description of what this skill does
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
|
platforms: [macos, linux] # Optional — restrict to specific OS platforms
|
||||||
metadata:
|
metadata:
|
||||||
hermes:
|
hermes:
|
||||||
tags: [python, automation]
|
tags: [python, automation]
|
||||||
|
|
@ -72,6 +73,23 @@ Trigger conditions for this skill.
|
||||||
How to confirm it worked.
|
How to confirm it worked.
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Platform-Specific Skills
|
||||||
|
|
||||||
|
Skills can restrict themselves to specific operating systems using the `platforms` field:
|
||||||
|
|
||||||
|
| Value | Matches |
|
||||||
|
|-------|---------|
|
||||||
|
| `macos` | macOS (Darwin) |
|
||||||
|
| `linux` | Linux |
|
||||||
|
| `windows` | Windows |
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
platforms: [macos] # macOS only (e.g., iMessage, Apple Reminders, FindMy)
|
||||||
|
platforms: [macos, linux] # macOS and Linux
|
||||||
|
```
|
||||||
|
|
||||||
|
When set, the skill is automatically hidden from the system prompt, `skills_list()`, and slash commands on incompatible platforms. If omitted, the skill loads on all platforms.
|
||||||
|
|
||||||
## Skill Directory Structure
|
## Skill Directory Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue