@saulwade/swl-ses 1.3.8 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +12 -4
- package/README.md +1 -1
- package/bin/swl-mcp-server.js +187 -187
- package/bin/swl-webhook-server.js +198 -0
- package/comandos/swl/.evolved.json +22 -22
- package/comandos/swl/adoptar-proyecto.md +21 -1
- package/comandos/swl/claudemd.md +14 -1
- package/comandos/swl/contribuir.md +233 -233
- package/comandos/swl/exportar-vault.md +108 -0
- package/comandos/swl/nuevo-proyecto.md +24 -2
- package/gateway/adapters/base.js +109 -0
- package/gateway/adapters/discord.js +167 -0
- package/gateway/adapters/email.js +221 -0
- package/gateway/adapters/slack.js +192 -0
- package/gateway/adapters/telegram.js +183 -0
- package/gateway/adapters/webhook.js +113 -0
- package/gateway/adapters/whatsapp.js +214 -0
- package/gateway/agent-executor.js +322 -0
- package/gateway/command-relay.js +271 -0
- package/gateway/cron/jobs.js +263 -0
- package/gateway/cron/scheduler.js +322 -0
- package/gateway/cron/store.js +335 -0
- package/gateway/index.js +320 -0
- package/gateway/lib/event-channel.js +191 -0
- package/gateway/session.js +131 -0
- package/gateway/webhook-server.js +324 -0
- package/habilidades/backend-production-resilience/SKILL.md +288 -288
- package/habilidades/benchmark-memoria/SKILL.md +186 -186
- package/habilidades/build-errors-nextjs/SKILL.md +55 -1
- package/habilidades/diagrama-arquitectura/assets/template.html +276 -276
- package/habilidades/doubt-driven-review/SKILL.md +171 -171
- package/habilidades/doubt-driven-review/recursos/EXAMPLES.md +130 -130
- package/habilidades/eval-framework/SKILL.md +212 -212
- package/habilidades/extractor-de-aprendizajes/SKILL.md +20 -10
- package/habilidades/harness-claude-code/SKILL.md +299 -299
- package/habilidades/infra-github-actions/SKILL.md +166 -166
- package/habilidades/legacy-code-rescue/SKILL.md +267 -267
- package/habilidades/manejo-errores/.evolved.json +8 -8
- package/habilidades/meta-skills-estandar/recursos/convencion-examples.md +93 -93
- package/habilidades/meta-skills-estandar/recursos/skills-as-agents.md +163 -163
- package/habilidades/nextjs-testing/SKILL.md +89 -5
- package/habilidades/node-experto/SKILL.md +37 -1
- package/habilidades/patrones-python/SKILL.md +229 -229
- package/habilidades/patrones-python/recursos/patrones-avanzados.md +469 -469
- package/habilidades/planear-fase/SKILL.md +319 -319
- package/habilidades/react-experto/SKILL.md +45 -4
- package/habilidades/release-semver/.evolved.json +8 -8
- package/habilidades/tdd-workflow/SKILL.md +36 -4
- package/habilidades/testing-python/SKILL.md +340 -340
- package/hooks/claudemd-bloat-detector.js +161 -161
- package/hooks/inyeccion-contexto.js +8 -3
- package/hooks/lib/agent-routing.js +107 -107
- package/hooks/lib/auto-consolidator.js +335 -335
- package/hooks/lib/error-classifier.js +308 -308
- package/hooks/lib/merkle-audit.js +96 -96
- package/hooks/lib/provenance-tracker.js +191 -191
- package/hooks/lib/rate-limit-ip.js +177 -0
- package/hooks/lib/rate-limit-tracker.js +253 -253
- package/hooks/lib/resource-quota.js +122 -122
- package/hooks/lib/retry-jitter.js +165 -165
- package/hooks/lib/skill-auditor.js +588 -588
- package/hooks/lib/sync-status.js +228 -228
- package/hooks/lib/taint-tracker.js +107 -107
- package/hooks/lib/text-similarity.js +241 -241
- package/hooks/lib/toon-compressor.js +245 -245
- package/hooks/lib/webhook-dedup.js +184 -0
- package/hooks/lib/webhook-verify.js +123 -0
- package/hooks/proteccion-rutas.js +120 -15
- package/hooks/registro-turnos.js +209 -209
- package/hooks/sugerir-regenerar-inventario.js +170 -170
- package/hooks/validar-formato-post-subagente.js +140 -140
- package/hooks/validar-memoria-hook.js +218 -218
- package/instintos/prompt-appendices.yaml +57 -57
- package/manifiestos/agent-output-schemas.json +57 -57
- package/manifiestos/modulos.json +1 -0
- package/manifiestos/skills-lock.json +34 -34
- package/package.json +5 -3
- package/plantillas/auditor-veto-template.md +105 -105
- package/plantillas/github-workflows/README.md +47 -47
- package/plantillas/github-workflows/release-please.yml +44 -44
- package/plantillas/github-workflows/swl-ci.yml +107 -107
- package/plantillas/github-workflows/swl-security.yml +51 -51
- package/plugin.json +1 -1
- package/reglas/analisis-previo-tareas-grandes.md +172 -172
- package/reglas/arreglar-al-detectar.md +147 -147
- package/reglas/fragmentos-compartidos.md +152 -152
- package/reglas/harness-claude-code.md +213 -213
- package/reglas/usar-context7.md +226 -226
- package/reglas/usar-sistema-swl.md +251 -0
- package/schemas/diary-entry.schema.json +80 -80
- package/scripts/benchmark-memoria.js +167 -167
- package/scripts/comandos/skills.js +251 -2
- package/scripts/configurar-branch-protection.js +418 -418
- package/scripts/detectar-aprendizajes-duplicados.js +151 -151
- package/scripts/field-report.js +199 -199
- package/scripts/generar-checklists-consolidados.js +273 -273
- package/scripts/generar-inventario.js +420 -420
- package/scripts/generar-matriz-lenguajes.js +271 -271
- package/scripts/lib/artefactos-python.js +43 -43
- package/scripts/lib/benchmark-metrics.js +160 -160
- package/scripts/lib/budget-enforcer.js +252 -252
- package/scripts/lib/configurar-ci.js +380 -380
- package/scripts/lib/contadores-inventario.js +217 -217
- package/scripts/lib/detectar-stack-detallado.js +307 -307
- package/scripts/lib/diary-entry.js +234 -234
- package/scripts/lib/eval-metrics-store.js +218 -218
- package/scripts/lib/eval-quality.js +171 -171
- package/scripts/lib/eval-schemas.js +144 -144
- package/scripts/lib/eval-self-correct.js +106 -106
- package/scripts/lib/eval-validator.js +185 -185
- package/scripts/lib/jaccard-similarity.js +98 -98
- package/scripts/lib/longmemeval-runner.js +125 -125
- package/scripts/lib/npm-version.js +261 -261
- package/scripts/lib/paquetes-conocidos.js +50 -50
- package/scripts/lib/prompt-builder.js +264 -264
- package/scripts/lib/rrf-fusion.js +175 -175
- package/scripts/lib/scoring-instintos.js +277 -277
- package/scripts/lib/semantic-search.js +252 -252
- package/scripts/limpiar-artefactos-python.js +131 -131
- package/scripts/mcp-server/README.md +128 -128
- package/scripts/mcp-server/handlers.js +206 -206
- package/scripts/migrar-csv-a-array.js +168 -168
- package/scripts/migrar-fase-dominio.js +201 -201
- package/scripts/publicar.js +511 -511
- package/scripts/run-eval.js +141 -141
- package/scripts/validar-manifest.js +195 -195
- package/scripts/validar-userland-vacio.js +110 -110
- package/scripts/verificar-release.js +110 -0
|
@@ -55,6 +55,66 @@ documentado en `reglas/consultar-vault-primero.md § Workflow forzoso para
|
|
|
55
55
|
escritura al vault`. El bloqueo por `proteccion-rutas.js` no es una falla a
|
|
56
56
|
esquivar — es una señal de que el canal correcto es el MCP.
|
|
57
57
|
|
|
58
|
+
### 0c — Verificar que el vault activo de Obsidian apunta a `Vault\SWL\` (CRÍTICO)
|
|
59
|
+
|
|
60
|
+
> Origen del paso: sesión 2026-05-13 v1.4.0. El MCP de Obsidian resuelve paths
|
|
61
|
+
> relativos contra el **vault que Obsidian tiene abierto**, no contra una
|
|
62
|
+
> ruta hardcodeada en el plugin. Si Obsidian abrió un vault distinto del
|
|
63
|
+
> esperado (ej. `F:\Google Drive\Developer\Obsidian\Vault\` raíz en lugar
|
|
64
|
+
> de `Vault\SWL\`), todas las escrituras del MCP irán al vault equivocado.
|
|
65
|
+
> El plugin **crea carpetas inexistentes** sobre la marcha, así que ni
|
|
66
|
+
> siquiera obtendrás error — los archivos terminan en una ubicación huérfana.
|
|
67
|
+
|
|
68
|
+
Verifica antes de cualquier llamada MCP:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
# Path canónico de la config global de Obsidian en Windows
|
|
72
|
+
APP="$APPDATA/obsidian/obsidian.json"
|
|
73
|
+
node -e "
|
|
74
|
+
const fs = require('fs');
|
|
75
|
+
const data = JSON.parse(fs.readFileSync(process.argv[1], 'utf8'));
|
|
76
|
+
const abierto = Object.values(data.vaults).find(v => v.open === true);
|
|
77
|
+
console.log('Vault abierto:', abierto ? abierto.path : 'NINGUNO');
|
|
78
|
+
console.log('Esperado: F:\\\\Google Drive\\\\Developer\\\\Obsidian\\\\Vault\\\\SWL');
|
|
79
|
+
console.log('OK:', abierto && abierto.path.endsWith('SWL') ? 'sí' : 'NO');
|
|
80
|
+
" "$APP"
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Resultado esperado:
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
Vault abierto: F:\Google Drive\Developer\Obsidian\Vault\SWL
|
|
87
|
+
Esperado: F:\Google Drive\Developer\Obsidian\Vault\SWL
|
|
88
|
+
OK: sí
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Si `OK: NO` o `Vault abierto: NINGUNO`, **NO usar MCP** — caer al filesystem
|
|
92
|
+
para esta sesión y reportar al usuario que debe abrir el vault `SWL` en
|
|
93
|
+
Obsidian (File → Open vault → seleccionar `F:\Google Drive\Developer\Obsidian\Vault\SWL`).
|
|
94
|
+
|
|
95
|
+
### 0d — Verificar que la apiKey del MCP coincide con la del plugin activo
|
|
96
|
+
|
|
97
|
+
> Origen: cuando el vault activo cambia, la apiKey del plugin Local REST API
|
|
98
|
+
> cambia (cada vault tiene su propio `.obsidian/plugins/obsidian-local-rest-api/data.json`
|
|
99
|
+
> con apiKey, cert y privateKey distintos). El MCP server registrado en
|
|
100
|
+
> Claude.ai tiene una apiKey hardcodeada — si no se actualiza tras cambiar de
|
|
101
|
+
> vault, todas las llamadas retornan `40101 Authorization required`.
|
|
102
|
+
|
|
103
|
+
Si tras `ToolSearch` el MCP carga sus tools pero la primera llamada (ej.
|
|
104
|
+
`obsidian_list_files_in_dir`) retorna `Error 40101: Authorization required`,
|
|
105
|
+
el síntoma es claro: apiKey desincronizada.
|
|
106
|
+
|
|
107
|
+
Acción:
|
|
108
|
+
1. Leer la apiKey actual del plugin con
|
|
109
|
+
`grep '"apiKey"' "F:/Google Drive/Developer/Obsidian/Vault/SWL/.obsidian/plugins/obsidian-local-rest-api/data.json"`
|
|
110
|
+
2. Reportar al usuario que actualice en **claude.ai → Settings → Connectors
|
|
111
|
+
→ Obsidian → API Key**. Disconnect + Reconnect tras pegar el valor.
|
|
112
|
+
3. Mientras tanto, usar filesystem como fallback para escribir al Inbox.
|
|
113
|
+
|
|
114
|
+
Si hay procesos zombie de Obsidian o `mcp-obsidian` cliente con apiKey
|
|
115
|
+
cacheada, requieren terminación + reinicio de Claude Code para tomar la
|
|
116
|
+
nueva apiKey.
|
|
117
|
+
|
|
58
118
|
## Paso 1 — Identificación del proyecto actual
|
|
59
119
|
|
|
60
120
|
Detecta qué proyecto eres leyendo:
|
|
@@ -296,6 +356,40 @@ escribe a `_userland/staging/<timestamp>.md` dentro del CWD, luego mueve con
|
|
|
296
356
|
Reportar al usuario qué canal se usó:
|
|
297
357
|
- Canal A → `[OK] Vía: MCP Obsidian (puerto 27124)`
|
|
298
358
|
- Canal B → `[OK] Vía: filesystem directo (MCP no disponible)`
|
|
359
|
+
- Canal Híbrido → `[OK] Vía: filesystem para Inbox, MCP para promociones`
|
|
360
|
+
|
|
361
|
+
### Modo HÍBRIDO (validado en sesión 2026-05-13)
|
|
362
|
+
|
|
363
|
+
Cuando la sesión incluye **autorización ampliada para promover documentos**
|
|
364
|
+
a `02-Projects/`, `07-Decisions/` y `04-Resources/`, el flujo óptimo combina
|
|
365
|
+
ambos canales:
|
|
366
|
+
|
|
367
|
+
1. **Inbox** (`00-Inbox/YYYY-MM-DD_HHMM_export-{slug}.md`): usar siempre
|
|
368
|
+
**filesystem directo via Bash heredoc con ruta absoluta** (canal B). Razón:
|
|
369
|
+
el Bash heredoc bypassa cualquier desalineación del vault activo de
|
|
370
|
+
Obsidian — escribe directamente al disco en la ruta exacta.
|
|
371
|
+
|
|
372
|
+
2. **Promociones** (a `02-Projects/`, `07-Decisions/`, `04-Resources/`):
|
|
373
|
+
usar **MCP append_content** (canal A) tras validar Pasos 0c y 0d. Razón:
|
|
374
|
+
las promociones requieren enlaces bidireccionales con `[[...]]` que el
|
|
375
|
+
plugin de Obsidian indexa al detectar la escritura. El filesystem directo
|
|
376
|
+
crea el archivo pero no dispara la reindexación hasta que Obsidian
|
|
377
|
+
detecte el cambio.
|
|
378
|
+
|
|
379
|
+
3. **Aprobación de revisiones** (cambiar `status: pending-review` → `reviewed`
|
|
380
|
+
en frontmatter): si el archivo está en `00-Inbox/`, usar `sed` via Bash;
|
|
381
|
+
si está en `02-Projects/` etc., usar `mcp__obsidian__obsidian_patch_content`
|
|
382
|
+
con `target_type: "frontmatter"`. Validado que `patch_content` puede
|
|
383
|
+
responder 404 en algunos archivos pre-existentes — caer al filesystem si
|
|
384
|
+
falla.
|
|
385
|
+
|
|
386
|
+
Reportar al usuario el desglose final del modo híbrido:
|
|
387
|
+
|
|
388
|
+
```
|
|
389
|
+
[OK] Inbox creado via filesystem: F:\...\00-Inbox\...md
|
|
390
|
+
[OK] 3 promociones via MCP: 02-Projects, 07-Decisions, 04-Resources
|
|
391
|
+
[OK] Revisión aprobada (reviewed: true) en el Inbox
|
|
392
|
+
```
|
|
299
393
|
|
|
300
394
|
## Paso 5 — Confirmación
|
|
301
395
|
|
|
@@ -373,6 +467,20 @@ Próximo paso: al abrir el vault, ejecuta:
|
|
|
373
467
|
|
|
374
468
|
## Historial de cambios del comando
|
|
375
469
|
|
|
470
|
+
- **v1.4.0** (2026-05-13) — Agregados Pasos 0c y 0d con verificación previa
|
|
471
|
+
del vault activo de Obsidian y de la apiKey del MCP. Origen: sesión
|
|
472
|
+
2026-05-13 con autorización ampliada del usuario para promover docs;
|
|
473
|
+
el MCP escribió `02-Projects/DEV - swl-ses.md` en `Vault\` raíz en lugar
|
|
474
|
+
de `Vault\SWL\` porque Obsidian tenía abierto el vault un nivel arriba
|
|
475
|
+
del esperado. Hallazgo crítico: el plugin Local REST API resuelve paths
|
|
476
|
+
relativos contra el vault que Obsidian tiene abierto en `obsidian.json`
|
|
477
|
+
global, no contra una ruta hardcodeada. Si el vault activo está mal,
|
|
478
|
+
el MCP escribe a la ubicación equivocada sin error (el plugin crea
|
|
479
|
+
carpetas inexistentes). Agregado también Paso 0d para apiKey
|
|
480
|
+
desincronizada (síntoma `40101 Authorization required`). Y nuevo
|
|
481
|
+
modo HÍBRIDO en Paso 4: filesystem directo para Inbox + MCP para
|
|
482
|
+
promociones, validado en la misma sesión.
|
|
483
|
+
|
|
376
484
|
- **v1.3.8** (2026-05-11) — Agregado flujo MCP-first en Paso 4 con detección
|
|
377
485
|
en Paso 0b. Origen: en la sesión v1.3.4 → v1.3.8 el comando defaultó a
|
|
378
486
|
`Write` directo al filesystem que fue bloqueado por `proteccion-rutas.js`.
|
|
@@ -135,11 +135,33 @@ Estructura:
|
|
|
135
135
|
- Si el usuario no definió fases claras, propón una división lógica basada en las respuestas del Bloque C
|
|
136
136
|
- Estado inicial de todas las fases: "Pendiente"
|
|
137
137
|
|
|
138
|
-
## Paso 6 —
|
|
138
|
+
## Paso 6 — Generar CLAUDE.md inicial del proyecto
|
|
139
|
+
|
|
140
|
+
Si NO existe `CLAUDE.md` en la raíz del proyecto, generarlo con la estructura
|
|
141
|
+
mínima definida en `/swl:claudemd init-project`. **OBLIGATORIO** incluir como
|
|
142
|
+
primera sección bajo el título:
|
|
143
|
+
|
|
144
|
+
```markdown
|
|
145
|
+
## Reglas obligatorias
|
|
146
|
+
|
|
147
|
+
@reglas/usar-sistema-swl.md
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Esta referencia carga la matriz operacional del sistema SWL al inicio de cada
|
|
151
|
+
sesión del proyecto y previene que el agente haga trabajo directo cuando
|
|
152
|
+
existe un componente especializado. Sin ella, el proyecto pierde el contrato
|
|
153
|
+
de uso del sistema SWL.
|
|
154
|
+
|
|
155
|
+
Si ya existe `CLAUDE.md` (verificado en Paso 1), revisar que incluya
|
|
156
|
+
`@reglas/usar-sistema-swl.md` en la sección de reglas obligatorias. Si NO
|
|
157
|
+
lo incluye, agregarlo en este paso preservando el resto del contenido.
|
|
158
|
+
|
|
159
|
+
## Paso 7 — Reporte al usuario
|
|
139
160
|
|
|
140
161
|
Al terminar, reporta:
|
|
141
162
|
|
|
142
|
-
1. Lista de archivos creados con sus rutas absolutas
|
|
163
|
+
1. Lista de archivos creados con sus rutas absolutas (incluyendo CLAUDE.md
|
|
164
|
+
si se generó o se modificó)
|
|
143
165
|
2. Resumen de la investigación del agente (cuando esté disponible)
|
|
144
166
|
3. Próximo paso recomendado: "Para comenzar a trabajar en la Fase 1, usa `/swl:discutir-fase 1`"
|
|
145
167
|
4. Si encontraste riesgos o ambigüedades durante la entrevista, listarlos aquí como "Puntos a resolver"
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Base Adapter — Clase base abstracta para adaptadores de plataforma.
|
|
5
|
+
*
|
|
6
|
+
* Cada plataforma (Telegram, Discord, Webhook) extiende esta clase
|
|
7
|
+
* e implementa los métodos abstractos. El gateway/index.js orquesta
|
|
8
|
+
* múltiples adaptadores de forma uniforme.
|
|
9
|
+
*
|
|
10
|
+
* Patrón adoptado de Hermes Agent (gateway/platforms/base.py).
|
|
11
|
+
*
|
|
12
|
+
* @module gateway/adapters/base
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @abstract
|
|
17
|
+
*/
|
|
18
|
+
class BaseAdapter {
|
|
19
|
+
/**
|
|
20
|
+
* @param {string} name - Nombre de la plataforma (ej: 'telegram').
|
|
21
|
+
* @param {object} config - Configuración específica de la plataforma.
|
|
22
|
+
*/
|
|
23
|
+
constructor(name, config) {
|
|
24
|
+
if (new.target === BaseAdapter) {
|
|
25
|
+
throw new Error('BaseAdapter es abstracto — usar un adaptador concreto.');
|
|
26
|
+
}
|
|
27
|
+
this.name = name;
|
|
28
|
+
this.config = config;
|
|
29
|
+
this.running = false;
|
|
30
|
+
this._messageHandler = null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Inicia la conexión con la plataforma.
|
|
35
|
+
* @abstract
|
|
36
|
+
* @returns {Promise<void>}
|
|
37
|
+
*/
|
|
38
|
+
async start() {
|
|
39
|
+
throw new Error(`${this.name}: start() no implementado`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Detiene la conexión con la plataforma.
|
|
44
|
+
* @abstract
|
|
45
|
+
* @returns {Promise<void>}
|
|
46
|
+
*/
|
|
47
|
+
async stop() {
|
|
48
|
+
this.running = false;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Envía un mensaje formateado a la plataforma.
|
|
53
|
+
* @abstract
|
|
54
|
+
* @param {object} message
|
|
55
|
+
* @param {string} message.text - Texto del mensaje (Markdown).
|
|
56
|
+
* @param {string} [message.chatId] - Destino específico.
|
|
57
|
+
* @param {string} [message.type] - Tipo: 'notification', 'alert', 'response'.
|
|
58
|
+
* @returns {Promise<boolean>} true si se envió correctamente.
|
|
59
|
+
*/
|
|
60
|
+
async send(message) {
|
|
61
|
+
throw new Error(`${this.name}: send() no implementado`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Registra un handler para mensajes entrantes de la plataforma.
|
|
66
|
+
* @param {function} handler - Callback (message) => void.
|
|
67
|
+
*/
|
|
68
|
+
onMessage(handler) {
|
|
69
|
+
this._messageHandler = handler;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Emite un mensaje entrante al handler registrado.
|
|
74
|
+
* @protected
|
|
75
|
+
* @param {object} message
|
|
76
|
+
*/
|
|
77
|
+
_emitMessage(message) {
|
|
78
|
+
if (typeof this._messageHandler === 'function') {
|
|
79
|
+
this._messageHandler({
|
|
80
|
+
platform: this.name,
|
|
81
|
+
...message,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Formatea un mensaje SWL para la plataforma específica.
|
|
88
|
+
* Override en cada adaptador para formato nativo (embeds, markdown, etc.).
|
|
89
|
+
*
|
|
90
|
+
* @param {object} swlMessage - Mensaje del sistema SWL.
|
|
91
|
+
* @returns {string} Texto formateado para la plataforma.
|
|
92
|
+
*/
|
|
93
|
+
formatMessage(swlMessage) {
|
|
94
|
+
const { jobName, status, output, type } = swlMessage.payload || swlMessage;
|
|
95
|
+
const lines = [];
|
|
96
|
+
|
|
97
|
+
if (type === 'gateway_notification' || jobName) {
|
|
98
|
+
lines.push(`*${jobName || 'SWL Notificación'}*`);
|
|
99
|
+
if (status) lines.push(`Estado: ${status}`);
|
|
100
|
+
if (output) lines.push(`\`\`\`\n${output.substring(0, 500)}\n\`\`\``);
|
|
101
|
+
} else {
|
|
102
|
+
lines.push(swlMessage.text || JSON.stringify(swlMessage));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return lines.join('\n');
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
module.exports = BaseAdapter;
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Discord Adapter — Adaptador de plataforma para Discord.
|
|
5
|
+
*
|
|
6
|
+
* Usa discord.js para conectar un bot. Soporta:
|
|
7
|
+
* - Recepción de mensajes en canal dedicado
|
|
8
|
+
* - Envío con embeds para notificaciones estructuradas
|
|
9
|
+
* - Comandos slash nativos
|
|
10
|
+
* - Filtro por guildId y channelId
|
|
11
|
+
*
|
|
12
|
+
* Dependencia: discord.js (npm install discord.js)
|
|
13
|
+
* Si no está instalada, el adaptador se desactiva silenciosamente.
|
|
14
|
+
*
|
|
15
|
+
* Inspirado en Hermes Agent (gateway/platforms/discord.py).
|
|
16
|
+
*
|
|
17
|
+
* @module gateway/adapters/discord
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const BaseAdapter = require('./base');
|
|
21
|
+
|
|
22
|
+
/** Límite de caracteres por mensaje de Discord. */
|
|
23
|
+
const MAX_MSG_LENGTH = 2000;
|
|
24
|
+
|
|
25
|
+
class DiscordAdapter extends BaseAdapter {
|
|
26
|
+
constructor(config) {
|
|
27
|
+
super('discord', config);
|
|
28
|
+
this._client = null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async start() {
|
|
32
|
+
let Discord;
|
|
33
|
+
try {
|
|
34
|
+
Discord = require('discord.js');
|
|
35
|
+
} catch (_) {
|
|
36
|
+
console.log('[discord] discord.js no instalado. Ejecutar: npm install discord.js');
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const token = this.config.token || process.env.DISCORD_BOT_TOKEN;
|
|
41
|
+
if (!token) {
|
|
42
|
+
console.log('[discord] Token no configurado. Establecer DISCORD_BOT_TOKEN.');
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const { Client, GatewayIntentBits } = Discord;
|
|
47
|
+
|
|
48
|
+
this._client = new Client({
|
|
49
|
+
intents: [
|
|
50
|
+
GatewayIntentBits.Guilds,
|
|
51
|
+
GatewayIntentBits.GuildMessages,
|
|
52
|
+
GatewayIntentBits.MessageContent,
|
|
53
|
+
],
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
this._client.on('ready', () => {
|
|
57
|
+
this.running = true;
|
|
58
|
+
console.log(`[discord] Bot conectado como ${this._client.user.tag}`);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
this._client.on('messageCreate', (msg) => {
|
|
62
|
+
// Ignorar mensajes del bot
|
|
63
|
+
if (msg.author.bot) return;
|
|
64
|
+
|
|
65
|
+
// Filtrar por guild y canal si están configurados
|
|
66
|
+
if (this.config.guildId && msg.guildId !== this.config.guildId) return;
|
|
67
|
+
if (this.config.channelId && msg.channelId !== this.config.channelId) return;
|
|
68
|
+
|
|
69
|
+
const text = msg.content || '';
|
|
70
|
+
const parts = text.split(/\s+/);
|
|
71
|
+
const command = parts[0].startsWith('/') ? parts[0] : null;
|
|
72
|
+
const args = command ? parts.slice(1).join(' ') : '';
|
|
73
|
+
|
|
74
|
+
this._emitMessage({
|
|
75
|
+
chatId: msg.channelId,
|
|
76
|
+
userId: msg.author.id,
|
|
77
|
+
userName: msg.author.username,
|
|
78
|
+
text: text,
|
|
79
|
+
command: command,
|
|
80
|
+
args: args,
|
|
81
|
+
guildId: msg.guildId,
|
|
82
|
+
raw: msg,
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
this._client.on('error', (err) => {
|
|
87
|
+
console.error(`[discord] Error: ${err.message}`);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
await this._client.login(token);
|
|
91
|
+
console.log('[discord] Adaptador iniciado.');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async stop() {
|
|
95
|
+
if (this._client) {
|
|
96
|
+
try { await this._client.destroy(); } catch (_) {}
|
|
97
|
+
this._client = null;
|
|
98
|
+
}
|
|
99
|
+
this.running = false;
|
|
100
|
+
console.log('[discord] Adaptador detenido.');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async send(message) {
|
|
104
|
+
if (!this._client) return false;
|
|
105
|
+
|
|
106
|
+
const channelId = message.chatId || this.config.channelId;
|
|
107
|
+
if (!channelId) return false;
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
const channel = await this._client.channels.fetch(channelId);
|
|
111
|
+
if (!channel || !channel.isTextBased()) return false;
|
|
112
|
+
|
|
113
|
+
const embed = this._buildEmbed(message);
|
|
114
|
+
if (embed) {
|
|
115
|
+
await channel.send({ embeds: [embed] });
|
|
116
|
+
} else {
|
|
117
|
+
const text = this.formatMessage(message);
|
|
118
|
+
// Split si excede límite
|
|
119
|
+
if (text.length > MAX_MSG_LENGTH) {
|
|
120
|
+
const chunks = [];
|
|
121
|
+
for (let i = 0; i < text.length; i += MAX_MSG_LENGTH) {
|
|
122
|
+
chunks.push(text.substring(i, i + MAX_MSG_LENGTH));
|
|
123
|
+
}
|
|
124
|
+
for (const chunk of chunks) await channel.send(chunk);
|
|
125
|
+
} else {
|
|
126
|
+
await channel.send(text);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return true;
|
|
130
|
+
} catch (err) {
|
|
131
|
+
console.error(`[discord] Error enviando: ${err.message}`);
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Construye un embed de Discord para notificaciones estructuradas.
|
|
138
|
+
* @private
|
|
139
|
+
*/
|
|
140
|
+
_buildEmbed(message) {
|
|
141
|
+
const payload = message.payload || message;
|
|
142
|
+
if (!payload.jobName && !payload.title) return null;
|
|
143
|
+
|
|
144
|
+
let Discord;
|
|
145
|
+
try { Discord = require('discord.js'); } catch (_) { return null; }
|
|
146
|
+
|
|
147
|
+
const { EmbedBuilder } = Discord;
|
|
148
|
+
const isError = payload.status === 'error';
|
|
149
|
+
|
|
150
|
+
const embed = new EmbedBuilder()
|
|
151
|
+
.setTitle(payload.jobName || payload.title || 'SWL Notificación')
|
|
152
|
+
.setColor(isError ? 0xFF0000 : 0x00CC66)
|
|
153
|
+
.setTimestamp();
|
|
154
|
+
|
|
155
|
+
if (payload.status) {
|
|
156
|
+
embed.addFields({ name: 'Estado', value: `\`${payload.status}\``, inline: true });
|
|
157
|
+
}
|
|
158
|
+
if (payload.output) {
|
|
159
|
+
const output = payload.output.substring(0, 1000);
|
|
160
|
+
embed.addFields({ name: 'Output', value: `\`\`\`\n${output}\n\`\`\`` });
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return embed;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
module.exports = DiscordAdapter;
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Email Adapter — Adaptador para notificaciones por correo electrónico.
|
|
5
|
+
*
|
|
6
|
+
* Soporta:
|
|
7
|
+
* - Envío via SMTP directo (nodemailer)
|
|
8
|
+
* - Envío via API de servicios (SendGrid, Mailgun, SES) — HTTP nativo
|
|
9
|
+
* - Formato HTML con template básico para notificaciones
|
|
10
|
+
* - Unidireccional (solo envío, sin recepción)
|
|
11
|
+
*
|
|
12
|
+
* Dependencia SMTP: nodemailer (npm install nodemailer)
|
|
13
|
+
* Dependencia API: ninguna (HTTP nativo)
|
|
14
|
+
*
|
|
15
|
+
* @module gateway/adapters/email
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const BaseAdapter = require('./base');
|
|
19
|
+
const https = require('https');
|
|
20
|
+
|
|
21
|
+
class EmailAdapter extends BaseAdapter {
|
|
22
|
+
constructor(config) {
|
|
23
|
+
super('email', config);
|
|
24
|
+
this._transporter = null;
|
|
25
|
+
this._mode = config.mode || 'api'; // 'smtp' | 'api'
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async start() {
|
|
29
|
+
if (this._mode === 'smtp') {
|
|
30
|
+
return this._startSMTP();
|
|
31
|
+
}
|
|
32
|
+
return this._startAPI();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Modo SMTP con nodemailer.
|
|
37
|
+
*/
|
|
38
|
+
async _startSMTP() {
|
|
39
|
+
let nodemailer;
|
|
40
|
+
try {
|
|
41
|
+
nodemailer = require('nodemailer');
|
|
42
|
+
} catch (_) {
|
|
43
|
+
console.log('[email] nodemailer no instalado. Ejecutar: npm install nodemailer');
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const host = this.config.smtpHost || process.env.SMTP_HOST;
|
|
48
|
+
const port = this.config.smtpPort || process.env.SMTP_PORT || 587;
|
|
49
|
+
const user = this.config.smtpUser || process.env.SMTP_USER;
|
|
50
|
+
const pass = this.config.smtpPass || process.env.SMTP_PASS;
|
|
51
|
+
|
|
52
|
+
if (!host || !user) {
|
|
53
|
+
console.log('[email] SMTP no configurado. Establecer SMTP_HOST y SMTP_USER.');
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
this._transporter = nodemailer.createTransport({
|
|
58
|
+
host,
|
|
59
|
+
port: Number(port),
|
|
60
|
+
secure: Number(port) === 465,
|
|
61
|
+
auth: { user, pass },
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
this.running = true;
|
|
65
|
+
console.log(`[email] Adaptador iniciado (SMTP: ${host}:${port}).`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Modo API (SendGrid, Mailgun, SES — via HTTP).
|
|
70
|
+
*/
|
|
71
|
+
async _startAPI() {
|
|
72
|
+
const apiKey = this.config.apiKey || process.env.EMAIL_API_KEY;
|
|
73
|
+
const service = this.config.service || process.env.EMAIL_SERVICE || 'sendgrid';
|
|
74
|
+
|
|
75
|
+
if (!apiKey) {
|
|
76
|
+
console.log(`[email] API key no configurada. Establecer EMAIL_API_KEY para ${service}.`);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
this._apiKey = apiKey;
|
|
81
|
+
this._service = service;
|
|
82
|
+
this.running = true;
|
|
83
|
+
console.log(`[email] Adaptador iniciado (API: ${service}).`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async stop() {
|
|
87
|
+
if (this._transporter) {
|
|
88
|
+
this._transporter.close();
|
|
89
|
+
this._transporter = null;
|
|
90
|
+
}
|
|
91
|
+
this.running = false;
|
|
92
|
+
console.log('[email] Adaptador detenido.');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async send(message) {
|
|
96
|
+
const to = message.chatId || this.config.defaultTo || process.env.EMAIL_DEFAULT_TO;
|
|
97
|
+
const from = this.config.from || process.env.EMAIL_FROM || 'swl-ses@noreply.com';
|
|
98
|
+
|
|
99
|
+
if (!to) return false;
|
|
100
|
+
|
|
101
|
+
const subject = this._buildSubject(message);
|
|
102
|
+
const html = this._buildHTML(message);
|
|
103
|
+
const text = this.formatMessage(message);
|
|
104
|
+
|
|
105
|
+
if (this._mode === 'smtp') {
|
|
106
|
+
return this._sendSMTP(from, to, subject, text, html);
|
|
107
|
+
}
|
|
108
|
+
return this._sendAPI(from, to, subject, text, html);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Envía via SMTP con nodemailer.
|
|
113
|
+
*/
|
|
114
|
+
async _sendSMTP(from, to, subject, text, html) {
|
|
115
|
+
if (!this._transporter) return false;
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
await this._transporter.sendMail({ from, to, subject, text, html });
|
|
119
|
+
return true;
|
|
120
|
+
} catch (err) {
|
|
121
|
+
console.error(`[email] Error SMTP: ${err.message}`);
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Envía via SendGrid API (HTTP POST).
|
|
128
|
+
*/
|
|
129
|
+
async _sendAPI(from, to, subject, text, html) {
|
|
130
|
+
if (!this._apiKey) return false;
|
|
131
|
+
|
|
132
|
+
if (this._service === 'sendgrid') {
|
|
133
|
+
return this._sendSendGrid(from, to, subject, text, html);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Fallback genérico — tratar como webhook
|
|
137
|
+
console.log(`[email] Servicio "${this._service}" no implementado. Usar "sendgrid" o modo "smtp".`);
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* SendGrid v3 API.
|
|
143
|
+
*/
|
|
144
|
+
_sendSendGrid(from, to, subject, text, html) {
|
|
145
|
+
const body = JSON.stringify({
|
|
146
|
+
personalizations: [{ to: [{ email: to }] }],
|
|
147
|
+
from: { email: from },
|
|
148
|
+
subject,
|
|
149
|
+
content: [
|
|
150
|
+
{ type: 'text/plain', value: text },
|
|
151
|
+
{ type: 'text/html', value: html },
|
|
152
|
+
],
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
return new Promise((resolve) => {
|
|
156
|
+
const req = https.request({
|
|
157
|
+
hostname: 'api.sendgrid.com',
|
|
158
|
+
path: '/v3/mail/send',
|
|
159
|
+
method: 'POST',
|
|
160
|
+
headers: {
|
|
161
|
+
'Authorization': `Bearer ${this._apiKey}`,
|
|
162
|
+
'Content-Type': 'application/json',
|
|
163
|
+
'Content-Length': Buffer.byteLength(body),
|
|
164
|
+
},
|
|
165
|
+
timeout: 10000,
|
|
166
|
+
}, (res) => {
|
|
167
|
+
let data = '';
|
|
168
|
+
res.on('data', chunk => data += chunk);
|
|
169
|
+
res.on('end', () => resolve(res.statusCode >= 200 && res.statusCode < 300));
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
req.on('error', () => resolve(false));
|
|
173
|
+
req.on('timeout', () => { req.destroy(); resolve(false); });
|
|
174
|
+
req.write(body);
|
|
175
|
+
req.end();
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
_buildSubject(message) {
|
|
180
|
+
const payload = message.payload || message;
|
|
181
|
+
const status = payload.status === 'completed' ? '✓' : '✗';
|
|
182
|
+
return `[SWL] ${status} ${payload.jobName || payload.title || 'Notificación'}`;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
_buildHTML(message) {
|
|
186
|
+
const payload = message.payload || message;
|
|
187
|
+
const isError = payload.status === 'error';
|
|
188
|
+
const color = isError ? '#dc3545' : '#28a745';
|
|
189
|
+
|
|
190
|
+
return `
|
|
191
|
+
<!DOCTYPE html>
|
|
192
|
+
<html><body style="font-family:system-ui,sans-serif;max-width:600px;margin:0 auto;padding:20px">
|
|
193
|
+
<div style="border-left:4px solid ${color};padding:12px 16px;margin-bottom:16px">
|
|
194
|
+
<h2 style="margin:0 0 8px;color:${color}">${payload.jobName || 'SWL Notificación'}</h2>
|
|
195
|
+
<p style="margin:0;color:#666">Estado: <strong>${payload.status || 'unknown'}</strong></p>
|
|
196
|
+
</div>
|
|
197
|
+
${payload.output ? `<pre style="background:#f5f5f5;padding:12px;border-radius:4px;overflow-x:auto;font-size:13px">${_escapeHTML(payload.output.substring(0, 2000))}</pre>` : ''}
|
|
198
|
+
<hr style="border:none;border-top:1px solid #eee;margin:20px 0">
|
|
199
|
+
<p style="color:#999;font-size:12px">Enviado por SWL-SES Gateway — ${new Date().toISOString()}</p>
|
|
200
|
+
</body></html>`.trim();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
formatMessage(swlMessage) {
|
|
204
|
+
const payload = swlMessage.payload || swlMessage;
|
|
205
|
+
|
|
206
|
+
if (payload.jobName) {
|
|
207
|
+
const icon = payload.status === 'completed' ? '[OK]' : '[ERROR]';
|
|
208
|
+
const lines = [`${icon} ${payload.jobName}`, `Estado: ${payload.status}`];
|
|
209
|
+
if (payload.output) lines.push('', payload.output.substring(0, 1000));
|
|
210
|
+
return lines.join('\n');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return swlMessage.text || JSON.stringify(payload).substring(0, 500);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function _escapeHTML(str) {
|
|
218
|
+
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
module.exports = EmailAdapter;
|