#!/usr/bin/env python3 """ Serveur HTTP minimal pour déclencher hermes-auto-improve depuis n8n. Écoute sur 0.0.0.0:9095. Les POST /trigger* retournent immédiatement {"started": true}. Le script tourne en background thread. Polling du résultat via GET /status. POST /trigger → run standard (MAX_FILES fichiers) POST /trigger/subdir → body JSON {"subdir": "ia"} POST /trigger/dry-run → analyse sans commit/push POST /trigger/daily-pr → crée PR GitHub + résumé journalier GET /status → dernier résultat + état running GET /health → {"ok": true} """ import json import subprocess import threading from datetime import datetime from http.server import BaseHTTPRequestHandler, HTTPServer from pathlib import Path SCRIPT = "/usr/local/bin/hermes-auto-improve" STATE_FILE = Path("/var/lib/hermes-auto-improve/state.json") LOG_FILE = Path("/var/log/hermes-auto-improve.log") _lock = threading.Lock() _running = False _last_result = None def run_script(extra_args: list = None): global _running, _last_result with _lock: if _running: return _running = True try: cmd = [SCRIPT] + (extra_args or []) LOG_FILE.parent.mkdir(parents=True, exist_ok=True) with open(LOG_FILE, "a") as log: log.write(f"\n=== {datetime.now().isoformat()} — {' '.join(cmd)} ===\n") result = subprocess.run( cmd, capture_output=True, text=True, timeout=600 ) log.write(result.stdout) if result.stderr: log.write(f"STDERR: {result.stderr}") out = result.stdout start = out.rfind("{") end = out.rfind("}") + 1 if start >= 0 and end > start: _last_result = json.loads(out[start:end]) else: _last_result = {"error": "no_json", "stdout": out[-500:]} except subprocess.TimeoutExpired: _last_result = {"error": "timeout"} except Exception as e: _last_result = {"error": str(e)} finally: _running = False def trigger_async(extra_args: list = None) -> dict: with _lock: if _running: return {"started": False, "error": "already_running"} t = threading.Thread(target=run_script, args=(extra_args,), daemon=True) t.start() return {"started": True, "running": True} class Handler(BaseHTTPRequestHandler): def log_message(self, fmt, *args): pass def send_json(self, code: int, data: dict): body = json.dumps(data, ensure_ascii=False, indent=2).encode() self.send_response(code) self.send_header("Content-Type", "application/json") self.send_header("Content-Length", str(len(body))) self.end_headers() self.wfile.write(body) def do_GET(self): if self.path == "/health": self.send_json(200, {"ok": True, "running": _running}) elif self.path == "/status": state = json.loads(STATE_FILE.read_text()) if STATE_FILE.exists() else None self.send_json(200, {"state": state, "last_result": _last_result, "running": _running}) else: self.send_json(404, {"error": "not_found"}) def do_POST(self): length = int(self.headers.get("Content-Length", 0)) body = json.loads(self.rfile.read(length)) if length > 0 else {} if self.path == "/trigger": self.send_json(200, trigger_async()) elif self.path == "/trigger/all": self.send_json(200, trigger_async(["--all"])) elif self.path == "/trigger/subdir": subdir = body.get("subdir") if not subdir: self.send_json(400, {"error": "missing 'subdir' in body"}) return self.send_json(200, trigger_async(["--subdir", subdir])) elif self.path == "/trigger/dry-run": self.send_json(200, trigger_async(["--dry-run"])) elif self.path == "/trigger/daily-pr": self.send_json(200, trigger_async(["--daily-pr"])) elif self.path == "/trigger/reset": self.send_json(200, trigger_async(["--reset"])) else: self.send_json(404, {"error": "not_found"}) if __name__ == "__main__": server = HTTPServer(("0.0.0.0", 9095), Handler) print("hermes-auto-improve trigger server — port 9095", flush=True) server.serve_forever()