/ hermes_cli / skills_config.py
skills_config.py
  1  """
  2  Skills configuration for Hermes Agent.
  3  `hermes skills` enters this module.
  4  
  5  Toggle individual skills or categories on/off, globally or per-platform.
  6  Config stored in ~/.hermes/config.yaml under:
  7  
  8    skills:
  9      disabled: [skill-a, skill-b]          # global disabled list
 10      platform_disabled:                    # per-platform overrides
 11        telegram: [skill-c]
 12        cli: []
 13  """
 14  from typing import List, Optional, Set
 15  
 16  from hermes_cli.config import cfg_get, load_config, save_config
 17  from hermes_cli.colors import Colors, color
 18  from hermes_cli.platforms import PLATFORMS as _PLATFORMS
 19  
 20  # Backward-compatible view: {key: label_string} so existing code that
 21  # iterates ``PLATFORMS.items()`` or calls ``PLATFORMS.get(key)`` keeps
 22  # working without changes to every call site.
 23  PLATFORMS = {k: info.label for k, info in _PLATFORMS.items() if k != "api_server"}
 24  
 25  # ─── Config Helpers ───────────────────────────────────────────────────────────
 26  
 27  def get_disabled_skills(config: dict, platform: Optional[str] = None) -> Set[str]:
 28      """Return disabled skill names. Platform-specific list falls back to global."""
 29      skills_cfg = config.get("skills", {})
 30      global_disabled = set(skills_cfg.get("disabled", []))
 31      if platform is None:
 32          return global_disabled
 33      platform_disabled = cfg_get(skills_cfg, "platform_disabled", platform)
 34      if platform_disabled is None:
 35          return global_disabled
 36      return set(platform_disabled)
 37  
 38  
 39  def save_disabled_skills(config: dict, disabled: Set[str], platform: Optional[str] = None):
 40      """Persist disabled skill names to config."""
 41      config.setdefault("skills", {})
 42      if platform is None:
 43          config["skills"]["disabled"] = sorted(disabled)
 44      else:
 45          config["skills"].setdefault("platform_disabled", {})
 46          config["skills"]["platform_disabled"][platform] = sorted(disabled)
 47      save_config(config)
 48  
 49  
 50  # ─── Skill Discovery ─────────────────────────────────────────────────────────
 51  
 52  def _list_all_skills() -> List[dict]:
 53      """Return all installed skills (ignoring disabled state)."""
 54      try:
 55          from tools.skills_tool import _find_all_skills
 56          return _find_all_skills(skip_disabled=True)
 57      except Exception:
 58          return []
 59  
 60  
 61  def _get_categories(skills: List[dict]) -> List[str]:
 62      """Return sorted unique category names (None -> 'uncategorized')."""
 63      return sorted({s["category"] or "uncategorized" for s in skills})
 64  
 65  
 66  # ─── Platform Selection ──────────────────────────────────────────────────────
 67  
 68  def _select_platform() -> Optional[str]:
 69      """Ask user which platform to configure, or global."""
 70      options = [("global", "All platforms (global default)")] + list(PLATFORMS.items())
 71      print()
 72      print(color("  Configure skills for:", Colors.BOLD))
 73      for i, (key, label) in enumerate(options, 1):
 74          print(f"  {i}. {label}")
 75      print()
 76      try:
 77          raw = input(color("  Select [1]: ", Colors.YELLOW)).strip()
 78      except (KeyboardInterrupt, EOFError):
 79          return None
 80      if not raw:
 81          return None  # global
 82      try:
 83          idx = int(raw) - 1
 84          if 0 <= idx < len(options):
 85              key = options[idx][0]
 86              return None if key == "global" else key
 87      except ValueError:
 88          pass
 89      return None
 90  
 91  
 92  # ─── Category Toggle ─────────────────────────────────────────────────────────
 93  
 94  def _toggle_by_category(skills: List[dict], disabled: Set[str]) -> Set[str]:
 95      """Toggle all skills in a category at once."""
 96      from hermes_cli.curses_ui import curses_checklist
 97  
 98      categories = _get_categories(skills)
 99      cat_labels = []
100      # A category is "enabled" (checked) when NOT all its skills are disabled
101      pre_selected = set()
102      for i, cat in enumerate(categories):
103          cat_skills = [s["name"] for s in skills if (s["category"] or "uncategorized") == cat]
104          cat_labels.append(f"{cat} ({len(cat_skills)} skills)")
105          if not all(s in disabled for s in cat_skills):
106              pre_selected.add(i)
107  
108      chosen = curses_checklist(
109          "Categories — toggle entire categories",
110          cat_labels, pre_selected, cancel_returns=pre_selected,
111      )
112  
113      new_disabled = set(disabled)
114      for i, cat in enumerate(categories):
115          cat_skills = {s["name"] for s in skills if (s["category"] or "uncategorized") == cat}
116          if i in chosen:
117              new_disabled -= cat_skills  # category enabled → remove from disabled
118          else:
119              new_disabled |= cat_skills  # category disabled → add to disabled
120      return new_disabled
121  
122  
123  # ─── Entry Point ──────────────────────────────────────────────────────────────
124  
125  def skills_command(args=None):
126      """Entry point for `hermes skills`."""
127      from hermes_cli.curses_ui import curses_checklist
128  
129      config = load_config()
130      skills = _list_all_skills()
131  
132      if not skills:
133          print(color("  No skills installed.", Colors.DIM))
134          return
135  
136      # Step 1: Select platform
137      platform = _select_platform()
138      platform_label = PLATFORMS.get(platform, "All platforms") if platform else "All platforms"
139  
140      # Step 2: Select mode — individual or by category
141      print()
142      print(color(f"  Configure for: {platform_label}", Colors.DIM))
143      print()
144      print("  1. Toggle individual skills")
145      print("  2. Toggle by category")
146      print()
147      try:
148          mode = input(color("  Select [1]: ", Colors.YELLOW)).strip() or "1"
149      except (KeyboardInterrupt, EOFError):
150          return
151  
152      disabled = get_disabled_skills(config, platform)
153  
154      if mode == "2":
155          new_disabled = _toggle_by_category(skills, disabled)
156      else:
157          # Build labels and map indices → skill names
158          labels = [
159              f"{s['name']}  ({s['category'] or 'uncategorized'})  —  {s['description'][:55]}"
160              for s in skills
161          ]
162          # "selected" = enabled (not disabled) — matches the [✓] convention
163          pre_selected = {i for i, s in enumerate(skills) if s["name"] not in disabled}
164          chosen = curses_checklist(
165              f"Skills for {platform_label}",
166              labels, pre_selected, cancel_returns=pre_selected,
167          )
168          # Anything NOT chosen is disabled
169          new_disabled = {skills[i]["name"] for i in range(len(skills)) if i not in chosen}
170  
171      if new_disabled == disabled:
172          print(color("  No changes.", Colors.DIM))
173          return
174  
175      save_disabled_skills(config, new_disabled, platform)
176      enabled_count = len(skills) - len(new_disabled)
177      print(color(f"✓ Saved: {enabled_count} enabled, {len(new_disabled)} disabled ({platform_label}).", Colors.GREEN))