diff --git a/hermes_cli/plugin_catalog.py b/hermes_cli/plugin_catalog.py index 560b3fd034..dad4d8ac91 100644 --- a/hermes_cli/plugin_catalog.py +++ b/hermes_cli/plugin_catalog.py @@ -27,8 +27,9 @@ import yaml logger = logging.getLogger(__name__) CATALOG_TIERS = ("official", "community") -# Browse taxonomy for the catalog page / picker. Entries without one land in "other". -CATALOG_CATEGORIES = ("memory", "desktop", "platform", "web", "tools", "voice", "automation", "models", "other") +# Browse taxonomy for the catalog page / picker. Entries without one land on the Desktop shelf +# (the common case for community submissions); "general" is for plugins that fit no shelf. +CATALOG_CATEGORIES = ("desktop", "memory", "platform", "web", "tools", "voice", "automation", "models", "general") LIVE_CATALOG_URL = "https://hermes-agent.nousresearch.com/docs/api/plugin-catalog.json" LIVE_CATALOG_TTL_SECONDS = 6 * 60 * 60 _REQUEST_TIMEOUT = 5.0 @@ -62,7 +63,7 @@ class PluginCatalogEntry: description: str maintainer: str tier: str = "community" - category: str = "other" + category: str = "desktop" requires_hermes: str = "" subdir: str = "" docs_url: str = "" @@ -108,7 +109,7 @@ def entry_from_mapping(data: Any, label: str) -> Optional[PluginCatalogEntry]: repo = str(data.get("repo") or "") sha = str(data.get("sha") or "").strip().lower() tier = str(data.get("tier") or "community") - category = str(data.get("category") or "other") + category = str(data.get("category") or "desktop") problem = ( f"invalid name {name!r} (must match [a-z0-9_-]{{1,64}})" if not _NAME_RE.match(name) else f"repo must be an https:// URL (got {repo!r})" if not repo.startswith("https://") diff --git a/plugin-catalog/README.md b/plugin-catalog/README.md index b964c8333e..6ed5105c59 100644 --- a/plugin-catalog/README.md +++ b/plugin-catalog/README.md @@ -44,8 +44,8 @@ subdir: "" # optional path within the repo description: One-line description. maintainer: OwnerName tier: official # official | community (default community) -category: memory # memory | desktop | platform | web | tools | voice | automation | models | other - # (default other) — the shelf the entry sits on at /docs/plugins +category: memory # desktop | memory | platform | web | tools | voice | automation | models | general + # (default desktop) — the shelf the entry sits on at /docs/plugins requires_hermes: ">=0.19" # optional docs_url: "" # optional platforms: [] # optional, e.g. [linux, macos]; empty = all diff --git a/plugin-catalog/hermes-auto-titler.yaml b/plugin-catalog/hermes-auto-titler.yaml index a110d4ae77..51c62cd24a 100644 --- a/plugin-catalog/hermes-auto-titler.yaml +++ b/plugin-catalog/hermes-auto-titler.yaml @@ -4,6 +4,7 @@ sha: 307993c36e261af2769e486befc6841e96f93423 description: Provenance-aware automatic session title maintenance that never overrides user titles. maintainer: Vocllum tier: community +category: automation docs_url: https://github.com/Vocllum/hermes-auto-titler platforms: [] capabilities: diff --git a/plugin-catalog/hermes-live-voice.yaml b/plugin-catalog/hermes-live-voice.yaml index 114990bad6..c6f7a57924 100644 --- a/plugin-catalog/hermes-live-voice.yaml +++ b/plugin-catalog/hermes-live-voice.yaml @@ -4,6 +4,7 @@ sha: 5f0022b97105da26783854aca9dac855c9ca865f description: 'Full-duplex Live Voice for Hermes Desktop — GPT-Live-1 on the ChatGPT/Codex subscription via the local Codex app-server, with live transcript, voice tool calls and task delegation to the user chat.' maintainer: Synero tier: community +category: voice docs_url: https://github.com/Synero/hermes-live-voice platforms: [] capabilities: diff --git a/plugin-catalog/hermes-newswire.yaml b/plugin-catalog/hermes-newswire.yaml index 964dab77a3..c3efa25fb2 100644 --- a/plugin-catalog/hermes-newswire.yaml +++ b/plugin-catalog/hermes-newswire.yaml @@ -6,6 +6,7 @@ description: 'Breaking-news ticker for Hermes Desktop: a scrolling RSS/Atom/JSON in-app reading. Zero API keys, zero model tokens.' maintainer: tony-simons-aiowa tier: community +category: desktop requires_hermes: '>=0.19' docs_url: https://github.com/tony-simons-aiowa/hermes-newswire platforms: [] diff --git a/plugin-catalog/hermes-pets-all.yaml b/plugin-catalog/hermes-pets-all.yaml index d0b212231a..888f78597d 100644 --- a/plugin-catalog/hermes-pets-all.yaml +++ b/plugin-catalog/hermes-pets-all.yaml @@ -5,6 +5,7 @@ description: Browse every pet installed for this Hermes profile in its own deskt (thumbnails, search, click-to-adopt), plus a read-only list_pets tool for the agent. maintainer: poponline63 tier: community +category: desktop requires_hermes: ">=0.19" docs_url: https://github.com/poponline63/hermes-pets-all platforms: [] diff --git a/plugin-catalog/metamask-wallet.yaml b/plugin-catalog/metamask-wallet.yaml index 6759e6d1e1..88ec7d9aa1 100644 --- a/plugin-catalog/metamask-wallet.yaml +++ b/plugin-catalog/metamask-wallet.yaml @@ -5,6 +5,7 @@ subdir: "" description: MetaMask Agent Wallet for Hermes — chat-only setup, reads, and approval-gated transfers/swaps/signatures, a gated Guard Mode policy editor, and a background 2FA watcher. Keys stay in MetaMask. maintainer: akugone tier: community +category: tools requires_hermes: ">=0.21" docs_url: https://github.com/akugone/hermes-metamask-wallet platforms: [macos, linux] diff --git a/plugin-catalog/web-octen.yaml b/plugin-catalog/web-octen.yaml index b0220de7e9..3f59093487 100644 --- a/plugin-catalog/web-octen.yaml +++ b/plugin-catalog/web-octen.yaml @@ -4,6 +4,7 @@ sha: 3af8a3fe200d93934c823485f66c3375a4508474 description: Octen web search with query-focused highlights and page extraction. maintainer: Octen-Team tier: community +category: web docs_url: https://github.com/Octen-Team/hermes-octen-plugin#readme capabilities: provides_tools: [] diff --git a/scripts/validate_plugin_catalog.py b/scripts/validate_plugin_catalog.py index 8c3afacae4..82cafb920b 100644 --- a/scripts/validate_plugin_catalog.py +++ b/scripts/validate_plugin_catalog.py @@ -42,7 +42,7 @@ except ImportError: # pragma: no cover - dependency guidance only NAME_RE = re.compile(r"^[a-z0-9_-]{1,64}$") SHA_RE = re.compile(r"^[0-9a-f]{40}$") TIERS = ("official", "community") -CATEGORIES = ("memory", "desktop", "platform", "web", "tools", "voice", "automation", "models", "other") +CATEGORIES = ("desktop", "memory", "platform", "web", "tools", "voice", "automation", "models", "general") PLATFORMS = ("linux", "macos", "windows") CAPABILITY_KEYS = ( "provides_tools", @@ -127,7 +127,7 @@ def validate_entry(data: object) -> tuple[list[str], list[str]]: if tier not in TIERS: errors.append(f"tier {tier!r} must be one of {list(TIERS)}") - category = data.get("category", "other") + category = data.get("category", "desktop") if category not in CATEGORIES: errors.append(f"category {category!r} must be one of {list(CATEGORIES)}") diff --git a/tests/hermes_cli/test_plugin_catalog.py b/tests/hermes_cli/test_plugin_catalog.py index fc69d053eb..8d438cd294 100644 --- a/tests/hermes_cli/test_plugin_catalog.py +++ b/tests/hermes_cli/test_plugin_catalog.py @@ -31,14 +31,14 @@ def test_shipped_catalog_entries_are_all_valid_and_pinned(): def test_category_defaults_to_other_and_unknown_category_is_rejected(tmp_path): - """``category`` is the browse shelf: absent → ``other``; a value outside CATALOG_CATEGORIES is an + """``category`` is the browse shelf: absent → ``desktop``; a value outside CATALOG_CATEGORIES is an invalid entry (skipped with a warning) so a typo cannot create a phantom shelf on the site.""" (tmp_path / "a.yaml").write_text(yaml.safe_dump(_entry("no-cat"))) (tmp_path / "b.yaml").write_text(yaml.safe_dump(_entry("mem", category="memory"))) (tmp_path / "c.yaml").write_text(yaml.safe_dump(_entry("typo", category="memmory"))) entries = {e.name: e for e in pc.load_catalog(tmp_path)} assert set(entries) == {"no-cat", "mem"} - assert entries["no-cat"].category == "other" and entries["mem"].category == "memory" + assert entries["no-cat"].category == "desktop" and entries["mem"].category == "memory" assert entries["mem"].to_dict()["category"] == "memory" diff --git a/tests/website/test_extract_plugins.py b/tests/website/test_extract_plugins.py index 1fbc2c3d5b..f3e4ac0431 100644 --- a/tests/website/test_extract_plugins.py +++ b/tests/website/test_extract_plugins.py @@ -141,7 +141,7 @@ def test_main_writes_catalog_and_meta(mod, tmp_path): catalog = tmp_path / "plugin-catalog" catalog.mkdir() _write_entry(catalog, "alpha", tier="official", category="memory") - _write_entry(catalog, "beta") # no category → "other" shelf + _write_entry(catalog, "beta") # no category → default "desktop" shelf (catalog / "removed.yaml").write_text( "removed:\n - name: gone\n", encoding="utf-8" ) @@ -155,8 +155,8 @@ def test_main_writes_catalog_and_meta(mod, tmp_path): assert [p["name"] for p in plugins] == ["alpha", "beta"] assert meta["total"] == 2 assert meta["byTier"] == {"official": 1, "community": 1} - assert {p["name"]: p["category"] for p in plugins} == {"alpha": "memory", "beta": "other"} - assert meta["byCategory"] == {"memory": 1, "other": 1} + assert {p["name"]: p["category"] for p in plugins} == {"alpha": "memory", "beta": "desktop"} + assert meta["byCategory"] == {"desktop": 1, "memory": 1} assert meta["removedCount"] == 1 assert meta["generatedAt"] # The live-refresh document consumed by installed clients: loader-schema entries + the kill list. diff --git a/website/docs/user-guide/features/plugin-catalog.md b/website/docs/user-guide/features/plugin-catalog.md index 45f0dacff0..52d3f2a06b 100644 --- a/website/docs/user-guide/features/plugin-catalog.md +++ b/website/docs/user-guide/features/plugin-catalog.md @@ -36,7 +36,7 @@ directory of the hermes-agent repository, declaring: | `repo` | The plugin's public git repository | | `sha` | The **exact 40-hex commit** that was reviewed — installs check out this pin, not a branch tip | | `tier` | `official` (maintained by NousResearch) or `community` | -| `category` | Browse shelf: `memory`, `desktop`, `platform`, `web`, `tools`, `voice`, `automation`, `models` or `other` (default) | +| `category` | Browse shelf: `desktop` (default), `memory`, `platform`, `web`, `tools`, `voice`, `automation`, `models` or `general` | | `maintainer` | Who owns the plugin | | `capabilities` | Declared tools, hooks, middleware, and required env vars | | `requires_hermes` | Minimum Hermes version, e.g. `>=0.19` (optional) | diff --git a/website/scripts/extract-plugins.py b/website/scripts/extract-plugins.py index f592a9d0a2..93ba1fb59e 100644 --- a/website/scripts/extract-plugins.py +++ b/website/scripts/extract-plugins.py @@ -40,7 +40,7 @@ DEFAULT_CATALOG_DIR = REPO_ROOT / "plugin-catalog" DEFAULT_OUTPUT_DIR = REPO_ROOT / "website" / "static" / "api" CATALOG_TIERS = ("official", "community") -CATALOG_CATEGORIES = ("memory", "desktop", "platform", "web", "tools", "voice", "automation", "models", "other") +CATALOG_CATEGORIES = ("desktop", "memory", "platform", "web", "tools", "voice", "automation", "models", "general") SHA_RE = re.compile(r"^[0-9a-f]{40}$") @@ -107,10 +107,10 @@ def load_catalog_entries(catalog_dir: Path) -> list[dict]: if tier not in CATALOG_TIERS: _log(f"{path.name} ({name}): unknown tier {tier!r}, treating as community") tier = "community" - category = str(raw.get("category") or "other").strip().lower() + category = str(raw.get("category") or "desktop").strip().lower() if category not in CATALOG_CATEGORIES: - _log(f"{path.name} ({name}): unknown category {category!r}, treating as other") - category = "other" + _log(f"{path.name} ({name}): unknown category {category!r}, treating as general") + category = "general" entries.append({ "name": name, diff --git a/website/src/pages/plugins/index.tsx b/website/src/pages/plugins/index.tsx index 9d96de4d10..26fc4d3932 100644 --- a/website/src/pages/plugins/index.tsx +++ b/website/src/pages/plugins/index.tsx @@ -71,15 +71,15 @@ const TIER_ORDER = ["all", "official", "community"]; // grouped sections; keep it in sync with CATALOG_CATEGORIES in // hermes_cli/plugin_catalog.py and website/scripts/extract-plugins.py. const CATEGORY_CONFIG: Record = { - memory: { label: "Memory", icon: "\u{1F9E0}", blurb: "Memory providers and context engines" }, desktop: { label: "Desktop", icon: "\u{1F5A5}\u{FE0F}", blurb: "Panes, tabs and views for Hermes Desktop" }, + memory: { label: "Memory", icon: "\u{1F9E0}", blurb: "Memory providers and context engines" }, platform: { label: "Platforms", icon: "\u{1F4AC}", blurb: "Messaging and channel adapters" }, web: { label: "Web & Browser", icon: "\u{1F310}", blurb: "Search backends, extraction and browser control" }, tools: { label: "Tools", icon: "\u{1F6E0}\u{FE0F}", blurb: "New tools the agent can call" }, voice: { label: "Voice", icon: "\u{1F399}\u{FE0F}", blurb: "Speech, TTS and realtime audio" }, automation: { label: "Automation", icon: "\u{23F1}\u{FE0F}", blurb: "Hooks, wake triggers and session automation" }, models: { label: "Models", icon: "\u{2728}", blurb: "Model and inference providers" }, - other: { label: "Other", icon: "\u{1F4E6}", blurb: "Everything else" }, + general: { label: "General", icon: "\u{1F4E6}", blurb: "Plugins that span several areas" }, }; const CATEGORY_ORDER = Object.keys(CATEGORY_CONFIG); @@ -465,7 +465,7 @@ export default function PluginCatalogPage() { if (search.trim() || categoryFilter !== "all") return null; const buckets = new Map(); for (const p of filtered) { - const key = CATEGORY_CONFIG[p.category] ? p.category : "other"; + const key = CATEGORY_CONFIG[p.category] ? p.category : "general"; (buckets.get(key) ?? buckets.set(key, []).get(key)!).push(p); } return CATEGORY_ORDER.filter((c) => buckets.has(c)).map((c) => [c, buckets.get(c)!] as const); @@ -475,7 +475,7 @@ export default function PluginCatalogPage() { const counts: Record = {}; for (const p of allPlugins) { if (tierFilter !== "all" && p.tier !== tierFilter) continue; - const key = CATEGORY_CONFIG[p.category] ? p.category : "other"; + const key = CATEGORY_CONFIG[p.category] ? p.category : "general"; counts[key] = (counts[key] || 0) + 1; } return counts;