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:
parent
5404a8fcd8
commit
69aa35a51c
23 changed files with 2080 additions and 32 deletions
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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
100
hermes_cli/pairing.py
Normal 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")
|
||||
Loading…
Add table
Add a link
Reference in a new issue