fix: unify skill metadata format
This commit is contained in:
parent
d888e51d1c
commit
ac527d40d7
@ -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):
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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):
|
||||||
|
|||||||
@ -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
24
nanobot/skills/README.md
Normal 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 |
|
||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user