/ 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