-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy path__init__.py
More file actions
124 lines (105 loc) · 4.16 KB
/
Copy path__init__.py
File metadata and controls
124 lines (105 loc) · 4.16 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
"""
Hermes Agent native plugin adapter for ContentForge.
This file is read ONLY by Hermes Agent (Nous Research). Every other platform
ignores it: Claude Code / Cowork / Codex / Cursor / Copilot CLI / Antigravity
all read their own manifest files. OpenClaw reads openclaw.plugin.json (and
falls back to the .claude-plugin/ bundle).
When Hermes loads us via `hermes plugins install indranilbanerjee/contentforge`,
it clones the repo into ~/.hermes/plugins/contentforge/, reads plugin.yaml at
the root, then calls register(ctx) below. The register() walks the skills/
directory and exposes each one to Hermes via ctx.register_skill().
Defensive coding throughout — stdlib only, no third-party Python dependencies;
if Hermes API surface differs from the documented spec, the adapter logs and
degrades gracefully rather than crashing.
Spec source: https://hermes-agent.nousresearch.com/docs/guides/build-a-hermes-plugin
Tested against: Hermes Desktop v0.15.2 (June 2026 public preview)
"""
from __future__ import annotations
import logging
import re
from pathlib import Path
logger = logging.getLogger("contentforge")
PLUGIN_ROOT = Path(__file__).resolve().parent
SKILLS_DIR = PLUGIN_ROOT / "skills"
PLUGIN_VERSION = "3.16.1"
def _parse_skill_frontmatter(skill_md_path: Path) -> dict:
try:
text = skill_md_path.read_text(encoding="utf-8", errors="replace")
except OSError as exc:
logger.debug("could not read %s: %s", skill_md_path, exc)
return {}
match = re.match(r"^---\s*\n(.*?)\n---\s*\n", text, re.DOTALL)
if not match:
return {}
fm_text = match.group(1)
fields: dict = {}
for key in ("name", "description"):
m = re.search(rf'^{key}:\s*["\']?(.*?)["\']?\s*$', fm_text, re.MULTILINE)
if m:
fields[key] = m.group(1).strip().rstrip('"\'')
return fields
def _walk_skills() -> list[dict]:
discovered: list[dict] = []
if not SKILLS_DIR.exists():
logger.warning("skills/ directory not found at %s", SKILLS_DIR)
return discovered
for entry in sorted(SKILLS_DIR.iterdir()):
if not entry.is_dir():
continue
skill_md = entry / "SKILL.md"
if not skill_md.exists():
continue
meta = _parse_skill_frontmatter(skill_md)
discovered.append({
"dir_name": entry.name,
"name": meta.get("name") or entry.name,
"description": meta.get("description") or "",
"skill_md_path": skill_md,
})
return discovered
def register(ctx) -> None:
logger.info("contentforge v%s registering with Hermes", PLUGIN_VERSION)
skills = _walk_skills()
if not skills:
logger.warning(
"contentforge found 0 skills in %s — plugin will be inert. "
"Confirm the plugin was cloned with its full skills/ tree.",
SKILLS_DIR,
)
return
if not hasattr(ctx, "register_skill"):
logger.error(
"contentforge: Hermes ctx is missing register_skill(). "
"Check Hermes version (targets v0.15.2+). Plugin will be inert. "
"Found %d skills that could not be registered.",
len(skills),
)
return
registered = failed = 0
for skill in skills:
try:
ctx.register_skill(skill["name"], skill["skill_md_path"])
registered += 1
except Exception as exc: # pragma: no cover
failed += 1
logger.warning("contentforge: failed to register %r: %s", skill["name"], exc)
logger.info(
"contentforge v%s: registered %d skills (failed: %d) under namespace 'contentforge:'.",
PLUGIN_VERSION, registered, failed,
)
def audit() -> dict:
skills = _walk_skills()
return {
"plugin_root": str(PLUGIN_ROOT),
"plugin_version": PLUGIN_VERSION,
"skills_dir": str(SKILLS_DIR),
"skills_dir_exists": SKILLS_DIR.exists(),
"skill_count": len(skills),
"first_5_skills": [
{"name": s["name"], "description": s["description"][:80]}
for s in skills[:5]
],
}
if __name__ == "__main__":
import json
print(json.dumps(audit(), indent=2, ensure_ascii=False))