""" Hermes Zulip Plugin — Core adapter for Hermes Python agents Connects Hermes agents (Tanko, Mumuni, Koonimo, Koby) to the Sysloggh Zulip agent mesh via event queue polling + subprocess agent invocation. Architecture mirrors pi-zulip-extension: Zulip event queue → poll loop → parse message → spawn agent subprocess → capture stdout → post response to Zulip (with placeholder + edit) Usage (systemd service): python3 -m hermes_zulip --config /path/to/config.yaml Path: hermes-zulip-plugin/src/hermes_zulip/ Config: config.yaml (per-agent, deployed alongside plugin) @see ADR-001 DM-first, ADR-005 @mention detection @see ADR-006 @all-bots, ADR-009 error handling """ import argparse import logging import sys from typing import Dict, Any # Ensure the src directory is on the path for the adapter import import os import sys sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) from hermes_zulip.adapter import ZulipAdapter __version__ = "0.2.0" def load_config(config_path: str) -> Dict[str, Any]: """Load YAML config and inject env vars.""" import yaml with open(config_path) as f: cfg = yaml.safe_load(f) # Override api_key from env if set env_key = os.environ.get("ZULIP_API_KEY") if env_key: cfg.setdefault("zulip", {})["api_key"] = env_key return cfg def setup_logging(cfg: Dict[str, Any]) -> None: """Configure logging from config.""" level = ( cfg.get("monitoring", {}).get("log_level", "INFO").upper() ) logging.basicConfig( level=getattr(logging, level, logging.INFO), format="%(asctime)s [%(name)s] %(levelname)s: %(message)s", datefmt="%Y-%m-%d %H:%M:%S", ) def start_health_server(cfg: Dict[str, Any], adapter: ZulipAdapter) -> None: """Start a minimal health HTTP endpoint in a background thread.""" import threading from http.server import HTTPServer, BaseHTTPRequestHandler port = cfg.get("monitoring", {}).get("health_port", 9200) start_time = time.time() class HealthHandler(BaseHTTPRequestHandler): def do_GET(self) -> None: if self.path in ("/", "/health"): import json body = json.dumps({ "status": "ok", "agent": cfg.get("agent", {}).get("name", "unknown"), "connected": adapter.connected, "uptime_seconds": int(time.time() - start_time), "timestamp": datetime.utcnow().isoformat(), }) self.send_response(200) self.send_header("Content-Type", "application/json") self.end_headers() self.wfile.write(body.encode()) else: self.send_response(404) self.end_headers() def log_message(self, fmt, *args): pass # Suppress health check log spam server = HTTPServer(("127.0.0.1", port), HealthHandler) thread = threading.Thread(target=server.serve_forever, daemon=True) thread.start() logging.getLogger(__name__).info(f"Health endpoint on :{port}") def main() -> None: """CLI entry point for the Hermes Zulip plugin. Usage: python3 -m hermes_zulip --config /path/to/config.yaml """ import time from datetime import datetime parser = argparse.ArgumentParser( description="Hermes Zulip Plugin — Connects Hermes agents to Zulip" ) parser.add_argument( "--config", "-c", default="config.yaml", help="Path to config.yaml (default: config.yaml)", ) args = parser.parse_args() # Load config cfg = load_config(args.config) setup_logging(cfg) logger = logging.getLogger(__name__) logger.info( f"Starting Hermes Zulip Plugin v{__version__} " f"for {cfg.get('agent', {}).get('name', 'unknown')}" ) # Create adapter adapter = ZulipAdapter(cfg) # Start health endpoint (if enabled) if cfg.get("monitoring", {}).get("health_endpoint_enabled", True): start_health_server(cfg, adapter) # Run the event loop (blocks until interrupted) try: adapter.run() except KeyboardInterrupt: logger.info("Shutting down...") except Exception as e: logger.error(f"Fatal error: {e}") sys.exit(1) if __name__ == "__main__": main()