Add messaging platform enhancements: STT, stickers, Discord UX, Slack, pairing, hooks

Major feature additions inspired by OpenClaw/ClawdBot integration analysis:

Voice Message Transcription (STT):
- Auto-transcribe voice/audio messages via OpenAI Whisper API
- Download voice to ~/.hermes/audio_cache/ on Telegram/Discord/WhatsApp
- Inject transcript as text so all models can understand voice input
- Configurable model (whisper-1, gpt-4o-mini-transcribe, gpt-4o-transcribe)

Telegram Sticker Understanding:
- Describe static stickers via vision tool with JSON-backed cache
- Cache keyed by file_unique_id avoids redundant API calls
- Animated/video stickers get emoji-based fallback description

Discord Rich UX:
- Native slash commands (/ask, /reset, /status, /stop) via app_commands
- Button-based exec approvals (Allow Once / Always Allow / Deny)
- ExecApprovalView with user authorization and timeout handling

Slack Integration:
- Full SlackAdapter using slack-bolt with Socket Mode
- DMs, channel messages (mention-gated), /hermes slash command
- File attachment handling with bot-token-authenticated downloads

DM Pairing System:
- Code-based user authorization as alternative to static allowlists
- 8-char codes from unambiguous alphabet, 1-hour expiry
- Rate limiting, lockout after failed attempts, chmod 0600 on data
- CLI: hermes pairing list/approve/revoke/clear-pending

Event Hook System:
- File-based hook discovery from ~/.hermes/hooks/
- HOOK.yaml + handler.py per hook, sync/async handler support
- Events: gateway:startup, session:start/reset, agent:start/step/end
- Wildcard matching (command:* catches all command events)

Cross-Channel Messaging:
- send_message agent tool for delivering to any connected platform
- Enables cron job delivery and cross-platform notifications

Human-Like Response Pacing:
- Configurable delays between message chunks (off/natural/custom)
- HERMES_HUMAN_DELAY_MODE env var with min/max ms settings

Warm Injection Message Style:
- Retrofitted image vision messages with friendly kawaii-consistent tone
- All new injection messages (STT, stickers, errors) use warm style

Also: updated config migration to prompt for optional keys interactively,
bumped config version, updated README, AGENTS.md, .env.example,
cli-config.yaml.example, install scripts, pyproject.toml, and toolsets.
This commit is contained in:
teknium1 2026-02-15 21:38:59 -08:00
parent 5404a8fcd8
commit 69aa35a51c
23 changed files with 2080 additions and 32 deletions

View file

@ -117,11 +117,22 @@ DEFAULT_CONFIG = {
},
},
"stt": {
"enabled": True,
"model": "whisper-1",
},
"human_delay": {
"mode": "off",
"min_ms": 800,
"max_ms": 2500,
},
# Permanently allowed dangerous command patterns (added via "always" approval)
"command_allowlist": [],
# Config schema version - bump this when adding new required fields
"_config_version": 1,
"_config_version": 2,
}
# =============================================================================
@ -195,6 +206,20 @@ OPTIONAL_ENV_VARS = {
"url": None,
"password": True,
},
"SLACK_BOT_TOKEN": {
"description": "Slack bot integration",
"prompt": "Slack Bot Token (xoxb-...)",
"url": "https://api.slack.com/apps",
"tools": ["slack"],
"password": True,
},
"SLACK_APP_TOKEN": {
"description": "Slack Socket Mode connection",
"prompt": "Slack App Token (xapp-...)",
"url": "https://api.slack.com/apps",
"tools": ["slack"],
"password": True,
},
# Messaging platform tokens
"TELEGRAM_BOT_TOKEN": {
"description": "Telegram bot token from @BotFather",
@ -375,6 +400,44 @@ def migrate_config(interactive: bool = True, quiet: bool = False) -> Dict[str, A
results["warnings"].append(f"Skipped {var['name']} - some features may not work")
print()
# Check for missing optional env vars and offer to configure
missing_optional = get_missing_env_vars(required_only=False)
# Filter to only truly optional ones (not already handled as required above)
required_names = {v["name"] for v in missing_env} if missing_env else set()
missing_optional = [v for v in missing_optional if v["name"] not in required_names]
if missing_optional and not quiet:
print(f"\n {len(missing_optional)} optional API key(s) not configured:")
for var in missing_optional:
tools = var.get("tools", [])
tools_str = f" → enables: {', '.join(tools)}" if tools else ""
print(f"{var['name']}: {var['description']}{tools_str}")
if interactive and missing_optional:
print("\n Would you like to configure any optional keys now?")
try:
answer = input(" Configure optional keys? [y/N]: ").strip().lower()
except (EOFError, KeyboardInterrupt):
answer = "n"
if answer in ("y", "yes"):
print()
for var in missing_optional:
if var.get("url"):
print(f" Get your key at: {var['url']}")
if var.get("password"):
import getpass
value = getpass.getpass(f" {var['prompt']} (Enter to skip): ")
else:
value = input(f" {var['prompt']} (Enter to skip): ").strip()
if value:
save_env_value(var["name"], value)
results["env_added"].append(var["name"])
print(f" ✓ Saved {var['name']}")
print()
# Check for missing config fields
missing_config = get_missing_config_fields()

View file

@ -446,6 +446,34 @@ For more help on a command:
config_parser.set_defaults(func=cmd_config)
# =========================================================================
# pairing command
# =========================================================================
pairing_parser = subparsers.add_parser(
"pairing",
help="Manage DM pairing codes for user authorization",
description="Approve or revoke user access via pairing codes"
)
pairing_sub = pairing_parser.add_subparsers(dest="pairing_action")
pairing_list_parser = pairing_sub.add_parser("list", help="Show pending + approved users")
pairing_approve_parser = pairing_sub.add_parser("approve", help="Approve a pairing code")
pairing_approve_parser.add_argument("platform", help="Platform name (telegram, discord, slack, whatsapp)")
pairing_approve_parser.add_argument("code", help="Pairing code to approve")
pairing_revoke_parser = pairing_sub.add_parser("revoke", help="Revoke user access")
pairing_revoke_parser.add_argument("platform", help="Platform name")
pairing_revoke_parser.add_argument("user_id", help="User ID to revoke")
pairing_clear_parser = pairing_sub.add_parser("clear-pending", help="Clear all pending codes")
def cmd_pairing(args):
from hermes_cli.pairing import pairing_command
pairing_command(args)
pairing_parser.set_defaults(func=cmd_pairing)
# =========================================================================
# version command
# =========================================================================

100
hermes_cli/pairing.py Normal file
View file

@ -0,0 +1,100 @@
"""
CLI commands for the DM pairing system.
Usage:
hermes pairing list # Show all pending + approved users
hermes pairing approve <platform> <code> # Approve a pairing code
hermes pairing revoke <platform> <user_id> # Revoke user access
hermes pairing clear-pending # Clear all expired/pending codes
"""
import sys
def pairing_command(args):
"""Handle hermes pairing subcommands."""
from gateway.pairing import PairingStore
store = PairingStore()
action = getattr(args, "pairing_action", None)
if action == "list":
_cmd_list(store)
elif action == "approve":
_cmd_approve(store, args.platform, args.code)
elif action == "revoke":
_cmd_revoke(store, args.platform, args.user_id)
elif action == "clear-pending":
_cmd_clear_pending(store)
else:
print("Usage: hermes pairing {list|approve|revoke|clear-pending}")
print("Run 'hermes pairing --help' for details.")
def _cmd_list(store):
"""List all pending and approved users."""
pending = store.list_pending()
approved = store.list_approved()
if not pending and not approved:
print("No pairing data found. No one has tried to pair yet~")
return
if pending:
print(f"\n Pending Pairing Requests ({len(pending)}):")
print(f" {'Platform':<12} {'Code':<10} {'User ID':<20} {'Name':<20} {'Age'}")
print(f" {'--------':<12} {'----':<10} {'-------':<20} {'----':<20} {'---'}")
for p in pending:
print(
f" {p['platform']:<12} {p['code']:<10} {p['user_id']:<20} "
f"{p.get('user_name', ''):<20} {p['age_minutes']}m ago"
)
else:
print("\n No pending pairing requests.")
if approved:
print(f"\n Approved Users ({len(approved)}):")
print(f" {'Platform':<12} {'User ID':<20} {'Name':<20}")
print(f" {'--------':<12} {'-------':<20} {'----':<20}")
for a in approved:
print(f" {a['platform']:<12} {a['user_id']:<20} {a.get('user_name', ''):<20}")
else:
print("\n No approved users.")
print()
def _cmd_approve(store, platform: str, code: str):
"""Approve a pairing code."""
platform = platform.lower().strip()
code = code.upper().strip()
result = store.approve_code(platform, code)
if result:
uid = result["user_id"]
name = result.get("user_name", "")
display = f"{name} ({uid})" if name else uid
print(f"\n Approved! User {display} on {platform} can now use the bot~")
print(f" They'll be recognized automatically on their next message.\n")
else:
print(f"\n Code '{code}' not found or expired for platform '{platform}'.")
print(f" Run 'hermes pairing list' to see pending codes.\n")
def _cmd_revoke(store, platform: str, user_id: str):
"""Revoke a user's access."""
platform = platform.lower().strip()
if store.revoke(platform, user_id):
print(f"\n Revoked access for user {user_id} on {platform}.\n")
else:
print(f"\n User {user_id} not found in approved list for {platform}.\n")
def _cmd_clear_pending(store):
"""Clear all pending pairing codes."""
count = store.clear_pending()
if count:
print(f"\n Cleared {count} pending pairing request(s).\n")
else:
print("\n No pending requests to clear.\n")