@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
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Cron Store — Persistencia de jobs CRON en JSON.
|
|
5
|
+
*
|
|
6
|
+
* Almacena jobs en .planning/cron/jobs.json con escritura atómica.
|
|
7
|
+
* Cada job tiene: id, name, schedule, command, deliver, status, nextRun, lastRunAt.
|
|
8
|
+
*
|
|
9
|
+
* @module gateway/cron/store
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
|
|
15
|
+
const { atomicWriteJSON } = require('../../hooks/lib/atomic-write');
|
|
16
|
+
const { parseSchedule, computeNextRun } = require('./jobs');
|
|
17
|
+
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Constantes
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
const CRON_DIR = '.planning/cron';
|
|
23
|
+
const JOBS_FILE = 'jobs.json';
|
|
24
|
+
const LOG_FILE = 'log.json';
|
|
25
|
+
const AGENT_STATE_FILE = 'agent-state.json';
|
|
26
|
+
const MAX_LOG_ENTRIES = 500;
|
|
27
|
+
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// Helpers
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
function ensureDir(baseDir) {
|
|
33
|
+
const dir = path.join(baseDir, CRON_DIR);
|
|
34
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
35
|
+
return dir;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function jobsPath(baseDir) {
|
|
39
|
+
return path.join(ensureDir(baseDir), JOBS_FILE);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function logPath(baseDir) {
|
|
43
|
+
return path.join(ensureDir(baseDir), LOG_FILE);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function agentStatePath(baseDir) {
|
|
47
|
+
return path.join(ensureDir(baseDir), AGENT_STATE_FILE);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
// CRUD de jobs
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Carga todos los jobs.
|
|
56
|
+
* @param {string} baseDir
|
|
57
|
+
* @returns {object[]}
|
|
58
|
+
*/
|
|
59
|
+
function loadJobs(baseDir) {
|
|
60
|
+
const p = jobsPath(baseDir);
|
|
61
|
+
try {
|
|
62
|
+
if (!fs.existsSync(p)) return [];
|
|
63
|
+
return JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
64
|
+
} catch (_) {
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Persiste todos los jobs atómicamente.
|
|
71
|
+
* @param {string} baseDir
|
|
72
|
+
* @param {object[]} jobs
|
|
73
|
+
*/
|
|
74
|
+
function saveJobs(baseDir, jobs) {
|
|
75
|
+
atomicWriteJSON(jobsPath(baseDir), jobs);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Agrega un job nuevo.
|
|
80
|
+
*
|
|
81
|
+
* @param {string} baseDir
|
|
82
|
+
* @param {object} opts
|
|
83
|
+
* @param {string} opts.name - Nombre descriptivo.
|
|
84
|
+
* @param {string} opts.schedule - Schedule string (ej: "every 2h", "0 9 * * 1-5").
|
|
85
|
+
* @param {string} opts.command - Comando a ejecutar.
|
|
86
|
+
* @param {string} [opts.deliver='local'] - Destino: "local", "telegram", "discord".
|
|
87
|
+
* @param {boolean} [opts.enabled=true]
|
|
88
|
+
* @returns {object} Job creado.
|
|
89
|
+
*/
|
|
90
|
+
function addJob(baseDir, opts) {
|
|
91
|
+
const jobs = loadJobs(baseDir);
|
|
92
|
+
|
|
93
|
+
const parsed = parseSchedule(opts.schedule);
|
|
94
|
+
const id = `cron-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
|
|
95
|
+
|
|
96
|
+
const job = {
|
|
97
|
+
id,
|
|
98
|
+
name: opts.name,
|
|
99
|
+
schedule: opts.schedule,
|
|
100
|
+
parsedSchedule: parsed,
|
|
101
|
+
command: opts.command,
|
|
102
|
+
deliver: opts.deliver || 'local',
|
|
103
|
+
status: opts.enabled !== false ? 'scheduled' : 'paused',
|
|
104
|
+
nextRun: null,
|
|
105
|
+
lastRunAt: null,
|
|
106
|
+
createdAt: new Date().toISOString(),
|
|
107
|
+
repeat: parsed.kind === 'once' ? { times: 1, completed: 0 } : null,
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// Calcular próxima ejecución
|
|
111
|
+
job.nextRun = computeNextRun(job);
|
|
112
|
+
|
|
113
|
+
jobs.push(job);
|
|
114
|
+
saveJobs(baseDir, jobs);
|
|
115
|
+
return job;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Elimina un job por ID.
|
|
120
|
+
* @param {string} baseDir
|
|
121
|
+
* @param {string} jobId
|
|
122
|
+
* @returns {boolean}
|
|
123
|
+
*/
|
|
124
|
+
function removeJob(baseDir, jobId) {
|
|
125
|
+
const jobs = loadJobs(baseDir);
|
|
126
|
+
const idx = jobs.findIndex(j => j.id === jobId);
|
|
127
|
+
if (idx < 0) return false;
|
|
128
|
+
jobs.splice(idx, 1);
|
|
129
|
+
saveJobs(baseDir, jobs);
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Pausa un job.
|
|
135
|
+
* @param {string} baseDir
|
|
136
|
+
* @param {string} jobId
|
|
137
|
+
* @returns {boolean}
|
|
138
|
+
*/
|
|
139
|
+
function pauseJob(baseDir, jobId) {
|
|
140
|
+
const jobs = loadJobs(baseDir);
|
|
141
|
+
const job = jobs.find(j => j.id === jobId);
|
|
142
|
+
if (!job) return false;
|
|
143
|
+
job.status = 'paused';
|
|
144
|
+
saveJobs(baseDir, jobs);
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Reanuda un job pausado.
|
|
150
|
+
* @param {string} baseDir
|
|
151
|
+
* @param {string} jobId
|
|
152
|
+
* @returns {boolean}
|
|
153
|
+
*/
|
|
154
|
+
function resumeJob(baseDir, jobId) {
|
|
155
|
+
const jobs = loadJobs(baseDir);
|
|
156
|
+
const job = jobs.find(j => j.id === jobId);
|
|
157
|
+
if (!job) return false;
|
|
158
|
+
job.status = 'scheduled';
|
|
159
|
+
if (!job.nextRun) job.nextRun = computeNextRun(job);
|
|
160
|
+
saveJobs(baseDir, jobs);
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Marca un job como ejecutado y calcula su próxima ejecución.
|
|
166
|
+
* @param {string} baseDir
|
|
167
|
+
* @param {string} jobId
|
|
168
|
+
* @param {object} [result={}] - Resultado de la ejecución.
|
|
169
|
+
*/
|
|
170
|
+
function markExecuted(baseDir, jobId, result = {}) {
|
|
171
|
+
const jobs = loadJobs(baseDir);
|
|
172
|
+
const job = jobs.find(j => j.id === jobId);
|
|
173
|
+
if (!job) return;
|
|
174
|
+
|
|
175
|
+
job.lastRunAt = new Date().toISOString();
|
|
176
|
+
|
|
177
|
+
// Actualizar repeticiones
|
|
178
|
+
if (job.repeat) {
|
|
179
|
+
job.repeat.completed = (job.repeat.completed || 0) + 1;
|
|
180
|
+
if (job.repeat.times !== null && job.repeat.completed >= job.repeat.times) {
|
|
181
|
+
job.status = 'completed';
|
|
182
|
+
job.nextRun = null;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Calcular próxima ejecución si aún está activo
|
|
187
|
+
if (job.status === 'scheduled') {
|
|
188
|
+
job.nextRun = computeNextRun(job);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
saveJobs(baseDir, jobs);
|
|
192
|
+
|
|
193
|
+
// Registrar en log
|
|
194
|
+
_appendLog(baseDir, {
|
|
195
|
+
jobId,
|
|
196
|
+
jobName: job.name,
|
|
197
|
+
executedAt: job.lastRunAt,
|
|
198
|
+
status: result.status || 'completed',
|
|
199
|
+
output: (result.output || '').substring(0, 1000),
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ---------------------------------------------------------------------------
|
|
204
|
+
// Log de ejecuciones
|
|
205
|
+
// ---------------------------------------------------------------------------
|
|
206
|
+
|
|
207
|
+
function _appendLog(baseDir, entry) {
|
|
208
|
+
const p = logPath(baseDir);
|
|
209
|
+
let log = [];
|
|
210
|
+
try {
|
|
211
|
+
if (fs.existsSync(p)) {
|
|
212
|
+
log = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
213
|
+
}
|
|
214
|
+
} catch (e) { /* log malformado — resetear */ }
|
|
215
|
+
|
|
216
|
+
log.push(entry);
|
|
217
|
+
|
|
218
|
+
// Mantener solo las últimas MAX_LOG_ENTRIES entradas
|
|
219
|
+
if (log.length > MAX_LOG_ENTRIES) {
|
|
220
|
+
log = log.slice(-MAX_LOG_ENTRIES);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
atomicWriteJSON(p, log);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Lee el log de ejecuciones.
|
|
228
|
+
* @param {string} baseDir
|
|
229
|
+
* @param {number} [limit=20]
|
|
230
|
+
* @returns {object[]}
|
|
231
|
+
*/
|
|
232
|
+
function readLog(baseDir, limit = 20) {
|
|
233
|
+
const p = logPath(baseDir);
|
|
234
|
+
try {
|
|
235
|
+
if (!fs.existsSync(p)) return [];
|
|
236
|
+
const log = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
237
|
+
return log.slice(-limit);
|
|
238
|
+
} catch (_) {
|
|
239
|
+
return [];
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// ---------------------------------------------------------------------------
|
|
244
|
+
// State per agent (portado de rowboat agent-schedule/state-repo.ts)
|
|
245
|
+
// ---------------------------------------------------------------------------
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Carga el estado histórico de un agente cron.
|
|
249
|
+
* Rastrea: lastRunAt, consecutiveErrors, runCount, lastError.
|
|
250
|
+
*
|
|
251
|
+
* @param {string} baseDir
|
|
252
|
+
* @param {string} jobId
|
|
253
|
+
* @returns {{ lastRunAt: string|null, consecutiveErrors: number, runCount: number, lastError: string|null, lastStatus: string|null }}
|
|
254
|
+
*/
|
|
255
|
+
function getAgentState(baseDir, jobId) {
|
|
256
|
+
const p = agentStatePath(baseDir);
|
|
257
|
+
const DEFAULT = { lastRunAt: null, consecutiveErrors: 0, runCount: 0, lastError: null, lastStatus: null };
|
|
258
|
+
try {
|
|
259
|
+
if (!fs.existsSync(p)) return { ...DEFAULT };
|
|
260
|
+
const all = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
261
|
+
return all[jobId] ? { ...DEFAULT, ...all[jobId] } : { ...DEFAULT };
|
|
262
|
+
} catch (_) {
|
|
263
|
+
return { ...DEFAULT };
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Actualiza el estado de un agente cron tras una ejecución.
|
|
269
|
+
*
|
|
270
|
+
* @param {string} baseDir
|
|
271
|
+
* @param {string} jobId
|
|
272
|
+
* @param {{ status: 'completed'|'failed'|'skipped', error?: string }} result
|
|
273
|
+
*/
|
|
274
|
+
function updateAgentState(baseDir, jobId, result) {
|
|
275
|
+
const p = agentStatePath(baseDir);
|
|
276
|
+
let all = {};
|
|
277
|
+
try {
|
|
278
|
+
if (fs.existsSync(p)) all = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
279
|
+
} catch (e) { /* agent-state malformado — resetear */ }
|
|
280
|
+
|
|
281
|
+
const prev = all[jobId] || { lastRunAt: null, consecutiveErrors: 0, runCount: 0, lastError: null, lastStatus: null };
|
|
282
|
+
|
|
283
|
+
all[jobId] = {
|
|
284
|
+
lastRunAt: new Date().toISOString(),
|
|
285
|
+
runCount: (prev.runCount || 0) + 1,
|
|
286
|
+
lastStatus: result.status,
|
|
287
|
+
lastError: result.status === 'failed' ? (result.error || 'unknown error') : null,
|
|
288
|
+
consecutiveErrors: result.status === 'failed'
|
|
289
|
+
? (prev.consecutiveErrors || 0) + 1
|
|
290
|
+
: 0, // reset en éxito
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
atomicWriteJSON(p, all);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Lee el historial resumido de todos los agentes cron.
|
|
298
|
+
* Útil para diagnóstico: ver qué jobs tienen errores consecutivos.
|
|
299
|
+
*
|
|
300
|
+
* @param {string} baseDir
|
|
301
|
+
* @returns {Array<{ jobId: string, jobName: string, state: object }>}
|
|
302
|
+
*/
|
|
303
|
+
function getAllAgentStates(baseDir) {
|
|
304
|
+
const p = agentStatePath(baseDir);
|
|
305
|
+
try {
|
|
306
|
+
if (!fs.existsSync(p)) return [];
|
|
307
|
+
const all = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
308
|
+
const jobs = loadJobs(baseDir);
|
|
309
|
+
return Object.entries(all).map(([jobId, state]) => {
|
|
310
|
+
const job = jobs.find(j => j.id === jobId);
|
|
311
|
+
return { jobId, jobName: job?.name || jobId, state };
|
|
312
|
+
});
|
|
313
|
+
} catch (_) {
|
|
314
|
+
return [];
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// ---------------------------------------------------------------------------
|
|
319
|
+
// Exports
|
|
320
|
+
// ---------------------------------------------------------------------------
|
|
321
|
+
|
|
322
|
+
module.exports = {
|
|
323
|
+
loadJobs,
|
|
324
|
+
saveJobs,
|
|
325
|
+
addJob,
|
|
326
|
+
removeJob,
|
|
327
|
+
pauseJob,
|
|
328
|
+
resumeJob,
|
|
329
|
+
markExecuted,
|
|
330
|
+
readLog,
|
|
331
|
+
getAgentState,
|
|
332
|
+
updateAgentState,
|
|
333
|
+
getAllAgentStates,
|
|
334
|
+
CRON_DIR,
|
|
335
|
+
};
|
package/gateway/index.js
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Gateway Runner — Orquestador multi-plataforma para SWL-SES.
|
|
6
|
+
*
|
|
7
|
+
* Conecta el sistema SWL con plataformas de mensajería externas
|
|
8
|
+
* (Telegram, Discord, Webhook) para notificaciones bidireccionales.
|
|
9
|
+
*
|
|
10
|
+
* Arquitectura:
|
|
11
|
+
* Claude Code (SWL) ←→ .planning/comms/ ←→ Gateway ←→ Plataformas
|
|
12
|
+
*
|
|
13
|
+
* Flujos:
|
|
14
|
+
* SWL → Plataforma: Polling de .planning/comms/ cada N segundos
|
|
15
|
+
* Plataforma → SWL: Escribir comandos en .planning/comms/
|
|
16
|
+
*
|
|
17
|
+
* Inspirado en Hermes Agent (gateway/run.py).
|
|
18
|
+
*
|
|
19
|
+
* Uso:
|
|
20
|
+
* node gateway/index.js [baseDir]
|
|
21
|
+
* node gateway/index.js --config manifiestos/gateway-config.json
|
|
22
|
+
*
|
|
23
|
+
* @module gateway/index
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const fs = require('fs');
|
|
27
|
+
const path = require('path');
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// Constantes
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
const DEFAULT_POLL_INTERVAL = 2000; // 2 segundos
|
|
34
|
+
const COMMS_DIR = '.planning/comms';
|
|
35
|
+
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// Carga de configuración
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Carga la configuración del gateway desde archivo o defaults.
|
|
42
|
+
* @param {string} baseDir
|
|
43
|
+
* @returns {object}
|
|
44
|
+
*/
|
|
45
|
+
function loadConfig(baseDir) {
|
|
46
|
+
const configPath = path.join(baseDir, 'manifiestos', 'gateway-config.json');
|
|
47
|
+
const defaults = {
|
|
48
|
+
enabled: false,
|
|
49
|
+
pollIntervalMs: DEFAULT_POLL_INTERVAL,
|
|
50
|
+
adapters: {
|
|
51
|
+
telegram: { enabled: false },
|
|
52
|
+
discord: { enabled: false },
|
|
53
|
+
webhook: { enabled: false },
|
|
54
|
+
},
|
|
55
|
+
notifications: {
|
|
56
|
+
onSessionComplete: true,
|
|
57
|
+
onCheckpoint: true,
|
|
58
|
+
onError: true,
|
|
59
|
+
onRelease: true,
|
|
60
|
+
onBuildFail: true,
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
if (fs.existsSync(configPath)) {
|
|
66
|
+
const loaded = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
67
|
+
return { ...defaults, ...loaded, adapters: { ...defaults.adapters, ...loaded.adapters } };
|
|
68
|
+
}
|
|
69
|
+
} catch (_) {}
|
|
70
|
+
|
|
71
|
+
return defaults;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
// Gateway Runner
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
class GatewayRunner {
|
|
79
|
+
constructor(baseDir) {
|
|
80
|
+
this.baseDir = baseDir;
|
|
81
|
+
this.config = loadConfig(baseDir);
|
|
82
|
+
this.adapters = [];
|
|
83
|
+
this._pollInterval = null;
|
|
84
|
+
this._running = false;
|
|
85
|
+
|
|
86
|
+
// CommandRelay opt-in — activo si config.relay.enabled === true.
|
|
87
|
+
// Permite recibir comandos bidireccionales desde canales externos
|
|
88
|
+
// (ej. Telegram) hacia Claude Code con validaciones de seguridad.
|
|
89
|
+
try {
|
|
90
|
+
const CommandRelay = require('./command-relay');
|
|
91
|
+
this.relay = new CommandRelay(baseDir, this.config);
|
|
92
|
+
} catch (_) {
|
|
93
|
+
this.relay = null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Inicializa y arranca todos los adaptadores habilitados.
|
|
99
|
+
*/
|
|
100
|
+
async start() {
|
|
101
|
+
if (!this.config.enabled) {
|
|
102
|
+
console.log('[gateway] Gateway deshabilitado en configuración. Activar con enabled: true.');
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
console.log('[gateway] Iniciando gateway...');
|
|
107
|
+
|
|
108
|
+
// Cargar adaptadores habilitados
|
|
109
|
+
const adapterConfigs = this.config.adapters;
|
|
110
|
+
|
|
111
|
+
if (adapterConfigs.telegram?.enabled) {
|
|
112
|
+
const TelegramAdapter = require('./adapters/telegram');
|
|
113
|
+
const adapter = new TelegramAdapter(adapterConfigs.telegram);
|
|
114
|
+
adapter.onMessage((msg) => this._handleIncoming(msg));
|
|
115
|
+
await adapter.start();
|
|
116
|
+
if (adapter.running) this.adapters.push(adapter);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (adapterConfigs.discord?.enabled) {
|
|
120
|
+
const DiscordAdapter = require('./adapters/discord');
|
|
121
|
+
const adapter = new DiscordAdapter(adapterConfigs.discord);
|
|
122
|
+
adapter.onMessage((msg) => this._handleIncoming(msg));
|
|
123
|
+
await adapter.start();
|
|
124
|
+
if (adapter.running) this.adapters.push(adapter);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (adapterConfigs.webhook?.enabled) {
|
|
128
|
+
const WebhookAdapter = require('./adapters/webhook');
|
|
129
|
+
const adapter = new WebhookAdapter(adapterConfigs.webhook);
|
|
130
|
+
await adapter.start();
|
|
131
|
+
if (adapter.running) this.adapters.push(adapter);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (adapterConfigs.whatsapp?.enabled) {
|
|
135
|
+
const WhatsAppAdapter = require('./adapters/whatsapp');
|
|
136
|
+
const adapter = new WhatsAppAdapter(adapterConfigs.whatsapp);
|
|
137
|
+
adapter.onMessage((msg) => this._handleIncoming(msg));
|
|
138
|
+
await adapter.start();
|
|
139
|
+
if (adapter.running) this.adapters.push(adapter);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (adapterConfigs.slack?.enabled) {
|
|
143
|
+
const SlackAdapter = require('./adapters/slack');
|
|
144
|
+
const adapter = new SlackAdapter(adapterConfigs.slack);
|
|
145
|
+
adapter.onMessage((msg) => this._handleIncoming(msg));
|
|
146
|
+
await adapter.start();
|
|
147
|
+
if (adapter.running) this.adapters.push(adapter);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (adapterConfigs.email?.enabled) {
|
|
151
|
+
const EmailAdapter = require('./adapters/email');
|
|
152
|
+
const adapter = new EmailAdapter(adapterConfigs.email);
|
|
153
|
+
await adapter.start();
|
|
154
|
+
if (adapter.running) this.adapters.push(adapter);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (this.adapters.length === 0) {
|
|
158
|
+
console.log('[gateway] Ningún adaptador habilitado o disponible.');
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
console.log(`[gateway] ${this.adapters.length} adaptador(es) activo(s): ${this.adapters.map(a => a.name).join(', ')}`);
|
|
163
|
+
|
|
164
|
+
// Iniciar polling de agent-comms
|
|
165
|
+
this._running = true;
|
|
166
|
+
this._pollInterval = setInterval(
|
|
167
|
+
() => this._pollComms(),
|
|
168
|
+
this.config.pollIntervalMs || DEFAULT_POLL_INTERVAL,
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
console.log(`[gateway] Polling activo (cada ${this.config.pollIntervalMs || DEFAULT_POLL_INTERVAL}ms).`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Detiene todos los adaptadores y el polling.
|
|
176
|
+
*/
|
|
177
|
+
async stop() {
|
|
178
|
+
this._running = false;
|
|
179
|
+
if (this._pollInterval) {
|
|
180
|
+
clearInterval(this._pollInterval);
|
|
181
|
+
this._pollInterval = null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
for (const adapter of this.adapters) {
|
|
185
|
+
await adapter.stop();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
this.adapters = [];
|
|
189
|
+
console.log('[gateway] Detenido.');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Poll de .planning/comms/ buscando mensajes tipo gateway_notification.
|
|
194
|
+
* @private
|
|
195
|
+
*/
|
|
196
|
+
_pollComms() {
|
|
197
|
+
const commsDir = path.join(this.baseDir, COMMS_DIR);
|
|
198
|
+
if (!fs.existsSync(commsDir)) return;
|
|
199
|
+
|
|
200
|
+
let files;
|
|
201
|
+
try {
|
|
202
|
+
files = fs.readdirSync(commsDir)
|
|
203
|
+
.filter(f => f.startsWith('msg-') && f.endsWith('.json'));
|
|
204
|
+
} catch (_) {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
for (const file of files) {
|
|
209
|
+
const filePath = path.join(commsDir, file);
|
|
210
|
+
let msg;
|
|
211
|
+
try {
|
|
212
|
+
msg = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
213
|
+
} catch (_) {
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Solo procesar mensajes de gateway pendientes
|
|
218
|
+
if (msg.status !== 'pending') continue;
|
|
219
|
+
if (msg.type !== 'gateway_notification' && msg.type !== 'gateway_command_response') continue;
|
|
220
|
+
|
|
221
|
+
// Despachar a adaptador(es) correspondientes
|
|
222
|
+
const target = msg.to || 'all';
|
|
223
|
+
this._dispatch(target, msg);
|
|
224
|
+
|
|
225
|
+
// Marcar como procesado
|
|
226
|
+
try {
|
|
227
|
+
msg.status = 'processed';
|
|
228
|
+
msg.processedAt = new Date().toISOString();
|
|
229
|
+
fs.writeFileSync(filePath, JSON.stringify(msg, null, 2), 'utf8');
|
|
230
|
+
} catch (_) {}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Despacha un mensaje a el/los adaptador(es) objetivo.
|
|
236
|
+
* @private
|
|
237
|
+
*/
|
|
238
|
+
_dispatch(target, message) {
|
|
239
|
+
for (const adapter of this.adapters) {
|
|
240
|
+
if (target === 'all' || target === adapter.name || target === 'broadcast') {
|
|
241
|
+
adapter.send(message).catch(err => {
|
|
242
|
+
console.error(`[gateway] Error despachando a ${adapter.name}: ${err.message}`);
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Maneja un mensaje entrante de una plataforma.
|
|
250
|
+
* Escribe en .planning/comms/ para que Claude Code lo procese.
|
|
251
|
+
* Adicionalmente, si el CommandRelay está habilitado para la plataforma
|
|
252
|
+
* y el usuario, encola en .planning/inbox/ para consumo vía /swl:inbox.
|
|
253
|
+
* @private
|
|
254
|
+
*/
|
|
255
|
+
_handleIncoming(message) {
|
|
256
|
+
const commsDir = path.join(this.baseDir, COMMS_DIR);
|
|
257
|
+
if (!fs.existsSync(commsDir)) {
|
|
258
|
+
fs.mkdirSync(commsDir, { recursive: true });
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const id = `msg-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
|
|
262
|
+
const msg = {
|
|
263
|
+
id,
|
|
264
|
+
type: 'gateway_command',
|
|
265
|
+
from: `${message.platform}:${message.userId || 'unknown'}`,
|
|
266
|
+
to: 'swl-system',
|
|
267
|
+
payload: {
|
|
268
|
+
platform: message.platform,
|
|
269
|
+
chatId: message.chatId,
|
|
270
|
+
userId: message.userId,
|
|
271
|
+
userName: message.userName,
|
|
272
|
+
text: message.text,
|
|
273
|
+
command: message.command,
|
|
274
|
+
args: message.args,
|
|
275
|
+
},
|
|
276
|
+
timestamp: new Date().toISOString(),
|
|
277
|
+
status: 'pending',
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
const filePath = path.join(commsDir, `${id}.json`);
|
|
281
|
+
try {
|
|
282
|
+
fs.writeFileSync(filePath, JSON.stringify(msg, null, 2), 'utf8');
|
|
283
|
+
} catch (_) {}
|
|
284
|
+
|
|
285
|
+
// Relay bidireccional: encolar también en .planning/inbox/ si aplica.
|
|
286
|
+
if (this.relay && this.relay.habilitado()) {
|
|
287
|
+
try {
|
|
288
|
+
this.relay.recibirComando(message);
|
|
289
|
+
} catch (_) { /* silencioso */ }
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// ---------------------------------------------------------------------------
|
|
295
|
+
// Entrypoint CLI
|
|
296
|
+
// ---------------------------------------------------------------------------
|
|
297
|
+
|
|
298
|
+
if (require.main === module) {
|
|
299
|
+
const baseDir = process.argv[2] || process.cwd();
|
|
300
|
+
|
|
301
|
+
const gateway = new GatewayRunner(baseDir);
|
|
302
|
+
|
|
303
|
+
const cleanup = async () => {
|
|
304
|
+
await gateway.stop();
|
|
305
|
+
process.exit(0);
|
|
306
|
+
};
|
|
307
|
+
process.on('SIGTERM', cleanup);
|
|
308
|
+
process.on('SIGINT', cleanup);
|
|
309
|
+
|
|
310
|
+
gateway.start().catch(err => {
|
|
311
|
+
console.error(`[gateway] Error fatal: ${err.message}`);
|
|
312
|
+
process.exit(1);
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// ---------------------------------------------------------------------------
|
|
317
|
+
// Exports
|
|
318
|
+
// ---------------------------------------------------------------------------
|
|
319
|
+
|
|
320
|
+
module.exports = { GatewayRunner, loadConfig };
|