diff --git a/hermes_cli/skills_hub.py b/hermes_cli/skills_hub.py index db6db9e39..9efb139e7 100644 --- a/hermes_cli/skills_hub.py +++ b/hermes_cli/skills_hub.py @@ -99,12 +99,13 @@ def do_search(query: str, source: str = "all", limit: int = 10, table.add_column("Identifier", style="dim") for r in results: - trust_style = {"trusted": "green", "community": "yellow"}.get(r.trust_level, "dim") + trust_style = {"builtin": "bright_cyan", "trusted": "green", "community": "yellow"}.get(r.trust_level, "dim") + trust_label = "official" if r.source == "official" else r.trust_level table.add_row( r.name, r.description[:60] + ("..." if len(r.description) > 60 else ""), r.source, - f"[{trust_style}]{r.trust_level}[/]", + f"[{trust_style}]{trust_label}[/]", r.identifier, ) @@ -147,6 +148,12 @@ def do_install(identifier: str, category: str = "", force: bool = False, c.print(f"[bold red]Error:[/] Could not fetch '{identifier}' from any source.\n") return + # Auto-detect category for official skills (e.g. "official/autonomous-ai-agents/blackbox") + if bundle.source == "official" and not category: + id_parts = bundle.identifier.split("/") # ["official", "category", "skill"] + if len(id_parts) >= 3: + category = id_parts[1] + # Check if already installed lock = HubLockFile() existing = lock.get_installed(bundle.name) @@ -177,18 +184,28 @@ def do_install(identifier: str, category: str = "", force: bool = False, f"{len(result.findings)}_findings") return - # Confirm with user — always show risk warning regardless of source + # Confirm with user — show appropriate warning based on source if not force: c.print() - c.print(Panel( - "[bold yellow]You are installing a third-party skill at your own risk.[/]\n\n" - "External skills can contain instructions that influence agent behavior,\n" - "shell commands, and scripts. Even after automated scanning, you should\n" - "review the installed files before use.\n\n" - f"Files will be at: [cyan]~/.hermes/skills/{category + '/' if category else ''}{bundle.name}/[/]", - title="Disclaimer", - border_style="yellow", - )) + if bundle.source == "official": + c.print(Panel( + "[bold bright_cyan]This is an official optional skill maintained by Nous Research.[/]\n\n" + "It ships with hermes-agent but is not activated by default.\n" + "Installing will copy it to your skills directory where the agent can use it.\n\n" + f"Files will be at: [cyan]~/.hermes/skills/{category + '/' if category else ''}{bundle.name}/[/]", + title="Official Skill", + border_style="bright_cyan", + )) + else: + c.print(Panel( + "[bold yellow]You are installing a third-party skill at your own risk.[/]\n\n" + "External skills can contain instructions that influence agent behavior,\n" + "shell commands, and scripts. Even after automated scanning, you should\n" + "review the installed files before use.\n\n" + f"Files will be at: [cyan]~/.hermes/skills/{category + '/' if category else ''}{bundle.name}/[/]", + title="Disclaimer", + border_style="yellow", + )) c.print(f"[bold]Install '{bundle.name}'?[/]") try: answer = input("Confirm [y/N]: ").strip().lower() @@ -297,8 +314,9 @@ def do_list(source_filter: str = "all", console: Optional[Console] = None) -> No if source_filter == "builtin" and hub_entry: continue - trust_style = {"builtin": "blue", "trusted": "green", "community": "yellow"}.get(trust, "dim") - table.add_row(name, category, source_display, f"[{trust_style}]{trust}[/]") + trust_style = {"builtin": "bright_cyan", "trusted": "green", "community": "yellow"}.get(trust, "dim") + trust_label = "official" if source_display == "official" else trust + table.add_row(name, category, source_display, f"[{trust_style}]{trust_label}[/]") c.print(table) c.print(f"[dim]{len(hub_installed)} hub-installed, " diff --git a/optional-skills/DESCRIPTION.md b/optional-skills/DESCRIPTION.md new file mode 100644 index 000000000..3e3b96610 --- /dev/null +++ b/optional-skills/DESCRIPTION.md @@ -0,0 +1,22 @@ +# Optional Skills + +Official skills maintained by Nous Research that are **not activated by default**. + +These skills ship with the hermes-agent repository but are not copied to +`~/.hermes/skills/` during setup. They are discoverable via the Skills Hub: + +```bash +hermes skills search # finds optional skills labeled "official" +hermes skills install # copies to ~/.hermes/skills/ and activates +``` + +## Why optional? + +Some skills are useful but not broadly needed by every user: + +- **Niche integrations** — specific paid services, specialized tools +- **Experimental features** — promising but not yet proven +- **Heavyweight dependencies** — require significant setup (API keys, installs) + +By keeping them optional, we keep the default skill set lean while still +providing curated, tested, official skills for users who want them. diff --git a/optional-skills/autonomous-ai-agents/DESCRIPTION.md b/optional-skills/autonomous-ai-agents/DESCRIPTION.md new file mode 100644 index 000000000..b7b82724e --- /dev/null +++ b/optional-skills/autonomous-ai-agents/DESCRIPTION.md @@ -0,0 +1,2 @@ +Optional autonomous AI agent integrations — external coding agent CLIs +that can be delegated to for independent coding tasks. diff --git a/optional-skills/autonomous-ai-agents/blackbox/SKILL.md b/optional-skills/autonomous-ai-agents/blackbox/SKILL.md new file mode 100644 index 000000000..cc190af35 --- /dev/null +++ b/optional-skills/autonomous-ai-agents/blackbox/SKILL.md @@ -0,0 +1,143 @@ +--- +name: blackbox +description: Delegate coding tasks to Blackbox AI CLI agent. Multi-model agent with built-in judge that runs tasks through multiple LLMs and picks the best result. Requires the blackbox CLI and a Blackbox AI API key. +version: 1.0.0 +author: Hermes Agent (Nous Research) +license: MIT +metadata: + hermes: + tags: [Coding-Agent, Blackbox, Multi-Agent, Judge, Multi-Model] + related_skills: [claude-code, codex, hermes-agent] +--- + +# Blackbox CLI + +Delegate coding tasks to [Blackbox AI](https://www.blackbox.ai/) via the Hermes terminal. Blackbox is a multi-model coding agent CLI that dispatches tasks to multiple LLMs (Claude, Codex, Gemini, Blackbox Pro) and uses a judge to select the best implementation. + +The CLI is [open-source](https://github.com/blackboxaicode/cli) (GPL-3.0, TypeScript, forked from Gemini CLI) and supports interactive sessions, non-interactive one-shots, checkpointing, MCP, and vision model switching. + +## Prerequisites + +- Node.js 20+ installed +- Blackbox CLI installed: `npm install -g @blackboxai/cli` +- Or install from source: + ``` + git clone https://github.com/blackboxaicode/cli.git + cd cli && npm install && npm install -g . + ``` +- API key from [app.blackbox.ai/dashboard](https://app.blackbox.ai/dashboard) +- Configured: run `blackbox configure` and enter your API key +- Use `pty=true` in terminal calls — Blackbox CLI is an interactive terminal app + +## One-Shot Tasks + +``` +terminal(command="blackbox --prompt 'Add JWT authentication with refresh tokens to the Express API'", workdir="/path/to/project", pty=true) +``` + +For quick scratch work: +``` +terminal(command="cd $(mktemp -d) && git init && blackbox --prompt 'Build a REST API for todos with SQLite'", pty=true) +``` + +## Background Mode (Long Tasks) + +For tasks that take minutes, use background mode so you can monitor progress: + +``` +# Start in background with PTY +terminal(command="blackbox --prompt 'Refactor the auth module to use OAuth 2.0'", workdir="~/project", background=true, pty=true) +# Returns session_id + +# Monitor progress +process(action="poll", session_id="") +process(action="log", session_id="") + +# Send input if Blackbox asks a question +process(action="submit", session_id="", data="yes") + +# Kill if needed +process(action="kill", session_id="") +``` + +## Checkpoints & Resume + +Blackbox CLI has built-in checkpoint support for pausing and resuming tasks: + +``` +# After a task completes, Blackbox shows a checkpoint tag +# Resume with a follow-up task: +terminal(command="blackbox --resume-checkpoint 'task-abc123-2026-03-06' --prompt 'Now add rate limiting to the endpoints'", workdir="~/project", pty=true) +``` + +## Session Commands + +During an interactive session, use these commands: + +| Command | Effect | +|---------|--------| +| `/compress` | Shrink conversation history to save tokens | +| `/clear` | Wipe history and start fresh | +| `/stats` | View current token usage | +| `Ctrl+C` | Cancel current operation | + +## PR Reviews + +Clone to a temp directory to avoid modifying the working tree: + +``` +terminal(command="REVIEW=$(mktemp -d) && git clone https://github.com/user/repo.git $REVIEW && cd $REVIEW && gh pr checkout 42 && blackbox --prompt 'Review this PR against main. Check for bugs, security issues, and code quality.'", pty=true) +``` + +## Parallel Work + +Spawn multiple Blackbox instances for independent tasks: + +``` +terminal(command="blackbox --prompt 'Fix the login bug'", workdir="/tmp/issue-1", background=true, pty=true) +terminal(command="blackbox --prompt 'Add unit tests for auth'", workdir="/tmp/issue-2", background=true, pty=true) + +# Monitor all +process(action="list") +``` + +## Multi-Model Mode + +Blackbox's unique feature is running the same task through multiple models and judging the results. Configure which models to use via `blackbox configure` — select multiple providers to enable the Chairman/judge workflow where the CLI evaluates outputs from different models and picks the best one. + +## Key Flags + +| Flag | Effect | +|------|--------| +| `--prompt "task"` | Non-interactive one-shot execution | +| `--resume-checkpoint "tag"` | Resume from a saved checkpoint | +| `--yolo` | Auto-approve all actions and model switches | +| `blackbox session` | Start interactive chat session | +| `blackbox configure` | Change settings, providers, models | +| `blackbox info` | Display system information | + +## Vision Support + +Blackbox automatically detects images in input and can switch to multimodal analysis. VLM modes: +- `"once"` — Switch model for current query only +- `"session"` — Switch for entire session +- `"persist"` — Stay on current model (no switch) + +## Token Limits + +Control token usage via `.blackboxcli/settings.json`: +```json +{ + "sessionTokenLimit": 32000 +} +``` + +## Rules + +1. **Always use `pty=true`** — Blackbox CLI is an interactive terminal app and will hang without a PTY +2. **Use `workdir`** — keep the agent focused on the right directory +3. **Background for long tasks** — use `background=true` and monitor with `process` tool +4. **Don't interfere** — monitor with `poll`/`log`, don't kill sessions because they're slow +5. **Report results** — after completion, check what changed and summarize for the user +6. **Credits cost money** — Blackbox uses a credit-based system; multi-model mode consumes credits faster +7. **Check prerequisites** — verify `blackbox` CLI is installed before attempting delegation diff --git a/tools/skills_guard.py b/tools/skills_guard.py index a20a9b753..34a4294e8 100644 --- a/tools/skills_guard.py +++ b/tools/skills_guard.py @@ -1046,6 +1046,9 @@ def _get_configured_model() -> str: def _resolve_trust_level(source: str) -> str: """Map a source identifier to a trust level.""" + # Official optional skills shipped with the repo + if source.startswith("official/") or source == "official": + return "builtin" # Check if source matches any trusted repo for trusted in TRUSTED_REPOS: if source.startswith(trusted) or source == trusted: diff --git a/tools/skills_hub.py b/tools/skills_hub.py index 002b51f90..27b233525 100644 --- a/tools/skills_hub.py +++ b/tools/skills_hub.py @@ -5,6 +5,7 @@ Skills Hub — Source adapters and hub state management for the Hermes Skills Hu This is a library module (not an agent tool). It provides: - GitHubAuth: Shared GitHub API authentication (PAT, gh CLI, GitHub App) - SkillSource ABC: Interface for all skill registry adapters + - OptionalSkillSource: Official optional skills shipped with the repo (not activated by default) - GitHubSource: Fetch skills from any GitHub repo via the Contents API - HubLockFile: Track provenance of installed hub skills - Hub state directory management (quarantine, audit log, taps, index cache) @@ -941,6 +942,160 @@ class LobeHubSource(SkillSource): return "\n".join(fm_lines) + "\n\n" + "\n".join(body_lines) + "\n" +# --------------------------------------------------------------------------- +# Official optional skills source adapter +# --------------------------------------------------------------------------- + +class OptionalSkillSource(SkillSource): + """ + Fetch skills from the optional-skills/ directory shipped with the repo. + + These skills are official (maintained by Nous Research) but not activated + by default — they don't appear in the system prompt and aren't copied to + ~/.hermes/skills/ during setup. They are discoverable via the Skills Hub + (search / install / inspect) and labelled "official" with "builtin" trust. + """ + + def __init__(self): + self._optional_dir = Path(__file__).parent.parent / "optional-skills" + + def source_id(self) -> str: + return "official" + + def trust_level_for(self, identifier: str) -> str: + return "builtin" + + # -- search ----------------------------------------------------------- + + def search(self, query: str, limit: int = 10) -> List[SkillMeta]: + results: List[SkillMeta] = [] + query_lower = query.lower() + + for meta in self._scan_all(): + searchable = f"{meta.name} {meta.description} {' '.join(meta.tags)}".lower() + if query_lower in searchable: + results.append(meta) + if len(results) >= limit: + break + + return results + + # -- fetch ------------------------------------------------------------ + + def fetch(self, identifier: str) -> Optional[SkillBundle]: + # identifier format: "official/category/skill" or "official/skill" + rel = identifier.split("/", 1)[-1] if identifier.startswith("official/") else identifier + skill_dir = self._optional_dir / rel + + if not skill_dir.is_dir(): + # Try searching by skill name only (last segment) + skill_name = rel.rsplit("/", 1)[-1] + skill_dir = self._find_skill_dir(skill_name) + if not skill_dir: + return None + + files: Dict[str, str] = {} + for f in skill_dir.rglob("*"): + if f.is_file() and not f.name.startswith("."): + rel_path = str(f.relative_to(skill_dir)) + try: + files[rel_path] = f.read_text(encoding="utf-8") + except (OSError, UnicodeDecodeError): + continue + + if not files: + return None + + # Determine category from directory structure + name = skill_dir.name + + return SkillBundle( + name=name, + files=files, + source="official", + identifier=f"official/{skill_dir.relative_to(self._optional_dir)}", + trust_level="builtin", + ) + + # -- inspect ---------------------------------------------------------- + + def inspect(self, identifier: str) -> Optional[SkillMeta]: + rel = identifier.split("/", 1)[-1] if identifier.startswith("official/") else identifier + skill_name = rel.rsplit("/", 1)[-1] + + for meta in self._scan_all(): + if meta.name == skill_name: + return meta + return None + + # -- internal helpers ------------------------------------------------- + + def _find_skill_dir(self, name: str) -> Optional[Path]: + """Find a skill directory by name anywhere in optional-skills/.""" + if not self._optional_dir.is_dir(): + return None + for skill_md in self._optional_dir.rglob("SKILL.md"): + if skill_md.parent.name == name: + return skill_md.parent + return None + + def _scan_all(self) -> List[SkillMeta]: + """Enumerate all optional skills with metadata.""" + if not self._optional_dir.is_dir(): + return [] + + results: List[SkillMeta] = [] + for skill_md in sorted(self._optional_dir.rglob("SKILL.md")): + parent = skill_md.parent + rel_parts = parent.relative_to(self._optional_dir).parts + if any(part.startswith(".") for part in rel_parts): + continue + + try: + content = skill_md.read_text(encoding="utf-8") + except (OSError, UnicodeDecodeError): + continue + + fm = self._parse_frontmatter(content) + name = fm.get("name", parent.name) + desc = fm.get("description", "") + tags = [] + meta_block = fm.get("metadata", {}) + if isinstance(meta_block, dict): + hermes_meta = meta_block.get("hermes", {}) + if isinstance(hermes_meta, dict): + tags = hermes_meta.get("tags", []) + + rel_path = str(parent.relative_to(self._optional_dir)) + + results.append(SkillMeta( + name=name, + description=desc[:200], + source="official", + identifier=f"official/{rel_path}", + trust_level="builtin", + path=rel_path, + tags=tags if isinstance(tags, list) else [], + )) + + return results + + @staticmethod + def _parse_frontmatter(content: str) -> dict: + """Parse YAML frontmatter from SKILL.md content.""" + if not content.startswith("---"): + return {} + match = re.search(r'\n---\s*\n', content[3:]) + if not match: + return {} + yaml_text = content[3:match.start() + 3] + try: + parsed = yaml.safe_load(yaml_text) + return parsed if isinstance(parsed, dict) else {} + except yaml.YAMLError: + return {} + + # --------------------------------------------------------------------------- # Shared cache helpers (used by multiple adapters) # --------------------------------------------------------------------------- @@ -1219,6 +1374,7 @@ def create_source_router(auth: Optional[GitHubAuth] = None) -> List[SkillSource] extra_taps = taps_mgr.list_taps() sources: List[SkillSource] = [ + OptionalSkillSource(), # Official optional skills (highest priority) GitHubSource(auth=auth, extra_taps=extra_taps), ClawHubSource(), ClaudeMarketplaceSource(auth=auth),