Add apps modules and improve captain_claude logging
- Add apps/ directory with modular components: - captain.py: Main orchestrator - corp/, deck/, devops/, docker/, hst/: Domain-specific apps - Fix duplicate logger handlers in long sessions - Add flush=True to print statements for real-time output Note: flow-ui, mindlink, tzzr-cli are separate repos (not included) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
165
apps/captain.py
Normal file
165
apps/captain.py
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
CAPTAIN CLAUDE - CLI Unificado para el Sistema TZZR
|
||||||
|
Punto de entrada principal para todas las apps
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
APPS_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
|
||||||
|
APPS = {
|
||||||
|
"devops": {
|
||||||
|
"path": f"{APPS_DIR}/devops/app.py",
|
||||||
|
"desc": "Gestión de despliegues y construcción",
|
||||||
|
"alias": ["deploy", "build"]
|
||||||
|
},
|
||||||
|
"deck": {
|
||||||
|
"path": f"{APPS_DIR}/deck/app.py",
|
||||||
|
"desc": "Servidor DECK (72.62.1.113) - Agentes, Mail, Apps",
|
||||||
|
"alias": ["d"]
|
||||||
|
},
|
||||||
|
"corp": {
|
||||||
|
"path": f"{APPS_DIR}/corp/app.py",
|
||||||
|
"desc": "Servidor CORP (92.112.181.188) - Margaret, Jared, Mason, Feldman",
|
||||||
|
"alias": ["c"]
|
||||||
|
},
|
||||||
|
"hst": {
|
||||||
|
"path": f"{APPS_DIR}/hst/app.py",
|
||||||
|
"desc": "Servidor HST (72.62.2.84) - Directus, Imágenes",
|
||||||
|
"alias": ["h"]
|
||||||
|
},
|
||||||
|
"docker": {
|
||||||
|
"path": f"{APPS_DIR}/docker/app.py",
|
||||||
|
"desc": "Gestión Docker multi-servidor",
|
||||||
|
"alias": ["dk"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def print_banner():
|
||||||
|
print("""
|
||||||
|
╔═══════════════════════════════════════════════════════════════╗
|
||||||
|
║ CAPTAIN CLAUDE ║
|
||||||
|
║ Sistema Multiagente TZZR ║
|
||||||
|
╠═══════════════════════════════════════════════════════════════╣
|
||||||
|
║ Servidor Central: 69.62.126.110 (Gitea, PostgreSQL) ║
|
||||||
|
║ DECK: 72.62.1.113 │ CORP: 92.112.181.188 ║
|
||||||
|
║ HST: 72.62.2.84 │ R2: Cloudflare Storage ║
|
||||||
|
╚═══════════════════════════════════════════════════════════════╝
|
||||||
|
""")
|
||||||
|
|
||||||
|
def print_help():
|
||||||
|
print_banner()
|
||||||
|
print("Uso: python captain.py <app> [comando] [argumentos]\n")
|
||||||
|
print("Apps disponibles:")
|
||||||
|
print("─" * 60)
|
||||||
|
|
||||||
|
for name, info in APPS.items():
|
||||||
|
aliases = ", ".join(info["alias"]) if info["alias"] else ""
|
||||||
|
alias_str = f" (alias: {aliases})" if aliases else ""
|
||||||
|
print(f" {name}{alias_str}")
|
||||||
|
print(f" └─ {info['desc']}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
print("Ejemplos:")
|
||||||
|
print(" python captain.py devops deploy clara deck")
|
||||||
|
print(" python captain.py deck agents")
|
||||||
|
print(" python captain.py corp pending")
|
||||||
|
print(" python captain.py docker ps all")
|
||||||
|
print(" python captain.py hst directus")
|
||||||
|
print()
|
||||||
|
print("Atajos rápidos:")
|
||||||
|
print(" python captain.py d agents # DECK agents")
|
||||||
|
print(" python captain.py c flows # CORP flows")
|
||||||
|
print(" python captain.py dk dashboard # Docker dashboard")
|
||||||
|
|
||||||
|
def resolve_app(name: str) -> str:
|
||||||
|
"""Resuelve nombre o alias a nombre de app"""
|
||||||
|
if name in APPS:
|
||||||
|
return name
|
||||||
|
|
||||||
|
for app_name, info in APPS.items():
|
||||||
|
if name in info.get("alias", []):
|
||||||
|
return app_name
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def run_app(app: str, args: list):
|
||||||
|
"""Ejecuta una app con los argumentos dados"""
|
||||||
|
app_name = resolve_app(app)
|
||||||
|
|
||||||
|
if not app_name:
|
||||||
|
print(f"❌ App no encontrada: {app}")
|
||||||
|
print(f" Apps disponibles: {', '.join(APPS.keys())}")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
app_path = APPS[app_name]["path"]
|
||||||
|
|
||||||
|
if not os.path.exists(app_path):
|
||||||
|
print(f"❌ Archivo no encontrado: {app_path}")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
cmd = ["python3", app_path] + args
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = subprocess.run(cmd)
|
||||||
|
return result.returncode
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\n⚠️ Interrumpido")
|
||||||
|
return 130
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Error: {e}")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
def quick_status():
|
||||||
|
"""Muestra estado rápido de todos los servidores"""
|
||||||
|
print_banner()
|
||||||
|
print("📊 Estado rápido del sistema:\n")
|
||||||
|
|
||||||
|
servers = [
|
||||||
|
("DECK", "root@72.62.1.113"),
|
||||||
|
("CORP", "root@92.112.181.188"),
|
||||||
|
("HST", "root@72.62.2.84")
|
||||||
|
]
|
||||||
|
|
||||||
|
for name, host in servers:
|
||||||
|
cmd = f"ssh -i ~/.ssh/tzzr -o ConnectTimeout=3 {host} 'docker ps -q | wc -l' 2>/dev/null"
|
||||||
|
try:
|
||||||
|
result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=5)
|
||||||
|
if result.returncode == 0:
|
||||||
|
count = result.stdout.strip()
|
||||||
|
print(f" ✅ {name} ({host.split('@')[1]}): {count} contenedores")
|
||||||
|
else:
|
||||||
|
print(f" ❌ {name} ({host.split('@')[1]}): No responde")
|
||||||
|
except:
|
||||||
|
print(f" ❌ {name} ({host.split('@')[1]}): Timeout")
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
print_help()
|
||||||
|
return 0
|
||||||
|
|
||||||
|
cmd = sys.argv[1]
|
||||||
|
|
||||||
|
# Comandos especiales
|
||||||
|
if cmd in ["-h", "--help", "help"]:
|
||||||
|
print_help()
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if cmd in ["status", "s"]:
|
||||||
|
quick_status()
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if cmd in ["version", "-v", "--version"]:
|
||||||
|
print("Captain Claude v1.0.0")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# Ejecutar app
|
||||||
|
args = sys.argv[2:] if len(sys.argv) > 2 else []
|
||||||
|
return run_app(cmd, args)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
343
apps/corp/app.py
Normal file
343
apps/corp/app.py
Normal file
@@ -0,0 +1,343 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
TZZR CORP App - Gestión del servidor CORP (92.112.181.188)
|
||||||
|
Servicios: Agentes Margaret/Jared/Mason/Feldman, Context Manager, Apps
|
||||||
|
"""
|
||||||
|
import subprocess
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import List, Dict, Optional
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
SERVER = "root@92.112.181.188"
|
||||||
|
SSH_KEY = "~/.ssh/tzzr"
|
||||||
|
|
||||||
|
# Servicios conocidos en CORP
|
||||||
|
SERVICES = {
|
||||||
|
# Microservicios TZZR
|
||||||
|
"margaret": {"port": 5051, "type": "service", "desc": "Recepción de datos", "path": "/opt/margaret"},
|
||||||
|
"jared": {"port": 5052, "type": "service", "desc": "Automatización de flujos", "path": "/opt/jared"},
|
||||||
|
"mason": {"port": 5053, "type": "service", "desc": "Espacio de enriquecimiento", "path": "/opt/mason"},
|
||||||
|
"feldman": {"port": 5054, "type": "service", "desc": "Registro final + Merkle", "path": "/opt/feldman"},
|
||||||
|
|
||||||
|
# Aplicaciones
|
||||||
|
"nextcloud": {"port": 8080, "type": "app", "desc": "Cloud storage", "url": "nextcloud.tzzrcorp.me"},
|
||||||
|
"directus": {"port": 8055, "type": "app", "desc": "CMS", "url": "tzzrcorp.me"},
|
||||||
|
"vaultwarden": {"port": 8081, "type": "app", "desc": "Passwords", "url": "vault.tzzrcorp.me"},
|
||||||
|
"shlink": {"port": 8082, "type": "app", "desc": "URL shortener", "url": "shlink.tzzrcorp.me"},
|
||||||
|
"addy": {"port": 8083, "type": "app", "desc": "Email aliases", "url": "addy.tzzrcorp.me"},
|
||||||
|
"odoo": {"port": 8069, "type": "app", "desc": "ERP", "url": "erp.tzzrcorp.me"},
|
||||||
|
"ntfy": {"port": 8880, "type": "app", "desc": "Notifications", "url": "ntfy.tzzrcorp.me"},
|
||||||
|
|
||||||
|
# APIs y Automatización
|
||||||
|
"hsu": {"port": 5002, "type": "api", "desc": "HSU User Library", "url": "hsu.tzzrcorp.me"},
|
||||||
|
"windmill": {"port": 8000, "type": "automation", "desc": "Automatización de flujos", "url": "windmill.tzzrcorp.me"},
|
||||||
|
"context-manager": {"port": None, "type": "system", "desc": "Context Manager IA", "path": "/opt/context-manager"},
|
||||||
|
|
||||||
|
# Infraestructura
|
||||||
|
"postgres": {"port": 5432, "type": "db", "desc": "PostgreSQL 16"},
|
||||||
|
"redis": {"port": 6379, "type": "db", "desc": "Cache Redis"},
|
||||||
|
}
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Result:
|
||||||
|
success: bool
|
||||||
|
data: any
|
||||||
|
error: str = ""
|
||||||
|
|
||||||
|
def ssh(cmd: str, timeout: int = 60) -> Result:
|
||||||
|
"""Ejecuta comando en CORP"""
|
||||||
|
full_cmd = f'ssh -i {SSH_KEY} {SERVER} "{cmd}"'
|
||||||
|
try:
|
||||||
|
result = subprocess.run(full_cmd, shell=True, capture_output=True, text=True, timeout=timeout)
|
||||||
|
return Result(success=result.returncode == 0, data=result.stdout.strip(), error=result.stderr.strip())
|
||||||
|
except Exception as e:
|
||||||
|
return Result(success=False, data="", error=str(e))
|
||||||
|
|
||||||
|
def get_all_containers() -> List[Dict]:
|
||||||
|
"""Lista todos los contenedores Docker"""
|
||||||
|
result = ssh("docker ps -a --format '{{.Names}}|{{.Status}}|{{.Ports}}|{{.Image}}'")
|
||||||
|
if not result.success:
|
||||||
|
return []
|
||||||
|
|
||||||
|
containers = []
|
||||||
|
for line in result.data.split('\n'):
|
||||||
|
if line:
|
||||||
|
parts = line.split('|')
|
||||||
|
containers.append({
|
||||||
|
"name": parts[0],
|
||||||
|
"status": parts[1] if len(parts) > 1 else "",
|
||||||
|
"ports": parts[2] if len(parts) > 2 else "",
|
||||||
|
"image": parts[3] if len(parts) > 3 else ""
|
||||||
|
})
|
||||||
|
return containers
|
||||||
|
|
||||||
|
def get_service_status(service: str) -> Dict:
|
||||||
|
"""Estado detallado de un servicio"""
|
||||||
|
info = SERVICES.get(service, {})
|
||||||
|
container = f"{service}-service" if info.get("type") == "agent" else service
|
||||||
|
|
||||||
|
status_result = ssh(f"docker ps --filter name={container} --format '{{{{.Status}}}}'")
|
||||||
|
|
||||||
|
health = "unknown"
|
||||||
|
if info.get("port"):
|
||||||
|
health_result = ssh(f"curl -s http://localhost:{info.get('port')}/health 2>/dev/null | head -c 100")
|
||||||
|
health = "healthy" if health_result.success and health_result.data else "unhealthy"
|
||||||
|
|
||||||
|
return {
|
||||||
|
"service": service,
|
||||||
|
"container": container,
|
||||||
|
"status": status_result.data if status_result.success else "not found",
|
||||||
|
"health": health,
|
||||||
|
"port": info.get("port"),
|
||||||
|
"url": info.get("url"),
|
||||||
|
"desc": info.get("desc")
|
||||||
|
}
|
||||||
|
|
||||||
|
def restart_service(service: str) -> Result:
|
||||||
|
"""Reinicia un servicio"""
|
||||||
|
info = SERVICES.get(service, {})
|
||||||
|
container = f"{service}-service" if info.get("type") == "agent" else service
|
||||||
|
return ssh(f"docker restart {container}")
|
||||||
|
|
||||||
|
def get_logs(service: str, lines: int = 100) -> str:
|
||||||
|
"""Obtiene logs de un servicio"""
|
||||||
|
info = SERVICES.get(service, {})
|
||||||
|
container = f"{service}-service" if info.get("type") == "agent" else service
|
||||||
|
result = ssh(f"docker logs {container} --tail {lines} 2>&1")
|
||||||
|
return result.data if result.success else result.error
|
||||||
|
|
||||||
|
def query_agent(agent: str, endpoint: str, method: str = "GET", data: dict = None) -> Result:
|
||||||
|
"""Hace petición a un agente TZZR"""
|
||||||
|
info = SERVICES.get(agent)
|
||||||
|
if not info or info.get("type") != "agent":
|
||||||
|
return Result(success=False, data="", error="Agente no encontrado")
|
||||||
|
|
||||||
|
port = info["port"]
|
||||||
|
|
||||||
|
if method == "GET":
|
||||||
|
cmd = f"curl -s http://localhost:{port}{endpoint}"
|
||||||
|
else:
|
||||||
|
json_data = json.dumps(data) if data else "{}"
|
||||||
|
cmd = f"curl -s -X {method} -H 'Content-Type: application/json' -d '{json_data}' http://localhost:{port}{endpoint}"
|
||||||
|
|
||||||
|
result = ssh(cmd)
|
||||||
|
|
||||||
|
try:
|
||||||
|
return Result(success=result.success, data=json.loads(result.data) if result.data else {})
|
||||||
|
except:
|
||||||
|
return Result(success=result.success, data=result.data)
|
||||||
|
|
||||||
|
def get_system_stats() -> Dict:
|
||||||
|
"""Estadísticas del sistema"""
|
||||||
|
stats = {}
|
||||||
|
|
||||||
|
mem_result = ssh("free -h | grep Mem | awk '{print $2,$3,$4}'")
|
||||||
|
if mem_result.success:
|
||||||
|
parts = mem_result.data.split()
|
||||||
|
stats["memory"] = {"total": parts[0], "used": parts[1], "available": parts[2]}
|
||||||
|
|
||||||
|
disk_result = ssh("df -h / | tail -1 | awk '{print $2,$3,$4,$5}'")
|
||||||
|
if disk_result.success:
|
||||||
|
parts = disk_result.data.split()
|
||||||
|
stats["disk"] = {"total": parts[0], "used": parts[1], "available": parts[2], "percent": parts[3]}
|
||||||
|
|
||||||
|
containers_result = ssh("docker ps -q | wc -l")
|
||||||
|
stats["containers_running"] = int(containers_result.data) if containers_result.success else 0
|
||||||
|
|
||||||
|
return stats
|
||||||
|
|
||||||
|
def get_postgres_stats() -> Dict:
|
||||||
|
"""Estadísticas de PostgreSQL"""
|
||||||
|
result = ssh("sudo -u postgres psql -c \"SELECT datname, pg_size_pretty(pg_database_size(datname)) as size FROM pg_database WHERE datistemplate = false;\" -t")
|
||||||
|
|
||||||
|
databases = {}
|
||||||
|
if result.success:
|
||||||
|
for line in result.data.split('\n'):
|
||||||
|
if '|' in line:
|
||||||
|
parts = line.split('|')
|
||||||
|
databases[parts[0].strip()] = parts[1].strip()
|
||||||
|
|
||||||
|
return databases
|
||||||
|
|
||||||
|
def ingest_to_margaret(contenedor: dict, auth_key: str) -> Result:
|
||||||
|
"""Envía datos a Margaret para ingestión"""
|
||||||
|
json_data = json.dumps(contenedor)
|
||||||
|
cmd = f"curl -s -X POST -H 'Content-Type: application/json' -H 'X-Auth-Key: {auth_key}' -d '{json_data}' http://localhost:5051/ingest"
|
||||||
|
result = ssh(cmd)
|
||||||
|
|
||||||
|
try:
|
||||||
|
return Result(success=result.success, data=json.loads(result.data) if result.data else {})
|
||||||
|
except:
|
||||||
|
return Result(success=result.success, data=result.data)
|
||||||
|
|
||||||
|
def execute_flow(flow_id: int, data: dict, auth_key: str) -> Result:
|
||||||
|
"""Ejecuta un flujo en Jared"""
|
||||||
|
json_data = json.dumps(data)
|
||||||
|
cmd = f"curl -s -X POST -H 'Content-Type: application/json' -H 'X-Auth-Key: {auth_key}' -d '{json_data}' http://localhost:5052/ejecutar/{flow_id}"
|
||||||
|
result = ssh(cmd)
|
||||||
|
|
||||||
|
try:
|
||||||
|
return Result(success=result.success, data=json.loads(result.data) if result.data else {})
|
||||||
|
except:
|
||||||
|
return Result(success=result.success, data=result.data)
|
||||||
|
|
||||||
|
def get_pending_issues() -> Result:
|
||||||
|
"""Obtiene incidencias pendientes de Mason"""
|
||||||
|
result = ssh("curl -s http://localhost:5053/pendientes")
|
||||||
|
try:
|
||||||
|
return Result(success=result.success, data=json.loads(result.data) if result.data else [])
|
||||||
|
except:
|
||||||
|
return Result(success=result.success, data=result.data)
|
||||||
|
|
||||||
|
def verify_merkle(hash: str) -> Result:
|
||||||
|
"""Verifica registro en Feldman"""
|
||||||
|
result = ssh(f"curl -s http://localhost:5054/verify/{hash}")
|
||||||
|
try:
|
||||||
|
return Result(success=result.success, data=json.loads(result.data) if result.data else {})
|
||||||
|
except:
|
||||||
|
return Result(success=result.success, data=result.data)
|
||||||
|
|
||||||
|
# CLI
|
||||||
|
def main():
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
print("""
|
||||||
|
TZZR CORP App - Servidor 92.112.181.188
|
||||||
|
=======================================
|
||||||
|
|
||||||
|
Uso: python app.py <comando> [argumentos]
|
||||||
|
|
||||||
|
Comandos Generales:
|
||||||
|
status Estado general del sistema
|
||||||
|
containers Lista todos los contenedores
|
||||||
|
stats Estadísticas del sistema
|
||||||
|
|
||||||
|
Gestión de Servicios:
|
||||||
|
service <name> Estado detallado de servicio
|
||||||
|
restart <service> Reiniciar servicio
|
||||||
|
logs <service> [lines] Ver logs
|
||||||
|
|
||||||
|
Agentes TZZR (Flujo de datos):
|
||||||
|
agents Estado de todos los agentes
|
||||||
|
agent <name> <endpoint> Query a agente (GET)
|
||||||
|
|
||||||
|
margaret Estado de Margaret (Secretaria)
|
||||||
|
jared Estado de Jared (Flujos)
|
||||||
|
mason Estado de Mason (Editor)
|
||||||
|
feldman Estado de Feldman (Contable)
|
||||||
|
|
||||||
|
flows Lista flujos predefinidos en Jared
|
||||||
|
pending Incidencias pendientes en Mason
|
||||||
|
verify <hash> Verificar hash en Feldman
|
||||||
|
|
||||||
|
Bases de Datos:
|
||||||
|
postgres Estadísticas PostgreSQL
|
||||||
|
|
||||||
|
Servicios disponibles:
|
||||||
|
Agentes: margaret, jared, mason, feldman
|
||||||
|
Apps: nextcloud, directus, vaultwarden, shlink, addy, odoo, ntfy
|
||||||
|
APIs: hsu, context-manager
|
||||||
|
DB: postgres, redis
|
||||||
|
""")
|
||||||
|
return
|
||||||
|
|
||||||
|
cmd = sys.argv[1]
|
||||||
|
|
||||||
|
if cmd == "status":
|
||||||
|
print("\n📊 CORP Status (92.112.181.188)")
|
||||||
|
print("=" * 40)
|
||||||
|
stats = get_system_stats()
|
||||||
|
print(f"💾 Memoria: {stats.get('memory', {}).get('used', '?')}/{stats.get('memory', {}).get('total', '?')}")
|
||||||
|
print(f"💿 Disco: {stats.get('disk', {}).get('used', '?')}/{stats.get('disk', {}).get('total', '?')} ({stats.get('disk', {}).get('percent', '?')})")
|
||||||
|
print(f"📦 Contenedores: {stats.get('containers_running', 0)}")
|
||||||
|
|
||||||
|
elif cmd == "containers":
|
||||||
|
containers = get_all_containers()
|
||||||
|
print(f"\n📦 Contenedores en CORP ({len(containers)} total):")
|
||||||
|
for c in containers:
|
||||||
|
icon = "✅" if "Up" in c["status"] else "❌"
|
||||||
|
print(f" {icon} {c['name']}: {c['status'][:30]}")
|
||||||
|
|
||||||
|
elif cmd == "service" and len(sys.argv) >= 3:
|
||||||
|
status = get_service_status(sys.argv[2])
|
||||||
|
print(f"\n🔧 {status['service']}")
|
||||||
|
print(f" Container: {status['container']}")
|
||||||
|
print(f" Status: {status['status']}")
|
||||||
|
print(f" Health: {status['health']}")
|
||||||
|
print(f" Desc: {status.get('desc', '')}")
|
||||||
|
if status.get('url'):
|
||||||
|
print(f" URL: https://{status['url']}")
|
||||||
|
|
||||||
|
elif cmd == "restart" and len(sys.argv) >= 3:
|
||||||
|
result = restart_service(sys.argv[2])
|
||||||
|
print(f"{'✅' if result.success else '❌'} {sys.argv[2]}: {'reiniciado' if result.success else result.error}")
|
||||||
|
|
||||||
|
elif cmd == "logs" and len(sys.argv) >= 3:
|
||||||
|
lines = int(sys.argv[3]) if len(sys.argv) > 3 else 50
|
||||||
|
print(get_logs(sys.argv[2], lines))
|
||||||
|
|
||||||
|
elif cmd == "agents":
|
||||||
|
print("\n🤖 Agentes TZZR en CORP:")
|
||||||
|
print(" PACKET → MARGARET → JARED → MASON/FELDMAN")
|
||||||
|
print()
|
||||||
|
for agent in ["margaret", "jared", "mason", "feldman"]:
|
||||||
|
status = get_service_status(agent)
|
||||||
|
icon = "✅" if "Up" in status["status"] else "❌"
|
||||||
|
health = f"({status['health']})" if status['health'] != "unknown" else ""
|
||||||
|
print(f" {icon} {agent}: {status['status'][:20]} {health}")
|
||||||
|
print(f" └─ {SERVICES[agent]['desc']}")
|
||||||
|
|
||||||
|
elif cmd == "agent" and len(sys.argv) >= 4:
|
||||||
|
result = query_agent(sys.argv[2], sys.argv[3])
|
||||||
|
if result.success:
|
||||||
|
print(json.dumps(result.data, indent=2) if isinstance(result.data, dict) else result.data)
|
||||||
|
else:
|
||||||
|
print(f"❌ Error: {result.error}")
|
||||||
|
|
||||||
|
elif cmd in ["margaret", "jared", "mason", "feldman"]:
|
||||||
|
status = get_service_status(cmd)
|
||||||
|
print(f"\n🤖 {cmd.upper()} - {SERVICES[cmd]['desc']}")
|
||||||
|
print(f" Puerto: {SERVICES[cmd]['port']}")
|
||||||
|
print(f" Status: {status['status']}")
|
||||||
|
print(f" Health: {status['health']}")
|
||||||
|
|
||||||
|
# Endpoints específicos
|
||||||
|
contracts = query_agent(cmd, "/s-contract")
|
||||||
|
if contracts.success and isinstance(contracts.data, dict):
|
||||||
|
print(f" Versión: {contracts.data.get('version', 'N/A')}")
|
||||||
|
|
||||||
|
elif cmd == "flows":
|
||||||
|
result = query_agent("jared", "/flujos")
|
||||||
|
if result.success:
|
||||||
|
flows = result.data if isinstance(result.data, list) else []
|
||||||
|
print("\n📋 Flujos predefinidos en Jared:")
|
||||||
|
for f in flows:
|
||||||
|
print(f" • [{f.get('id')}] {f.get('nombre')}")
|
||||||
|
|
||||||
|
elif cmd == "pending":
|
||||||
|
result = get_pending_issues()
|
||||||
|
if result.success:
|
||||||
|
issues = result.data if isinstance(result.data, list) else []
|
||||||
|
print(f"\n⚠️ Incidencias pendientes en Mason: {len(issues)}")
|
||||||
|
for i in issues[:10]:
|
||||||
|
print(f" • {i.get('id')}: {i.get('tipo', 'N/A')} - {i.get('descripcion', '')[:40]}")
|
||||||
|
|
||||||
|
elif cmd == "verify" and len(sys.argv) >= 3:
|
||||||
|
result = verify_merkle(sys.argv[2])
|
||||||
|
if result.success:
|
||||||
|
print(json.dumps(result.data, indent=2))
|
||||||
|
else:
|
||||||
|
print(f"❌ Error: {result.error}")
|
||||||
|
|
||||||
|
elif cmd == "postgres":
|
||||||
|
dbs = get_postgres_stats()
|
||||||
|
print("\n🐘 PostgreSQL en CORP:")
|
||||||
|
for db, size in dbs.items():
|
||||||
|
print(f" • {db}: {size}")
|
||||||
|
|
||||||
|
else:
|
||||||
|
print("❌ Comando no reconocido. Usa 'python app.py' para ver ayuda.")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
317
apps/deck/app.py
Normal file
317
apps/deck/app.py
Normal file
@@ -0,0 +1,317 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
TZZR DECK App - Gestión del servidor DECK (72.62.1.113)
|
||||||
|
Servicios: Agentes TZZR, Mailcow, Nextcloud, Odoo, Vaultwarden, etc.
|
||||||
|
"""
|
||||||
|
import subprocess
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Optional, List, Dict
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
SERVER = "root@72.62.1.113"
|
||||||
|
SSH_KEY = "~/.ssh/tzzr"
|
||||||
|
|
||||||
|
# Servicios conocidos en DECK
|
||||||
|
SERVICES = {
|
||||||
|
# Microservicios TZZR
|
||||||
|
"clara": {"port": 5051, "type": "service", "desc": "Log inmutable"},
|
||||||
|
"alfred": {"port": 5052, "type": "service", "desc": "Automatización de flujos"},
|
||||||
|
"mason": {"port": 5053, "type": "service", "desc": "Espacio de enriquecimiento"},
|
||||||
|
"feldman": {"port": 5054, "type": "service", "desc": "Validador Merkle"},
|
||||||
|
|
||||||
|
# Aplicaciones
|
||||||
|
"nextcloud": {"port": 8084, "type": "app", "desc": "Cloud storage", "url": "cloud.tzzrdeck.me"},
|
||||||
|
"odoo": {"port": 8069, "type": "app", "desc": "ERP", "url": "odoo.tzzrdeck.me", "container": "deck-odoo"},
|
||||||
|
"vaultwarden": {"port": 8085, "type": "app", "desc": "Passwords", "url": "vault.tzzrdeck.me"},
|
||||||
|
"directus": {"port": 8055, "type": "app", "desc": "CMS", "url": "directus.tzzrdeck.me"},
|
||||||
|
"shlink": {"port": 8083, "type": "app", "desc": "URL shortener", "url": "shlink.tzzrdeck.me"},
|
||||||
|
"ntfy": {"port": 8080, "type": "app", "desc": "Notifications", "url": "ntfy.tzzrdeck.me"},
|
||||||
|
"filebrowser": {"port": 8082, "type": "app", "desc": "File manager", "url": "files.tzzrdeck.me"},
|
||||||
|
|
||||||
|
# Infraestructura
|
||||||
|
"postgres": {"port": 5432, "type": "db", "desc": "PostgreSQL con pgvector"},
|
||||||
|
"redis": {"port": 6379, "type": "db", "desc": "Cache Redis"},
|
||||||
|
"mailcow": {"port": 8180, "type": "mail", "desc": "Servidor correo", "url": "mail.tzzr.net"},
|
||||||
|
}
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Result:
|
||||||
|
success: bool
|
||||||
|
data: any
|
||||||
|
error: str = ""
|
||||||
|
|
||||||
|
def ssh(cmd: str, timeout: int = 60) -> Result:
|
||||||
|
"""Ejecuta comando en DECK"""
|
||||||
|
full_cmd = f'ssh -i {SSH_KEY} {SERVER} "{cmd}"'
|
||||||
|
try:
|
||||||
|
result = subprocess.run(full_cmd, shell=True, capture_output=True, text=True, timeout=timeout)
|
||||||
|
return Result(success=result.returncode == 0, data=result.stdout.strip(), error=result.stderr.strip())
|
||||||
|
except Exception as e:
|
||||||
|
return Result(success=False, data="", error=str(e))
|
||||||
|
|
||||||
|
def get_all_containers() -> List[Dict]:
|
||||||
|
"""Lista todos los contenedores Docker"""
|
||||||
|
result = ssh("docker ps -a --format '{{.Names}}|{{.Status}}|{{.Ports}}|{{.Image}}'")
|
||||||
|
if not result.success:
|
||||||
|
return []
|
||||||
|
|
||||||
|
containers = []
|
||||||
|
for line in result.data.split('\n'):
|
||||||
|
if line:
|
||||||
|
parts = line.split('|')
|
||||||
|
containers.append({
|
||||||
|
"name": parts[0],
|
||||||
|
"status": parts[1] if len(parts) > 1 else "",
|
||||||
|
"ports": parts[2] if len(parts) > 2 else "",
|
||||||
|
"image": parts[3] if len(parts) > 3 else ""
|
||||||
|
})
|
||||||
|
return containers
|
||||||
|
|
||||||
|
def get_service_status(service: str) -> Dict:
|
||||||
|
"""Estado detallado de un servicio"""
|
||||||
|
info = SERVICES.get(service, {})
|
||||||
|
container = info.get("container", f"{service}-service" if info.get("type") == "service" else service)
|
||||||
|
|
||||||
|
# Estado del contenedor
|
||||||
|
status_result = ssh(f"docker ps --filter name={container} --format '{{{{.Status}}}}'")
|
||||||
|
|
||||||
|
# Logs recientes
|
||||||
|
logs_result = ssh(f"docker logs {container} --tail 5 2>&1")
|
||||||
|
|
||||||
|
# Health check si tiene endpoint
|
||||||
|
health = "unknown"
|
||||||
|
if info.get("type") == "service":
|
||||||
|
health_result = ssh(f"curl -s http://localhost:{info.get('port')}/health 2>/dev/null")
|
||||||
|
health = "healthy" if health_result.success and health_result.data else "unhealthy"
|
||||||
|
|
||||||
|
return {
|
||||||
|
"service": service,
|
||||||
|
"container": container,
|
||||||
|
"status": status_result.data if status_result.success else "not found",
|
||||||
|
"health": health,
|
||||||
|
"port": info.get("port"),
|
||||||
|
"url": info.get("url"),
|
||||||
|
"logs": logs_result.data[:500] if logs_result.success else ""
|
||||||
|
}
|
||||||
|
|
||||||
|
def restart_service(service: str) -> Result:
|
||||||
|
"""Reinicia un servicio"""
|
||||||
|
info = SERVICES.get(service, {})
|
||||||
|
container = info.get("container", f"{service}-service" if info.get("type") == "agent" else service)
|
||||||
|
return ssh(f"docker restart {container}")
|
||||||
|
|
||||||
|
def stop_service(service: str) -> Result:
|
||||||
|
"""Detiene un servicio"""
|
||||||
|
info = SERVICES.get(service, {})
|
||||||
|
container = info.get("container", f"{service}-service" if info.get("type") == "agent" else service)
|
||||||
|
return ssh(f"docker stop {container}")
|
||||||
|
|
||||||
|
def start_service(service: str) -> Result:
|
||||||
|
"""Inicia un servicio"""
|
||||||
|
info = SERVICES.get(service, {})
|
||||||
|
container = info.get("container", f"{service}-service" if info.get("type") == "agent" else service)
|
||||||
|
return ssh(f"docker start {container}")
|
||||||
|
|
||||||
|
def get_logs(service: str, lines: int = 100) -> str:
|
||||||
|
"""Obtiene logs de un servicio"""
|
||||||
|
info = SERVICES.get(service, {})
|
||||||
|
container = info.get("container", f"{service}-service" if info.get("type") == "agent" else service)
|
||||||
|
result = ssh(f"docker logs {container} --tail {lines} 2>&1")
|
||||||
|
return result.data if result.success else result.error
|
||||||
|
|
||||||
|
def get_system_stats() -> Dict:
|
||||||
|
"""Estadísticas del sistema"""
|
||||||
|
stats = {}
|
||||||
|
|
||||||
|
# CPU y memoria
|
||||||
|
mem_result = ssh("free -h | grep Mem | awk '{print $2,$3,$4}'")
|
||||||
|
if mem_result.success:
|
||||||
|
parts = mem_result.data.split()
|
||||||
|
stats["memory"] = {"total": parts[0], "used": parts[1], "available": parts[2]}
|
||||||
|
|
||||||
|
# Disco
|
||||||
|
disk_result = ssh("df -h / | tail -1 | awk '{print $2,$3,$4,$5}'")
|
||||||
|
if disk_result.success:
|
||||||
|
parts = disk_result.data.split()
|
||||||
|
stats["disk"] = {"total": parts[0], "used": parts[1], "available": parts[2], "percent": parts[3]}
|
||||||
|
|
||||||
|
# Contenedores
|
||||||
|
containers_result = ssh("docker ps -q | wc -l")
|
||||||
|
stats["containers_running"] = int(containers_result.data) if containers_result.success else 0
|
||||||
|
|
||||||
|
# Load average
|
||||||
|
load_result = ssh("cat /proc/loadavg | awk '{print $1,$2,$3}'")
|
||||||
|
if load_result.success:
|
||||||
|
parts = load_result.data.split()
|
||||||
|
stats["load"] = {"1m": parts[0], "5m": parts[1], "15m": parts[2]}
|
||||||
|
|
||||||
|
return stats
|
||||||
|
|
||||||
|
def query_service(service: str, endpoint: str, method: str = "GET", data: dict = None) -> Result:
|
||||||
|
"""Hace petición a un servicio TZZR"""
|
||||||
|
info = SERVICES.get(service)
|
||||||
|
if not info or info.get("type") != "service":
|
||||||
|
return Result(success=False, data="", error="Servicio no encontrado")
|
||||||
|
|
||||||
|
port = info["port"]
|
||||||
|
|
||||||
|
if method == "GET":
|
||||||
|
cmd = f"curl -s http://localhost:{port}{endpoint}"
|
||||||
|
else:
|
||||||
|
json_data = json.dumps(data) if data else "{}"
|
||||||
|
cmd = f"curl -s -X {method} -H 'Content-Type: application/json' -d '{json_data}' http://localhost:{port}{endpoint}"
|
||||||
|
|
||||||
|
result = ssh(cmd)
|
||||||
|
|
||||||
|
try:
|
||||||
|
return Result(success=result.success, data=json.loads(result.data) if result.data else {})
|
||||||
|
except:
|
||||||
|
return Result(success=result.success, data=result.data)
|
||||||
|
|
||||||
|
def get_postgres_stats() -> Dict:
|
||||||
|
"""Estadísticas de PostgreSQL"""
|
||||||
|
result = ssh("sudo -u postgres psql -c \"SELECT datname, pg_size_pretty(pg_database_size(datname)) as size FROM pg_database WHERE datistemplate = false;\" -t")
|
||||||
|
|
||||||
|
databases = {}
|
||||||
|
if result.success:
|
||||||
|
for line in result.data.split('\n'):
|
||||||
|
if '|' in line:
|
||||||
|
parts = line.split('|')
|
||||||
|
databases[parts[0].strip()] = parts[1].strip()
|
||||||
|
|
||||||
|
return databases
|
||||||
|
|
||||||
|
def get_mailcow_status() -> Dict:
|
||||||
|
"""Estado de Mailcow"""
|
||||||
|
containers = ["postfix-mailcow", "dovecot-mailcow", "nginx-mailcow", "mysql-mailcow", "redis-mailcow"]
|
||||||
|
status = {}
|
||||||
|
|
||||||
|
for c in containers:
|
||||||
|
result = ssh(f"docker ps --filter name={c} --format '{{{{.Status}}}}'")
|
||||||
|
status[c.replace("-mailcow", "")] = result.data if result.success and result.data else "stopped"
|
||||||
|
|
||||||
|
return status
|
||||||
|
|
||||||
|
# CLI
|
||||||
|
def main():
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
print("""
|
||||||
|
TZZR DECK App - Servidor 72.62.1.113
|
||||||
|
====================================
|
||||||
|
|
||||||
|
Uso: python app.py <comando> [argumentos]
|
||||||
|
|
||||||
|
Comandos Generales:
|
||||||
|
status Estado general del sistema
|
||||||
|
containers Lista todos los contenedores
|
||||||
|
stats Estadísticas del sistema
|
||||||
|
|
||||||
|
Gestión de Servicios:
|
||||||
|
service <name> Estado detallado de servicio
|
||||||
|
restart <service> Reiniciar servicio
|
||||||
|
stop <service> Detener servicio
|
||||||
|
start <service> Iniciar servicio
|
||||||
|
logs <service> [lines] Ver logs
|
||||||
|
|
||||||
|
Servicios TZZR:
|
||||||
|
services Estado de todos los servicios
|
||||||
|
query <name> <endpoint> Query a servicio (GET)
|
||||||
|
|
||||||
|
Bases de Datos:
|
||||||
|
postgres Estadísticas PostgreSQL
|
||||||
|
|
||||||
|
Mail:
|
||||||
|
mailcow Estado de Mailcow
|
||||||
|
|
||||||
|
Servicios disponibles:
|
||||||
|
TZZR: clara, alfred, mason, feldman
|
||||||
|
Apps: nextcloud, odoo, vaultwarden, directus, shlink, ntfy, filebrowser
|
||||||
|
DB: postgres, redis
|
||||||
|
Mail: mailcow
|
||||||
|
""")
|
||||||
|
return
|
||||||
|
|
||||||
|
cmd = sys.argv[1]
|
||||||
|
|
||||||
|
if cmd == "status":
|
||||||
|
print("\n📊 DECK Status (72.62.1.113)")
|
||||||
|
print("=" * 40)
|
||||||
|
stats = get_system_stats()
|
||||||
|
print(f"💾 Memoria: {stats.get('memory', {}).get('used', '?')}/{stats.get('memory', {}).get('total', '?')}")
|
||||||
|
print(f"💿 Disco: {stats.get('disk', {}).get('used', '?')}/{stats.get('disk', {}).get('total', '?')} ({stats.get('disk', {}).get('percent', '?')})")
|
||||||
|
print(f"📦 Contenedores: {stats.get('containers_running', 0)}")
|
||||||
|
print(f"⚡ Load: {stats.get('load', {}).get('1m', '?')}")
|
||||||
|
|
||||||
|
elif cmd == "containers":
|
||||||
|
containers = get_all_containers()
|
||||||
|
print(f"\n📦 Contenedores en DECK ({len(containers)} total):")
|
||||||
|
for c in containers:
|
||||||
|
icon = "✅" if "Up" in c["status"] else "❌"
|
||||||
|
print(f" {icon} {c['name']}: {c['status'][:30]}")
|
||||||
|
|
||||||
|
elif cmd == "stats":
|
||||||
|
stats = get_system_stats()
|
||||||
|
print(json.dumps(stats, indent=2))
|
||||||
|
|
||||||
|
elif cmd == "service" and len(sys.argv) >= 3:
|
||||||
|
status = get_service_status(sys.argv[2])
|
||||||
|
print(f"\n🔧 {status['service']}")
|
||||||
|
print(f" Container: {status['container']}")
|
||||||
|
print(f" Status: {status['status']}")
|
||||||
|
print(f" Health: {status['health']}")
|
||||||
|
if status.get('url'):
|
||||||
|
print(f" URL: https://{status['url']}")
|
||||||
|
if status.get('port'):
|
||||||
|
print(f" Puerto: {status['port']}")
|
||||||
|
|
||||||
|
elif cmd == "restart" and len(sys.argv) >= 3:
|
||||||
|
result = restart_service(sys.argv[2])
|
||||||
|
print(f"{'✅' if result.success else '❌'} {sys.argv[2]}: {'reiniciado' if result.success else result.error}")
|
||||||
|
|
||||||
|
elif cmd == "stop" and len(sys.argv) >= 3:
|
||||||
|
result = stop_service(sys.argv[2])
|
||||||
|
print(f"{'✅' if result.success else '❌'} {sys.argv[2]}: {'detenido' if result.success else result.error}")
|
||||||
|
|
||||||
|
elif cmd == "start" and len(sys.argv) >= 3:
|
||||||
|
result = start_service(sys.argv[2])
|
||||||
|
print(f"{'✅' if result.success else '❌'} {sys.argv[2]}: {'iniciado' if result.success else result.error}")
|
||||||
|
|
||||||
|
elif cmd == "logs" and len(sys.argv) >= 3:
|
||||||
|
lines = int(sys.argv[3]) if len(sys.argv) > 3 else 50
|
||||||
|
print(get_logs(sys.argv[2], lines))
|
||||||
|
|
||||||
|
elif cmd == "services":
|
||||||
|
print("\n⚙️ Servicios TZZR en DECK:")
|
||||||
|
for svc in ["clara", "alfred", "mason", "feldman"]:
|
||||||
|
status = get_service_status(svc)
|
||||||
|
icon = "✅" if "Up" in status["status"] else "❌"
|
||||||
|
health = f"({status['health']})" if status['health'] != "unknown" else ""
|
||||||
|
print(f" {icon} {svc}: {status['status'][:25]} {health} - {SERVICES[svc]['desc']}")
|
||||||
|
|
||||||
|
elif cmd == "query" and len(sys.argv) >= 4:
|
||||||
|
result = query_service(sys.argv[2], sys.argv[3])
|
||||||
|
if result.success:
|
||||||
|
print(json.dumps(result.data, indent=2) if isinstance(result.data, dict) else result.data)
|
||||||
|
else:
|
||||||
|
print(f"❌ Error: {result.error}")
|
||||||
|
|
||||||
|
elif cmd == "postgres":
|
||||||
|
dbs = get_postgres_stats()
|
||||||
|
print("\n🐘 PostgreSQL en DECK:")
|
||||||
|
for db, size in dbs.items():
|
||||||
|
print(f" • {db}: {size}")
|
||||||
|
|
||||||
|
elif cmd == "mailcow":
|
||||||
|
status = get_mailcow_status()
|
||||||
|
print("\n📧 Mailcow Status:")
|
||||||
|
for service, st in status.items():
|
||||||
|
icon = "✅" if "Up" in st else "❌"
|
||||||
|
print(f" {icon} {service}: {st[:30]}")
|
||||||
|
|
||||||
|
else:
|
||||||
|
print("❌ Comando no reconocido. Usa 'python app.py' para ver ayuda.")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
230
apps/devops/app.py
Normal file
230
apps/devops/app.py
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
TZZR DevOps App - Gestión de despliegues y construcción
|
||||||
|
"""
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Optional
|
||||||
|
import json
|
||||||
|
|
||||||
|
# Configuración de servidores
|
||||||
|
SERVERS = {
|
||||||
|
"deck": {"host": "root@72.62.1.113", "name": "DECK"},
|
||||||
|
"corp": {"host": "root@92.112.181.188", "name": "CORP"},
|
||||||
|
"hst": {"host": "root@72.62.2.84", "name": "HST"},
|
||||||
|
"local": {"host": None, "name": "LOCAL (69.62.126.110)"}
|
||||||
|
}
|
||||||
|
|
||||||
|
SSH_KEY = "~/.ssh/tzzr"
|
||||||
|
|
||||||
|
# Agentes conocidos
|
||||||
|
AGENTS = {
|
||||||
|
"deck": ["clara", "alfred", "mason", "feldman"],
|
||||||
|
"corp": ["margaret", "jared", "mason", "feldman"],
|
||||||
|
"hst": ["hst-api", "directus_hst", "directus_lumalia", "directus_personal"]
|
||||||
|
}
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class CommandResult:
|
||||||
|
success: bool
|
||||||
|
output: str
|
||||||
|
error: str = ""
|
||||||
|
|
||||||
|
def ssh_cmd(server: str, command: str) -> CommandResult:
|
||||||
|
"""Ejecuta comando SSH en servidor remoto"""
|
||||||
|
if server == "local":
|
||||||
|
full_cmd = command
|
||||||
|
else:
|
||||||
|
host = SERVERS[server]["host"]
|
||||||
|
full_cmd = f'ssh -i {SSH_KEY} {host} "{command}"'
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = subprocess.run(full_cmd, shell=True, capture_output=True, text=True, timeout=60)
|
||||||
|
return CommandResult(
|
||||||
|
success=result.returncode == 0,
|
||||||
|
output=result.stdout.strip(),
|
||||||
|
error=result.stderr.strip()
|
||||||
|
)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
return CommandResult(success=False, output="", error="Timeout")
|
||||||
|
except Exception as e:
|
||||||
|
return CommandResult(success=False, output="", error=str(e))
|
||||||
|
|
||||||
|
def deploy_agent(agent: str, server: str) -> CommandResult:
|
||||||
|
"""Despliega un agente en el servidor especificado"""
|
||||||
|
print(f"🚀 Desplegando {agent} en {server}...")
|
||||||
|
|
||||||
|
# Usar el script de deploy en DECK
|
||||||
|
cmd = f"/opt/scripts/deploy-agent.sh {agent} {server}"
|
||||||
|
result = ssh_cmd("deck", cmd)
|
||||||
|
|
||||||
|
if result.success:
|
||||||
|
print(f"✅ {agent} desplegado exitosamente en {server}")
|
||||||
|
else:
|
||||||
|
print(f"❌ Error desplegando {agent}: {result.error}")
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def backup_postgres(server: str = "deck") -> CommandResult:
|
||||||
|
"""Ejecuta backup de PostgreSQL"""
|
||||||
|
print(f"💾 Ejecutando backup en {server}...")
|
||||||
|
result = ssh_cmd(server, "/opt/scripts/backup_postgres.sh")
|
||||||
|
|
||||||
|
if result.success:
|
||||||
|
print("✅ Backup completado")
|
||||||
|
else:
|
||||||
|
print(f"❌ Error: {result.error}")
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def sync_r2() -> CommandResult:
|
||||||
|
"""Sincroniza backups a R2"""
|
||||||
|
print("☁️ Sincronizando con R2...")
|
||||||
|
result = ssh_cmd("deck", "/opt/scripts/sync_backups_r2.sh")
|
||||||
|
|
||||||
|
if result.success:
|
||||||
|
print("✅ Sincronización completada")
|
||||||
|
else:
|
||||||
|
print(f"❌ Error: {result.error}")
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def onboard_user(email: str, username: str) -> CommandResult:
|
||||||
|
"""Da de alta un nuevo usuario"""
|
||||||
|
print(f"👤 Creando usuario {username} ({email})...")
|
||||||
|
result = ssh_cmd("deck", f"/opt/scripts/onboard-user.sh {email} {username}")
|
||||||
|
|
||||||
|
if result.success:
|
||||||
|
print(f"✅ Usuario {username} creado")
|
||||||
|
else:
|
||||||
|
print(f"❌ Error: {result.error}")
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def get_agent_status(server: str) -> dict:
|
||||||
|
"""Obtiene estado de agentes en un servidor"""
|
||||||
|
agents = AGENTS.get(server, [])
|
||||||
|
status = {}
|
||||||
|
|
||||||
|
for agent in agents:
|
||||||
|
result = ssh_cmd(server, f"docker ps --filter name={agent} --format '{{{{.Status}}}}'")
|
||||||
|
status[agent] = result.output if result.success else "unknown"
|
||||||
|
|
||||||
|
return status
|
||||||
|
|
||||||
|
def restart_agent(agent: str, server: str) -> CommandResult:
|
||||||
|
"""Reinicia un agente específico"""
|
||||||
|
print(f"🔄 Reiniciando {agent} en {server}...")
|
||||||
|
result = ssh_cmd(server, f"docker restart {agent}-service 2>/dev/null || docker restart {agent}")
|
||||||
|
|
||||||
|
if result.success:
|
||||||
|
print(f"✅ {agent} reiniciado")
|
||||||
|
else:
|
||||||
|
print(f"❌ Error: {result.error}")
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def get_logs(agent: str, server: str, lines: int = 50) -> CommandResult:
|
||||||
|
"""Obtiene logs de un agente"""
|
||||||
|
container = f"{agent}-service" if agent in ["clara", "alfred", "mason", "feldman", "margaret", "jared"] else agent
|
||||||
|
return ssh_cmd(server, f"docker logs {container} --tail {lines} 2>&1")
|
||||||
|
|
||||||
|
def list_deployments() -> dict:
|
||||||
|
"""Lista todos los deployments activos"""
|
||||||
|
deployments = {}
|
||||||
|
|
||||||
|
for server in ["deck", "corp", "hst"]:
|
||||||
|
result = ssh_cmd(server, "docker ps --format '{{.Names}}|{{.Status}}|{{.Ports}}'")
|
||||||
|
if result.success:
|
||||||
|
containers = []
|
||||||
|
for line in result.output.split('\n'):
|
||||||
|
if line:
|
||||||
|
parts = line.split('|')
|
||||||
|
containers.append({
|
||||||
|
"name": parts[0],
|
||||||
|
"status": parts[1] if len(parts) > 1 else "",
|
||||||
|
"ports": parts[2] if len(parts) > 2 else ""
|
||||||
|
})
|
||||||
|
deployments[server] = containers
|
||||||
|
|
||||||
|
return deployments
|
||||||
|
|
||||||
|
def git_pull_all(server: str) -> dict:
|
||||||
|
"""Hace git pull en todos los proyectos de /opt"""
|
||||||
|
result = ssh_cmd(server, "for d in /opt/*/; do echo \"=== $d ===\"; cd $d && git pull 2>/dev/null || echo 'no git'; done")
|
||||||
|
return {"output": result.output, "success": result.success}
|
||||||
|
|
||||||
|
# CLI
|
||||||
|
def main():
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
print("""
|
||||||
|
TZZR DevOps App
|
||||||
|
===============
|
||||||
|
|
||||||
|
Uso: python app.py <comando> [argumentos]
|
||||||
|
|
||||||
|
Comandos:
|
||||||
|
deploy <agent> <server> Despliega un agente
|
||||||
|
backup [server] Ejecuta backup PostgreSQL
|
||||||
|
sync Sincroniza backups a R2
|
||||||
|
onboard <email> <username> Alta de usuario
|
||||||
|
status <server> Estado de agentes
|
||||||
|
restart <agent> <server> Reinicia agente
|
||||||
|
logs <agent> <server> Ver logs de agente
|
||||||
|
list Lista todos los deployments
|
||||||
|
pull <server> Git pull en todos los proyectos
|
||||||
|
|
||||||
|
Servidores: deck, corp, hst, local
|
||||||
|
Agentes DECK: clara, alfred, mason, feldman
|
||||||
|
Agentes CORP: margaret, jared, mason, feldman
|
||||||
|
""")
|
||||||
|
return
|
||||||
|
|
||||||
|
cmd = sys.argv[1]
|
||||||
|
|
||||||
|
if cmd == "deploy" and len(sys.argv) >= 4:
|
||||||
|
deploy_agent(sys.argv[2], sys.argv[3])
|
||||||
|
|
||||||
|
elif cmd == "backup":
|
||||||
|
server = sys.argv[2] if len(sys.argv) > 2 else "deck"
|
||||||
|
backup_postgres(server)
|
||||||
|
|
||||||
|
elif cmd == "sync":
|
||||||
|
sync_r2()
|
||||||
|
|
||||||
|
elif cmd == "onboard" and len(sys.argv) >= 4:
|
||||||
|
onboard_user(sys.argv[2], sys.argv[3])
|
||||||
|
|
||||||
|
elif cmd == "status" and len(sys.argv) >= 3:
|
||||||
|
status = get_agent_status(sys.argv[2])
|
||||||
|
print(f"\n📊 Estado de agentes en {sys.argv[2]}:")
|
||||||
|
for agent, st in status.items():
|
||||||
|
icon = "✅" if "Up" in st else "❌"
|
||||||
|
print(f" {icon} {agent}: {st}")
|
||||||
|
|
||||||
|
elif cmd == "restart" and len(sys.argv) >= 4:
|
||||||
|
restart_agent(sys.argv[2], sys.argv[3])
|
||||||
|
|
||||||
|
elif cmd == "logs" and len(sys.argv) >= 4:
|
||||||
|
result = get_logs(sys.argv[2], sys.argv[3])
|
||||||
|
print(result.output)
|
||||||
|
|
||||||
|
elif cmd == "list":
|
||||||
|
deployments = list_deployments()
|
||||||
|
for server, containers in deployments.items():
|
||||||
|
print(f"\n📦 {server.upper()}:")
|
||||||
|
for c in containers[:10]:
|
||||||
|
print(f" • {c['name']}: {c['status']}")
|
||||||
|
if len(containers) > 10:
|
||||||
|
print(f" ... y {len(containers)-10} más")
|
||||||
|
|
||||||
|
elif cmd == "pull" and len(sys.argv) >= 3:
|
||||||
|
result = git_pull_all(sys.argv[2])
|
||||||
|
print(result["output"])
|
||||||
|
|
||||||
|
else:
|
||||||
|
print("❌ Comando no reconocido. Usa 'python app.py' para ver ayuda.")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
425
apps/docker/app.py
Normal file
425
apps/docker/app.py
Normal file
@@ -0,0 +1,425 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
TZZR Docker App - Gestión unificada de Docker en todos los servidores
|
||||||
|
Servidores: DECK (72.62.1.113), CORP (92.112.181.188), HST (72.62.2.84)
|
||||||
|
"""
|
||||||
|
import subprocess
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import List, Dict, Optional
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# Configuración de servidores
|
||||||
|
SERVERS = {
|
||||||
|
"deck": {
|
||||||
|
"host": "root@72.62.1.113",
|
||||||
|
"name": "DECK",
|
||||||
|
"desc": "Producción - Agentes TZZR, Mail, Apps"
|
||||||
|
},
|
||||||
|
"corp": {
|
||||||
|
"host": "root@92.112.181.188",
|
||||||
|
"name": "CORP",
|
||||||
|
"desc": "Aplicaciones - Margaret, Jared, Mason, Feldman"
|
||||||
|
},
|
||||||
|
"hst": {
|
||||||
|
"host": "root@72.62.2.84",
|
||||||
|
"name": "HST",
|
||||||
|
"desc": "Contenido - Directus, Imágenes"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SSH_KEY = "~/.ssh/tzzr"
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Container:
|
||||||
|
name: str
|
||||||
|
status: str
|
||||||
|
image: str
|
||||||
|
ports: str = ""
|
||||||
|
created: str = ""
|
||||||
|
server: str = ""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_running(self) -> bool:
|
||||||
|
return "Up" in self.status
|
||||||
|
|
||||||
|
@property
|
||||||
|
def icon(self) -> str:
|
||||||
|
return "✅" if self.is_running else "❌"
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Result:
|
||||||
|
success: bool
|
||||||
|
data: any
|
||||||
|
error: str = ""
|
||||||
|
|
||||||
|
def ssh(server: str, cmd: str, timeout: int = 60) -> Result:
|
||||||
|
"""Ejecuta comando SSH"""
|
||||||
|
host = SERVERS[server]["host"]
|
||||||
|
full_cmd = f'ssh -i {SSH_KEY} {host} "{cmd}"'
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = subprocess.run(full_cmd, shell=True, capture_output=True, text=True, timeout=timeout)
|
||||||
|
return Result(success=result.returncode == 0, data=result.stdout.strip(), error=result.stderr.strip())
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
return Result(success=False, data="", error="Timeout")
|
||||||
|
except Exception as e:
|
||||||
|
return Result(success=False, data="", error=str(e))
|
||||||
|
|
||||||
|
def get_containers(server: str, all: bool = False) -> List[Container]:
|
||||||
|
"""Lista contenedores de un servidor"""
|
||||||
|
flag = "-a" if all else ""
|
||||||
|
result = ssh(server, f"docker ps {flag} --format '{{{{.Names}}}}|{{{{.Status}}}}|{{{{.Image}}}}|{{{{.Ports}}}}'")
|
||||||
|
|
||||||
|
if not result.success:
|
||||||
|
return []
|
||||||
|
|
||||||
|
containers = []
|
||||||
|
for line in result.data.split('\n'):
|
||||||
|
if line:
|
||||||
|
parts = line.split('|')
|
||||||
|
containers.append(Container(
|
||||||
|
name=parts[0],
|
||||||
|
status=parts[1] if len(parts) > 1 else "",
|
||||||
|
image=parts[2] if len(parts) > 2 else "",
|
||||||
|
ports=parts[3] if len(parts) > 3 else "",
|
||||||
|
server=server
|
||||||
|
))
|
||||||
|
|
||||||
|
return containers
|
||||||
|
|
||||||
|
def get_all_containers(all: bool = False) -> Dict[str, List[Container]]:
|
||||||
|
"""Lista contenedores de todos los servidores"""
|
||||||
|
result = {}
|
||||||
|
for server in SERVERS:
|
||||||
|
result[server] = get_containers(server, all)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def container_action(server: str, container: str, action: str) -> Result:
|
||||||
|
"""Ejecuta acción en contenedor (start, stop, restart, rm)"""
|
||||||
|
return ssh(server, f"docker {action} {container}")
|
||||||
|
|
||||||
|
def get_logs(server: str, container: str, lines: int = 100, follow: bool = False) -> str:
|
||||||
|
"""Obtiene logs de contenedor"""
|
||||||
|
follow_flag = "-f" if follow else ""
|
||||||
|
result = ssh(server, f"docker logs {container} --tail {lines} {follow_flag} 2>&1", timeout=120 if follow else 60)
|
||||||
|
return result.data if result.success else result.error
|
||||||
|
|
||||||
|
def inspect_container(server: str, container: str) -> Dict:
|
||||||
|
"""Inspecciona contenedor"""
|
||||||
|
result = ssh(server, f"docker inspect {container}")
|
||||||
|
if result.success:
|
||||||
|
try:
|
||||||
|
return json.loads(result.data)[0]
|
||||||
|
except:
|
||||||
|
return {}
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def get_images(server: str) -> List[Dict]:
|
||||||
|
"""Lista imágenes Docker"""
|
||||||
|
result = ssh(server, "docker images --format '{{.Repository}}|{{.Tag}}|{{.Size}}|{{.ID}}'")
|
||||||
|
|
||||||
|
if not result.success:
|
||||||
|
return []
|
||||||
|
|
||||||
|
images = []
|
||||||
|
for line in result.data.split('\n'):
|
||||||
|
if line:
|
||||||
|
parts = line.split('|')
|
||||||
|
images.append({
|
||||||
|
"repository": parts[0],
|
||||||
|
"tag": parts[1] if len(parts) > 1 else "",
|
||||||
|
"size": parts[2] if len(parts) > 2 else "",
|
||||||
|
"id": parts[3] if len(parts) > 3 else ""
|
||||||
|
})
|
||||||
|
|
||||||
|
return images
|
||||||
|
|
||||||
|
def get_networks(server: str) -> List[Dict]:
|
||||||
|
"""Lista redes Docker"""
|
||||||
|
result = ssh(server, "docker network ls --format '{{.Name}}|{{.Driver}}|{{.Scope}}'")
|
||||||
|
|
||||||
|
if not result.success:
|
||||||
|
return []
|
||||||
|
|
||||||
|
networks = []
|
||||||
|
for line in result.data.split('\n'):
|
||||||
|
if line:
|
||||||
|
parts = line.split('|')
|
||||||
|
networks.append({
|
||||||
|
"name": parts[0],
|
||||||
|
"driver": parts[1] if len(parts) > 1 else "",
|
||||||
|
"scope": parts[2] if len(parts) > 2 else ""
|
||||||
|
})
|
||||||
|
|
||||||
|
return networks
|
||||||
|
|
||||||
|
def get_volumes(server: str) -> List[Dict]:
|
||||||
|
"""Lista volúmenes Docker"""
|
||||||
|
result = ssh(server, "docker volume ls --format '{{.Name}}|{{.Driver}}'")
|
||||||
|
|
||||||
|
if not result.success:
|
||||||
|
return []
|
||||||
|
|
||||||
|
volumes = []
|
||||||
|
for line in result.data.split('\n'):
|
||||||
|
if line:
|
||||||
|
parts = line.split('|')
|
||||||
|
volumes.append({
|
||||||
|
"name": parts[0],
|
||||||
|
"driver": parts[1] if len(parts) > 1 else ""
|
||||||
|
})
|
||||||
|
|
||||||
|
return volumes
|
||||||
|
|
||||||
|
def docker_stats(server: str) -> List[Dict]:
|
||||||
|
"""Estadísticas de contenedores"""
|
||||||
|
result = ssh(server, "docker stats --no-stream --format '{{.Name}}|{{.CPUPerc}}|{{.MemUsage}}|{{.NetIO}}'")
|
||||||
|
|
||||||
|
if not result.success:
|
||||||
|
return []
|
||||||
|
|
||||||
|
stats = []
|
||||||
|
for line in result.data.split('\n'):
|
||||||
|
if line:
|
||||||
|
parts = line.split('|')
|
||||||
|
stats.append({
|
||||||
|
"name": parts[0],
|
||||||
|
"cpu": parts[1] if len(parts) > 1 else "",
|
||||||
|
"memory": parts[2] if len(parts) > 2 else "",
|
||||||
|
"network": parts[3] if len(parts) > 3 else ""
|
||||||
|
})
|
||||||
|
|
||||||
|
return stats
|
||||||
|
|
||||||
|
def prune(server: str, what: str = "all") -> Result:
|
||||||
|
"""Limpia recursos Docker no usados"""
|
||||||
|
if what == "all":
|
||||||
|
return ssh(server, "docker system prune -f")
|
||||||
|
elif what == "images":
|
||||||
|
return ssh(server, "docker image prune -f")
|
||||||
|
elif what == "volumes":
|
||||||
|
return ssh(server, "docker volume prune -f")
|
||||||
|
elif what == "networks":
|
||||||
|
return ssh(server, "docker network prune -f")
|
||||||
|
return Result(success=False, data="", error="Tipo no válido")
|
||||||
|
|
||||||
|
def compose_action(server: str, path: str, action: str) -> Result:
|
||||||
|
"""Ejecuta docker compose en un directorio"""
|
||||||
|
return ssh(server, f"cd {path} && docker compose {action}")
|
||||||
|
|
||||||
|
def exec_container(server: str, container: str, command: str) -> Result:
|
||||||
|
"""Ejecuta comando dentro de contenedor"""
|
||||||
|
return ssh(server, f"docker exec {container} {command}")
|
||||||
|
|
||||||
|
def find_container(name: str) -> List[tuple]:
|
||||||
|
"""Busca contenedor por nombre en todos los servidores"""
|
||||||
|
results = []
|
||||||
|
for server in SERVERS:
|
||||||
|
containers = get_containers(server, all=True)
|
||||||
|
for c in containers:
|
||||||
|
if name.lower() in c.name.lower():
|
||||||
|
results.append((server, c))
|
||||||
|
return results
|
||||||
|
|
||||||
|
def get_system_df(server: str) -> Dict:
|
||||||
|
"""Uso de disco de Docker"""
|
||||||
|
result = ssh(server, "docker system df --format '{{.Type}}|{{.Size}}|{{.Reclaimable}}'")
|
||||||
|
|
||||||
|
if not result.success:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
df = {}
|
||||||
|
for line in result.data.split('\n'):
|
||||||
|
if line:
|
||||||
|
parts = line.split('|')
|
||||||
|
df[parts[0]] = {
|
||||||
|
"size": parts[1] if len(parts) > 1 else "",
|
||||||
|
"reclaimable": parts[2] if len(parts) > 2 else ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return df
|
||||||
|
|
||||||
|
# CLI
|
||||||
|
def main():
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
print("""
|
||||||
|
TZZR Docker App - Gestión Multi-servidor
|
||||||
|
=========================================
|
||||||
|
|
||||||
|
Uso: python app.py <comando> [argumentos]
|
||||||
|
|
||||||
|
Servidores: deck, corp, hst (o 'all' para todos)
|
||||||
|
|
||||||
|
Contenedores:
|
||||||
|
ps [server] Lista contenedores activos
|
||||||
|
ps -a [server] Lista todos los contenedores
|
||||||
|
start <server> <name> Iniciar contenedor
|
||||||
|
stop <server> <name> Detener contenedor
|
||||||
|
restart <server> <name> Reiniciar contenedor
|
||||||
|
rm <server> <name> Eliminar contenedor
|
||||||
|
logs <server> <name> Ver logs
|
||||||
|
inspect <server> <name> Inspeccionar contenedor
|
||||||
|
exec <server> <name> <cmd> Ejecutar comando en contenedor
|
||||||
|
find <name> Buscar contenedor en todos los servidores
|
||||||
|
|
||||||
|
Recursos:
|
||||||
|
images <server> Lista imágenes
|
||||||
|
networks <server> Lista redes
|
||||||
|
volumes <server> Lista volúmenes
|
||||||
|
stats <server> Estadísticas de recursos
|
||||||
|
df <server> Uso de disco Docker
|
||||||
|
|
||||||
|
Mantenimiento:
|
||||||
|
prune <server> [type] Limpiar recursos (all/images/volumes/networks)
|
||||||
|
|
||||||
|
Compose:
|
||||||
|
up <server> <path> docker compose up -d
|
||||||
|
down <server> <path> docker compose down
|
||||||
|
build <server> <path> docker compose build
|
||||||
|
|
||||||
|
Dashboard:
|
||||||
|
dashboard Vista general de todos los servidores
|
||||||
|
""")
|
||||||
|
return
|
||||||
|
|
||||||
|
cmd = sys.argv[1]
|
||||||
|
|
||||||
|
# PS - Lista contenedores
|
||||||
|
if cmd == "ps":
|
||||||
|
show_all = "-a" in sys.argv
|
||||||
|
server = None
|
||||||
|
for arg in sys.argv[2:]:
|
||||||
|
if arg != "-a" and arg in SERVERS:
|
||||||
|
server = arg
|
||||||
|
|
||||||
|
if server:
|
||||||
|
containers = get_containers(server, show_all)
|
||||||
|
print(f"\n📦 {SERVERS[server]['name']} ({len(containers)} contenedores):")
|
||||||
|
for c in containers:
|
||||||
|
print(f" {c.icon} {c.name}: {c.status[:30]}")
|
||||||
|
else:
|
||||||
|
# Todos los servidores
|
||||||
|
all_containers = get_all_containers(show_all)
|
||||||
|
for srv, containers in all_containers.items():
|
||||||
|
print(f"\n📦 {SERVERS[srv]['name']} ({len(containers)}):")
|
||||||
|
for c in containers[:15]:
|
||||||
|
print(f" {c.icon} {c.name}: {c.status[:25]}")
|
||||||
|
if len(containers) > 15:
|
||||||
|
print(f" ... y {len(containers)-15} más")
|
||||||
|
|
||||||
|
# Acciones de contenedor
|
||||||
|
elif cmd in ["start", "stop", "restart", "rm"] and len(sys.argv) >= 4:
|
||||||
|
server, container = sys.argv[2], sys.argv[3]
|
||||||
|
result = container_action(server, container, cmd)
|
||||||
|
icon = "✅" if result.success else "❌"
|
||||||
|
action_name = {"start": "iniciado", "stop": "detenido", "restart": "reiniciado", "rm": "eliminado"}
|
||||||
|
print(f"{icon} {container} {action_name.get(cmd, cmd)}" if result.success else f"❌ Error: {result.error}")
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
elif cmd == "logs" and len(sys.argv) >= 4:
|
||||||
|
server, container = sys.argv[2], sys.argv[3]
|
||||||
|
lines = int(sys.argv[4]) if len(sys.argv) > 4 else 50
|
||||||
|
print(get_logs(server, container, lines))
|
||||||
|
|
||||||
|
# Inspect
|
||||||
|
elif cmd == "inspect" and len(sys.argv) >= 4:
|
||||||
|
server, container = sys.argv[2], sys.argv[3]
|
||||||
|
info = inspect_container(server, container)
|
||||||
|
print(json.dumps(info, indent=2)[:3000])
|
||||||
|
|
||||||
|
# Exec
|
||||||
|
elif cmd == "exec" and len(sys.argv) >= 5:
|
||||||
|
server, container = sys.argv[2], sys.argv[3]
|
||||||
|
command = " ".join(sys.argv[4:])
|
||||||
|
result = exec_container(server, container, command)
|
||||||
|
print(result.data if result.success else f"Error: {result.error}")
|
||||||
|
|
||||||
|
# Find
|
||||||
|
elif cmd == "find" and len(sys.argv) >= 3:
|
||||||
|
name = sys.argv[2]
|
||||||
|
results = find_container(name)
|
||||||
|
if results:
|
||||||
|
print(f"\n🔍 Encontrados {len(results)} contenedores con '{name}':")
|
||||||
|
for server, c in results:
|
||||||
|
print(f" {c.icon} [{server}] {c.name}: {c.status[:25]}")
|
||||||
|
else:
|
||||||
|
print(f"❌ No se encontró '{name}'")
|
||||||
|
|
||||||
|
# Images
|
||||||
|
elif cmd == "images" and len(sys.argv) >= 3:
|
||||||
|
server = sys.argv[2]
|
||||||
|
images = get_images(server)
|
||||||
|
print(f"\n🖼️ Imágenes en {server}:")
|
||||||
|
for img in images[:20]:
|
||||||
|
print(f" • {img['repository']}:{img['tag']} ({img['size']})")
|
||||||
|
|
||||||
|
# Networks
|
||||||
|
elif cmd == "networks" and len(sys.argv) >= 3:
|
||||||
|
server = sys.argv[2]
|
||||||
|
networks = get_networks(server)
|
||||||
|
print(f"\n🌐 Redes en {server}:")
|
||||||
|
for net in networks:
|
||||||
|
print(f" • {net['name']} ({net['driver']})")
|
||||||
|
|
||||||
|
# Volumes
|
||||||
|
elif cmd == "volumes" and len(sys.argv) >= 3:
|
||||||
|
server = sys.argv[2]
|
||||||
|
volumes = get_volumes(server)
|
||||||
|
print(f"\n💾 Volúmenes en {server}:")
|
||||||
|
for vol in volumes:
|
||||||
|
print(f" • {vol['name']}")
|
||||||
|
|
||||||
|
# Stats
|
||||||
|
elif cmd == "stats" and len(sys.argv) >= 3:
|
||||||
|
server = sys.argv[2]
|
||||||
|
stats = docker_stats(server)
|
||||||
|
print(f"\n📊 Estadísticas en {server}:")
|
||||||
|
for s in stats[:15]:
|
||||||
|
print(f" {s['name'][:20]}: CPU {s['cpu']} | MEM {s['memory']}")
|
||||||
|
|
||||||
|
# DF
|
||||||
|
elif cmd == "df" and len(sys.argv) >= 3:
|
||||||
|
server = sys.argv[2]
|
||||||
|
df = get_system_df(server)
|
||||||
|
print(f"\n💿 Uso de Docker en {server}:")
|
||||||
|
for type_, info in df.items():
|
||||||
|
print(f" {type_}: {info['size']} (recuperable: {info['reclaimable']})")
|
||||||
|
|
||||||
|
# Prune
|
||||||
|
elif cmd == "prune" and len(sys.argv) >= 3:
|
||||||
|
server = sys.argv[2]
|
||||||
|
what = sys.argv[3] if len(sys.argv) > 3 else "all"
|
||||||
|
result = prune(server, what)
|
||||||
|
print(f"{'✅' if result.success else '❌'} Limpieza completada" if result.success else f"Error: {result.error}")
|
||||||
|
|
||||||
|
# Compose
|
||||||
|
elif cmd in ["up", "down", "build"] and len(sys.argv) >= 4:
|
||||||
|
server, path = sys.argv[2], sys.argv[3]
|
||||||
|
action = f"{cmd} -d" if cmd == "up" else cmd
|
||||||
|
result = compose_action(server, path, action)
|
||||||
|
print(f"{'✅' if result.success else '❌'} compose {cmd}" if result.success else f"Error: {result.error}")
|
||||||
|
|
||||||
|
# Dashboard
|
||||||
|
elif cmd == "dashboard":
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("🐳 TZZR Docker Dashboard")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
for server, info in SERVERS.items():
|
||||||
|
containers = get_containers(server)
|
||||||
|
running = sum(1 for c in containers if c.is_running)
|
||||||
|
print(f"\n📦 {info['name']} ({info['host'].split('@')[1]})")
|
||||||
|
print(f" {info['desc']}")
|
||||||
|
print(f" Contenedores: {running}/{len(containers)} activos")
|
||||||
|
|
||||||
|
# Top 5 contenedores
|
||||||
|
for c in containers[:5]:
|
||||||
|
print(f" {c.icon} {c.name}")
|
||||||
|
|
||||||
|
else:
|
||||||
|
print("❌ Comando no reconocido. Usa 'python app.py' para ver ayuda.")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
273
apps/hst/app.py
Normal file
273
apps/hst/app.py
Normal file
@@ -0,0 +1,273 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
TZZR HST App - Gestión del servidor HST (72.62.2.84)
|
||||||
|
Servicios: Directus (3 instancias), Servidor de imágenes, APIs
|
||||||
|
"""
|
||||||
|
import subprocess
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import List, Dict
|
||||||
|
|
||||||
|
SERVER = "root@72.62.2.84"
|
||||||
|
SSH_KEY = "~/.ssh/tzzr"
|
||||||
|
|
||||||
|
# Servicios conocidos en HST
|
||||||
|
SERVICES = {
|
||||||
|
# Directus instances
|
||||||
|
"directus_hst": {"port": 8055, "type": "cms", "desc": "Directus HST principal", "url": "hst.tzrtech.org"},
|
||||||
|
"directus_lumalia": {"port": 8056, "type": "cms", "desc": "Directus Lumalia", "url": "lumalia.tzrtech.org"},
|
||||||
|
"directus_personal": {"port": 8057, "type": "cms", "desc": "Directus Personal", "url": "personal.tzrtech.org"},
|
||||||
|
|
||||||
|
# APIs
|
||||||
|
"hst-api": {"port": 5001, "type": "api", "desc": "HST Flask API"},
|
||||||
|
"hst-images": {"port": 80, "type": "web", "desc": "Servidor NGINX imágenes", "url": "tzrtech.org"},
|
||||||
|
|
||||||
|
# Infraestructura
|
||||||
|
"postgres_hst": {"port": 5432, "type": "db", "desc": "PostgreSQL 15"},
|
||||||
|
"filebrowser": {"port": 8081, "type": "app", "desc": "File Browser"},
|
||||||
|
}
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Result:
|
||||||
|
success: bool
|
||||||
|
data: any
|
||||||
|
error: str = ""
|
||||||
|
|
||||||
|
def ssh(cmd: str, timeout: int = 60) -> Result:
|
||||||
|
"""Ejecuta comando en HST"""
|
||||||
|
full_cmd = f'ssh -i {SSH_KEY} {SERVER} "{cmd}"'
|
||||||
|
try:
|
||||||
|
result = subprocess.run(full_cmd, shell=True, capture_output=True, text=True, timeout=timeout)
|
||||||
|
return Result(success=result.returncode == 0, data=result.stdout.strip(), error=result.stderr.strip())
|
||||||
|
except Exception as e:
|
||||||
|
return Result(success=False, data="", error=str(e))
|
||||||
|
|
||||||
|
def get_all_containers() -> List[Dict]:
|
||||||
|
"""Lista todos los contenedores Docker"""
|
||||||
|
result = ssh("docker ps -a --format '{{.Names}}|{{.Status}}|{{.Ports}}|{{.Image}}'")
|
||||||
|
if not result.success:
|
||||||
|
return []
|
||||||
|
|
||||||
|
containers = []
|
||||||
|
for line in result.data.split('\n'):
|
||||||
|
if line:
|
||||||
|
parts = line.split('|')
|
||||||
|
containers.append({
|
||||||
|
"name": parts[0],
|
||||||
|
"status": parts[1] if len(parts) > 1 else "",
|
||||||
|
"ports": parts[2] if len(parts) > 2 else "",
|
||||||
|
"image": parts[3] if len(parts) > 3 else ""
|
||||||
|
})
|
||||||
|
return containers
|
||||||
|
|
||||||
|
def get_service_status(service: str) -> Dict:
|
||||||
|
"""Estado detallado de un servicio"""
|
||||||
|
info = SERVICES.get(service, {})
|
||||||
|
|
||||||
|
status_result = ssh(f"docker ps --filter name={service} --format '{{{{.Status}}}}'")
|
||||||
|
|
||||||
|
health = "unknown"
|
||||||
|
if info.get("port") and info.get("type") in ["cms", "api"]:
|
||||||
|
if info.get("type") == "cms":
|
||||||
|
health_result = ssh(f"curl -s http://localhost:{info.get('port')}/server/health 2>/dev/null | head -c 50")
|
||||||
|
else:
|
||||||
|
health_result = ssh(f"curl -s http://localhost:{info.get('port')}/health 2>/dev/null | head -c 50")
|
||||||
|
health = "healthy" if health_result.success and health_result.data else "unhealthy"
|
||||||
|
|
||||||
|
return {
|
||||||
|
"service": service,
|
||||||
|
"status": status_result.data if status_result.success else "not found",
|
||||||
|
"health": health,
|
||||||
|
"port": info.get("port"),
|
||||||
|
"url": info.get("url"),
|
||||||
|
"desc": info.get("desc")
|
||||||
|
}
|
||||||
|
|
||||||
|
def restart_service(service: str) -> Result:
|
||||||
|
"""Reinicia un servicio"""
|
||||||
|
return ssh(f"docker restart {service}")
|
||||||
|
|
||||||
|
def get_logs(service: str, lines: int = 100) -> str:
|
||||||
|
"""Obtiene logs de un servicio"""
|
||||||
|
result = ssh(f"docker logs {service} --tail {lines} 2>&1")
|
||||||
|
return result.data if result.success else result.error
|
||||||
|
|
||||||
|
def get_system_stats() -> Dict:
|
||||||
|
"""Estadísticas del sistema"""
|
||||||
|
stats = {}
|
||||||
|
|
||||||
|
mem_result = ssh("free -h | grep Mem | awk '{print $2,$3,$4}'")
|
||||||
|
if mem_result.success:
|
||||||
|
parts = mem_result.data.split()
|
||||||
|
stats["memory"] = {"total": parts[0], "used": parts[1], "available": parts[2]}
|
||||||
|
|
||||||
|
disk_result = ssh("df -h / | tail -1 | awk '{print $2,$3,$4,$5}'")
|
||||||
|
if disk_result.success:
|
||||||
|
parts = disk_result.data.split()
|
||||||
|
stats["disk"] = {"total": parts[0], "used": parts[1], "available": parts[2], "percent": parts[3]}
|
||||||
|
|
||||||
|
containers_result = ssh("docker ps -q | wc -l")
|
||||||
|
stats["containers_running"] = int(containers_result.data) if containers_result.success else 0
|
||||||
|
|
||||||
|
return stats
|
||||||
|
|
||||||
|
def query_directus(instance: str, endpoint: str, token: str = None) -> Result:
|
||||||
|
"""Hace petición a una instancia de Directus"""
|
||||||
|
info = SERVICES.get(instance)
|
||||||
|
if not info or info.get("type") != "cms":
|
||||||
|
return Result(success=False, data="", error="Instancia Directus no encontrada")
|
||||||
|
|
||||||
|
port = info["port"]
|
||||||
|
auth = f"-H 'Authorization: Bearer {token}'" if token else ""
|
||||||
|
|
||||||
|
cmd = f"curl -s {auth} http://localhost:{port}{endpoint}"
|
||||||
|
result = ssh(cmd)
|
||||||
|
|
||||||
|
try:
|
||||||
|
return Result(success=result.success, data=json.loads(result.data) if result.data else {})
|
||||||
|
except:
|
||||||
|
return Result(success=result.success, data=result.data)
|
||||||
|
|
||||||
|
def get_directus_collections(instance: str) -> List[str]:
|
||||||
|
"""Lista colecciones de una instancia Directus"""
|
||||||
|
result = query_directus(instance, "/collections")
|
||||||
|
if result.success and isinstance(result.data, dict):
|
||||||
|
collections = result.data.get("data", [])
|
||||||
|
return [c.get("collection") for c in collections if not c.get("collection", "").startswith("directus_")]
|
||||||
|
return []
|
||||||
|
|
||||||
|
def list_images(path: str = "/var/www/images") -> List[str]:
|
||||||
|
"""Lista imágenes en el servidor"""
|
||||||
|
result = ssh(f"ls -la {path} 2>/dev/null | head -20")
|
||||||
|
return result.data.split('\n') if result.success else []
|
||||||
|
|
||||||
|
def get_postgres_databases() -> List[str]:
|
||||||
|
"""Lista bases de datos PostgreSQL"""
|
||||||
|
result = ssh("docker exec postgres_hst psql -U postgres -c '\\l' -t 2>/dev/null")
|
||||||
|
if not result.success:
|
||||||
|
return []
|
||||||
|
|
||||||
|
databases = []
|
||||||
|
for line in result.data.split('\n'):
|
||||||
|
if '|' in line:
|
||||||
|
db = line.split('|')[0].strip()
|
||||||
|
if db and db not in ['template0', 'template1']:
|
||||||
|
databases.append(db)
|
||||||
|
|
||||||
|
return databases
|
||||||
|
|
||||||
|
# CLI
|
||||||
|
def main():
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
print("""
|
||||||
|
TZZR HST App - Servidor 72.62.2.84
|
||||||
|
==================================
|
||||||
|
|
||||||
|
Uso: python app.py <comando> [argumentos]
|
||||||
|
|
||||||
|
Comandos Generales:
|
||||||
|
status Estado general del sistema
|
||||||
|
containers Lista todos los contenedores
|
||||||
|
stats Estadísticas del sistema
|
||||||
|
|
||||||
|
Gestión de Servicios:
|
||||||
|
service <name> Estado detallado de servicio
|
||||||
|
restart <service> Reiniciar servicio
|
||||||
|
logs <service> [lines] Ver logs
|
||||||
|
|
||||||
|
Directus (CMS):
|
||||||
|
directus Estado de las 3 instancias
|
||||||
|
collections <instance> Lista colecciones de instancia
|
||||||
|
query <instance> <path> Query a Directus API
|
||||||
|
|
||||||
|
Imágenes:
|
||||||
|
images [path] Lista imágenes
|
||||||
|
|
||||||
|
Bases de Datos:
|
||||||
|
postgres Lista bases de datos
|
||||||
|
|
||||||
|
Servicios disponibles:
|
||||||
|
CMS: directus_hst, directus_lumalia, directus_personal
|
||||||
|
API: hst-api
|
||||||
|
Web: hst-images
|
||||||
|
DB: postgres_hst
|
||||||
|
App: filebrowser
|
||||||
|
""")
|
||||||
|
return
|
||||||
|
|
||||||
|
cmd = sys.argv[1]
|
||||||
|
|
||||||
|
if cmd == "status":
|
||||||
|
print("\n📊 HST Status (72.62.2.84)")
|
||||||
|
print("=" * 40)
|
||||||
|
stats = get_system_stats()
|
||||||
|
print(f"💾 Memoria: {stats.get('memory', {}).get('used', '?')}/{stats.get('memory', {}).get('total', '?')}")
|
||||||
|
print(f"💿 Disco: {stats.get('disk', {}).get('used', '?')}/{stats.get('disk', {}).get('total', '?')} ({stats.get('disk', {}).get('percent', '?')})")
|
||||||
|
print(f"📦 Contenedores: {stats.get('containers_running', 0)}")
|
||||||
|
|
||||||
|
elif cmd == "containers":
|
||||||
|
containers = get_all_containers()
|
||||||
|
print(f"\n📦 Contenedores en HST ({len(containers)} total):")
|
||||||
|
for c in containers:
|
||||||
|
icon = "✅" if "Up" in c["status"] else "❌"
|
||||||
|
print(f" {icon} {c['name']}: {c['status'][:30]}")
|
||||||
|
|
||||||
|
elif cmd == "service" and len(sys.argv) >= 3:
|
||||||
|
status = get_service_status(sys.argv[2])
|
||||||
|
print(f"\n🔧 {status['service']}")
|
||||||
|
print(f" Status: {status['status']}")
|
||||||
|
print(f" Health: {status['health']}")
|
||||||
|
print(f" Desc: {status.get('desc', '')}")
|
||||||
|
if status.get('url'):
|
||||||
|
print(f" URL: https://{status['url']}")
|
||||||
|
|
||||||
|
elif cmd == "restart" and len(sys.argv) >= 3:
|
||||||
|
result = restart_service(sys.argv[2])
|
||||||
|
print(f"{'✅' if result.success else '❌'} {sys.argv[2]}: {'reiniciado' if result.success else result.error}")
|
||||||
|
|
||||||
|
elif cmd == "logs" and len(sys.argv) >= 3:
|
||||||
|
lines = int(sys.argv[3]) if len(sys.argv) > 3 else 50
|
||||||
|
print(get_logs(sys.argv[2], lines))
|
||||||
|
|
||||||
|
elif cmd == "directus":
|
||||||
|
print("\n📚 Instancias Directus en HST:")
|
||||||
|
for instance in ["directus_hst", "directus_lumalia", "directus_personal"]:
|
||||||
|
status = get_service_status(instance)
|
||||||
|
icon = "✅" if "Up" in status["status"] else "❌"
|
||||||
|
health = f"({status['health']})" if status['health'] != "unknown" else ""
|
||||||
|
url = f"https://{status['url']}" if status.get('url') else ""
|
||||||
|
print(f" {icon} {instance}: {status['status'][:20]} {health}")
|
||||||
|
print(f" └─ {url}")
|
||||||
|
|
||||||
|
elif cmd == "collections" and len(sys.argv) >= 3:
|
||||||
|
collections = get_directus_collections(sys.argv[2])
|
||||||
|
print(f"\n📋 Colecciones en {sys.argv[2]}:")
|
||||||
|
for c in collections:
|
||||||
|
print(f" • {c}")
|
||||||
|
|
||||||
|
elif cmd == "query" and len(sys.argv) >= 4:
|
||||||
|
result = query_directus(sys.argv[2], sys.argv[3])
|
||||||
|
if result.success:
|
||||||
|
print(json.dumps(result.data, indent=2)[:2000])
|
||||||
|
else:
|
||||||
|
print(f"❌ Error: {result.error}")
|
||||||
|
|
||||||
|
elif cmd == "images":
|
||||||
|
path = sys.argv[2] if len(sys.argv) > 2 else "/var/www/images"
|
||||||
|
images = list_images(path)
|
||||||
|
print(f"\n🖼️ Imágenes en {path}:")
|
||||||
|
for img in images[:20]:
|
||||||
|
print(f" {img}")
|
||||||
|
|
||||||
|
elif cmd == "postgres":
|
||||||
|
dbs = get_postgres_databases()
|
||||||
|
print("\n🐘 Bases de datos PostgreSQL:")
|
||||||
|
for db in dbs:
|
||||||
|
print(f" • {db}")
|
||||||
|
|
||||||
|
else:
|
||||||
|
print("❌ Comando no reconocido. Usa 'python app.py' para ver ayuda.")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -98,6 +98,11 @@ class CaptainClaude:
|
|||||||
self.cost_tracker = CostTracker()
|
self.cost_tracker = CostTracker()
|
||||||
logger = logging.getLogger("captain-claude")
|
logger = logging.getLogger("captain-claude")
|
||||||
logger.setLevel(logging.INFO)
|
logger.setLevel(logging.INFO)
|
||||||
|
# Evitar handlers duplicados en sesiones largas
|
||||||
|
if not logger.handlers:
|
||||||
|
handler = logging.StreamHandler()
|
||||||
|
handler.setFormatter(logging.Formatter('%(message)s'))
|
||||||
|
logger.addHandler(handler)
|
||||||
self.logger = StructuredLogger(logger)
|
self.logger = StructuredLogger(logger)
|
||||||
self.output_dir = Path(output_dir) if output_dir else Path.cwd() / "captain_output"
|
self.output_dir = Path(output_dir) if output_dir else Path.cwd() / "captain_output"
|
||||||
self.output_dir.mkdir(exist_ok=True)
|
self.output_dir.mkdir(exist_ok=True)
|
||||||
@@ -244,33 +249,33 @@ Continue and complete this work."""
|
|||||||
"""Execute a task using intelligent agent orchestration."""
|
"""Execute a task using intelligent agent orchestration."""
|
||||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||||
|
|
||||||
print(f"\n{'='*60}")
|
print(f"\n{'='*60}", flush=True)
|
||||||
print("CAPTAIN CLAUDE by dadiaar")
|
print("CAPTAIN CLAUDE by dadiaar", flush=True)
|
||||||
print(f"{'='*60}")
|
print(f"{'='*60}", flush=True)
|
||||||
print(f"Task: {task[:100]}...")
|
print(f"Task: {task[:100]}...", flush=True)
|
||||||
print(f"{'='*60}\n")
|
print(f"{'='*60}\n", flush=True)
|
||||||
|
|
||||||
# Phase 1: Analyze task
|
# Phase 1: Analyze task
|
||||||
print("[Captain] Analyzing task...")
|
print("[Captain] Analyzing task...", flush=True)
|
||||||
plan = await self.analyze_task(task)
|
plan = await self.analyze_task(task)
|
||||||
print(f"[Captain] Plan created: {len(plan.get('steps', []))} steps")
|
print(f"[Captain] Plan created: {len(plan.get('steps', []))} steps", flush=True)
|
||||||
|
|
||||||
# Phase 2: Execute plan
|
# Phase 2: Execute plan
|
||||||
results = []
|
results = []
|
||||||
if plan.get("parallel_possible") and len(plan.get("agents_needed", [])) > 1:
|
if plan.get("parallel_possible") and len(plan.get("agents_needed", [])) > 1:
|
||||||
print("[Captain] Executing agents in parallel...")
|
print("[Captain] Executing agents in parallel...", flush=True)
|
||||||
parallel_result = await self.run_parallel(
|
parallel_result = await self.run_parallel(
|
||||||
task,
|
task,
|
||||||
plan.get("agents_needed", ["coder"])
|
plan.get("agents_needed", ["coder"])
|
||||||
)
|
)
|
||||||
results.append(parallel_result)
|
results.append(parallel_result)
|
||||||
else:
|
else:
|
||||||
print("[Captain] Executing agents sequentially...")
|
print("[Captain] Executing agents sequentially...", flush=True)
|
||||||
sequential_results = await self.run_sequential(plan.get("steps", []))
|
sequential_results = await self.run_sequential(plan.get("steps", []))
|
||||||
results.extend(sequential_results)
|
results.extend(sequential_results)
|
||||||
|
|
||||||
# Phase 3: Synthesize results
|
# Phase 3: Synthesize results
|
||||||
print("[Captain] Synthesizing results...")
|
print("[Captain] Synthesizing results...", flush=True)
|
||||||
synthesis_prompt = f"""Synthesize these results into a coherent final output:
|
synthesis_prompt = f"""Synthesize these results into a coherent final output:
|
||||||
|
|
||||||
Original task: {task}
|
Original task: {task}
|
||||||
@@ -298,12 +303,12 @@ Provide a clear, actionable final result."""
|
|||||||
with open(output_file, "w") as f:
|
with open(output_file, "w") as f:
|
||||||
json.dump(final_result, f, indent=2, default=str)
|
json.dump(final_result, f, indent=2, default=str)
|
||||||
|
|
||||||
print(f"\n{'='*60}")
|
print(f"\n{'='*60}", flush=True)
|
||||||
print("EXECUTION COMPLETE")
|
print("EXECUTION COMPLETE", flush=True)
|
||||||
print(f"{'='*60}")
|
print(f"{'='*60}", flush=True)
|
||||||
print(f"Output saved: {output_file}")
|
print(f"Output saved: {output_file}", flush=True)
|
||||||
print(f"Cost: {self.cost_tracker.summary()}")
|
print(f"Cost: {self.cost_tracker.summary()}", flush=True)
|
||||||
print(f"{'='*60}\n")
|
print(f"{'='*60}\n", flush=True)
|
||||||
|
|
||||||
return final_result
|
return final_result
|
||||||
|
|
||||||
@@ -322,18 +327,18 @@ async def main():
|
|||||||
"""Interactive Captain Claude session."""
|
"""Interactive Captain Claude session."""
|
||||||
captain = CaptainClaude()
|
captain = CaptainClaude()
|
||||||
|
|
||||||
print("\n" + "="*60)
|
print("\n" + "="*60, flush=True)
|
||||||
print("CAPTAIN CLAUDE by dadiaar")
|
print("CAPTAIN CLAUDE by dadiaar", flush=True)
|
||||||
print("Multi-Agent Orchestration System")
|
print("Multi-Agent Orchestration System", flush=True)
|
||||||
print("="*60)
|
print("="*60, flush=True)
|
||||||
print("\nCommands:")
|
print("\nCommands:", flush=True)
|
||||||
print(" /execute <task> - Full multi-agent execution")
|
print(" /execute <task> - Full multi-agent execution", flush=True)
|
||||||
print(" /chat <message> - Chat with Captain")
|
print(" /chat <message> - Chat with Captain", flush=True)
|
||||||
print(" /agent <name> <message> - Chat with specific agent")
|
print(" /agent <name> <message> - Chat with specific agent", flush=True)
|
||||||
print(" /parallel <task> - Run all agents in parallel")
|
print(" /parallel <task> - Run all agents in parallel", flush=True)
|
||||||
print(" /cost - Show cost summary")
|
print(" /cost - Show cost summary", flush=True)
|
||||||
print(" /quit - Exit")
|
print(" /quit - Exit", flush=True)
|
||||||
print("="*60 + "\n")
|
print("="*60 + "\n", flush=True)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
@@ -343,24 +348,24 @@ async def main():
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
if user_input.lower() == "/quit":
|
if user_input.lower() == "/quit":
|
||||||
print("Goodbye!")
|
print("Goodbye!", flush=True)
|
||||||
break
|
break
|
||||||
|
|
||||||
if user_input.lower() == "/cost":
|
if user_input.lower() == "/cost":
|
||||||
print(f"Cost: {captain.cost_tracker.summary()}")
|
print(f"Cost: {captain.cost_tracker.summary()}", flush=True)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if user_input.startswith("/execute "):
|
if user_input.startswith("/execute "):
|
||||||
task = user_input[9:]
|
task = user_input[9:]
|
||||||
result = await captain.execute(task)
|
result = await captain.execute(task)
|
||||||
print(f"\nFinal Output:\n{result['final_output']}\n")
|
print(f"\nFinal Output:\n{result['final_output']}\n", flush=True)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if user_input.startswith("/parallel "):
|
if user_input.startswith("/parallel "):
|
||||||
task = user_input[10:]
|
task = user_input[10:]
|
||||||
agents = ["coder", "reviewer", "researcher"]
|
agents = ["coder", "reviewer", "researcher"]
|
||||||
result = await captain.run_parallel(task, agents)
|
result = await captain.run_parallel(task, agents)
|
||||||
print(f"\nParallel Results:\n{result}\n")
|
print(f"\nParallel Results:\n{result}\n", flush=True)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if user_input.startswith("/agent "):
|
if user_input.startswith("/agent "):
|
||||||
@@ -368,24 +373,24 @@ async def main():
|
|||||||
if len(parts) == 2:
|
if len(parts) == 2:
|
||||||
agent, message = parts
|
agent, message = parts
|
||||||
response = await captain.chat(message, agent)
|
response = await captain.chat(message, agent)
|
||||||
print(f"\n[{agent}]: {response}\n")
|
print(f"\n[{agent}]: {response}\n", flush=True)
|
||||||
else:
|
else:
|
||||||
print("Usage: /agent <name> <message>")
|
print("Usage: /agent <name> <message>", flush=True)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if user_input.startswith("/chat ") or not user_input.startswith("/"):
|
if user_input.startswith("/chat ") or not user_input.startswith("/"):
|
||||||
message = user_input[6:] if user_input.startswith("/chat ") else user_input
|
message = user_input[6:] if user_input.startswith("/chat ") else user_input
|
||||||
response = await captain.chat(message)
|
response = await captain.chat(message)
|
||||||
print(f"\n[Captain]: {response}\n")
|
print(f"\n[Captain]: {response}\n", flush=True)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
print("Unknown command. Use /quit to exit.")
|
print("Unknown command. Use /quit to exit.", flush=True)
|
||||||
|
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
print("\nGoodbye!")
|
print("\nGoodbye!", flush=True)
|
||||||
break
|
break
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error: {e}")
|
print(f"Error: {e}", flush=True)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
Reference in New Issue
Block a user