@saulwade/swl-ses 1.3.8 → 1.4.1

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 (148) hide show
  1. package/CLAUDE.md +15 -6
  2. package/README.md +15 -14
  3. package/agentes/nemesis-auditor-swl.md +161 -0
  4. package/bin/swl-mcp-server.js +187 -187
  5. package/bin/swl-webhook-server.js +198 -0
  6. package/comandos/swl/.evolved.json +22 -22
  7. package/comandos/swl/adoptar-proyecto.md +21 -1
  8. package/comandos/swl/claudemd.md +14 -1
  9. package/comandos/swl/contribuir.md +233 -233
  10. package/comandos/swl/exportar-vault.md +108 -0
  11. package/comandos/swl/nemesis.md +122 -0
  12. package/comandos/swl/nuevo-proyecto.md +24 -2
  13. package/comandos/swl/salud.md +34 -0
  14. package/comandos/swl/verificar.md +45 -0
  15. package/gateway/adapters/base.js +109 -0
  16. package/gateway/adapters/discord.js +167 -0
  17. package/gateway/adapters/email.js +221 -0
  18. package/gateway/adapters/slack.js +192 -0
  19. package/gateway/adapters/telegram.js +183 -0
  20. package/gateway/adapters/webhook.js +113 -0
  21. package/gateway/adapters/whatsapp.js +214 -0
  22. package/gateway/agent-executor.js +322 -0
  23. package/gateway/command-relay.js +271 -0
  24. package/gateway/cron/jobs.js +263 -0
  25. package/gateway/cron/scheduler.js +322 -0
  26. package/gateway/cron/store.js +335 -0
  27. package/gateway/index.js +320 -0
  28. package/gateway/lib/event-channel.js +191 -0
  29. package/gateway/session.js +131 -0
  30. package/gateway/webhook-server.js +324 -0
  31. package/habilidades/backend-production-resilience/SKILL.md +288 -288
  32. package/habilidades/benchmark-memoria/SKILL.md +186 -186
  33. package/habilidades/build-errors-nextjs/SKILL.md +55 -1
  34. package/habilidades/diagrama-arquitectura/assets/template.html +276 -276
  35. package/habilidades/doubt-driven-review/SKILL.md +171 -171
  36. package/habilidades/doubt-driven-review/recursos/EXAMPLES.md +130 -130
  37. package/habilidades/eval-framework/SKILL.md +212 -212
  38. package/habilidades/extractor-de-aprendizajes/SKILL.md +20 -10
  39. package/habilidades/feynman-auditor-swl/SKILL.md +123 -0
  40. package/habilidades/feynman-auditor-swl/recursos/preguntas-language-agnostic.md +108 -0
  41. package/habilidades/harness-claude-code/SKILL.md +299 -299
  42. package/habilidades/infra-github-actions/SKILL.md +166 -166
  43. package/habilidades/legacy-code-rescue/SKILL.md +267 -267
  44. package/habilidades/manejo-errores/.evolved.json +8 -8
  45. package/habilidades/meta-skills-estandar/recursos/convencion-examples.md +93 -93
  46. package/habilidades/meta-skills-estandar/recursos/skills-as-agents.md +163 -163
  47. package/habilidades/nextjs-testing/SKILL.md +89 -5
  48. package/habilidades/node-experto/SKILL.md +37 -1
  49. package/habilidades/patrones-python/SKILL.md +229 -229
  50. package/habilidades/patrones-python/recursos/patrones-avanzados.md +469 -469
  51. package/habilidades/planear-fase/SKILL.md +319 -319
  52. package/habilidades/react-experto/SKILL.md +45 -4
  53. package/habilidades/release-semver/.evolved.json +8 -8
  54. package/habilidades/state-inconsistency-auditor-swl/SKILL.md +166 -0
  55. package/habilidades/state-inconsistency-auditor-swl/recursos/coupled-state-patterns.md +147 -0
  56. package/habilidades/tdd-workflow/SKILL.md +36 -4
  57. package/habilidades/testing-python/SKILL.md +340 -340
  58. package/habilidades/web-fetcher-routing/SKILL.md +75 -0
  59. package/hooks/claudemd-bloat-detector.js +161 -161
  60. package/hooks/inyeccion-contexto.js +8 -3
  61. package/hooks/lib/agent-routing.js +107 -107
  62. package/hooks/lib/auto-consolidator.js +335 -335
  63. package/hooks/lib/error-classifier.js +308 -308
  64. package/hooks/lib/merkle-audit.js +96 -96
  65. package/hooks/lib/provenance-tracker.js +191 -191
  66. package/hooks/lib/rate-limit-ip.js +177 -0
  67. package/hooks/lib/rate-limit-tracker.js +253 -253
  68. package/hooks/lib/resource-quota.js +122 -122
  69. package/hooks/lib/retry-jitter.js +165 -165
  70. package/hooks/lib/security-net.js +201 -0
  71. package/hooks/lib/skill-auditor.js +588 -588
  72. package/hooks/lib/sync-status.js +228 -228
  73. package/hooks/lib/taint-tracker.js +107 -107
  74. package/hooks/lib/text-similarity.js +241 -241
  75. package/hooks/lib/toon-compressor.js +245 -245
  76. package/hooks/lib/webhook-dedup.js +184 -0
  77. package/hooks/lib/webhook-verify.js +123 -0
  78. package/hooks/proteccion-rutas.js +120 -15
  79. package/hooks/registro-turnos.js +209 -209
  80. package/hooks/sugerir-regenerar-inventario.js +170 -170
  81. package/hooks/validar-formato-post-subagente.js +140 -140
  82. package/hooks/validar-memoria-hook.js +218 -218
  83. package/instintos/prompt-appendices.yaml +57 -57
  84. package/manifiestos/agent-output-schemas.json +57 -57
  85. package/manifiestos/modulos.json +31 -0
  86. package/manifiestos/skills-lock.json +1114 -1093
  87. package/package.json +6 -4
  88. package/plantillas/auditor-veto-template.md +105 -105
  89. package/plantillas/github-workflows/README.md +47 -47
  90. package/plantillas/github-workflows/release-please.yml +44 -44
  91. package/plantillas/github-workflows/swl-ci.yml +107 -107
  92. package/plantillas/github-workflows/swl-security.yml +51 -51
  93. package/plugin.json +2 -2
  94. package/reglas/analisis-previo-tareas-grandes.md +172 -172
  95. package/reglas/arreglar-al-detectar.md +147 -147
  96. package/reglas/fragmentos-compartidos.md +152 -152
  97. package/reglas/harness-claude-code.md +213 -213
  98. package/reglas/usar-context7.md +226 -226
  99. package/reglas/usar-sistema-swl.md +251 -0
  100. package/schemas/diary-entry.schema.json +80 -80
  101. package/scripts/audit-tools/audit-history.js +330 -0
  102. package/scripts/audit-tools/bundle-tracker.js +290 -0
  103. package/scripts/audit-tools/canary-monitor.js +352 -0
  104. package/scripts/audit-tools/code-profiler.js +605 -0
  105. package/scripts/audit-tools/dep-doctor.js +320 -0
  106. package/scripts/audit-tools/env-validator.js +206 -0
  107. package/scripts/audit-tools/lib/fs-walk.js +48 -0
  108. package/scripts/audit-tools/lib/output.js +23 -0
  109. package/scripts/audit-tools/migration-checker.js +392 -0
  110. package/scripts/audit-tools/pentest-scanner.js +1436 -0
  111. package/scripts/benchmark-memoria.js +167 -167
  112. package/scripts/comandos/skills.js +251 -2
  113. package/scripts/configurar-branch-protection.js +418 -418
  114. package/scripts/detectar-aprendizajes-duplicados.js +151 -151
  115. package/scripts/field-report.js +199 -199
  116. package/scripts/generar-checklists-consolidados.js +273 -273
  117. package/scripts/generar-inventario.js +420 -420
  118. package/scripts/generar-matriz-lenguajes.js +271 -271
  119. package/scripts/lib/artefactos-python.js +43 -43
  120. package/scripts/lib/benchmark-metrics.js +160 -160
  121. package/scripts/lib/budget-enforcer.js +252 -252
  122. package/scripts/lib/configurar-ci.js +380 -380
  123. package/scripts/lib/contadores-inventario.js +217 -217
  124. package/scripts/lib/detectar-stack-detallado.js +307 -307
  125. package/scripts/lib/diary-entry.js +234 -234
  126. package/scripts/lib/eval-metrics-store.js +218 -218
  127. package/scripts/lib/eval-quality.js +171 -171
  128. package/scripts/lib/eval-schemas.js +144 -144
  129. package/scripts/lib/eval-self-correct.js +106 -106
  130. package/scripts/lib/eval-validator.js +185 -185
  131. package/scripts/lib/jaccard-similarity.js +98 -98
  132. package/scripts/lib/longmemeval-runner.js +125 -125
  133. package/scripts/lib/npm-version.js +261 -261
  134. package/scripts/lib/paquetes-conocidos.js +50 -50
  135. package/scripts/lib/prompt-builder.js +264 -264
  136. package/scripts/lib/rrf-fusion.js +175 -175
  137. package/scripts/lib/scoring-instintos.js +277 -277
  138. package/scripts/lib/semantic-search.js +252 -252
  139. package/scripts/limpiar-artefactos-python.js +131 -131
  140. package/scripts/mcp-server/README.md +128 -128
  141. package/scripts/mcp-server/handlers.js +206 -206
  142. package/scripts/migrar-csv-a-array.js +168 -168
  143. package/scripts/migrar-fase-dominio.js +201 -201
  144. package/scripts/publicar.js +511 -511
  145. package/scripts/run-eval.js +141 -141
  146. package/scripts/validar-manifest.js +195 -195
  147. package/scripts/validar-userland-vacio.js +110 -110
  148. 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;