__init__.py 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104
  1. import os
  2. import re
  3. from pathlib import Path
  4. BOOTSTRAP_MARKER = "superpowers:using-superpowers bootstrap for hermes"
  5. def _skills_dir() -> str:
  6. """Locate the stock skills/ tree for either supported install layout.
  7. - git-clone install (`hermes plugins install obra/superpowers`): the plugin
  8. dir is the repo root, so `.hermes-plugin/` and `skills/` are siblings and
  9. this module resolves `../skills`.
  10. - flattened install (plugin files copied to the plugin dir root): `skills/`
  11. sits next to this module.
  12. Raises loudly when neither matches — a bootstrap that silently skips is how
  13. a broken install masquerades as a working one.
  14. """
  15. here = os.path.dirname(os.path.realpath(__file__))
  16. candidates = (
  17. os.path.realpath(os.path.join(here, "..", "skills")),
  18. os.path.realpath(os.path.join(here, "skills")),
  19. )
  20. for cand in candidates:
  21. if os.path.isfile(os.path.join(cand, "using-superpowers", "SKILL.md")):
  22. return cand
  23. raise RuntimeError(
  24. "superpowers plugin: cannot find the skills/ tree "
  25. f"(looked at {candidates}). Reinstall with "
  26. "`hermes plugins install obra/superpowers`."
  27. )
  28. def _strip_frontmatter(content: str) -> str:
  29. match = re.match(r"^---\n[\s\S]*?\n---\n([\s\S]*)$", content)
  30. return (match.group(1) if match else content).strip()
  31. def _build_bootstrap(skills_dir: str) -> str:
  32. with open(
  33. os.path.join(skills_dir, "using-superpowers", "SKILL.md"),
  34. encoding="utf-8",
  35. ) as f:
  36. body = _strip_frontmatter(f.read())
  37. tools_path = os.path.join(
  38. skills_dir, "using-superpowers", "references", "hermes-tools.md"
  39. )
  40. with open(tools_path, encoding="utf-8") as f:
  41. tool_mapping = f.read().strip()
  42. return (
  43. f"<EXTREMELY_IMPORTANT>\n"
  44. f"{BOOTSTRAP_MARKER}\n\n"
  45. f"You have superpowers.\n\n"
  46. f"The using-superpowers skill content is included below and is already "
  47. f"loaded for this Hermes session. Follow it now. "
  48. f"Do not try to load using-superpowers again.\n\n"
  49. f"{body}\n\n"
  50. f"## Loading Superpowers Skills on Hermes\n\n"
  51. f"Superpowers skills are registered with Hermes' native skill loader: "
  52. f'invoke one with `skill_view("superpowers:skill-name")` '
  53. f'(for example `skill_view("superpowers:brainstorming")`). '
  54. f"If a namespaced lookup returns 'not found', read the skill file "
  55. f"directly instead:\n"
  56. f'`read_file("{skills_dir}/skill-name/SKILL.md")`\n\n'
  57. f"The superpowers skills directory is: `{skills_dir}`\n\n"
  58. f"{tool_mapping}\n"
  59. f"</EXTREMELY_IMPORTANT>"
  60. )
  61. def register(ctx):
  62. skills_dir = _skills_dir()
  63. bootstrap = _build_bootstrap(skills_dir)
  64. # Register every stock skill with Hermes' native loader so skill_view can
  65. # load them on demand. Standard markdown; no conversion (plugin guide).
  66. # register_skill requires a pathlib.Path — a str raises AttributeError and
  67. # hermes silently disables the whole plugin (verified 2026-07-23).
  68. for name in sorted(os.listdir(skills_dir)):
  69. skill_md = os.path.join(skills_dir, name, "SKILL.md")
  70. if os.path.isfile(skill_md):
  71. ctx.register_skill(name, Path(skill_md))
  72. # pre_llm_call returning {"context": ...} is the documented injection path
  73. # (on_session_start return values are ignored, and ctx.inject_message
  74. # refuses from that hook — verified empirically 2026-07-23). The context is
  75. # appended to the first turn's user message.
  76. def pre_llm_call(
  77. session_id=None,
  78. user_message=None,
  79. conversation_history=None,
  80. is_first_turn=None,
  81. model=None,
  82. platform=None,
  83. **kwargs,
  84. ):
  85. if is_first_turn:
  86. return {"context": bootstrap}
  87. return None
  88. ctx.register_hook("pre_llm_call", pre_llm_call)