fix: unify skill metadata format

This commit is contained in:
Re-bin 2026-02-01 18:45:42 +00:00
parent d888e51d1c
commit ac527d40d7
10 changed files with 56 additions and 37 deletions

View File

@ -11,7 +11,6 @@ class MemoryStore:
Memory system for the agent. Memory system for the agent.
Supports daily notes (memory/YYYY-MM-DD.md) and long-term memory (MEMORY.md). Supports daily notes (memory/YYYY-MM-DD.md) and long-term memory (MEMORY.md).
Compatible with clawbot memory format.
""" """
def __init__(self, workspace: Path): def __init__(self, workspace: Path):

View File

@ -53,7 +53,7 @@ class SkillsLoader:
# Filter by requirements # Filter by requirements
if filter_unavailable: if filter_unavailable:
return [s for s in skills if self._check_requirements(self._get_ocmeta(s["name"]))] return [s for s in skills if self._check_requirements(self._get_skill_meta(s["name"]))]
return skills return skills
def load_skill(self, name: str) -> str | None: def load_skill(self, name: str) -> str | None:
@ -120,8 +120,8 @@ class SkillsLoader:
name = escape_xml(s["name"]) name = escape_xml(s["name"])
path = s["path"] path = s["path"]
desc = escape_xml(self._get_skill_description(s["name"])) desc = escape_xml(self._get_skill_description(s["name"]))
ocmeta = self._get_ocmeta(s["name"]) skill_meta = self._get_skill_meta(s["name"])
available = self._check_requirements(ocmeta) available = self._check_requirements(skill_meta)
lines.append(f" <skill available=\"{str(available).lower()}\">") lines.append(f" <skill available=\"{str(available).lower()}\">")
lines.append(f" <name>{name}</name>") lines.append(f" <name>{name}</name>")
@ -130,7 +130,7 @@ class SkillsLoader:
# Show missing requirements for unavailable skills # Show missing requirements for unavailable skills
if not available: if not available:
missing = self._get_missing_requirements(ocmeta) missing = self._get_missing_requirements(skill_meta)
if missing: if missing:
lines.append(f" <requires>{escape_xml(missing)}</requires>") lines.append(f" <requires>{escape_xml(missing)}</requires>")
@ -139,10 +139,10 @@ class SkillsLoader:
return "\n".join(lines) return "\n".join(lines)
def _get_missing_requirements(self, ocmeta: dict) -> str: def _get_missing_requirements(self, skill_meta: dict) -> str:
"""Get a description of missing requirements.""" """Get a description of missing requirements."""
missing = [] missing = []
requires = ocmeta.get("requires", {}) requires = skill_meta.get("requires", {})
for b in requires.get("bins", []): for b in requires.get("bins", []):
if not shutil.which(b): if not shutil.which(b):
missing.append(f"CLI: {b}") missing.append(f"CLI: {b}")
@ -166,17 +166,17 @@ class SkillsLoader:
return content[match.end():].strip() return content[match.end():].strip()
return content return content
def _parse_openclaw_metadata(self, raw: str) -> dict: def _parse_nanobot_metadata(self, raw: str) -> dict:
"""Parse openclaw metadata JSON from frontmatter.""" """Parse nanobot metadata JSON from frontmatter."""
try: try:
data = json.loads(raw) data = json.loads(raw)
return data.get("openclaw", {}) if isinstance(data, dict) else {} return data.get("nanobot", {}) if isinstance(data, dict) else {}
except (json.JSONDecodeError, TypeError): except (json.JSONDecodeError, TypeError):
return {} return {}
def _check_requirements(self, ocmeta: dict) -> bool: def _check_requirements(self, skill_meta: dict) -> bool:
"""Check if skill requirements are met (bins, env vars).""" """Check if skill requirements are met (bins, env vars)."""
requires = ocmeta.get("requires", {}) requires = skill_meta.get("requires", {})
for b in requires.get("bins", []): for b in requires.get("bins", []):
if not shutil.which(b): if not shutil.which(b):
return False return False
@ -185,18 +185,18 @@ class SkillsLoader:
return False return False
return True return True
def _get_ocmeta(self, name: str) -> dict: def _get_skill_meta(self, name: str) -> dict:
"""Get openclaw metadata for a skill (cached in frontmatter).""" """Get nanobot metadata for a skill (cached in frontmatter)."""
meta = self.get_skill_metadata(name) or {} meta = self.get_skill_metadata(name) or {}
return self._parse_openclaw_metadata(meta.get("metadata", "")) return self._parse_nanobot_metadata(meta.get("metadata", ""))
def get_always_skills(self) -> list[str]: def get_always_skills(self) -> list[str]:
"""Get skills marked as always=true that meet requirements.""" """Get skills marked as always=true that meet requirements."""
result = [] result = []
for s in self.list_skills(filter_unavailable=True): for s in self.list_skills(filter_unavailable=True):
meta = self.get_skill_metadata(s["name"]) or {} meta = self.get_skill_metadata(s["name"]) or {}
ocmeta = self._parse_openclaw_metadata(meta.get("metadata", "")) skill_meta = self._parse_nanobot_metadata(meta.get("metadata", ""))
if ocmeta.get("always") or meta.get("always"): if skill_meta.get("always") or meta.get("always"):
result.append(s["name"]) result.append(s["name"])
return result return result

View File

@ -53,7 +53,7 @@ def save_config(config: Config, config_path: Path | None = None) -> None:
path = config_path or get_config_path() path = config_path or get_config_path()
path.parent.mkdir(parents=True, exist_ok=True) path.parent.mkdir(parents=True, exist_ok=True)
# Convert to clawbot-compatible format (camelCase) # Convert to camelCase format
data = config.model_dump() data = config.model_dump()
data = convert_to_camel(data) data = convert_to_camel(data)
@ -71,7 +71,7 @@ def convert_keys(data: Any) -> Any:
def convert_to_camel(data: Any) -> Any: def convert_to_camel(data: Any) -> Any:
"""Convert snake_case keys to camelCase for clawbot compatibility.""" """Convert snake_case keys to camelCase."""
if isinstance(data, dict): if isinstance(data, dict):
return {snake_to_camel(k): convert_to_camel(v) for k, v in data.items()} return {snake_to_camel(k): convert_to_camel(v) for k, v in data.items()}
if isinstance(data, list): if isinstance(data, list):

View File

@ -75,11 +75,7 @@ class ToolsConfig(BaseModel):
class Config(BaseSettings): class Config(BaseSettings):
""" """Root configuration for nanobot."""
Root configuration for nanobot.
Compatible with clawbot configuration format for easy migration.
"""
agents: AgentsConfig = Field(default_factory=AgentsConfig) agents: AgentsConfig = Field(default_factory=AgentsConfig)
channels: ChannelsConfig = Field(default_factory=ChannelsConfig) channels: ChannelsConfig = Field(default_factory=ChannelsConfig)
providers: ProvidersConfig = Field(default_factory=ProvidersConfig) providers: ProvidersConfig = Field(default_factory=ProvidersConfig)

24
nanobot/skills/README.md Normal file
View File

@ -0,0 +1,24 @@
# nanobot Skills
This directory contains built-in skills that extend nanobot's capabilities.
## Skill Format
Each skill is a directory containing a `SKILL.md` file with:
- YAML frontmatter (name, description, metadata)
- Markdown instructions for the agent
## Attribution
These skills are adapted from [OpenClaw](https://github.com/openclaw/openclaw)'s skill system.
The skill format and metadata structure follow OpenClaw's conventions to maintain compatibility.
## Available Skills
| Skill | Description |
|-------|-------------|
| `github` | Interact with GitHub using the `gh` CLI |
| `weather` | Get weather info using wttr.in and Open-Meteo |
| `summarize` | Summarize URLs, files, and YouTube videos |
| `tmux` | Remote-control tmux sessions |
| `skill-creator` | Create new skills |

View File

@ -1,7 +1,7 @@
--- ---
name: github name: github
description: "Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries." description: "Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries."
metadata: {"openclaw":{"emoji":"🐙","requires":{"bins":["gh"]},"install":[{"id":"brew","kind":"brew","formula":"gh","bins":["gh"],"label":"Install GitHub CLI (brew)"},{"id":"apt","kind":"apt","package":"gh","bins":["gh"],"label":"Install GitHub CLI (apt)"}]}} metadata: {"nanobot":{"emoji":"🐙","requires":{"bins":["gh"]},"install":[{"id":"brew","kind":"brew","formula":"gh","bins":["gh"],"label":"Install GitHub CLI (brew)"},{"id":"apt","kind":"apt","package":"gh","bins":["gh"],"label":"Install GitHub CLI (apt)"}]}}
--- ---
# GitHub Skill # GitHub Skill

View File

@ -2,7 +2,7 @@
name: summarize name: summarize
description: Summarize or extract text/transcripts from URLs, podcasts, and local files (great fallback for “transcribe this YouTube/video”). description: Summarize or extract text/transcripts from URLs, podcasts, and local files (great fallback for “transcribe this YouTube/video”).
homepage: https://summarize.sh homepage: https://summarize.sh
metadata: {"openclaw":{"emoji":"🧾","requires":{"bins":["summarize"]},"install":[{"id":"brew","kind":"brew","formula":"steipete/tap/summarize","bins":["summarize"],"label":"Install summarize (brew)"}]}} metadata: {"nanobot":{"emoji":"🧾","requires":{"bins":["summarize"]},"install":[{"id":"brew","kind":"brew","formula":"steipete/tap/summarize","bins":["summarize"],"label":"Install summarize (brew)"}]}}
--- ---
# Summarize # Summarize

View File

@ -1,20 +1,20 @@
--- ---
name: tmux name: tmux
description: Remote-control tmux sessions for interactive CLIs by sending keystrokes and scraping pane output. description: Remote-control tmux sessions for interactive CLIs by sending keystrokes and scraping pane output.
metadata: {"openclaw":{"emoji":"🧵","os":["darwin","linux"],"requires":{"bins":["tmux"]}}} metadata: {"nanobot":{"emoji":"🧵","os":["darwin","linux"],"requires":{"bins":["tmux"]}}}
--- ---
# tmux Skill (OpenClaw) # tmux Skill
Use tmux only when you need an interactive TTY. Prefer exec background mode for long-running, non-interactive tasks. Use tmux only when you need an interactive TTY. Prefer exec background mode for long-running, non-interactive tasks.
## Quickstart (isolated socket, exec tool) ## Quickstart (isolated socket, exec tool)
```bash ```bash
SOCKET_DIR="${OPENCLAW_TMUX_SOCKET_DIR:-${CLAWDBOT_TMUX_SOCKET_DIR:-${TMPDIR:-/tmp}/openclaw-tmux-sockets}}" SOCKET_DIR="${NANOBOT_TMUX_SOCKET_DIR:-${TMPDIR:-/tmp}/nanobot-tmux-sockets}"
mkdir -p "$SOCKET_DIR" mkdir -p "$SOCKET_DIR"
SOCKET="$SOCKET_DIR/openclaw.sock" SOCKET="$SOCKET_DIR/nanobot.sock"
SESSION=openclaw-python SESSION=nanobot-python
tmux -S "$SOCKET" new -d -s "$SESSION" -n shell tmux -S "$SOCKET" new -d -s "$SESSION" -n shell
tmux -S "$SOCKET" send-keys -t "$SESSION":0.0 -- 'PYTHON_BASIC_REPL=1 python3 -q' Enter tmux -S "$SOCKET" send-keys -t "$SESSION":0.0 -- 'PYTHON_BASIC_REPL=1 python3 -q' Enter
@ -31,8 +31,8 @@ To monitor:
## Socket convention ## Socket convention
- Use `OPENCLAW_TMUX_SOCKET_DIR` (legacy `CLAWDBOT_TMUX_SOCKET_DIR` also supported). - Use `NANOBOT_TMUX_SOCKET_DIR` environment variable.
- Default socket path: `"$OPENCLAW_TMUX_SOCKET_DIR/openclaw.sock"`. - Default socket path: `"$NANOBOT_TMUX_SOCKET_DIR/nanobot.sock"`.
## Targeting panes and naming ## Targeting panes and naming
@ -43,7 +43,7 @@ To monitor:
## Finding sessions ## Finding sessions
- List sessions on your socket: `{baseDir}/scripts/find-sessions.sh -S "$SOCKET"`. - List sessions on your socket: `{baseDir}/scripts/find-sessions.sh -S "$SOCKET"`.
- Scan all sockets: `{baseDir}/scripts/find-sessions.sh --all` (uses `OPENCLAW_TMUX_SOCKET_DIR`). - Scan all sockets: `{baseDir}/scripts/find-sessions.sh --all` (uses `NANOBOT_TMUX_SOCKET_DIR`).
## Sending input safely ## Sending input safely

View File

@ -10,7 +10,7 @@ List tmux sessions on a socket (default tmux socket if none provided).
Options: Options:
-L, --socket tmux socket name (passed to tmux -L) -L, --socket tmux socket name (passed to tmux -L)
-S, --socket-path tmux socket path (passed to tmux -S) -S, --socket-path tmux socket path (passed to tmux -S)
-A, --all scan all sockets under OPENCLAW_TMUX_SOCKET_DIR -A, --all scan all sockets under NANOBOT_TMUX_SOCKET_DIR
-q, --query case-insensitive substring to filter session names -q, --query case-insensitive substring to filter session names
-h, --help show this help -h, --help show this help
USAGE USAGE
@ -20,7 +20,7 @@ socket_name=""
socket_path="" socket_path=""
query="" query=""
scan_all=false scan_all=false
socket_dir="${OPENCLAW_TMUX_SOCKET_DIR:-${CLAWDBOT_TMUX_SOCKET_DIR:-${TMPDIR:-/tmp}/openclaw-tmux-sockets}}" socket_dir="${NANOBOT_TMUX_SOCKET_DIR:-${TMPDIR:-/tmp}/nanobot-tmux-sockets}"
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
case "$1" in case "$1" in

View File

@ -2,7 +2,7 @@
name: weather name: weather
description: Get current weather and forecasts (no API key required). description: Get current weather and forecasts (no API key required).
homepage: https://wttr.in/:help homepage: https://wttr.in/:help
metadata: {"openclaw":{"emoji":"🌤️","requires":{"bins":["curl"]}}} metadata: {"nanobot":{"emoji":"🌤️","requires":{"bins":["curl"]}}}
--- ---
# Weather # Weather