/ 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))