@saulwade/swl-ses 1.3.7 → 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.
Files changed (129) hide show
  1. package/CLAUDE.md +12 -4
  2. package/README.md +1 -1
  3. package/bin/swl-mcp-server.js +187 -187
  4. package/bin/swl-webhook-server.js +198 -0
  5. package/comandos/swl/.evolved.json +22 -22
  6. package/comandos/swl/adoptar-proyecto.md +21 -1
  7. package/comandos/swl/claudemd.md +14 -1
  8. package/comandos/swl/contribuir.md +233 -233
  9. package/comandos/swl/exportar-vault.md +207 -7
  10. package/comandos/swl/nuevo-proyecto.md +24 -2
  11. package/gateway/adapters/base.js +109 -0
  12. package/gateway/adapters/discord.js +167 -0
  13. package/gateway/adapters/email.js +221 -0
  14. package/gateway/adapters/slack.js +192 -0
  15. package/gateway/adapters/telegram.js +183 -0
  16. package/gateway/adapters/webhook.js +113 -0
  17. package/gateway/adapters/whatsapp.js +214 -0
  18. package/gateway/agent-executor.js +322 -0
  19. package/gateway/command-relay.js +271 -0
  20. package/gateway/cron/jobs.js +263 -0
  21. package/gateway/cron/scheduler.js +322 -0
  22. package/gateway/cron/store.js +335 -0
  23. package/gateway/index.js +320 -0
  24. package/gateway/lib/event-channel.js +191 -0
  25. package/gateway/session.js +131 -0
  26. package/gateway/webhook-server.js +324 -0
  27. package/habilidades/backend-production-resilience/SKILL.md +288 -288
  28. package/habilidades/benchmark-memoria/SKILL.md +186 -186
  29. package/habilidades/build-errors-nextjs/SKILL.md +55 -1
  30. package/habilidades/diagrama-arquitectura/assets/template.html +276 -276
  31. package/habilidades/doubt-driven-review/SKILL.md +171 -171
  32. package/habilidades/doubt-driven-review/recursos/EXAMPLES.md +130 -130
  33. package/habilidades/eval-framework/SKILL.md +212 -212
  34. package/habilidades/extractor-de-aprendizajes/SKILL.md +24 -10
  35. package/habilidades/harness-claude-code/SKILL.md +299 -299
  36. package/habilidades/infra-github-actions/SKILL.md +166 -166
  37. package/habilidades/legacy-code-rescue/SKILL.md +267 -267
  38. package/habilidades/manejo-errores/.evolved.json +8 -8
  39. package/habilidades/meta-skills-estandar/recursos/convencion-examples.md +93 -93
  40. package/habilidades/meta-skills-estandar/recursos/skills-as-agents.md +163 -163
  41. package/habilidades/nextjs-testing/SKILL.md +89 -5
  42. package/habilidades/node-experto/SKILL.md +37 -1
  43. package/habilidades/patrones-python/SKILL.md +229 -229
  44. package/habilidades/patrones-python/recursos/patrones-avanzados.md +469 -469
  45. package/habilidades/planear-fase/SKILL.md +319 -319
  46. package/habilidades/react-experto/SKILL.md +45 -4
  47. package/habilidades/release-semver/.evolved.json +8 -8
  48. package/habilidades/swl-claudemd/SKILL.md +15 -1
  49. package/habilidades/tdd-workflow/SKILL.md +36 -4
  50. package/habilidades/testing-python/SKILL.md +340 -340
  51. package/hooks/claudemd-bloat-detector.js +161 -161
  52. package/hooks/inyeccion-contexto.js +8 -3
  53. package/hooks/lib/agent-routing.js +107 -107
  54. package/hooks/lib/auto-consolidator.js +335 -335
  55. package/hooks/lib/error-classifier.js +308 -308
  56. package/hooks/lib/merkle-audit.js +96 -96
  57. package/hooks/lib/provenance-tracker.js +191 -191
  58. package/hooks/lib/rate-limit-ip.js +177 -0
  59. package/hooks/lib/rate-limit-tracker.js +253 -253
  60. package/hooks/lib/resource-quota.js +122 -122
  61. package/hooks/lib/retry-jitter.js +165 -165
  62. package/hooks/lib/skill-auditor.js +588 -588
  63. package/hooks/lib/sync-status.js +228 -228
  64. package/hooks/lib/taint-tracker.js +107 -107
  65. package/hooks/lib/text-similarity.js +241 -241
  66. package/hooks/lib/toon-compressor.js +245 -245
  67. package/hooks/lib/webhook-dedup.js +184 -0
  68. package/hooks/lib/webhook-verify.js +123 -0
  69. package/hooks/proteccion-rutas.js +120 -15
  70. package/hooks/registro-turnos.js +209 -209
  71. package/hooks/sugerir-regenerar-inventario.js +170 -170
  72. package/hooks/validar-formato-post-subagente.js +140 -140
  73. package/hooks/validar-memoria-hook.js +218 -218
  74. package/instintos/prompt-appendices.yaml +57 -57
  75. package/manifiestos/agent-output-schemas.json +57 -57
  76. package/manifiestos/modulos.json +1 -0
  77. package/manifiestos/skills-lock.json +37 -37
  78. package/package.json +5 -3
  79. package/plantillas/auditor-veto-template.md +105 -105
  80. package/plantillas/github-workflows/README.md +47 -47
  81. package/plantillas/github-workflows/release-please.yml +44 -44
  82. package/plantillas/github-workflows/swl-ci.yml +107 -107
  83. package/plantillas/github-workflows/swl-security.yml +51 -51
  84. package/plugin.json +1 -1
  85. package/reglas/analisis-previo-tareas-grandes.md +172 -172
  86. package/reglas/arreglar-al-detectar.md +147 -147
  87. package/reglas/fragmentos-compartidos.md +152 -152
  88. package/reglas/harness-claude-code.md +213 -213
  89. package/reglas/usar-context7.md +226 -226
  90. package/reglas/usar-sistema-swl.md +251 -0
  91. package/schemas/diary-entry.schema.json +80 -80
  92. package/scripts/benchmark-memoria.js +167 -167
  93. package/scripts/comandos/skills.js +251 -2
  94. package/scripts/configurar-branch-protection.js +418 -418
  95. package/scripts/detectar-aprendizajes-duplicados.js +151 -151
  96. package/scripts/field-report.js +199 -199
  97. package/scripts/generar-checklists-consolidados.js +273 -273
  98. package/scripts/generar-inventario.js +420 -420
  99. package/scripts/generar-matriz-lenguajes.js +271 -271
  100. package/scripts/lib/artefactos-python.js +43 -43
  101. package/scripts/lib/benchmark-metrics.js +160 -160
  102. package/scripts/lib/budget-enforcer.js +252 -252
  103. package/scripts/lib/configurar-ci.js +380 -380
  104. package/scripts/lib/contadores-inventario.js +217 -217
  105. package/scripts/lib/detectar-stack-detallado.js +307 -307
  106. package/scripts/lib/diary-entry.js +234 -234
  107. package/scripts/lib/eval-metrics-store.js +218 -218
  108. package/scripts/lib/eval-quality.js +171 -171
  109. package/scripts/lib/eval-schemas.js +144 -144
  110. package/scripts/lib/eval-self-correct.js +106 -106
  111. package/scripts/lib/eval-validator.js +185 -185
  112. package/scripts/lib/jaccard-similarity.js +98 -98
  113. package/scripts/lib/longmemeval-runner.js +125 -125
  114. package/scripts/lib/npm-version.js +261 -261
  115. package/scripts/lib/paquetes-conocidos.js +50 -50
  116. package/scripts/lib/prompt-builder.js +264 -264
  117. package/scripts/lib/rrf-fusion.js +175 -175
  118. package/scripts/lib/scoring-instintos.js +277 -277
  119. package/scripts/lib/semantic-search.js +252 -252
  120. package/scripts/limpiar-artefactos-python.js +131 -131
  121. package/scripts/mcp-server/README.md +128 -128
  122. package/scripts/mcp-server/handlers.js +206 -206
  123. package/scripts/migrar-csv-a-array.js +168 -168
  124. package/scripts/migrar-fase-dominio.js +201 -201
  125. package/scripts/publicar.js +511 -511
  126. package/scripts/run-eval.js +141 -141
  127. package/scripts/validar-manifest.js +195 -195
  128. package/scripts/validar-userland-vacio.js +110 -110
  129. package/scripts/verificar-release.js +110 -0
@@ -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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
219
+ }
220
+
221
+ module.exports = EmailAdapter;
@@ -0,0 +1,192 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Slack Adapter — Adaptador para Slack via Bolt SDK.
5
+ *
6
+ * Soporta:
7
+ * - Envío de notificaciones con Blocks (formato rico)
8
+ * - Recepción de comandos slash y mensajes directos
9
+ * - Threads para conversaciones agrupadas
10
+ * - Rate limiting (Slack: 1 msg/seg por canal)
11
+ *
12
+ * Dependencia: @slack/bolt (npm install @slack/bolt)
13
+ * Si no está instalada, el adaptador se desactiva silenciosamente.
14
+ *
15
+ * @module gateway/adapters/slack
16
+ */
17
+
18
+ const BaseAdapter = require('./base');
19
+
20
+ /** Límite de caracteres por mensaje de Slack. */
21
+ const MAX_MSG_LENGTH = 3000;
22
+
23
+ /** Comandos reconocidos en mensajes directos al bot. */
24
+ const COMMANDS = {
25
+ 'status': 'Estado del sistema SWL',
26
+ 'salud': 'Ejecutar diagnóstico de salud',
27
+ 'sesiones': 'Listar últimas sesiones',
28
+ 'approve': 'Aprobar operación pendiente',
29
+ 'reject': 'Rechazar operación pendiente',
30
+ };
31
+
32
+ class SlackAdapter extends BaseAdapter {
33
+ constructor(config) {
34
+ super('slack', config);
35
+ this._app = null;
36
+ }
37
+
38
+ async start() {
39
+ let Bolt;
40
+ try {
41
+ Bolt = require('@slack/bolt');
42
+ } catch (_) {
43
+ console.log('[slack] @slack/bolt no instalado. Ejecutar: npm install @slack/bolt');
44
+ return;
45
+ }
46
+
47
+ const token = this.config.token || process.env.SLACK_BOT_TOKEN;
48
+ const sigSecret = this.config.signingSecret || process.env.SLACK_SIGNING_SECRET;
49
+ const appToken = this.config.appToken || process.env.SLACK_APP_TOKEN;
50
+
51
+ if (!token) {
52
+ console.log('[slack] Token no configurado. Establecer SLACK_BOT_TOKEN.');
53
+ return;
54
+ }
55
+
56
+ try {
57
+ const opts = {
58
+ token,
59
+ signingSecret: sigSecret || 'dummy',
60
+ };
61
+
62
+ // Socket mode (recomendado, sin servidor público)
63
+ if (appToken) {
64
+ opts.socketMode = true;
65
+ opts.appToken = appToken;
66
+ }
67
+
68
+ this._app = new Bolt.App(opts);
69
+
70
+ // Escuchar mensajes directos al bot
71
+ this._app.message(async ({ message, say }) => {
72
+ if (message.subtype) return; // Ignorar ediciones, joins, etc.
73
+
74
+ const text = message.text || '';
75
+ const parts = text.trim().split(/\s+/);
76
+ const cmdWord = parts[0].toLowerCase().replace(/^\//, '');
77
+ const command = COMMANDS[cmdWord] ? cmdWord : null;
78
+ const args = command ? parts.slice(1).join(' ') : '';
79
+
80
+ this._emitMessage({
81
+ chatId: message.channel,
82
+ userId: message.user,
83
+ userName: message.user,
84
+ text: text,
85
+ command: command ? `/${command}` : null,
86
+ args: args,
87
+ threadTs: message.thread_ts || message.ts,
88
+ raw: message,
89
+ });
90
+ });
91
+
92
+ if (appToken) {
93
+ await this._app.start();
94
+ }
95
+
96
+ this.running = true;
97
+ console.log(`[slack] Adaptador iniciado (${appToken ? 'socket' : 'events'} mode).`);
98
+ } catch (err) {
99
+ console.error(`[slack] Error al iniciar: ${err.message}`);
100
+ }
101
+ }
102
+
103
+ async stop() {
104
+ if (this._app) {
105
+ try { await this._app.stop(); } catch (_) {}
106
+ this._app = null;
107
+ }
108
+ this.running = false;
109
+ console.log('[slack] Adaptador detenido.');
110
+ }
111
+
112
+ async send(message) {
113
+ if (!this._app) return false;
114
+
115
+ const channel = message.chatId || this.config.defaultChannel || process.env.SLACK_DEFAULT_CHANNEL;
116
+ if (!channel) return false;
117
+
118
+ const text = this.formatMessage(message);
119
+ const blocks = this._buildBlocks(message);
120
+
121
+ try {
122
+ await this._app.client.chat.postMessage({
123
+ token: this.config.token || process.env.SLACK_BOT_TOKEN,
124
+ channel,
125
+ text,
126
+ blocks,
127
+ unfurl_links: false,
128
+ });
129
+ return true;
130
+ } catch (err) {
131
+ console.error(`[slack] Error enviando: ${err.message}`);
132
+ return false;
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Construye Slack Blocks para notificaciones estructuradas.
138
+ * @private
139
+ */
140
+ _buildBlocks(message) {
141
+ const payload = message.payload || message;
142
+ if (!payload.jobName && !payload.title) return undefined;
143
+
144
+ const isError = payload.status === 'error';
145
+ const icon = isError ? ':x:' : ':white_check_mark:';
146
+ const blocks = [];
147
+
148
+ blocks.push({
149
+ type: 'header',
150
+ text: { type: 'plain_text', text: `${payload.jobName || payload.title || 'SWL'}` },
151
+ });
152
+
153
+ blocks.push({
154
+ type: 'section',
155
+ fields: [
156
+ { type: 'mrkdwn', text: `*Estado:*\n${icon} \`${payload.status || 'unknown'}\`` },
157
+ { type: 'mrkdwn', text: `*Fuente:*\nSWL-SES Gateway` },
158
+ ],
159
+ });
160
+
161
+ if (payload.output) {
162
+ blocks.push({
163
+ type: 'section',
164
+ text: { type: 'mrkdwn', text: '```' + payload.output.substring(0, 2500) + '```' },
165
+ });
166
+ }
167
+
168
+ blocks.push({ type: 'divider' });
169
+
170
+ return blocks;
171
+ }
172
+
173
+ formatMessage(swlMessage) {
174
+ const payload = swlMessage.payload || swlMessage;
175
+
176
+ if (payload.jobName) {
177
+ const icon = payload.status === 'completed' ? ':white_check_mark:' : ':x:';
178
+ const lines = [
179
+ `${icon} *${payload.jobName}*`,
180
+ `Estado: \`${payload.status}\``,
181
+ ];
182
+ if (payload.output) {
183
+ lines.push('```' + payload.output.substring(0, 1000) + '```');
184
+ }
185
+ return lines.join('\n');
186
+ }
187
+
188
+ return swlMessage.text || JSON.stringify(payload).substring(0, 500);
189
+ }
190
+ }
191
+
192
+ module.exports = SlackAdapter;
@@ -0,0 +1,183 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Telegram Adapter — Adaptador de plataforma para Telegram Bot API.
5
+ *
6
+ * Usa polling HTTP (sin webhook, sin servidor público). Soporta:
7
+ * - Recepción de mensajes y comandos (/status, /salud, /approve)
8
+ * - Envío de notificaciones con formato Markdown
9
+ * - Batching de mensajes largos (split a 4096 chars)
10
+ * - Rate limiting (30 msg/seg Telegram limit)
11
+ * - Filtro de usuarios permitidos (allowedUsers)
12
+ *
13
+ * Dependencia: node-telegram-bot-api (npm install node-telegram-bot-api)
14
+ * Si no está instalada, el adaptador se desactiva silenciosamente.
15
+ *
16
+ * Inspirado en Hermes Agent (gateway/platforms/telegram.py).
17
+ *
18
+ * @module gateway/adapters/telegram
19
+ */
20
+
21
+ const BaseAdapter = require('./base');
22
+
23
+ /** Límite de caracteres por mensaje de Telegram. */
24
+ const MAX_MSG_LENGTH = 4096;
25
+
26
+ /** Comandos reconocidos por el bot. */
27
+ const COMMANDS = {
28
+ '/status': 'Estado del sistema SWL',
29
+ '/salud': 'Ejecutar diagnóstico de salud',
30
+ '/sesiones': 'Listar últimas sesiones',
31
+ '/approve': 'Aprobar operación pendiente',
32
+ '/reject': 'Rechazar operación pendiente',
33
+ '/help': 'Mostrar ayuda',
34
+ };
35
+
36
+ class TelegramAdapter extends BaseAdapter {
37
+ constructor(config) {
38
+ super('telegram', config);
39
+ this._bot = null;
40
+ this._allowedUsers = new Set(config.allowedUsers || []);
41
+ }
42
+
43
+ async start() {
44
+ let TelegramBot;
45
+ try {
46
+ TelegramBot = require('node-telegram-bot-api');
47
+ } catch (_) {
48
+ console.log('[telegram] node-telegram-bot-api no instalado. Ejecutar: npm install node-telegram-bot-api');
49
+ return;
50
+ }
51
+
52
+ const token = this.config.token || process.env.TELEGRAM_BOT_TOKEN;
53
+ if (!token) {
54
+ console.log('[telegram] Token no configurado. Establecer TELEGRAM_BOT_TOKEN.');
55
+ return;
56
+ }
57
+
58
+ this._bot = new TelegramBot(token, { polling: true });
59
+ this.running = true;
60
+
61
+ this._bot.on('message', (msg) => {
62
+ // Filtrar usuarios no permitidos
63
+ if (this._allowedUsers.size > 0 && !this._allowedUsers.has(String(msg.from.id))) {
64
+ return;
65
+ }
66
+
67
+ const text = msg.text || '';
68
+
69
+ // Parsear comando
70
+ const parts = text.split(/\s+/);
71
+ const command = parts[0];
72
+ const args = parts.slice(1).join(' ');
73
+
74
+ this._emitMessage({
75
+ chatId: String(msg.chat.id),
76
+ userId: String(msg.from.id),
77
+ userName: msg.from.username || msg.from.first_name || 'unknown',
78
+ text: text,
79
+ command: COMMANDS[command] ? command : null,
80
+ args: args,
81
+ raw: msg,
82
+ });
83
+ });
84
+
85
+ this._bot.on('polling_error', (err) => {
86
+ // Log silencioso — no crashear
87
+ if (err.code === 'ETELEGRAM' && err.message.includes('409')) {
88
+ console.error('[telegram] Conflicto: otro bot con el mismo token. Deteniendo polling.');
89
+ this.stop();
90
+ }
91
+ });
92
+
93
+ console.log('[telegram] Adaptador iniciado (polling mode).');
94
+ }
95
+
96
+ async stop() {
97
+ if (this._bot) {
98
+ try { await this._bot.stopPolling(); } catch (_) {}
99
+ this._bot = null;
100
+ }
101
+ this.running = false;
102
+ console.log('[telegram] Adaptador detenido.');
103
+ }
104
+
105
+ async send(message) {
106
+ if (!this._bot) return false;
107
+
108
+ const chatId = message.chatId || this.config.homeChat;
109
+ if (!chatId) return false;
110
+
111
+ const text = this.formatMessage(message);
112
+
113
+ // Batching: split en chunks de MAX_MSG_LENGTH
114
+ const chunks = _splitMessage(text, MAX_MSG_LENGTH);
115
+
116
+ for (const chunk of chunks) {
117
+ try {
118
+ await this._bot.sendMessage(chatId, chunk, {
119
+ parse_mode: 'Markdown',
120
+ disable_web_page_preview: true,
121
+ });
122
+ } catch (err) {
123
+ // Retry sin Markdown si falla el parse
124
+ try {
125
+ await this._bot.sendMessage(chatId, chunk);
126
+ } catch (_) {
127
+ return false;
128
+ }
129
+ }
130
+ }
131
+
132
+ return true;
133
+ }
134
+
135
+ formatMessage(swlMessage) {
136
+ const payload = swlMessage.payload || swlMessage;
137
+
138
+ if (payload.jobName) {
139
+ // Notificación de cron
140
+ const status = payload.status === 'completed' ? '✓' : '✗';
141
+ const lines = [
142
+ `${status} *${payload.jobName}*`,
143
+ `Estado: \`${payload.status}\``,
144
+ ];
145
+ if (payload.output) {
146
+ lines.push('```', payload.output.substring(0, 1000), '```');
147
+ }
148
+ return lines.join('\n');
149
+ }
150
+
151
+ // Mensaje genérico
152
+ return swlMessage.text || JSON.stringify(payload).substring(0, 500);
153
+ }
154
+ }
155
+
156
+ /**
157
+ * Divide un mensaje largo en chunks respetando el límite de caracteres.
158
+ * Intenta dividir en líneas completas.
159
+ */
160
+ function _splitMessage(text, maxLength) {
161
+ if (text.length <= maxLength) return [text];
162
+
163
+ const chunks = [];
164
+ let remaining = text;
165
+
166
+ while (remaining.length > 0) {
167
+ if (remaining.length <= maxLength) {
168
+ chunks.push(remaining);
169
+ break;
170
+ }
171
+
172
+ // Buscar el último newline dentro del límite
173
+ let splitAt = remaining.lastIndexOf('\n', maxLength);
174
+ if (splitAt < maxLength * 0.3) splitAt = maxLength; // Si no hay newline útil, cortar
175
+
176
+ chunks.push(remaining.substring(0, splitAt));
177
+ remaining = remaining.substring(splitAt).trimStart();
178
+ }
179
+
180
+ return chunks;
181
+ }
182
+
183
+ module.exports = TelegramAdapter;
@@ -0,0 +1,113 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Webhook Adapter — Adaptador genérico para webhooks HTTP.
5
+ *
6
+ * Envía notificaciones a un endpoint configurable via POST.
7
+ * No recibe mensajes (unidireccional: SWL → Webhook).
8
+ *
9
+ * Compatible con: Slack Incoming Webhooks, Microsoft Teams, ntfy.sh,
10
+ * Pushover, cualquier endpoint que acepte JSON POST.
11
+ *
12
+ * @module gateway/adapters/webhook
13
+ */
14
+
15
+ const BaseAdapter = require('./base');
16
+ const https = require('https');
17
+ const http = require('http');
18
+ const { URL } = require('url');
19
+
20
+ class WebhookAdapter extends BaseAdapter {
21
+ constructor(config) {
22
+ super('webhook', config);
23
+ }
24
+
25
+ async start() {
26
+ const url = this.config.url || process.env.WEBHOOK_URL;
27
+ if (!url) {
28
+ console.log('[webhook] URL no configurada. Establecer WEBHOOK_URL.');
29
+ return;
30
+ }
31
+
32
+ this.running = true;
33
+ console.log('[webhook] Adaptador iniciado (unidireccional).');
34
+ }
35
+
36
+ async stop() {
37
+ this.running = false;
38
+ console.log('[webhook] Adaptador detenido.');
39
+ }
40
+
41
+ async send(message) {
42
+ const url = this.config.url || process.env.WEBHOOK_URL;
43
+ if (!url) return false;
44
+
45
+ const payload = {
46
+ source: 'swl-ses',
47
+ timestamp: new Date().toISOString(),
48
+ ...message,
49
+ text: this.formatMessage(message),
50
+ };
51
+
52
+ try {
53
+ await _postJSON(url, payload, this.config.secret || process.env.WEBHOOK_SECRET);
54
+ return true;
55
+ } catch (err) {
56
+ console.error(`[webhook] Error: ${err.message}`);
57
+ return false;
58
+ }
59
+ }
60
+ }
61
+
62
+ /**
63
+ * POST JSON a una URL con timeout y HMAC opcional.
64
+ * @param {string} url
65
+ * @param {object} data
66
+ * @param {string} [secret]
67
+ * @returns {Promise<string>}
68
+ */
69
+ function _postJSON(url, data, secret) {
70
+ return new Promise((resolve, reject) => {
71
+ const parsed = new URL(url);
72
+ const client = parsed.protocol === 'https:' ? https : http;
73
+ const body = JSON.stringify(data);
74
+ const headers = {
75
+ 'Content-Type': 'application/json',
76
+ 'Content-Length': Buffer.byteLength(body),
77
+ 'User-Agent': 'SWL-SES-Gateway/1.0',
78
+ };
79
+
80
+ // HMAC signature si hay secret
81
+ if (secret) {
82
+ const crypto = require('crypto');
83
+ const sig = crypto.createHmac('sha256', secret).update(body).digest('hex');
84
+ headers['X-SWL-Signature'] = `sha256=${sig}`;
85
+ }
86
+
87
+ const req = client.request({
88
+ hostname: parsed.hostname,
89
+ port: parsed.port,
90
+ path: parsed.pathname + parsed.search,
91
+ method: 'POST',
92
+ headers,
93
+ timeout: 10000,
94
+ }, (res) => {
95
+ let responseBody = '';
96
+ res.on('data', chunk => responseBody += chunk);
97
+ res.on('end', () => {
98
+ if (res.statusCode >= 200 && res.statusCode < 300) {
99
+ resolve(responseBody);
100
+ } else {
101
+ reject(new Error(`HTTP ${res.statusCode}: ${responseBody.substring(0, 200)}`));
102
+ }
103
+ });
104
+ });
105
+
106
+ req.on('error', reject);
107
+ req.on('timeout', () => { req.destroy(); reject(new Error('Timeout')); });
108
+ req.write(body);
109
+ req.end();
110
+ });
111
+ }
112
+
113
+ module.exports = WebhookAdapter;