hermes-agent/hermes_cli/slack_cli.py
2026-07-14 13:58:36 -07:00

224 lines
7.9 KiB
Python

"""``hermes slack ...`` CLI subcommands.
Today only ``hermes slack manifest`` is implemented — it generates the
Slack app manifest JSON for registering every gateway command as a native
Slack slash (``/btw``, ``/stop``, ``/model``, …) so users get the same
first-class slash UX Discord and Telegram already have.
Typical workflow::
$ hermes slack manifest > slack-manifest.json
# or:
$ hermes slack manifest --write
Then paste the printed JSON into the Slack app config (Features → App
Manifest → Edit) and click Save. Slack diffs the manifest and prompts
for reinstall when scopes/commands change.
"""
from __future__ import annotations
import json
import os
import sys
from pathlib import Path
def _build_full_manifest(
bot_name: str,
bot_description: str,
include_assistant: bool = True,
messaging_experience: str | None = None,
) -> dict:
"""Build a full Slack manifest merging display info + our slash list.
The slash-command list is always generated from ``COMMAND_REGISTRY`` so
it stays in sync with the rest of Hermes. Other manifest sections
(display info, OAuth scopes, socket mode) are set to sensible defaults
for a Hermes deployment — users can tweak them in the Slack UI after
pasting.
By default, this keeps Hermes on Slack's older Assistant messaging
experience (``assistant_view``) for backward compatibility. Pass
``messaging_experience="agent"`` (``--agent-view``) to emit Slack's Agent
messaging experience (``agent_view`` + ``app_home_opened``). Pass
``include_assistant=False`` or ``messaging_experience="none"``
(``--no-assistant``) to omit Slack AI messaging features and get a flat DM
surface where ``/help``, ``/new``, etc. work inline.
"""
from hermes_cli.commands import slack_app_manifest
if messaging_experience is None:
messaging_experience = "assistant" if include_assistant else "none"
messaging_experience = str(messaging_experience).strip().lower()
if messaging_experience not in {"assistant", "agent", "none"}:
raise ValueError(
"messaging_experience must be one of: assistant, agent, none"
)
partial = slack_app_manifest()
slashes = partial["features"]["slash_commands"]
features = {
"app_home": {
"home_tab_enabled": False,
"messages_tab_enabled": True,
"messages_tab_read_only_enabled": False,
},
"bot_user": {
"display_name": bot_name[:80],
"always_online": True,
},
"slash_commands": slashes,
}
bot_scopes = [
"app_mentions:read",
"channels:history",
"channels:read",
"chat:write",
"commands",
"files:read",
"files:write",
"groups:history",
"groups:read",
"im:history",
"im:read",
"im:write",
"mpim:history",
"mpim:read",
"users:read",
]
bot_events = [
"app_mention",
"message.channels",
"message.groups",
"message.im",
"message.mpim",
]
if messaging_experience == "assistant":
features["assistant_view"] = {
"assistant_description": "Chat with Hermes in threads and DMs.",
}
bot_scopes.append("assistant:write")
bot_events.extend(
[
"assistant_thread_context_changed",
"assistant_thread_started",
]
)
elif messaging_experience == "agent":
features["agent_view"] = {
"agent_description": "Chat with Hermes in Slack Messages.",
}
bot_scopes.append("assistant:write")
# Slack includes current viewing context in Agent DM events only after
# this subscription is enabled; the adapter consumes that context to
# preserve the referred channel across the agent turn.
bot_events.extend(["app_context_changed", "app_home_opened"])
bot_scopes.sort()
bot_events.sort()
return {
"_metadata": {
"major_version": 1,
"minor_version": 1,
},
"display_information": {
"name": bot_name[:35],
"description": (bot_description or "Your Hermes agent on Slack")[:140],
"background_color": "#1a1a2e",
},
"features": features,
"oauth_config": {
"scopes": {
"bot": bot_scopes,
},
},
"settings": {
"event_subscriptions": {
"bot_events": bot_events,
},
"interactivity": {
"is_enabled": True,
},
"org_deploy_enabled": False,
"socket_mode_enabled": True,
"token_rotation_enabled": False,
},
}
def slack_manifest_command(args) -> int:
"""Print or write a Slack app manifest JSON.
Flags (all parsed in ``hermes_cli/main.py``):
--write [PATH] Write to file instead of stdout (default path:
``$HERMES_HOME/slack-manifest.json``)
--name NAME Override the bot display name (default: "Hermes")
--description DESC Override the bot description
--slashes-only Emit only the ``features.slash_commands`` array (for
merging into an existing manifest manually)
--no-assistant Omit Slack AI Assistant mode (assistant_view feature,
assistant:write scope, assistant_thread_* events) so
DMs render as a flat chat where bare slash commands
work inline instead of the Assistant thread pane.
--agent-view Use Slack's Agent messaging experience (agent_view,
app_home_opened + message.im) instead of the legacy
Assistant messaging experience.
"""
name = getattr(args, "name", None) or "Hermes"
description = getattr(args, "description", None) or "Your Hermes agent on Slack"
if getattr(args, "agent_view", False):
messaging_experience = "agent"
elif getattr(args, "no_assistant", False):
messaging_experience = "none"
else:
messaging_experience = "assistant"
if getattr(args, "slashes_only", False):
from hermes_cli.commands import slack_app_manifest
manifest = slack_app_manifest()["features"]["slash_commands"]
else:
manifest = _build_full_manifest(
name,
description,
messaging_experience=messaging_experience,
)
payload = json.dumps(manifest, indent=2, ensure_ascii=False) + "\n"
write_target = getattr(args, "write", None)
if write_target is not None:
if isinstance(write_target, bool) and write_target:
# --write with no value → default location
try:
from hermes_constants import get_hermes_home
target = Path(get_hermes_home()) / "slack-manifest.json"
except Exception:
target = Path(os.environ.get("HERMES_HOME") or str(Path.home() / ".hermes")) / "slack-manifest.json"
else:
target = Path(write_target).expanduser()
target.parent.mkdir(parents=True, exist_ok=True)
target.write_text(payload, encoding="utf-8")
print(f"Slack manifest written to: {target}", file=sys.stderr)
print(
"\nNext steps:\n"
" 1. Open https://api.slack.com/apps and pick your Hermes app\n"
" (or create a new one: Create New App → From an app manifest).\n"
f" 2. Features → App Manifest → paste the contents of\n"
f" {target}\n"
" 3. Save; Slack will prompt to reinstall the app if scopes or\n"
" slash commands changed.\n"
" 4. Make sure Socket Mode is enabled and you have a bot token\n"
" (xoxb-...) and app token (xapp-...) configured via\n"
" `hermes setup`.\n",
file=sys.stderr,
)
else:
sys.stdout.write(payload)
return 0