/ hermes_cli / slack_cli.py
slack_cli.py
1 """``hermes slack ...`` CLI subcommands. 2 3 Today only ``hermes slack manifest`` is implemented — it generates the 4 Slack app manifest JSON for registering every gateway command as a native 5 Slack slash (``/btw``, ``/stop``, ``/model``, …) so users get the same 6 first-class slash UX Discord and Telegram already have. 7 8 Typical workflow:: 9 10 $ hermes slack manifest > slack-manifest.json 11 # or: 12 $ hermes slack manifest --write 13 14 Then paste the printed JSON into the Slack app config (Features → App 15 Manifest → Edit) and click Save. Slack diffs the manifest and prompts 16 for reinstall when scopes/commands change. 17 """ 18 from __future__ import annotations 19 20 import json 21 import os 22 import sys 23 from pathlib import Path 24 25 26 def _build_full_manifest(bot_name: str, bot_description: str) -> dict: 27 """Build a full Slack manifest merging display info + our slash list. 28 29 The slash-command list is always generated from ``COMMAND_REGISTRY`` so 30 it stays in sync with the rest of Hermes. Other manifest sections 31 (display info, OAuth scopes, socket mode) are set to sensible defaults 32 for a Hermes deployment — users can tweak them in the Slack UI after 33 pasting. 34 """ 35 from hermes_cli.commands import slack_app_manifest 36 37 partial = slack_app_manifest() 38 slashes = partial["features"]["slash_commands"] 39 40 return { 41 "_metadata": { 42 "major_version": 1, 43 "minor_version": 1, 44 }, 45 "display_information": { 46 "name": bot_name[:35], 47 "description": (bot_description or "Your Hermes agent on Slack")[:140], 48 "background_color": "#1a1a2e", 49 }, 50 "features": { 51 "bot_user": { 52 "display_name": bot_name[:80], 53 "always_online": True, 54 }, 55 "slash_commands": slashes, 56 "assistant_view": { 57 "assistant_description": "Chat with Hermes in threads and DMs.", 58 }, 59 }, 60 "oauth_config": { 61 "scopes": { 62 "bot": [ 63 "app_mentions:read", 64 "assistant:write", 65 "channels:history", 66 "channels:read", 67 "chat:write", 68 "commands", 69 "files:read", 70 "files:write", 71 "groups:history", 72 "im:history", 73 "im:read", 74 "im:write", 75 "users:read", 76 ], 77 }, 78 }, 79 "settings": { 80 "event_subscriptions": { 81 "bot_events": [ 82 "app_mention", 83 "assistant_thread_context_changed", 84 "assistant_thread_started", 85 "message.channels", 86 "message.groups", 87 "message.im", 88 ], 89 }, 90 "interactivity": { 91 "is_enabled": True, 92 }, 93 "org_deploy_enabled": False, 94 "socket_mode_enabled": True, 95 "token_rotation_enabled": False, 96 }, 97 } 98 99 100 def slack_manifest_command(args) -> int: 101 """Print or write a Slack app manifest JSON. 102 103 Flags (all parsed in ``hermes_cli/main.py``): 104 --write [PATH] Write to file instead of stdout (default path: 105 ``$HERMES_HOME/slack-manifest.json``) 106 --name NAME Override the bot display name (default: "Hermes") 107 --description DESC Override the bot description 108 --slashes-only Emit only the ``features.slash_commands`` array (for 109 merging into an existing manifest manually) 110 """ 111 name = getattr(args, "name", None) or "Hermes" 112 description = getattr(args, "description", None) or "Your Hermes agent on Slack" 113 114 if getattr(args, "slashes_only", False): 115 from hermes_cli.commands import slack_app_manifest 116 117 manifest = slack_app_manifest()["features"]["slash_commands"] 118 else: 119 manifest = _build_full_manifest(name, description) 120 121 payload = json.dumps(manifest, indent=2, ensure_ascii=False) + "\n" 122 123 write_target = getattr(args, "write", None) 124 if write_target is not None: 125 if isinstance(write_target, bool) and write_target: 126 # --write with no value → default location 127 try: 128 from hermes_constants import get_hermes_home 129 130 target = Path(get_hermes_home()) / "slack-manifest.json" 131 except Exception: 132 target = Path(os.environ.get("HERMES_HOME") or str(Path.home() / ".hermes")) / "slack-manifest.json" 133 else: 134 target = Path(write_target).expanduser() 135 target.parent.mkdir(parents=True, exist_ok=True) 136 target.write_text(payload, encoding="utf-8") 137 print(f"Slack manifest written to: {target}", file=sys.stderr) 138 print( 139 "\nNext steps:\n" 140 " 1. Open https://api.slack.com/apps and pick your Hermes app\n" 141 " (or create a new one: Create New App → From an app manifest).\n" 142 f" 2. Features → App Manifest → paste the contents of\n" 143 f" {target}\n" 144 " 3. Save; Slack will prompt to reinstall the app if scopes or\n" 145 " slash commands changed.\n" 146 " 4. Make sure Socket Mode is enabled and you have a bot token\n" 147 " (xoxb-...) and app token (xapp-...) configured via\n" 148 " `hermes setup`.\n", 149 file=sys.stderr, 150 ) 151 else: 152 sys.stdout.write(payload) 153 return 0