feat: import agents_and_robots platform as unibots (Matrix-out, unibus transport)

Reemplaza el scaffold del echobot por la plataforma completa de bots traida
desde ~/DataProyects/Github/agents_and_robots tras la operacion Matrix-out:
los bots ya no hablan por Matrix sino por el bus unibus (modelo todo-rooms +
E2E via shell/transportunibus sobre github.com/enmanuel/unibus/pkg/client).

- go.mod: replace de unibus -> ../unibus y de fn-registry -> ../../../.. (paths
  relativos reajustados a la nueva ubicacion dentro de fn_registry).
- app.md: bump a 0.2.0, descripcion + arquitectura + comandos + gotchas reales.
- modulo Go conservado como github.com/enmanuel/agents (sin reescribir imports).

agents_and_robots queda archivado como museo de la era Matrix.
This commit is contained in:
agent
2026-06-07 11:50:13 +02:00
parent bb5b0e09b1
commit fc644ecd6e
308 changed files with 38829 additions and 474 deletions
+42
View File
@@ -0,0 +1,42 @@
# dev-scripts
Scripts bash para operaciones del día a día con los bots Matrix.
Todos los scripts comparten funciones comunes definidas en `_common.sh` (colores, helpers de proceso, descubrimiento de agentes, carga de `.env`).
## Estructura
```
dev-scripts/
├── _common.sh funciones compartidas (sourced por todos los scripts)
├── server/ gestión del launcher (ciclo de vida del servidor)
└── agent/ gestión de agentes individuales (setup, registro, E2EE)
```
## server/
Scripts para controlar el launcher unificado que ejecuta todos los agentes.
| Script | Descripción |
|--------|-------------|
| `start.sh` | Inicia el launcher (compila si es necesario) |
| `stop.sh` | Detiene el launcher (SIGTERM, espera 5s, SIGKILL) |
| `restart.sh` | Reinicia el launcher (stop + start) |
| `ps.sh` | Muestra el proceso del launcher con detalle (PID, mem, CPU, uptime) |
| `logs.sh [lines]` | Tail -f de los logs del launcher |
| `dashboard.sh` | Abre la TUI interactiva de gestión |
| `server.sh <cmd>` | CLI unificado que enruta a los scripts anteriores |
## agent/
Scripts para crear, registrar, verificar y gestionar agentes individuales.
| Script | Descripción |
|--------|-------------|
| `new-agent.sh <id> [name]` | Genera scaffold completo (config, agent.go, prompts) |
| `register.sh <id> [name]` | Registra bot en Matrix via Synapse admin API |
| `verify.sh [id]` | Verifica/regenera dispositivos E2EE (cross-signing) |
| `avatar.sh <id> <img>` | Sube avatar y sincroniza displayname |
| `reset-password.sh <id>` | Resetea password sin invalidar el token |
| `remove.sh <id>` | Deshabilita un agente (enabled: false, no borra datos) |
| `list.sh` | Muestra todos los agentes y su estado |
+178
View File
@@ -0,0 +1,178 @@
#!/usr/bin/env bash
# _common.sh — sourced by all dev-scripts. Do not run directly.
set -euo pipefail
# ── Colores ────────────────────────────────────────────────────────────────
RED='\033[0;31m'
GRN='\033[0;32m'
YLW='\033[0;33m'
BLU='\033[0;34m'
DIM='\033[2m'
RST='\033[0m'
ok() { echo -e "${GRN}${RST} $*"; }
info() { echo -e "${BLU}${RST} $*"; }
warn() { echo -e "${YLW}!${RST} $*"; }
fail() { echo -e "${RED}${RST} $*" >&2; exit 1; }
dim() { echo -e "${DIM}$*${RST}"; }
# ── Repo root ──────────────────────────────────────────────────────────────
# Scripts can be called from any directory; we always operate from repo root.
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$REPO_ROOT"
# ── .env loader ───────────────────────────────────────────────────────────
load_env() {
local env_file="${1:-.env}"
[[ -f "$env_file" ]] || fail ".env not found at $REPO_ROOT/$env_file — copy .env.example and fill it in"
# Export only lines that are KEY=VALUE (skip comments and blanks)
set -o allexport
# shellcheck disable=SC1090
source <(grep -E '^[A-Z_][A-Z0-9_]*=.+' "$env_file")
set +o allexport
}
# ── Go tooling ─────────────────────────────────────────────────────────────
GO=${GO_BIN:-go}
export PATH="$PATH:/usr/local/go/bin"
command -v "$GO" &>/dev/null || fail "go not found — install Go or set GO_BIN"
# ── Process helpers ────────────────────────────────────────────────────────
RUN_DIR="$REPO_ROOT/run"
mkdir -p "$RUN_DIR"
pid_file() { echo "$RUN_DIR/$1.pid"; }
log_file() { echo "$RUN_DIR/$1.log"; }
read_pid() {
local f; f="$(pid_file "$1")"
[[ -f "$f" ]] && cat "$f" || echo 0
}
# Map agent ID to its config path by scanning agent directories.
config_path_for() {
local target_id="$1"
for cfg in agents/*/config.yaml; do
[[ -f "$cfg" ]] || continue
local id
id=$(grep -m1 '^ id:' "$cfg" | awk '{print $2}')
if [[ "$id" == "$target_id" ]]; then
echo "$cfg"
return
fi
done
}
# Find all PIDs of launcher processes for a given agent ID.
# Searches for the actual config path in the process command line.
# Returns newline-separated PIDs (may be empty).
find_agent_pids() {
local id="$1"
local cfg; cfg="$(config_path_for "$id")"
if [[ -z "$cfg" ]]; then
return
fi
pgrep -f "launcher.*-c.*${cfg}" 2>/dev/null || true
}
is_running() {
local id="$1"
# First check PID file
local pid; pid="$(read_pid "$id")"
if [[ "$pid" -gt 0 ]] && kill -0 "$pid" 2>/dev/null; then
return 0
fi
# PID file is stale or missing — search for actual processes
local pids; pids="$(find_agent_pids "$id")"
if [[ -n "$pids" ]]; then
# Update PID file with the first found process
local first_pid; first_pid="$(echo "$pids" | head -1)"
echo "$first_pid" > "$(pid_file "$id")"
return 0
fi
# Truly not running — clean up stale PID file
[[ "$pid" -gt 0 ]] && rm -f "$(pid_file "$id")"
return 1
}
# Count how many instances of an agent are running.
count_instances() {
local id="$1"
local pids; pids="$(find_agent_pids "$id")"
if [[ -z "$pids" ]]; then
echo 0
else
echo "$pids" | wc -l
fi
}
agent_status() {
local id="$1" enabled="$2"
if [[ "$enabled" != "true" ]]; then
echo "disabled"
elif is_launcher_running; then
echo "running"
else
echo "stopped"
fi
}
# ── Unified launcher helpers ───────────────────────────────────────────────
LAUNCHER_ID="launcher"
launcher_pid_file() { echo "$RUN_DIR/$LAUNCHER_ID.pid"; }
launcher_log_file() { echo "$RUN_DIR/$LAUNCHER_ID.log"; }
read_launcher_pid() {
local f; f="$(launcher_pid_file)"
[[ -f "$f" ]] && cat "$f" || echo 0
}
is_launcher_running() {
local pid; pid="$(read_launcher_pid)"
if [[ "$pid" -gt 0 ]] && kill -0 "$pid" 2>/dev/null; then
return 0
fi
# Fallback: search for launcher process without -c flag
local pids; pids="$(pgrep -f 'launcher.*--log-level' 2>/dev/null || true)"
if [[ -n "$pids" ]]; then
local first_pid; first_pid="$(echo "$pids" | head -1)"
echo "$first_pid" > "$(launcher_pid_file)"
return 0
fi
[[ "$pid" -gt 0 ]] && rm -f "$(launcher_pid_file)"
return 1
}
# ── Agent discovery ────────────────────────────────────────────────────────
# Prints: id|version|enabled|description (one line per agent)
list_agents_raw() {
for cfg in agents/*/config.yaml; do
[[ -f "$cfg" ]] || continue
local id version enabled desc
id=$(grep -m1 '^ id:' "$cfg" | awk '{print $2}')
version=$(grep -m1 '^ version:' "$cfg" | awk '{print $2}' | tr -d '"')
enabled=$(grep -m1 '^ enabled:' "$cfg" | awk '{print $2}')
desc=$(grep -m1 '^ description:' "$cfg" | cut -d'"' -f2)
[[ -n "$id" ]] && echo "${id}|${version}|${enabled}|${desc}|${cfg}"
done
}
# ── Naming convention ─────────────────────────────────────────────────────
# Normalizes an agent ID to an env-var suffix.
# Convention: uppercase, hyphens → underscores. No stripping of suffixes.
# "assistant-bot" → "ASSISTANT_BOT"
# "asistente-2" → "ASISTENTE_2"
# "devops-bot" → "DEVOPS_BOT"
normalize_id() {
echo "$1" | tr '[:lower:]-' '[:upper:]_'
}
# ── Usage helper ──────────────────────────────────────────────────────────
need_arg() {
[[ -n "${1:-}" ]] || { echo "Usage: $0 <agent-id>"; exit 1; }
}
+89
View File
@@ -0,0 +1,89 @@
# dev-scripts/agent
Scripts para crear, registrar, verificar y gestionar agentes individuales en el sistema Matrix.
## Scripts
### new-agent.sh
Genera el scaffold completo para un nuevo agente: `config.yaml`, `agent.go` (reglas puras), directorio de prompts y data. También registra automáticamente el import y la entrada en `rulesRegistry` de `cmd/launcher/main.go`.
```bash
./dev-scripts/agent/new-agent.sh <agent-id> "Display Name"
./dev-scripts/agent/new-agent.sh monitor-bot "Monitor Agent"
```
### register.sh
Registra un nuevo bot en el servidor Matrix via Synapse admin API. Genera y guarda en `.env`: access token (`MATRIX_TOKEN_*`), password (`MATRIX_PASSWORD_*`) y pickle key (`PICKLE_KEY_*`).
Requiere `MATRIX_ADMIN_TOKEN` y `MATRIX_HOMESERVER` en `.env`.
```bash
./dev-scripts/agent/register.sh <agent-id> "Display Name"
./dev-scripts/agent/register.sh assistant-bot "Assistant"
```
### verify.sh
Verifica o regenera los dispositivos E2EE de los agentes. Genera cross-signing keys, firma el device y guarda el recovery key en `.env`. Sin este paso, los mensajes del bot aparecen como "not verified by its owner".
```bash
./dev-scripts/agent/verify.sh # verifica todos los habilitados con E2EE
./dev-scripts/agent/verify.sh assistant-bot # verifica uno específico
```
### avatar.sh
Sube una imagen como avatar del bot en Matrix y sincroniza el displayname desde el `config.yaml`.
```bash
./dev-scripts/agent/avatar.sh <agent-id> <image-path>
./dev-scripts/agent/avatar.sh assistant-bot static/assistant.jpg
```
### reset-password.sh
Resetea la contraseña de un bot existente via Synapse admin API sin crear nueva sesión ni cambiar el device ID. El access token actual sigue siendo válido.
```bash
./dev-scripts/agent/reset-password.sh <agent-id>
./dev-scripts/agent/reset-password.sh assistant-bot
```
### remove.sh
Deshabilita un agente marcándolo como `enabled: false` en su `config.yaml`. No borra datos — los preserva en `agents/<id>/data/`.
```bash
./dev-scripts/agent/remove.sh <agent-id>
```
### list.sh
Muestra todos los agentes registrados con su estado (running/stopped/disabled), versión y descripción en una tabla formateada.
```bash
./dev-scripts/agent/list.sh
```
## Flujo típico para un nuevo agente
```bash
# 1. Crear scaffold
./dev-scripts/agent/new-agent.sh mi-bot "Mi Bot"
# 2. Editar config, reglas y prompt
# agents/mi-bot/config.yaml
# agents/mi-bot/agent.go
# agents/mi-bot/prompts/system.md
# 3. Registrar en Matrix
./dev-scripts/agent/register.sh mi-bot "Mi Bot"
# 4. Verificar E2EE
./dev-scripts/agent/verify.sh mi-bot
# 5. Arrancar
./dev-scripts/server/start.sh
```
+32
View File
@@ -0,0 +1,32 @@
#!/usr/bin/env bash
# avatar.sh — sube una imagen y la establece como avatar de un bot.
#
# Uso:
# ./dev-scripts/agent/avatar.sh <agent-id> <image-path>
#
# Ejemplos:
# ./dev-scripts/agent/avatar.sh assistant-bot assets/assistant.png
# ./dev-scripts/agent/avatar.sh devops-bot assets/devops.jpg
source "$(dirname "$0")/../_common.sh"
load_env
AGENT_ID="${1:-}"
IMAGE_PATH="${2:-}"
[[ -n "$AGENT_ID" ]] || fail "Uso: $0 <agent-id> <image-path>"
[[ -n "$IMAGE_PATH" ]] || fail "Uso: $0 <agent-id> <image-path>"
[[ -f "$IMAGE_PATH" ]] || fail "Imagen no encontrada: $IMAGE_PATH"
# Resuelve el binario de agentctl: compiled > go run
if [[ -f "$REPO_ROOT/bin/agentctl" ]]; then
CTL="$REPO_ROOT/bin/agentctl"
else
info "bin/agentctl no encontrado, usando go run ./cmd/agentctl"
CTL="$GO run ./cmd/agentctl"
fi
info "Subiendo avatar para $AGENT_ID desde $IMAGE_PATH..."
$CTL avatar "$AGENT_ID" "$IMAGE_PATH"
ok "Avatar de $AGENT_ID actualizado."
+101
View File
@@ -0,0 +1,101 @@
#!/usr/bin/env bash
# create-full.sh — pipeline completo para crear un agente funcional
#
# Ejecuta en orden: scaffold → build → register → verify E2EE
# NO arranca el agente — primero personalizar agent.go, config.yaml y prompts/system.md
#
# Uso:
# ./dev-scripts/agent/create-full.sh <agent-id> "Display Name"
#
# Ejemplo:
# ./dev-scripts/agent/create-full.sh monitor-bot "Monitor Agent"
#
# Requisitos en .env:
# MATRIX_ADMIN_TOKEN, MATRIX_HOMESERVER, MATRIX_SERVER_NAME
source "$(dirname "$0")/../_common.sh"
load_env
need_arg "${1:-}"
ID="$1"
DISPLAYNAME="${2:-$ID}"
NORM="$(normalize_id "$ID")"
SCRIPT_DIR="$(dirname "$0")"
echo ""
echo -e "${BLU}═══════════════════════════════════════════════════════${RST}"
echo -e "${BLU} Creando agente: ${GRN}$ID${BLU} ($DISPLAYNAME)${RST}"
echo -e "${BLU}═══════════════════════════════════════════════════════${RST}"
echo ""
# ── Paso 1: Scaffold ─────────────────────────────────────────────────────
info "Paso 1/4 — Scaffold (agent.go, config.yaml, prompts, launcher)"
echo ""
"$SCRIPT_DIR/new-agent.sh" "$ID" "$DISPLAYNAME"
echo ""
# ── Paso 2: Verificar compilación ─────────────────────────────────────────
info "Paso 2/4 — Verificando compilación..."
if "$GO" build -tags goolm ./... 2>&1; then
ok "Compilación exitosa"
else
fail "Error de compilación — revisa agents/$ID/agent.go y cmd/launcher/main.go"
fi
echo ""
# ── Paso 3: Registrar en Matrix ──────────────────────────────────────────
info "Paso 3/4 — Registrando en Matrix..."
echo ""
# Reload .env in case new-agent.sh or previous steps changed it
load_env
"$SCRIPT_DIR/register.sh" "$ID" "$DISPLAYNAME"
echo ""
# ── Paso 4: Verificar E2EE ───────────────────────────────────────────────
info "Paso 4/4 — Verificación E2EE (cross-signing + recovery key)..."
echo ""
# Reload .env to pick up token, password, pickle key from register.sh
load_env
"$SCRIPT_DIR/verify.sh" "$ID"
echo ""
# ── Resumen ──────────────────────────────────────────────────────────────
echo -e "${GRN}═══════════════════════════════════════════════════════${RST}"
echo -e "${GRN} ✓ Agente $ID creado exitosamente${RST}"
echo -e "${GRN}═══════════════════════════════════════════════════════${RST}"
echo ""
echo -e " ${BLU}Archivos creados:${RST}"
echo -e " agents/$ID/agent.go"
echo -e " agents/$ID/config.yaml"
echo -e " agents/$ID/prompts/system.md"
echo ""
echo -e " ${BLU}Variables en .env:${RST}"
echo -e " MATRIX_TOKEN_${NORM}"
echo -e " MATRIX_PASSWORD_${NORM}"
echo -e " PICKLE_KEY_${NORM}"
echo -e " SSSS_RECOVERY_KEY_${NORM}"
echo ""
echo -e " ${BLU}Launcher actualizado:${RST}"
echo -e " cmd/launcher/main.go (import + rulesRegistry)"
echo ""
echo -e "${YLW}Siguiente paso:${RST}"
echo ""
echo -e " 1. Personalizar los archivos del agente:"
echo -e " ${DIM}agents/$ID/agent.go${RST} — reglas de decisión"
echo -e " ${DIM}agents/$ID/config.yaml${RST} — LLM, tools, personalidad"
echo -e " ${DIM}agents/$ID/prompts/system.md${RST} — system prompt"
echo ""
echo -e " 2. Arrancar:"
echo -e " ${DIM}./dev-scripts/server/start.sh${RST}"
echo ""
+27
View File
@@ -0,0 +1,27 @@
#!/usr/bin/env bash
# list.sh — muestra todos los agentes y su estado actual
# Uso: ./dev-scripts/agent/list.sh
source "$(dirname "$0")/../_common.sh"
printf "%-22s %-12s %-8s %s\n" "ID" "STATUS" "VERSION" "DESCRIPTION"
printf '%s\n' "$(printf '─%.0s' {1..70})"
while IFS='|' read -r id version enabled desc _cfg; do
status=$(agent_status "$id" "$enabled")
case "$status" in
running) label="${GRN}● running${RST}" ;;
stopped) label="${DIM}○ stopped${RST}" ;;
disabled) label="${YLW} disabled${RST}" ;;
*) label="$status" ;;
esac
# Truncate description
[[ ${#desc} -gt 38 ]] && desc="${desc:0:37}"
printf "%-22s " "$id"
printf "${label}"
printf " %-8s %s\n" "$version" "$desc"
done < <(list_agents_raw)
+363
View File
@@ -0,0 +1,363 @@
#!/usr/bin/env bash
# new-agent.sh — genera el scaffold de un nuevo agente
#
# Uso:
# ./dev-scripts/agent/new-agent.sh <agent-id> [displayname]
#
# Ejemplo:
# ./dev-scripts/agent/new-agent.sh monitor-bot "Monitor Agent"
#
# Crea:
# agents/<agent-id>/config.yaml (copiado desde agents/_template/)
# agents/<agent-id>/agent.go (copiado desde agents/_template/)
# agents/<agent-id>/prompts/ (copiado desde agents/_template/prompts/)
# agents/<agent-id>/data/ (directorio de datos, en .gitignore)
#
# También te recuerda los dos pasos manuales que quedan.
source "$(dirname "$0")/../_common.sh"
load_env
need_arg "${1:-}"
ID="$1"
DISPLAYNAME="${2:-$ID}"
PACKAGE="$(echo "$ID" | tr '-' '_' | sed 's/_bot//')" # "monitor-bot" → "monitor"
NORM="$(normalize_id "$ID")" # "monitor-bot" → "MONITOR_BOT"
DIR="agents/$ID"
TEMPLATE="agents/_template"
[[ -d "$DIR" ]] && fail "Ya existe agents/$ID — ¿ya fue creado?"
[[ ! -d "$TEMPLATE" ]] && fail "No existe el directorio _template en agents/_template/"
info "Creando scaffold para $ID desde _template..."
mkdir -p "$DIR/prompts" "$DIR/data"
# ── Copiar config.yaml desde template y personalizar ─────────────────────
cp "$TEMPLATE/config.yaml" "$DIR/config.yaml"
sed -i "s/_template/$ID/g" "$DIR/config.yaml"
sed -i "s/Template Agent/$DISPLAYNAME/g" "$DIR/config.yaml"
sed -i "s/template: true/template: false/g" "$DIR/config.yaml"
sed -i "s/enabled: true/enabled: true/g" "$DIR/config.yaml"
sed -i "s/MATRIX_TOKEN_TEMPLATE/MATRIX_TOKEN_${NORM}/g" "$DIR/config.yaml"
sed -i "s/PICKLE_KEY_TEMPLATE/PICKLE_KEY_${NORM}/g" "$DIR/config.yaml"
sed -i "s/@template:matrix.example.com/@$ID:\${MATRIX_SERVER_NAME}/g" "$DIR/config.yaml"
sed -i "s|https://matrix.example.com|\${MATRIX_HOMESERVER}|g" "$DIR/config.yaml"
ok "config.yaml creado desde template"
# DEPRECATED: generacion inline — ahora copiamos desde _template
: <<'YAML'
# ============================================
# IDENTIDAD
# ============================================
agent:
id: $ID
name: "$DISPLAYNAME"
version: "1.0.0"
enabled: true
description: "Descripción del agente $DISPLAYNAME"
tags: [$(echo "$ID" | tr '-' ',')]
# ============================================
# PERSONALIDAD Y COMPORTAMIENTO
# ============================================
personality:
tone: friendly
verbosity: concise
language: es
languages_supported: [es, en]
emoji_style: minimal
prefix: "🤖"
error_style: helpful
templates:
greeting: "Hola, soy $DISPLAYNAME. ¿En qué puedo ayudarte?"
unknown_command: "No reconozco ese comando. Escríbeme directamente."
permission_denied: "No tengo permiso para hacer eso."
error: "Algo salió mal: {{.Error}}"
success: "{{.Summary}}"
busy: "Procesando, dame un momento..."
behavior:
proactive: false
ask_confirmation: false
show_reasoning: false
thread_replies: true
typing_indicator: true
acknowledge_receipt: false
# ============================================
# LLM
# ============================================
llm:
primary:
provider: openai
model: gpt-4o
api_key_env: OPENAI_API_KEY
base_url: ""
max_tokens: 4096
temperature: 0.7
fallback:
provider: ""
model: ""
api_key_env: ""
base_url: ""
max_tokens: 0
temperature: 0
reasoning:
system_prompt_file: "prompts/system.md"
context_window: 16384
memory_messages: 20
tool_use:
enabled: false
max_iterations: 3
parallel_calls: false
rate_limit:
requests_per_minute: 30
tokens_per_minute: 100000
concurrent_requests: 3
# ============================================
# TOOLS — ajustar según necesidades del agente
# ============================================
tools:
ssh:
enabled: false
allowed_targets: []
forbidden_commands: []
timeout: 0s
max_concurrent: 0
require_confirmation: []
http:
enabled: false
allowed_domains: []
timeout: 0s
max_retries: 0
scripts:
enabled: false
scripts_dir: ""
allowed: []
timeout: 0s
sandbox: false
file_ops:
enabled: false
allowed_paths: []
read_only: true
mcp:
enabled: false
servers: []
expose:
port: 0
tools: []
# ============================================
# MATRIX
# ============================================
matrix:
homeserver: "${MATRIX_HOMESERVER}"
user_id: "@$ID:${MATRIX_SERVER_NAME}"
access_token_env: MATRIX_TOKEN_${NORM}
device_id: ""
encryption:
enabled: true
store_path: "./agents/${ID}/data/crypto/"
pickle_key_env: PICKLE_KEY_${NORM}
trust_mode: tofu
recovery_key_env: SSSS_RECOVERY_KEY_${NORM}
rooms:
listen: []
respond: []
admin: []
filters:
command_prefix: "!"
mention_respond: true
dm_respond: true
ignore_bots: true
ignore_users: []
min_power_level: 0
threads:
enabled: true # responder en threads cuando el mensaje viene de un thread
auto_thread: false # true para crear thread automático por cada conversación nueva
# ============================================
# INTER-AGENTES
# ============================================
agents:
peers: []
delegation:
enabled: false
can_delegate_to: []
can_receive_from: []
max_delegation_depth: 1
timeout: 30s
protocol:
format: json
channel: matrix
heartbeat_interval: 60s
# ============================================
# SSH
# ============================================
ssh:
defaults:
user: ""
port: 22
key_file_env: ""
known_hosts: ""
keepalive_interval: 0s
timeout: 0s
targets: {}
# ============================================
# SEGURIDAD
# ============================================
security:
roles:
admin:
users: ["@admin:\${MATRIX_SERVER_NAME}"]
actions: ["*"]
user:
users: ["*"]
actions: ["help"]
audit:
enabled: false
log_file: "./data/audit.log"
log_to_room: ""
include: []
secrets:
provider: env
# ============================================
# SCHEDULING
# ============================================
schedules: []
# ============================================
# OBSERVABILIDAD
# ============================================
observability:
logging:
level: info
format: json
output: stdout
file: "./data/$ID.log"
metrics:
enabled: false
port: 0
path: /metrics
export: prometheus
health:
enabled: true
port: 0
path: /healthz
tracing:
enabled: false
provider: ""
endpoint: ""
# ============================================
# RESILIENCIA
# ============================================
resilience:
circuit_breaker:
failure_threshold: 5
timeout: 30s
half_open_max: 2
retry:
max_attempts: 2
backoff: exponential
initial_delay: 1s
max_delay: 10s
shutdown:
timeout: 10s
drain_messages: true
save_state: false
state_file: ""
queue:
enabled: true
max_size: 50
priority_users: ["@admin:\${MATRIX_SERVER_NAME}"]
# ============================================
# ALMACENAMIENTO
# ============================================
storage:
state:
backend: sqlite
path: "./data/$ID.db"
cache:
enabled: true
backend: memory
ttl: 5m
max_entries: 200
history:
backend: sqlite
path: "./data/history.db"
retention: 168h
YAML
# ── Copiar agent.go desde template y personalizar ────────────────────────
cp "$TEMPLATE/agent.go" "$DIR/agent.go"
sed -i "s/_template/$PACKAGE/g" "$DIR/agent.go"
sed -i "s/Package _template/Package $PACKAGE/g" "$DIR/agent.go"
sed -i "s/AGENT_ID_PLACEHOLDER/$ID/g" "$DIR/agent.go"
ok "agent.go creado desde template"
# ── Copiar prompts/system.md desde template y personalizar ───────────────
cp "$TEMPLATE/prompts/system.md" "$DIR/prompts/system.md"
sed -i "s/Template Agent/$DISPLAYNAME/g" "$DIR/prompts/system.md"
ok "prompts/system.md creado desde template"
ok "Scaffold creado en $DIR/"
echo ""
# ── Actualizar cmd/launcher/main.go — añadir blank import ────────────────
LAUNCHER="cmd/launcher/main.go"
BLANK_IMPORT="_ \"github.com/enmanuel/agents/agents/$ID\""
if grep -q "agents/$ID\"" "$LAUNCHER" 2>/dev/null; then
warn "$ID ya tiene blank import en $LAUNCHER — saltando"
else
TAB=$'\t'
IMPORT_LINE="${TAB}${BLANK_IMPORT}"
# Insertar blank import después del último blank import de agents/
if awk -v new_import="$IMPORT_LINE" '
{
lines[NR] = $0
if ($0 ~ /_ "github\.com\/enmanuel\/agents\/agents\//)
last_import = NR
}
END {
if (!last_import) { for (i=1;i<=NR;i++) print lines[i]; exit 1 }
for (i = 1; i <= NR; i++) {
print lines[i]
if (i == last_import) print new_import
}
}
' "$LAUNCHER" > /tmp/_launcher_tmp; then
mv /tmp/_launcher_tmp "$LAUNCHER"
ok "Blank import añadido en $LAUNCHER"
else
warn "No se pudo insertar el blank import — añádelo manualmente:"
echo -e " ${GRN}${IMPORT_LINE}${RST}"
fi
fi
echo ""
echo -e "${YLW}Quedan 3 pasos:${RST}"
echo ""
echo -e " ${DIM}1. ./dev-scripts/agent/register.sh $ID \"$DISPLAYNAME\"${RST} # registra en Matrix + genera token, password, pickle key"
echo -e " ${DIM}2. ./dev-scripts/agent/verify.sh $ID${RST} # genera cross-signing keys + verifica device"
echo -e " ${DIM}3. ./dev-scripts/server/start.sh $ID${RST} # arranca el agente"
echo ""
+85
View File
@@ -0,0 +1,85 @@
#!/usr/bin/env bash
# register.sh — registra un nuevo bot en el servidor Matrix via Synapse admin API
#
# Uso:
# ./dev-scripts/agent/register.sh <username> [displayname]
#
# Ejemplos:
# ./dev-scripts/agent/register.sh assistant-bot "Assistant"
# ./dev-scripts/agent/register.sh devops-bot "DevOps Agent"
#
# Genera y guarda en .env:
# MATRIX_TOKEN_<NORM>=... (access token)
# MATRIX_PASSWORD_<NORM>=... (password para UIA)
# PICKLE_KEY_<NORM>=... (E2EE crypto store key)
#
# Requiere en .env:
# MATRIX_ADMIN_TOKEN=syt_...
# MATRIX_HOMESERVER=https://...
source "$(dirname "$0")/../_common.sh"
load_env
need_arg "${1:-}"
USERNAME="$1"
DISPLAYNAME="${2:-$USERNAME}"
NORM="$(normalize_id "$USERNAME")"
ENV_VAR="MATRIX_TOKEN_${NORM}"
[[ -n "${MATRIX_ADMIN_TOKEN:-}" ]] || fail "MATRIX_ADMIN_TOKEN no está en .env"
[[ -n "${MATRIX_HOMESERVER:-}" ]] || fail "MATRIX_HOMESERVER no está en .env"
info "Registrando @${USERNAME}:${MATRIX_SERVER_NAME:-$MATRIX_HOMESERVER}..."
dim " Env var prefix: ${NORM}"
echo ""
# Ejecutar cmd/register y capturar su output completo
OUTPUT=$("$GO" run ./cmd/register \
--homeserver "$MATRIX_HOMESERVER" \
--username "$USERNAME" \
--displayname "$DISPLAYNAME" \
--env-var "$ENV_VAR" 2>&1) || fail "cmd/register falló:\n$OUTPUT"
echo "$OUTPUT"
echo ""
# ── Parsear y guardar cada variable en .env ──────────────────────────────
save_env_var() {
local key="$1" value="$2"
[[ -n "$value" ]] || return
# Quote values with spaces
if [[ "$value" == *" "* ]]; then
value="\"${value}\""
fi
if grep -q "^${key}=" .env; then
awk -v key="$key" -v val="$value" \
'index($0, key "=") == 1 { print key "=" val; next } { print }' \
.env > /tmp/_env_tmp && mv /tmp/_env_tmp .env
ok "$key actualizado en .env"
else
printf '%s=%s\n' "$key" "$value" >> .env
ok "$key añadido a .env"
fi
}
# Extract parseable lines from output
TOKEN=$(echo "$OUTPUT" | grep "^${ENV_VAR}=" | cut -d= -f2-)
PASSWORD=$(echo "$OUTPUT" | grep "^MATRIX_PASSWORD_${NORM}=" | cut -d= -f2-)
PICKLE_KEY=$(echo "$OUTPUT" | grep "^PICKLE_KEY_${NORM}=" | cut -d= -f2-)
[[ -n "$TOKEN" ]] || fail "No se encontró '${ENV_VAR}=' en el output"
save_env_var "$ENV_VAR" "$TOKEN"
save_env_var "MATRIX_PASSWORD_${NORM}" "$PASSWORD"
save_env_var "PICKLE_KEY_${NORM}" "$PICKLE_KEY"
echo ""
echo -e "${YLW}Siguientes pasos:${RST}"
echo ""
echo -e " ${DIM}1. ./dev-scripts/agent/verify.sh $USERNAME${RST} # genera cross-signing keys E2EE"
echo -e " ${DIM}2. ./dev-scripts/server/start.sh $USERNAME${RST} # arranca el agente"
echo ""
+30
View File
@@ -0,0 +1,30 @@
#!/usr/bin/env bash
# remove.sh — deshabilita un agente (enabled: false). No borra datos.
#
# Uso:
# ./dev-scripts/agent/remove.sh assistant-bot
source "$(dirname "$0")/../_common.sh"
need_arg "${1:-}"
TARGET="$1"
found=false
while IFS='|' read -r id _version _enabled _desc cfg; do
[[ "$id" != "$TARGET" ]] && continue
found=true
# Marcar como disabled en el config
if grep -q 'enabled: true' "$cfg"; then
sed -i 's/enabled: true/enabled: false/' "$cfg"
ok "$id marcado como disabled en $cfg"
info "Reinicia el launcher para aplicar: ./dev-scripts/server/server.sh restart"
else
warn "$id ya estaba marcado como disabled"
fi
dim " Datos preservados en agents/$id/data/"
done < <(list_agents_raw)
"$found" || fail "Agente '$TARGET' no encontrado"
+68
View File
@@ -0,0 +1,68 @@
#!/usr/bin/env bash
# reset-password.sh — resetea la contraseña de un bot existente y la escribe en .env
#
# A diferencia de register.sh, este script NO crea una nueva sesión ni cambia el device ID.
# El token de acceso actual sigue siendo válido.
# Útil para añadir MATRIX_PASSWORD_X a .env en bots ya registrados.
#
# Uso:
# ./dev-scripts/agent/reset-password.sh <agent-id>
#
# Ejemplo:
# ./dev-scripts/agent/reset-password.sh assistant-bot
#
# Requiere en .env:
# MATRIX_ADMIN_TOKEN=syt_...
# MATRIX_HOMESERVER=https://...
# MATRIX_SERVER_NAME=...
source "$(dirname "$0")/../_common.sh"
load_env
need_arg "${1:-}"
ID="$1"
USERNAME="$ID"
PASSWORD_ENV_VAR="MATRIX_PASSWORD_$(echo "$ID" | tr '[:lower:]-' '[:upper:]_' | sed 's/_BOT$//')"
[[ -n "${MATRIX_ADMIN_TOKEN:-}" ]] || fail "MATRIX_ADMIN_TOKEN no está en .env"
[[ -n "${MATRIX_HOMESERVER:-}" ]] || fail "MATRIX_HOMESERVER no está en .env"
[[ -n "${MATRIX_SERVER_NAME:-}" ]] || fail "MATRIX_SERVER_NAME no está en .env"
USER_ID="@${USERNAME}:${MATRIX_SERVER_NAME}"
# Generar nueva contraseña aleatoria
NEW_PASSWORD=$(head -c 24 /dev/urandom | od -A n -t x1 | tr -d ' \n')
info "Reseteando contraseña de ${USER_ID}..."
info "(El token de acceso actual NO cambia — solo la contraseña)"
# Synapse admin API: reset password sin cerrar sesiones existentes
RESPONSE=$(curl -s -w "\n%{http_code}" -X POST \
"${MATRIX_HOMESERVER}/_synapse/admin/v1/reset_password/${USER_ID}" \
-H "Authorization: Bearer ${MATRIX_ADMIN_TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"new_password\": \"${NEW_PASSWORD}\", \"logout_devices\": false}")
HTTP_CODE=$(echo "$RESPONSE" | tail -1)
BODY=$(echo "$RESPONSE" | head -1)
if [[ "$HTTP_CODE" != "200" ]]; then
fail "Admin API devolvió HTTP $HTTP_CODE: $BODY"
fi
# Escribir en .env
if grep -q "^${PASSWORD_ENV_VAR}=" .env; then
awk -v key="$PASSWORD_ENV_VAR" -v val="$NEW_PASSWORD" \
'index($0, key "=") == 1 { print key "=" val; next } { print }' \
.env > /tmp/_env_tmp && mv /tmp/_env_tmp .env
ok "$PASSWORD_ENV_VAR actualizado en .env"
else
printf '\n%s=%s\n' "$PASSWORD_ENV_VAR" "$NEW_PASSWORD" >> .env
ok "$PASSWORD_ENV_VAR añadido a .env"
fi
echo ""
ok "Contraseña reseteada para ${USER_ID}"
dim " El bot usará esta contraseña para cross-signing bootstrap en el próximo arranque."
dim " Reinícialo con: ./dev-scripts/server/stop.sh $ID && ./dev-scripts/server/start.sh $ID"
+167
View File
@@ -0,0 +1,167 @@
#!/usr/bin/env bash
# verify.sh — (re)verifica dispositivos E2EE de agentes Matrix
#
# Genera/sube cross-signing keys y firma el device de cada agente.
# Usa el MISMO crypto store que el agente para que las keys queden disponibles.
#
# Uso:
# ./dev-scripts/agent/verify.sh # verifica todos los habilitados con E2EE
# ./dev-scripts/agent/verify.sh assistant-bot # verifica uno específico
source "$(dirname "$0")/../_common.sh"
load_env
TARGET="${1:-}"
# ── YAML helpers (simple grep-based, no deps) ────────────────────────────
yaml_val() {
# Extract a simple YAML value: yaml_val file "key"
# Handles both quoted and unquoted values.
local file="$1" key="$2"
grep -m1 "^\s*${key}:" "$file" 2>/dev/null \
| sed 's/^[^:]*:\s*//' \
| tr -d '"' \
| tr -d "'" \
| xargs
}
# ── Verify a single agent ────────────────────────────────────────────────
verify_agent() {
local cfg="$1"
local agent_id; agent_id="$(yaml_val "$cfg" "id")"
local agent_dir; agent_dir="$(dirname "$cfg")"
# Check E2EE is enabled
local enc_enabled; enc_enabled="$(yaml_val "$cfg" "enabled")"
# The first "enabled" is agent.enabled; we need encryption.enabled specifically
enc_enabled="$(grep -A5 'encryption:' "$cfg" | grep -m1 'enabled:' | awk '{print $2}')"
if [[ "$enc_enabled" != "true" ]]; then
dim " $agent_id — E2EE deshabilitado, saltando"
return 0
fi
# Extract config values
local user_id; user_id="$(yaml_val "$cfg" "user_id")"
local username; username="$(echo "$user_id" | sed 's/@\([^:]*\):.*/\1/')"
local token_env; token_env="$(yaml_val "$cfg" "access_token_env")"
local pickle_env; pickle_env="$(yaml_val "$cfg" "pickle_key_env")"
local recovery_env; recovery_env="$(yaml_val "$cfg" "recovery_key_env")"
local store_path; store_path="$(grep -A5 'encryption:' "$cfg" | grep -m1 'store_path:' | sed 's/^[^:]*:\s*//' | tr -d '"' | xargs)"
local token="${!token_env:-}"
local pickle_key="${!pickle_env:-}"
# Find password — convention: MATRIX_PASSWORD_<NORMALIZED>
local norm; norm="$(echo "$username" | tr '-' '_' | tr '[:lower:]' '[:upper:]')"
local pass_env="MATRIX_PASSWORD_${norm}"
local password="${!pass_env:-}"
# Validate required values
if [[ -z "$token" ]]; then
fail " $agent_id$token_env no está en .env"
return 1
fi
if [[ -z "$password" ]]; then
warn " $agent_id$pass_env no está en .env, intentando sin password..."
fi
info "$agent_id — verificando device..."
dim " user: $username"
dim " store: $store_path"
dim " pickle_env: $pickle_env"
dim " token_env: $token_env"
# Stop agent if running (crypto store can't be shared)
local was_running=false
if is_running "$agent_id"; then
was_running=true
info " Deteniendo $agent_id antes de verificar..."
"$REPO_ROOT/dev-scripts/server/stop.sh" "$agent_id"
sleep 1
fi
# Build verify command
local verify_bin="$REPO_ROOT/bin/verify"
if [[ ! -x "$verify_bin" ]] || [[ "$(find ./cmd/verify -newer "$verify_bin" 2>/dev/null | head -1)" ]]; then
info " Compilando cmd/verify..."
mkdir -p "$(dirname "$verify_bin")"
"$GO" build -tags goolm -o "$verify_bin" ./cmd/verify || {
fail " No se pudo compilar cmd/verify"
return 1
}
fi
# Run verification
local verify_args=(
--homeserver "$MATRIX_HOMESERVER"
--username "$username"
--token "$token"
--store "$store_path"
)
if [[ -n "$password" ]]; then
verify_args+=(--password "$password")
fi
if [[ -n "$pickle_key" ]]; then
verify_args+=(--pickle-key "$pickle_key")
fi
local output
if output=$("$verify_bin" "${verify_args[@]}" 2>&1); then
ok "$agent_id — verificación exitosa"
# Extract recovery key from output if present
local new_rk
new_rk="$(echo "$output" | grep "^SSSS_RECOVERY_KEY_" | cut -d= -f2-)"
if [[ -n "$new_rk" && -n "$recovery_env" ]]; then
# Update .env with new recovery key (quoted — keys contain spaces)
local quoted_rk="\"${new_rk}\""
if grep -q "^${recovery_env}=" "$REPO_ROOT/.env"; then
sed -i "s|^${recovery_env}=.*|${recovery_env}=${quoted_rk}|" "$REPO_ROOT/.env"
ok " Recovery key actualizada en .env ($recovery_env)"
else
echo "${recovery_env}=${quoted_rk}" >> "$REPO_ROOT/.env"
ok " Recovery key añadida a .env ($recovery_env)"
fi
fi
else
warn "$agent_id — verify output:"
echo "$output"
# If it says keys already exist, that's usually fine
if echo "$output" | grep -q "signed with cross-signing key"; then
ok "$agent_id — device firmado con keys existentes"
else
warn "$agent_id — puede necesitar atención manual"
fi
fi
echo "$output" | sed 's/^/ /'
# Restart agent if it was running
if [[ "$was_running" == "true" ]]; then
info " Reiniciando $agent_id..."
"$REPO_ROOT/dev-scripts/server/start.sh" "$agent_id"
fi
echo
}
# ── Main ──────────────────────────────────────────────────────────────────
echo
info "Verificación E2EE de agentes Matrix"
echo
if [[ -n "$TARGET" ]]; then
cfg="$(config_path_for "$TARGET")"
[[ -n "$cfg" ]] || fail "Agente '$TARGET' no encontrado"
verify_agent "$cfg"
else
while IFS='|' read -r id version enabled desc cfg; do
[[ "$enabled" == "true" ]] || continue
verify_agent "$cfg"
done < <(list_agents_raw)
fi
ok "Verificación completada"
+45
View File
@@ -0,0 +1,45 @@
# dev-scripts/cron/ — Gestión de automatizaciones cron
Scripts para crear, listar y aplicar automatizaciones del catálogo `crons/`.
## Scripts
### `new.sh` — Scaffolder interactivo
Crea una nueva automatización en `crons/<nombre>/`:
```bash
./dev-scripts/cron/new.sh
```
Pregunta: nombre, descripción, tipo de acción (`send_message` o `llm_prompt`) y cron expression.
Crea `schedule.yaml` y el archivo de prompt/mensaje vacío.
Imprime el bloque YAML listo para añadir a `config.yaml`.
### `list.sh` — Listar automatizaciones
Lista todas las automatizaciones del catálogo con nombre, tipo, cron y descripción:
```bash
./dev-scripts/cron/list.sh
```
### `apply.sh` — Aplicar a un agente
Añade una automatización al `config.yaml` de un agente:
```bash
./dev-scripts/cron/apply.sh <nombre> <agent-id>
# Ejemplo:
./dev-scripts/cron/apply.sh good-morning assistant-bot
```
Usa `yq` si está disponible para parchear el YAML directamente.
Si `yq` no está instalado, imprime el bloque YAML para copiar a mano.
Recuerda editar `output_room` en `config.yaml` con la sala real del agente.
## Catálogo
Las automatizaciones viven en `crons/`. Ver `crons/README.md` para la documentación completa.
+78
View File
@@ -0,0 +1,78 @@
#!/usr/bin/env bash
# apply.sh <name> <agent-id>
# Añade la automatización <name> al config.yaml del agente <agent-id>.
# Usa yq si está disponible; en caso contrario imprime el bloque YAML para copiar a mano.
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
if [[ $# -ne 2 ]]; then
echo "Uso: $0 <nombre-automatizacion> <agent-id>" >&2
echo "Ejemplo: $0 good-morning assistant-bot" >&2
exit 1
fi
NAME="$1"
AGENT_ID="$2"
SCHEDULE_FILE="$REPO_ROOT/crons/$NAME/schedule.yaml"
AGENT_CONFIG="$REPO_ROOT/agents/$AGENT_ID/config.yaml"
if [[ ! -f "$SCHEDULE_FILE" ]]; then
echo "Error: no existe crons/$NAME/schedule.yaml" >&2
echo "Usa ./dev-scripts/cron/list.sh para ver las automatizaciones disponibles." >&2
exit 1
fi
if [[ ! -f "$AGENT_CONFIG" ]]; then
echo "Error: no existe agents/$AGENT_ID/config.yaml" >&2
exit 1
fi
# Parse schedule.yaml fields
kind=""
template=""
cron_expr=""
while IFS= read -r line; do
case "$line" in
" kind:"*) kind="${line#*kind:}"; kind="${kind// /}" ;;
" template:"*) template="${line#*template:}"; template="${template# }" ;;
default_cron:*) cron_expr="${line#default_cron:}"; cron_expr="${cron_expr# }"; cron_expr="${cron_expr//\"/}" ;;
esac
done < "$SCHEDULE_FILE"
if [[ -z "$kind" || -z "$template" || -z "$cron_expr" ]]; then
echo "Error: schedule.yaml incompleto (falta kind, template o default_cron)." >&2
exit 1
fi
# Build YAML block
YAML_BLOCK=" - name: $NAME
cron: \"$cron_expr\"
output_room: \"\" # TODO: reemplaza con la sala real del agente
action:
kind: $kind
template: \"$template\""
# Try yq first
if command -v yq &>/dev/null; then
# Check if schedules key already has this entry
existing=$(yq ".schedules // [] | .[] | select(.name == \"$NAME\") | .name" "$AGENT_CONFIG" 2>/dev/null || true)
if [[ -n "$existing" ]]; then
echo "Advertencia: el agente $AGENT_ID ya tiene un schedule llamado '$NAME'. No se añade de nuevo."
exit 0
fi
# Append using yq
yq -i ".schedules += [{\"name\": \"$NAME\", \"cron\": \"$cron_expr\", \"output_room\": \"\", \"action\": {\"kind\": \"$kind\", \"template\": \"$template\"}}]" "$AGENT_CONFIG"
echo "✓ Añadido schedule '$NAME' a agents/$AGENT_ID/config.yaml"
echo "→ Edita output_room en agents/$AGENT_ID/config.yaml para apuntar a la sala correcta."
else
echo "yq no está disponible. Añade manualmente el siguiente bloque a agents/$AGENT_ID/config.yaml:"
echo ""
echo "schedules:"
echo "$YAML_BLOCK"
echo ""
echo "→ Edita output_room para apuntar a la sala correcta del agente."
fi
+47
View File
@@ -0,0 +1,47 @@
#!/usr/bin/env bash
# list.sh — Lista todas las automatizaciones del catálogo crons/ con nombre, tipo y descripción.
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
CRONS_DIR="$REPO_ROOT/crons"
if [[ ! -d "$CRONS_DIR" ]]; then
echo "No se encontró el directorio crons/." >&2
exit 1
fi
# Collect entries: name, kind, default_cron, description
entries=()
while IFS= read -r -d '' schedule_file; do
name=""
description=""
kind=""
cron_expr=""
while IFS= read -r line; do
case "$line" in
name:*) name="${line#name:}"; name="${name// /}" ;;
description:*) description="${line#description:}"; description="${description# }"; description="${description#\"}"; description="${description%\"}" ;;
" kind:"*) kind="${line#*kind:}"; kind="${kind// /}" ;;
default_cron:*) cron_expr="${line#default_cron:}"; cron_expr="${cron_expr# }"; cron_expr="${cron_expr//\"/}" ;;
esac
done < "$schedule_file"
if [[ -n "$name" ]]; then
entries+=("$name|$kind|$cron_expr|$description")
fi
done < <(find "$CRONS_DIR" -name "schedule.yaml" -print0 | sort -z)
if [[ ${#entries[@]} -eq 0 ]]; then
echo "No hay automatizaciones en crons/."
exit 0
fi
# Print header
printf "%-22s %-15s %-15s %s\n" "NOMBRE" "TIPO" "CRON" "DESCRIPCIÓN"
printf "%-22s %-15s %-15s %s\n" "------" "----" "----" "-----------"
for entry in "${entries[@]}"; do
IFS='|' read -r name kind cron_expr description <<< "$entry"
printf "%-22s %-15s %-15s %s\n" "$name" "$kind" "$cron_expr" "$description"
done
+94
View File
@@ -0,0 +1,94 @@
#!/usr/bin/env bash
# new.sh — Scaffolder interactivo para automatizaciones cron
# Crea crons/<name>/schedule.yaml y el archivo de prompt/mensaje vacío.
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
echo "=== Nueva automatización cron ==="
echo ""
# Nombre
read -rp "Nombre de la automatización (ej: weekly-report): " NAME
NAME="${NAME// /-}"
NAME="${NAME,,}" # lowercase
if [[ -z "$NAME" ]]; then
echo "Error: el nombre no puede estar vacío." >&2
exit 1
fi
if [[ -d "$REPO_ROOT/crons/$NAME" ]]; then
echo "Error: ya existe crons/$NAME/" >&2
exit 1
fi
# Descripción
read -rp "Descripción breve: " DESCRIPTION
if [[ -z "$DESCRIPTION" ]]; then
echo "Error: la descripción no puede estar vacía." >&2
exit 1
fi
# Tipo de acción
echo ""
echo "Tipo de acción:"
echo " 1) send_message — envía un mensaje estático o plantilla"
echo " 2) llm_prompt — llama al LLM con un prompt y envía la respuesta"
read -rp "Selecciona [1/2]: " ACTION_TYPE_NUM
case "$ACTION_TYPE_NUM" in
1) ACTION_KIND="send_message"; PROMPT_FILE="message.md" ;;
2) ACTION_KIND="llm_prompt"; PROMPT_FILE="prompt.md" ;;
*)
echo "Error: selección inválida. Usa 1 o 2." >&2
exit 1
;;
esac
# Cron expression
DEFAULT_CRON="0 9 * * *"
read -rp "Cron expression [default: $DEFAULT_CRON]: " CRON_EXPR
CRON_EXPR="${CRON_EXPR:-$DEFAULT_CRON}"
# Crear estructura
CRON_DIR="$REPO_ROOT/crons/$NAME"
PROMPTS_DIR="$CRON_DIR/prompts"
mkdir -p "$PROMPTS_DIR"
# schedule.yaml
cat > "$CRON_DIR/schedule.yaml" <<EOF
# Automatización: $NAME
name: $NAME
description: "$DESCRIPTION"
# Cron por defecto
default_cron: "$CRON_EXPR"
# Acción
action:
kind: $ACTION_KIND
# Relativo a la raíz del proyecto
template: crons/$NAME/prompts/$PROMPT_FILE
# Sala de salida por defecto (vacío = el agente debe configurar output_room)
default_output_room: ""
EOF
# Archivo de prompt/mensaje
touch "$PROMPTS_DIR/$PROMPT_FILE"
echo ""
echo "✓ Creado: crons/$NAME/schedule.yaml"
echo "✓ Creado: crons/$NAME/prompts/$PROMPT_FILE"
echo ""
echo "Edita crons/$NAME/prompts/$PROMPT_FILE con el contenido deseado."
echo ""
echo "Añade esto a agents/<id>/config.yaml:"
echo ""
echo " schedules:"
echo " - name: $NAME"
echo " cron: \"$CRON_EXPR\""
echo " output_room: \"!TUROOM:matrix-af2f3d.organic-machine.com\""
echo " action:"
echo " kind: $ACTION_KIND"
echo " template: \"crons/$NAME/prompts/$PROMPT_FILE\""
echo ""
echo "O usa: ./dev-scripts/cron/apply.sh $NAME <agent-id>"
+42
View File
@@ -0,0 +1,42 @@
#!/usr/bin/env bash
# install.sh — instalar dependencias para E2E tests
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
E2E_DIR="$REPO_ROOT/e2e"
echo "=== Instalacion de E2E tests ==="
# 1. Verificar Node.js
if ! command -v node &>/dev/null; then
echo "ERROR: Node.js no encontrado."
echo "Instalar Node.js v18+ con:"
echo " curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -"
echo " sudo apt-get install -y nodejs"
exit 1
fi
NODE_VERSION=$(node -v | sed 's/v//' | cut -d. -f1)
if [ "$NODE_VERSION" -lt 18 ]; then
echo "ERROR: Se requiere Node.js v18+, encontrado v$(node -v)"
exit 1
fi
echo "Node.js $(node -v) OK"
# 2. Instalar dependencias del proyecto
echo "Instalando dependencias npm..."
cd "$E2E_DIR"
npm ci
# 3. Instalar Chromium para Playwright
echo "Instalando Chromium para Playwright..."
npx playwright install chromium
# 4. Instalar dependencias del sistema para Playwright
echo "Instalando dependencias del sistema (requiere sudo)..."
sudo npx playwright install-deps chromium
echo ""
echo "=== Instalacion completa ==="
echo "Siguiente paso: copiar e2e/.env.example a e2e/.env y configurar credenciales"
+134
View File
@@ -0,0 +1,134 @@
#!/usr/bin/env bash
# run.sh — ejecutar E2E tests con Playwright
#
# Uso:
# ./dev-scripts/e2e/run.sh # headless (default)
# ./dev-scripts/e2e/run.sh --headed # con browser visible (requiere DISPLAY)
#
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
E2E_DIR="$REPO_ROOT/e2e"
ELEMENT_SCRIPT="$E2E_DIR/scripts/setup-element.sh"
PS_SCRIPT="$REPO_ROOT/dev-scripts/server/ps.sh"
HEADED=false
EXTRA_ARGS=()
for arg in "$@"; do
case "$arg" in
--headed)
HEADED=true
;;
*)
EXTRA_ARGS+=("$arg")
;;
esac
done
# --- Verificaciones previas ---
# 1. Verificar dependencias instaladas
if [ ! -d "$E2E_DIR/node_modules" ]; then
echo "ERROR: node_modules no encontrado. Ejecutar primero:"
echo " ./dev-scripts/e2e/install.sh"
exit 1
fi
# 2. Verificar .env
if [ ! -f "$E2E_DIR/.env" ]; then
echo "ERROR: e2e/.env no encontrado. Crear desde el template:"
echo " cp e2e/.env.example e2e/.env"
echo " # editar e2e/.env con credenciales"
exit 1
fi
# 3. Verificar que los agentes estan corriendo
echo "=== Verificando agentes ==="
if [ -x "$PS_SCRIPT" ]; then
if ! "$PS_SCRIPT" 2>/dev/null | grep -q "running"; then
echo "WARN: el launcher no parece estar corriendo."
echo " Iniciar con: ./dev-scripts/server/start.sh"
echo " Continuando de todas formas..."
else
echo "Launcher corriendo OK"
fi
else
echo "WARN: no se encontro ps.sh, no se puede verificar el estado de los agentes"
fi
# --- Element Web ---
echo ""
echo "=== Element Web ==="
ELEMENT_STARTED_BY_US=false
if [ -x "$ELEMENT_SCRIPT" ]; then
if "$ELEMENT_SCRIPT" status 2>/dev/null | grep -q "corriendo\|running\|listening"; then
echo "Element Web ya esta corriendo"
else
echo "Levantando Element Web..."
"$ELEMENT_SCRIPT" start
ELEMENT_STARTED_BY_US=true
# Esperar a que el servidor este listo
sleep 2
fi
else
echo "WARN: setup-element.sh no encontrado. Asegurarse de que Element Web esta corriendo."
fi
# --- Ejecutar tests ---
echo ""
echo "=== Ejecutando E2E tests ==="
PLAYWRIGHT_ARGS=()
if [ "$HEADED" = true ]; then
if [ -z "${DISPLAY:-}" ] && [ -z "${WAYLAND_DISPLAY:-}" ]; then
echo "WARN: --headed solicitado pero no se detecta DISPLAY. Ejecutando headless."
else
PLAYWRIGHT_ARGS+=("--headed")
fi
fi
# Agregar argumentos extra del usuario
if [ ${#EXTRA_ARGS[@]} -gt 0 ]; then
PLAYWRIGHT_ARGS+=("${EXTRA_ARGS[@]}")
fi
EXIT_CODE=0
cd "$E2E_DIR"
npx playwright test "${PLAYWRIGHT_ARGS[@]}" || EXIT_CODE=$?
# Generar reporte HTML si hay fallos
if [ "$EXIT_CODE" -ne 0 ]; then
echo ""
echo "=== Generando reporte HTML ==="
npx playwright show-report --host 0.0.0.0 --port 0 2>/dev/null &
REPORT_PID=$!
sleep 1
kill "$REPORT_PID" 2>/dev/null || true
echo "Reporte disponible en: $E2E_DIR/playwright-report/"
echo " Para verlo: cd e2e && npx playwright show-report"
fi
# --- Teardown ---
if [ "$ELEMENT_STARTED_BY_US" = true ]; then
echo ""
echo "=== Deteniendo Element Web ==="
"$ELEMENT_SCRIPT" stop 2>/dev/null || true
fi
# --- Resultado ---
echo ""
if [ "$EXIT_CODE" -eq 0 ]; then
echo "=== Todos los tests pasaron ==="
else
echo "=== Algunos tests fallaron (exit code: $EXIT_CODE) ==="
echo "Ver screenshots en: $E2E_DIR/test-results/"
fi
exit "$EXIT_CODE"
+71
View File
@@ -0,0 +1,71 @@
# dev-scripts/server
Scripts para gestionar el ciclo de vida del launcher unificado que ejecuta todos los agentes habilitados.
## Scripts
### start.sh
Inicia el launcher unificado. Compila el binario y ejecuta los tests si es necesario antes de arrancar. Reporta el número de agentes habilitados.
```bash
./dev-scripts/server/start.sh
```
### stop.sh
Detiene el launcher de forma ordenada. Envía SIGTERM, espera 5 segundos, y si no termina usa SIGKILL.
```bash
./dev-scripts/server/stop.sh
```
### restart.sh
Reinicia el launcher (ejecuta stop.sh seguido de start.sh).
```bash
./dev-scripts/server/restart.sh
```
### ps.sh
Muestra el estado del proceso del launcher con métricas detalladas: PID, uptime, uso de memoria, CPU y tamaño de logs.
```bash
./dev-scripts/server/ps.sh
```
### logs.sh
Sigue los logs del launcher en tiempo real (tail -f). Acepta un argumento opcional para el número de líneas iniciales.
```bash
./dev-scripts/server/logs.sh # últimas líneas por defecto
./dev-scripts/server/logs.sh 50 # últimas 50 líneas
```
### dashboard.sh
Abre la TUI interactiva (bubbletea) para gestión visual de bots. Permite ver estado, iniciar/detener agentes y ver logs desde una interfaz de terminal.
```bash
./dev-scripts/server/dashboard.sh
```
### server.sh
CLI unificado que enruta comandos a los scripts individuales. Útil como punto de entrada único.
```bash
./dev-scripts/server/server.sh start # → start.sh
./dev-scripts/server/server.sh stop # → stop.sh
./dev-scripts/server/server.sh restart # → restart.sh
./dev-scripts/server/server.sh status # resumen general del servidor
./dev-scripts/server/server.sh ps # → ps.sh
./dev-scripts/server/server.sh logs # → logs.sh
./dev-scripts/server/server.sh kill # SIGKILL forzado (emergencia)
./dev-scripts/server/server.sh enable <id> # habilita un agente
./dev-scripts/server/server.sh disable <id> # deshabilita un agente
./dev-scripts/server/server.sh dashboard # → dashboard.sh
```
+8
View File
@@ -0,0 +1,8 @@
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")/../.."
echo "Building dashboard..."
/usr/local/go/bin/go build -tags goolm -o bin/dashboard ./cmd/dashboard/
echo "Done → bin/dashboard"
+7
View File
@@ -0,0 +1,7 @@
#!/usr/bin/env bash
# dashboard.sh — lanza el TUI interactivo de gestión de bots
# Uso: ./dev-scripts/server/dashboard.sh
source "$(dirname "$0")/../_common.sh"
exec "$GO" run ./cmd/dashboard "$@"
+55
View File
@@ -0,0 +1,55 @@
#!/usr/bin/env bash
# logs.sh — sigue los logs de agentes (logs/<agent>/YYYY-MM-DD.jsonl)
#
# Uso:
# ./dev-scripts/server/logs.sh # tail -f de todos los agentes (hoy)
# ./dev-scripts/server/logs.sh assistant-bot # tail -f de un agente específico
# ./dev-scripts/server/logs.sh assistant-bot 100 # últimas 100 líneas
source "$(dirname "$0")/../_common.sh"
LOG_DIR="logs"
AGENT_ID="${1:-}"
LINES="${2:-50}"
if [[ ! -d "$LOG_DIR" ]]; then
fail "No hay logs todavía — inicia el launcher primero"
fi
TODAY="$(date -u +%Y-%m-%d)"
if [[ -n "$AGENT_ID" ]]; then
# Logs de un agente específico
LOG_FILE="$LOG_DIR/$AGENT_ID/$TODAY.jsonl"
if [[ ! -f "$LOG_FILE" ]]; then
# Try to find the latest log file for this agent
LATEST="$(ls -t "$LOG_DIR/$AGENT_ID/"*.jsonl 2>/dev/null | head -1)"
if [[ -z "$LATEST" ]]; then
fail "No hay logs para $AGENT_ID"
fi
LOG_FILE="$LATEST"
fi
info "Siguiendo logs: $LOG_FILE"
dim " Ctrl+C para salir"
echo ""
tail -n "$LINES" -f "$LOG_FILE"
else
# Logs de todos los agentes (archivos de hoy)
FILES=$(find "$LOG_DIR" -name "$TODAY.jsonl" 2>/dev/null)
if [[ -z "$FILES" ]]; then
# Fallback: latest file from each agent
FILES=""
for d in "$LOG_DIR"/*/; do
LATEST="$(ls -t "$d"*.jsonl 2>/dev/null | head -1)"
[[ -n "$LATEST" ]] && FILES="$FILES $LATEST"
done
fi
if [[ -z "$FILES" ]]; then
fail "No hay logs todavía — inicia el launcher primero"
fi
info "Siguiendo logs de todos los agentes (hoy: $TODAY)"
dim " Ctrl+C para salir"
echo ""
# shellcheck disable=SC2086
tail -n "$LINES" -f $FILES
fi
+85
View File
@@ -0,0 +1,85 @@
#!/usr/bin/env bash
# ps.sh — muestra el estado del launcher unificado y agentes
#
# Uso:
# ./dev-scripts/server/ps.sh
source "$(dirname "$0")/../_common.sh"
if ! is_launcher_running; then
echo ""
dim " Launcher no está corriendo."
echo ""
echo " Agentes:"
while IFS='|' read -r id _v enabled _d _c; do
if [[ "$enabled" == "true" ]]; then
echo -e " ${GRN}${RST} $id (enabled)"
else
echo -e " ${DIM}$id (disabled)${RST}"
fi
done < <(list_agents_raw)
exit 0
fi
pid="$(read_launcher_pid)"
# Uptime
if [[ -f /proc/$pid/stat ]]; then
start_ticks=$(awk '{print $22}' /proc/$pid/stat 2>/dev/null || echo 0)
clk_tck=$(getconf CLK_TCK)
boot_time=$(awk '/btime/{print $2}' /proc/stat)
proc_start=$((boot_time + start_ticks / clk_tck))
now=$(date +%s)
elapsed=$((now - proc_start))
days=$((elapsed / 86400))
hours=$(( (elapsed % 86400) / 3600 ))
mins=$(( (elapsed % 3600) / 60 ))
if [[ $days -gt 0 ]]; then
uptime="${days}d ${hours}h"
elif [[ $hours -gt 0 ]]; then
uptime="${hours}h ${mins}m"
else
uptime="${mins}m"
fi
else
uptime="n/a"
fi
# Memory and CPU
read -r mem_kb cpu_pct < <(ps -p "$pid" -o rss=,pcpu= 2>/dev/null || echo "0 0")
if [[ "$mem_kb" -gt 1048576 ]]; then
mem="$(( mem_kb / 1048576 )) GB"
elif [[ "$mem_kb" -gt 1024 ]]; then
mem="$(( mem_kb / 1024 )) MB"
else
mem="${mem_kb} KB"
fi
# Log size
log="$(launcher_log_file)"
if [[ -f "$log" ]]; then
log_bytes=$(stat -c%s "$log" 2>/dev/null || echo 0)
if [[ "$log_bytes" -gt 1048576 ]]; then
log_size="$(( log_bytes / 1048576 )) MB"
elif [[ "$log_bytes" -gt 1024 ]]; then
log_size="$(( log_bytes / 1024 )) KB"
else
log_size="${log_bytes} B"
fi
else
log_size="-"
fi
echo ""
echo -e " ${GRN}● Launcher running${RST} PID $pid"
echo -e " uptime: $uptime mem: $mem cpu: ${cpu_pct}% log: $log_size"
echo ""
echo " Agentes:"
while IFS='|' read -r id _v enabled _d _c; do
if [[ "$enabled" == "true" ]]; then
echo -e " ${GRN}${RST} $id (enabled, running in launcher)"
else
echo -e " ${DIM}$id (disabled)${RST}"
fi
done < <(list_agents_raw)
+14
View File
@@ -0,0 +1,14 @@
#!/usr/bin/env bash
# restart.sh — reinicia el launcher unificado
#
# Uso:
# ./dev-scripts/server/restart.sh
source "$(dirname "$0")/../_common.sh"
info "Deteniendo launcher..."
"$REPO_ROOT/dev-scripts/server/stop.sh"
echo ""
info "Iniciando launcher..."
"$REPO_ROOT/dev-scripts/server/start.sh"
+139
View File
@@ -0,0 +1,139 @@
#!/usr/bin/env bash
# server.sh — gestión unificada del servidor de bots
#
# Uso:
# ./dev-scripts/server/server.sh start # iniciar el launcher
# ./dev-scripts/server/server.sh stop # detener el launcher
# ./dev-scripts/server/server.sh restart # reiniciar el launcher
# ./dev-scripts/server/server.sh status # resumen general del servidor
# ./dev-scripts/server/server.sh ps # proceso con detalle
# ./dev-scripts/server/server.sh logs [lines] # tail -f de logs
# ./dev-scripts/server/server.sh kill # SIGKILL forzado (emergencia)
# ./dev-scripts/server/server.sh enable <id> # habilitar un agente
# ./dev-scripts/server/server.sh disable <id> # deshabilitar un agente
# ./dev-scripts/server/server.sh dashboard # TUI interactivo
source "$(dirname "$0")/../_common.sh"
CMD="${1:-status}"
shift || true
ARG="${1:-}"
toggle_agent_enabled() {
local id="$1" value="$2"
for cfg in agents/*/config.yaml; do
[[ -f "$cfg" ]] || continue
local cid
cid=$(grep -m1 '^ id:' "$cfg" | awk '{print $2}')
if [[ "$cid" == "$id" ]]; then
sed -i "s/^\\( enabled:\\).*/\\1 $value/" "$cfg"
ok "$id enabled: $value"
info "Reinicia el launcher para aplicar: ./dev-scripts/server/server.sh restart"
return 0
fi
done
fail "Agente '$id' no encontrado"
}
case "$CMD" in
start)
exec "$REPO_ROOT/dev-scripts/server/start.sh"
;;
stop)
exec "$REPO_ROOT/dev-scripts/server/stop.sh"
;;
restart)
exec "$REPO_ROOT/dev-scripts/server/restart.sh"
;;
ps)
exec "$REPO_ROOT/dev-scripts/server/ps.sh"
;;
logs)
exec "$REPO_ROOT/dev-scripts/server/logs.sh" ${ARG:+"$ARG"}
;;
dashboard|tui)
exec "$REPO_ROOT/dev-scripts/server/dashboard.sh"
;;
enable)
[[ -n "$ARG" ]] || fail "Uso: $0 enable <agent-id>"
toggle_agent_enabled "$ARG" "true"
;;
disable)
[[ -n "$ARG" ]] || fail "Uso: $0 disable <agent-id>"
toggle_agent_enabled "$ARG" "false"
;;
kill)
if ! is_launcher_running; then
dim " El launcher no está corriendo."
exit 0
fi
pid="$(read_launcher_pid)"
kill -9 "$pid" 2>/dev/null || true
rm -f "$(launcher_pid_file)"
ok "Launcher killed (PID $pid)"
;;
status)
echo ""
echo -e " ${BLU}Bot Server Status${RST}"
printf '%s\n' " $(printf '─%.0s' {1..40})"
if is_launcher_running; then
pid="$(read_launcher_pid)"
echo -e " ${GRN}● Launcher running${RST} PID $pid"
else
echo -e " ${DIM}○ Launcher stopped${RST}"
fi
echo ""
enabled=0
disabled=0
total=0
while IFS='|' read -r id _v en _d _c; do
((total++)) || true
if [[ "$en" == "true" ]]; then
((enabled++)) || true
else
((disabled++)) || true
fi
done < <(list_agents_raw)
echo -e " Agentes totales: $total"
echo -e " ${GRN}● Enabled:${RST} $enabled"
echo -e " ${DIM}○ Disabled:${RST} $disabled"
echo ""
"$REPO_ROOT/dev-scripts/agent/list.sh"
if is_launcher_running; then
echo ""
"$REPO_ROOT/dev-scripts/server/ps.sh"
fi
;;
*)
echo "Uso: $0 {start|stop|restart|status|ps|logs|kill|enable|disable|dashboard}"
echo ""
echo "Comandos:"
echo " start Iniciar el launcher unificado"
echo " stop Detener el launcher"
echo " restart Reiniciar el launcher"
echo " status Resumen general del servidor"
echo " ps Proceso del launcher con detalle (PID, mem, CPU)"
echo " logs [lines] Tail -f de logs del launcher"
echo " kill SIGKILL forzado (solo emergencias)"
echo " enable <id> Habilitar un agente (requiere restart)"
echo " disable <id> Deshabilitar un agente (requiere restart)"
echo " dashboard TUI interactivo de gestión"
exit 1
;;
esac
+49
View File
@@ -0,0 +1,49 @@
#!/usr/bin/env bash
# start.sh — inicia el launcher unificado (todos los agentes habilitados)
#
# Uso:
# ./dev-scripts/server/start.sh # inicia el launcher unificado
source "$(dirname "$0")/../_common.sh"
load_env
if is_launcher_running; then
pid="$(read_launcher_pid)"
fail "El launcher ya está corriendo (PID $pid). Usa restart.sh para reiniciar."
fi
BIN="$REPO_ROOT/bin/launcher"
LOG="$(launcher_log_file)"
PID_F="$(launcher_pid_file)"
# Always rebuild — Go is fast and avoids stale binaries from changes in pkg/, agents/, etc.
info "Ejecutando tests..."
"$GO" test -tags goolm ./... || fail "Tests fallaron — corrige antes de compilar"
info "Compilando launcher..."
mkdir -p "$(dirname "$BIN")"
"$GO" build -tags goolm -o "$BIN" ./cmd/launcher || fail "Error de compilación"
info "Iniciando launcher unificado..."
nohup "$BIN" --log-level "${LOG_LEVEL:-info}" \
>> "$LOG" 2>&1 &
pid=$!
echo "$pid" > "$PID_F"
sleep 1
if kill -0 "$pid" 2>/dev/null; then
# Count enabled agents
enabled=0
total=0
while IFS='|' read -r _id _v en _d _c; do
((total++)) || true
[[ "$en" == "true" ]] && ((enabled++)) || true
done < <(list_agents_raw)
ok "Launcher PID $pid ($enabled/$total agentes habilitados) → logs: $LOG"
else
rm -f "$PID_F"
fail "Launcher arrancó pero murió — revisa: tail -f $LOG"
fi
+32
View File
@@ -0,0 +1,32 @@
#!/usr/bin/env bash
# stop.sh — detiene el launcher unificado
#
# Uso:
# ./dev-scripts/server/stop.sh
source "$(dirname "$0")/../_common.sh"
if ! is_launcher_running; then
dim " El launcher no está corriendo."
exit 0
fi
pid="$(read_launcher_pid)"
info "Deteniendo launcher (PID $pid)..."
kill -TERM "$pid" 2>/dev/null || true
# Wait up to 5s for graceful shutdown
for _ in {1..10}; do
kill -0 "$pid" 2>/dev/null || break
sleep 0.5
done
# SIGKILL if still alive
if kill -0 "$pid" 2>/dev/null; then
warn "Launcher no respondió a SIGTERM, enviando SIGKILL..."
kill -9 "$pid" 2>/dev/null || true
fi
rm -f "$(launcher_pid_file)"
ok "Launcher detenido"