@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.
- 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 +207 -7
- 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 +24 -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/swl-claudemd/SKILL.md +15 -1
- 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 +37 -37
- 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
package/hooks/lib/sync-status.js
CHANGED
|
@@ -1,228 +1,228 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Sync Status — Detección de desincronización entre fuente y destino.
|
|
5
|
-
*
|
|
6
|
-
* Compara el directorio fuente (habilidades/, agentes/) con los directorios
|
|
7
|
-
* de instalación (.claude/skills/, .claude/agents/) para detectar:
|
|
8
|
-
* - Skills/agentes faltantes en destino
|
|
9
|
-
* - Skills/agentes modificados localmente (divergidos)
|
|
10
|
-
* - Skills/agentes extra en destino (no en fuente)
|
|
11
|
-
*
|
|
12
|
-
* Patrón adoptado de skillshare (diff, status commands).
|
|
13
|
-
*
|
|
14
|
-
* Zero dependencias externas.
|
|
15
|
-
*
|
|
16
|
-
* Uso:
|
|
17
|
-
* const { calcularStatus, formatearStatus } = require('./lib/sync-status');
|
|
18
|
-
* const status = calcularStatus('/ruta/habilidades', '/ruta/.claude/skills');
|
|
19
|
-
* console.log(formatearStatus(status));
|
|
20
|
-
*
|
|
21
|
-
* @module hooks/lib/sync-status
|
|
22
|
-
*/
|
|
23
|
-
|
|
24
|
-
const fs = require('fs');
|
|
25
|
-
const path = require('path');
|
|
26
|
-
const crypto = require('crypto');
|
|
27
|
-
|
|
28
|
-
// ---------------------------------------------------------------------------
|
|
29
|
-
// Funciones internas
|
|
30
|
-
// ---------------------------------------------------------------------------
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Calcula un hash rápido del contenido de un directorio o archivo.
|
|
34
|
-
* Para directorios, hashea el contenido de SKILL.md (si existe).
|
|
35
|
-
*
|
|
36
|
-
* @param {string} ruta - Ruta al archivo o directorio
|
|
37
|
-
* @returns {string|null} Hash MD5 o null si no se puede leer
|
|
38
|
-
*/
|
|
39
|
-
function _hashContenido(ruta) {
|
|
40
|
-
try {
|
|
41
|
-
const stat = fs.statSync(ruta);
|
|
42
|
-
let contenido;
|
|
43
|
-
|
|
44
|
-
if (stat.isDirectory()) {
|
|
45
|
-
// Para directorios de skills, hashear SKILL.md
|
|
46
|
-
const skillMd = path.join(ruta, 'SKILL.md');
|
|
47
|
-
if (fs.existsSync(skillMd)) {
|
|
48
|
-
contenido = fs.readFileSync(skillMd, 'utf8');
|
|
49
|
-
} else {
|
|
50
|
-
// Para agentes (.md), hashear el archivo directamente
|
|
51
|
-
return null;
|
|
52
|
-
}
|
|
53
|
-
} else {
|
|
54
|
-
contenido = fs.readFileSync(ruta, 'utf8');
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
return crypto.createHash('md5').update(contenido).digest('hex');
|
|
58
|
-
} catch {
|
|
59
|
-
return null;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Lista los componentes en un directorio.
|
|
65
|
-
*
|
|
66
|
-
* @param {string} dir - Directorio a listar
|
|
67
|
-
* @param {'directory'|'file'} tipo - Tipo de entrada a buscar
|
|
68
|
-
* @returns {Map<string, string>} Mapa nombre → hash
|
|
69
|
-
*/
|
|
70
|
-
function _listarComponentes(dir, tipo = 'directory') {
|
|
71
|
-
const mapa = new Map();
|
|
72
|
-
if (!fs.existsSync(dir)) return mapa;
|
|
73
|
-
|
|
74
|
-
try {
|
|
75
|
-
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
76
|
-
for (const entry of entries) {
|
|
77
|
-
const esValido = tipo === 'directory' ? entry.isDirectory() : entry.isFile();
|
|
78
|
-
if (!esValido) continue;
|
|
79
|
-
if (entry.name.startsWith('.')) continue;
|
|
80
|
-
|
|
81
|
-
const ruta = path.join(dir, entry.name);
|
|
82
|
-
const hash = _hashContenido(ruta);
|
|
83
|
-
mapa.set(entry.name, hash);
|
|
84
|
-
}
|
|
85
|
-
} catch {
|
|
86
|
-
// Error leyendo directorio
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
return mapa;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// ---------------------------------------------------------------------------
|
|
93
|
-
// API pública
|
|
94
|
-
// ---------------------------------------------------------------------------
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* @typedef {object} SyncStatus
|
|
98
|
-
* @property {string[]} faltantes - En fuente pero no en destino
|
|
99
|
-
* @property {string[]} extra - En destino pero no en fuente
|
|
100
|
-
* @property {string[]} sincronizados - En ambos con mismo hash
|
|
101
|
-
* @property {string[]} divergidos - En ambos pero con hash diferente
|
|
102
|
-
* @property {number} total - Total de componentes en fuente
|
|
103
|
-
* @property {boolean} enSync - true si no hay faltantes ni divergidos
|
|
104
|
-
*/
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Calcula el estado de sincronización entre fuente y destino.
|
|
108
|
-
*
|
|
109
|
-
* @param {string} dirFuente - Directorio fuente (ej: habilidades/)
|
|
110
|
-
* @param {string} dirDestino - Directorio destino (ej: .claude/skills/)
|
|
111
|
-
* @param {object} [opciones]
|
|
112
|
-
* @param {'directory'|'file'} [opciones.tipo='directory'] - Tipo de componente
|
|
113
|
-
* @returns {SyncStatus}
|
|
114
|
-
*/
|
|
115
|
-
function calcularStatus(dirFuente, dirDestino, opciones = {}) {
|
|
116
|
-
const tipo = opciones.tipo || 'directory';
|
|
117
|
-
|
|
118
|
-
const fuente = _listarComponentes(dirFuente, tipo);
|
|
119
|
-
const destino = _listarComponentes(dirDestino, tipo);
|
|
120
|
-
|
|
121
|
-
const faltantes = [];
|
|
122
|
-
const sincronizados = [];
|
|
123
|
-
const divergidos = [];
|
|
124
|
-
const extra = [];
|
|
125
|
-
|
|
126
|
-
// Comparar fuente → destino
|
|
127
|
-
for (const [nombre, hashFuente] of fuente) {
|
|
128
|
-
if (!destino.has(nombre)) {
|
|
129
|
-
faltantes.push(nombre);
|
|
130
|
-
} else {
|
|
131
|
-
const hashDestino = destino.get(nombre);
|
|
132
|
-
if (hashFuente && hashDestino && hashFuente === hashDestino) {
|
|
133
|
-
sincronizados.push(nombre);
|
|
134
|
-
} else if (hashFuente && hashDestino) {
|
|
135
|
-
divergidos.push(nombre);
|
|
136
|
-
} else {
|
|
137
|
-
sincronizados.push(nombre); // No se pudo comparar hash — asumir sync
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// Componentes extra en destino (no en fuente)
|
|
143
|
-
for (const nombre of destino.keys()) {
|
|
144
|
-
if (!fuente.has(nombre)) {
|
|
145
|
-
extra.push(nombre);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
return {
|
|
150
|
-
faltantes: faltantes.sort(),
|
|
151
|
-
extra: extra.sort(),
|
|
152
|
-
sincronizados: sincronizados.sort(),
|
|
153
|
-
divergidos: divergidos.sort(),
|
|
154
|
-
total: fuente.size,
|
|
155
|
-
enSync: faltantes.length === 0 && divergidos.length === 0,
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* Formatea el estado de sincronización para mostrar en terminal.
|
|
161
|
-
*
|
|
162
|
-
* @param {SyncStatus} status
|
|
163
|
-
* @param {string} [label=''] - Etiqueta (ej: "Skills", "Agentes")
|
|
164
|
-
* @returns {string}
|
|
165
|
-
*/
|
|
166
|
-
function formatearStatus(status, label = '') {
|
|
167
|
-
const lineas = [];
|
|
168
|
-
const prefix = label ? `[${label}] ` : '';
|
|
169
|
-
|
|
170
|
-
if (status.enSync) {
|
|
171
|
-
lineas.push(`${prefix}Sincronizado (${status.sincronizados.length}/${status.total})`);
|
|
172
|
-
} else {
|
|
173
|
-
lineas.push(`${prefix}Desincronizado:`);
|
|
174
|
-
|
|
175
|
-
if (status.faltantes.length > 0) {
|
|
176
|
-
lineas.push(` Faltantes (${status.faltantes.length}): ${status.faltantes.slice(0, 10).join(', ')}${status.faltantes.length > 10 ? '...' : ''}`);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
if (status.divergidos.length > 0) {
|
|
180
|
-
lineas.push(` Divergidos (${status.divergidos.length}): ${status.divergidos.slice(0, 10).join(', ')}${status.divergidos.length > 10 ? '...' : ''}`);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
if (status.extra.length > 0) {
|
|
184
|
-
lineas.push(` Extra (${status.extra.length}): ${status.extra.slice(0, 10).join(', ')}${status.extra.length > 10 ? '...' : ''}`);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
lineas.push(` Sincronizados: ${status.sincronizados.length}/${status.total}`);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
return lineas.join('\n');
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
/**
|
|
194
|
-
* Calcula el status completo del sistema (skills + agentes + hooks).
|
|
195
|
-
*
|
|
196
|
-
* @param {string} raizProyecto - Ruta raíz del proyecto
|
|
197
|
-
* @param {string} raizSWL - Ruta raíz del paquete swl-ses
|
|
198
|
-
* @returns {{ skills: SyncStatus, agentes: SyncStatus, enSync: boolean }}
|
|
199
|
-
*/
|
|
200
|
-
function statusCompleto(raizProyecto, raizSWL) {
|
|
201
|
-
const skills = calcularStatus(
|
|
202
|
-
path.join(raizSWL, 'habilidades'),
|
|
203
|
-
path.join(raizProyecto, '.claude', 'skills'),
|
|
204
|
-
{ tipo: 'directory' }
|
|
205
|
-
);
|
|
206
|
-
|
|
207
|
-
const agentes = calcularStatus(
|
|
208
|
-
path.join(raizSWL, 'agentes'),
|
|
209
|
-
path.join(raizProyecto, '.claude', 'agents'),
|
|
210
|
-
{ tipo: 'file' }
|
|
211
|
-
);
|
|
212
|
-
|
|
213
|
-
return {
|
|
214
|
-
skills,
|
|
215
|
-
agentes,
|
|
216
|
-
enSync: skills.enSync && agentes.enSync,
|
|
217
|
-
};
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// ---------------------------------------------------------------------------
|
|
221
|
-
// Exports
|
|
222
|
-
// ---------------------------------------------------------------------------
|
|
223
|
-
|
|
224
|
-
module.exports = {
|
|
225
|
-
calcularStatus,
|
|
226
|
-
formatearStatus,
|
|
227
|
-
statusCompleto,
|
|
228
|
-
};
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Sync Status — Detección de desincronización entre fuente y destino.
|
|
5
|
+
*
|
|
6
|
+
* Compara el directorio fuente (habilidades/, agentes/) con los directorios
|
|
7
|
+
* de instalación (.claude/skills/, .claude/agents/) para detectar:
|
|
8
|
+
* - Skills/agentes faltantes en destino
|
|
9
|
+
* - Skills/agentes modificados localmente (divergidos)
|
|
10
|
+
* - Skills/agentes extra en destino (no en fuente)
|
|
11
|
+
*
|
|
12
|
+
* Patrón adoptado de skillshare (diff, status commands).
|
|
13
|
+
*
|
|
14
|
+
* Zero dependencias externas.
|
|
15
|
+
*
|
|
16
|
+
* Uso:
|
|
17
|
+
* const { calcularStatus, formatearStatus } = require('./lib/sync-status');
|
|
18
|
+
* const status = calcularStatus('/ruta/habilidades', '/ruta/.claude/skills');
|
|
19
|
+
* console.log(formatearStatus(status));
|
|
20
|
+
*
|
|
21
|
+
* @module hooks/lib/sync-status
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const fs = require('fs');
|
|
25
|
+
const path = require('path');
|
|
26
|
+
const crypto = require('crypto');
|
|
27
|
+
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// Funciones internas
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Calcula un hash rápido del contenido de un directorio o archivo.
|
|
34
|
+
* Para directorios, hashea el contenido de SKILL.md (si existe).
|
|
35
|
+
*
|
|
36
|
+
* @param {string} ruta - Ruta al archivo o directorio
|
|
37
|
+
* @returns {string|null} Hash MD5 o null si no se puede leer
|
|
38
|
+
*/
|
|
39
|
+
function _hashContenido(ruta) {
|
|
40
|
+
try {
|
|
41
|
+
const stat = fs.statSync(ruta);
|
|
42
|
+
let contenido;
|
|
43
|
+
|
|
44
|
+
if (stat.isDirectory()) {
|
|
45
|
+
// Para directorios de skills, hashear SKILL.md
|
|
46
|
+
const skillMd = path.join(ruta, 'SKILL.md');
|
|
47
|
+
if (fs.existsSync(skillMd)) {
|
|
48
|
+
contenido = fs.readFileSync(skillMd, 'utf8');
|
|
49
|
+
} else {
|
|
50
|
+
// Para agentes (.md), hashear el archivo directamente
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
} else {
|
|
54
|
+
contenido = fs.readFileSync(ruta, 'utf8');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return crypto.createHash('md5').update(contenido).digest('hex');
|
|
58
|
+
} catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Lista los componentes en un directorio.
|
|
65
|
+
*
|
|
66
|
+
* @param {string} dir - Directorio a listar
|
|
67
|
+
* @param {'directory'|'file'} tipo - Tipo de entrada a buscar
|
|
68
|
+
* @returns {Map<string, string>} Mapa nombre → hash
|
|
69
|
+
*/
|
|
70
|
+
function _listarComponentes(dir, tipo = 'directory') {
|
|
71
|
+
const mapa = new Map();
|
|
72
|
+
if (!fs.existsSync(dir)) return mapa;
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
76
|
+
for (const entry of entries) {
|
|
77
|
+
const esValido = tipo === 'directory' ? entry.isDirectory() : entry.isFile();
|
|
78
|
+
if (!esValido) continue;
|
|
79
|
+
if (entry.name.startsWith('.')) continue;
|
|
80
|
+
|
|
81
|
+
const ruta = path.join(dir, entry.name);
|
|
82
|
+
const hash = _hashContenido(ruta);
|
|
83
|
+
mapa.set(entry.name, hash);
|
|
84
|
+
}
|
|
85
|
+
} catch {
|
|
86
|
+
// Error leyendo directorio
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return mapa;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
// API pública
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* @typedef {object} SyncStatus
|
|
98
|
+
* @property {string[]} faltantes - En fuente pero no en destino
|
|
99
|
+
* @property {string[]} extra - En destino pero no en fuente
|
|
100
|
+
* @property {string[]} sincronizados - En ambos con mismo hash
|
|
101
|
+
* @property {string[]} divergidos - En ambos pero con hash diferente
|
|
102
|
+
* @property {number} total - Total de componentes en fuente
|
|
103
|
+
* @property {boolean} enSync - true si no hay faltantes ni divergidos
|
|
104
|
+
*/
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Calcula el estado de sincronización entre fuente y destino.
|
|
108
|
+
*
|
|
109
|
+
* @param {string} dirFuente - Directorio fuente (ej: habilidades/)
|
|
110
|
+
* @param {string} dirDestino - Directorio destino (ej: .claude/skills/)
|
|
111
|
+
* @param {object} [opciones]
|
|
112
|
+
* @param {'directory'|'file'} [opciones.tipo='directory'] - Tipo de componente
|
|
113
|
+
* @returns {SyncStatus}
|
|
114
|
+
*/
|
|
115
|
+
function calcularStatus(dirFuente, dirDestino, opciones = {}) {
|
|
116
|
+
const tipo = opciones.tipo || 'directory';
|
|
117
|
+
|
|
118
|
+
const fuente = _listarComponentes(dirFuente, tipo);
|
|
119
|
+
const destino = _listarComponentes(dirDestino, tipo);
|
|
120
|
+
|
|
121
|
+
const faltantes = [];
|
|
122
|
+
const sincronizados = [];
|
|
123
|
+
const divergidos = [];
|
|
124
|
+
const extra = [];
|
|
125
|
+
|
|
126
|
+
// Comparar fuente → destino
|
|
127
|
+
for (const [nombre, hashFuente] of fuente) {
|
|
128
|
+
if (!destino.has(nombre)) {
|
|
129
|
+
faltantes.push(nombre);
|
|
130
|
+
} else {
|
|
131
|
+
const hashDestino = destino.get(nombre);
|
|
132
|
+
if (hashFuente && hashDestino && hashFuente === hashDestino) {
|
|
133
|
+
sincronizados.push(nombre);
|
|
134
|
+
} else if (hashFuente && hashDestino) {
|
|
135
|
+
divergidos.push(nombre);
|
|
136
|
+
} else {
|
|
137
|
+
sincronizados.push(nombre); // No se pudo comparar hash — asumir sync
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Componentes extra en destino (no en fuente)
|
|
143
|
+
for (const nombre of destino.keys()) {
|
|
144
|
+
if (!fuente.has(nombre)) {
|
|
145
|
+
extra.push(nombre);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
faltantes: faltantes.sort(),
|
|
151
|
+
extra: extra.sort(),
|
|
152
|
+
sincronizados: sincronizados.sort(),
|
|
153
|
+
divergidos: divergidos.sort(),
|
|
154
|
+
total: fuente.size,
|
|
155
|
+
enSync: faltantes.length === 0 && divergidos.length === 0,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Formatea el estado de sincronización para mostrar en terminal.
|
|
161
|
+
*
|
|
162
|
+
* @param {SyncStatus} status
|
|
163
|
+
* @param {string} [label=''] - Etiqueta (ej: "Skills", "Agentes")
|
|
164
|
+
* @returns {string}
|
|
165
|
+
*/
|
|
166
|
+
function formatearStatus(status, label = '') {
|
|
167
|
+
const lineas = [];
|
|
168
|
+
const prefix = label ? `[${label}] ` : '';
|
|
169
|
+
|
|
170
|
+
if (status.enSync) {
|
|
171
|
+
lineas.push(`${prefix}Sincronizado (${status.sincronizados.length}/${status.total})`);
|
|
172
|
+
} else {
|
|
173
|
+
lineas.push(`${prefix}Desincronizado:`);
|
|
174
|
+
|
|
175
|
+
if (status.faltantes.length > 0) {
|
|
176
|
+
lineas.push(` Faltantes (${status.faltantes.length}): ${status.faltantes.slice(0, 10).join(', ')}${status.faltantes.length > 10 ? '...' : ''}`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (status.divergidos.length > 0) {
|
|
180
|
+
lineas.push(` Divergidos (${status.divergidos.length}): ${status.divergidos.slice(0, 10).join(', ')}${status.divergidos.length > 10 ? '...' : ''}`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (status.extra.length > 0) {
|
|
184
|
+
lineas.push(` Extra (${status.extra.length}): ${status.extra.slice(0, 10).join(', ')}${status.extra.length > 10 ? '...' : ''}`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
lineas.push(` Sincronizados: ${status.sincronizados.length}/${status.total}`);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return lineas.join('\n');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Calcula el status completo del sistema (skills + agentes + hooks).
|
|
195
|
+
*
|
|
196
|
+
* @param {string} raizProyecto - Ruta raíz del proyecto
|
|
197
|
+
* @param {string} raizSWL - Ruta raíz del paquete swl-ses
|
|
198
|
+
* @returns {{ skills: SyncStatus, agentes: SyncStatus, enSync: boolean }}
|
|
199
|
+
*/
|
|
200
|
+
function statusCompleto(raizProyecto, raizSWL) {
|
|
201
|
+
const skills = calcularStatus(
|
|
202
|
+
path.join(raizSWL, 'habilidades'),
|
|
203
|
+
path.join(raizProyecto, '.claude', 'skills'),
|
|
204
|
+
{ tipo: 'directory' }
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
const agentes = calcularStatus(
|
|
208
|
+
path.join(raizSWL, 'agentes'),
|
|
209
|
+
path.join(raizProyecto, '.claude', 'agents'),
|
|
210
|
+
{ tipo: 'file' }
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
skills,
|
|
215
|
+
agentes,
|
|
216
|
+
enSync: skills.enSync && agentes.enSync,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ---------------------------------------------------------------------------
|
|
221
|
+
// Exports
|
|
222
|
+
// ---------------------------------------------------------------------------
|
|
223
|
+
|
|
224
|
+
module.exports = {
|
|
225
|
+
calcularStatus,
|
|
226
|
+
formatearStatus,
|
|
227
|
+
statusCompleto,
|
|
228
|
+
};
|
|
@@ -1,107 +1,107 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Taint Tracker — Rastreo de flujo de informacion entre operaciones.
|
|
5
|
-
*
|
|
6
|
-
* Implementa Information Flow Taint Tracking adoptado de OpenFang
|
|
7
|
-
* (crates/openfang-runtime/src/tool_runner.rs).
|
|
8
|
-
*
|
|
9
|
-
* Labels de taint se propagan desde fuentes (sources) hasta sumideros (sinks).
|
|
10
|
-
* Detecta cadenas peligrosas que los scanners estaticos pierden:
|
|
11
|
-
* - Read de archivos sensibles seguido de Bash con envio a red
|
|
12
|
-
* - Read de credenciales seguido de Write a logs
|
|
13
|
-
* - WebFetch de URL externa seguido de Edit de config
|
|
14
|
-
*
|
|
15
|
-
* Zero dependencias externas.
|
|
16
|
-
*
|
|
17
|
-
* @module hooks/lib/taint-tracker
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
const LABEL = Object.freeze({
|
|
21
|
-
SECRET: 'secret',
|
|
22
|
-
EXTERNAL_NETWORK: 'external_network',
|
|
23
|
-
USER_INPUT: 'user_input',
|
|
24
|
-
CREDENTIAL: 'credential',
|
|
25
|
-
INTERNAL_CONFIG: 'internal_config',
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
const SINK = Object.freeze({
|
|
29
|
-
SHELL_EXEC: 'shell_exec',
|
|
30
|
-
NET_FETCH: 'net_fetch',
|
|
31
|
-
FILE_WRITE: 'file_write',
|
|
32
|
-
LOG_OUTPUT: 'log_output',
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
const SOURCE_PATTERNS = [
|
|
36
|
-
{ patron: /\.env$|credentials|\.pem$|\.key$/i, labels: [LABEL.SECRET, LABEL.CREDENTIAL] },
|
|
37
|
-
{ patron: /\.ssh\//i, labels: [LABEL.CREDENTIAL] },
|
|
38
|
-
{ patron: /tok[e]n|ap[i].?k[e]y|passw[o]rd|priv[a]te.?k[e]y/i, labels: [LABEL.SECRET] },
|
|
39
|
-
{ patron: /settings\.json|config\.ya?ml|\.claude\//i, labels: [LABEL.INTERNAL_CONFIG] },
|
|
40
|
-
{ patron: /^https?:\/\//i, labels: [LABEL.EXTERNAL_NETWORK] },
|
|
41
|
-
];
|
|
42
|
-
|
|
43
|
-
const BLOCKED_FLOWS = [
|
|
44
|
-
{ sourceLabel: LABEL.SECRET, sink: SINK.SHELL_EXEC, severity: 'CRITICAL', desc: 'Dato sensible en comando shell' },
|
|
45
|
-
{ sourceLabel: LABEL.SECRET, sink: SINK.NET_FETCH, severity: 'CRITICAL', desc: 'Dato sensible enviado a red' },
|
|
46
|
-
{ sourceLabel: LABEL.SECRET, sink: SINK.LOG_OUTPUT, severity: 'HIGH', desc: 'Dato sensible expuesto en output' },
|
|
47
|
-
{ sourceLabel: LABEL.CREDENTIAL, sink: SINK.SHELL_EXEC, severity: 'CRITICAL', desc: 'Credencial en comando shell' },
|
|
48
|
-
{ sourceLabel: LABEL.CREDENTIAL, sink: SINK.NET_FETCH, severity: 'CRITICAL', desc: 'Credencial enviada a red' },
|
|
49
|
-
{ sourceLabel: LABEL.CREDENTIAL, sink: SINK.FILE_WRITE, severity: 'HIGH', desc: 'Credencial escrita a archivo' },
|
|
50
|
-
{ sourceLabel: LABEL.EXTERNAL_NETWORK, sink: SINK.SHELL_EXEC, severity: 'HIGH', desc: 'Datos externos ejecutados como comando' },
|
|
51
|
-
{ sourceLabel: LABEL.EXTERNAL_NETWORK, sink: SINK.FILE_WRITE, severity: 'MEDIUM', desc: 'Datos externos escritos a config' },
|
|
52
|
-
{ sourceLabel: LABEL.INTERNAL_CONFIG, sink: SINK.NET_FETCH, severity: 'HIGH', desc: 'Config interna enviada a red' },
|
|
53
|
-
];
|
|
54
|
-
|
|
55
|
-
const _taintedSources = [];
|
|
56
|
-
const MAX_HISTORY = 100;
|
|
57
|
-
|
|
58
|
-
const tracker = {
|
|
59
|
-
registrarSource(tool, pathOrUrl, labelsOverride) {
|
|
60
|
-
let labels = labelsOverride;
|
|
61
|
-
if (!labels) {
|
|
62
|
-
labels = [];
|
|
63
|
-
for (const { patron, labels: sl } of SOURCE_PATTERNS) {
|
|
64
|
-
if (patron.test(pathOrUrl)) labels.push(...sl);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
if (labels.length === 0) return;
|
|
68
|
-
labels = [...new Set(labels)];
|
|
69
|
-
_taintedSources.push({ tool, path: pathOrUrl, labels, timestamp: Date.now() });
|
|
70
|
-
if (_taintedSources.length > MAX_HISTORY) _taintedSources.splice(0, _taintedSources.length - MAX_HISTORY);
|
|
71
|
-
},
|
|
72
|
-
|
|
73
|
-
verificarSink(tool, target) {
|
|
74
|
-
let sink;
|
|
75
|
-
if (/^Bash$/i.test(tool)) sink = SINK.SHELL_EXEC;
|
|
76
|
-
else if (/^WebFetch$/i.test(tool)) sink = SINK.NET_FETCH;
|
|
77
|
-
else if (/^Write|Edit$/i.test(tool)) sink = SINK.FILE_WRITE;
|
|
78
|
-
else return null;
|
|
79
|
-
|
|
80
|
-
const recientes = _taintedSources.slice(-20);
|
|
81
|
-
const ahora = Date.now();
|
|
82
|
-
const VENTANA_MS = 15 * 60 * 1000;
|
|
83
|
-
|
|
84
|
-
for (const source of recientes) {
|
|
85
|
-
if (ahora - source.timestamp > VENTANA_MS) continue;
|
|
86
|
-
for (const label of source.labels) {
|
|
87
|
-
const flow = BLOCKED_FLOWS.find(f => f.sourceLabel === label && f.sink === sink);
|
|
88
|
-
if (flow) {
|
|
89
|
-
return {
|
|
90
|
-
bloqueado: true,
|
|
91
|
-
severity: flow.severity,
|
|
92
|
-
descripcion: flow.desc,
|
|
93
|
-
source: { tool: source.tool, path: source.path, labels: source.labels },
|
|
94
|
-
sink: { tool, target: (target || '').substring(0, 100) },
|
|
95
|
-
flujo: source.tool + '(' + source.path + ') [' + label + '] -> ' + tool + '(' + (target || '').substring(0, 50) + ')',
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
return null;
|
|
101
|
-
},
|
|
102
|
-
|
|
103
|
-
historial() { return [..._taintedSources]; },
|
|
104
|
-
reset() { _taintedSources.length = 0; },
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
module.exports = { tracker, LABEL, SINK, BLOCKED_FLOWS };
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Taint Tracker — Rastreo de flujo de informacion entre operaciones.
|
|
5
|
+
*
|
|
6
|
+
* Implementa Information Flow Taint Tracking adoptado de OpenFang
|
|
7
|
+
* (crates/openfang-runtime/src/tool_runner.rs).
|
|
8
|
+
*
|
|
9
|
+
* Labels de taint se propagan desde fuentes (sources) hasta sumideros (sinks).
|
|
10
|
+
* Detecta cadenas peligrosas que los scanners estaticos pierden:
|
|
11
|
+
* - Read de archivos sensibles seguido de Bash con envio a red
|
|
12
|
+
* - Read de credenciales seguido de Write a logs
|
|
13
|
+
* - WebFetch de URL externa seguido de Edit de config
|
|
14
|
+
*
|
|
15
|
+
* Zero dependencias externas.
|
|
16
|
+
*
|
|
17
|
+
* @module hooks/lib/taint-tracker
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const LABEL = Object.freeze({
|
|
21
|
+
SECRET: 'secret',
|
|
22
|
+
EXTERNAL_NETWORK: 'external_network',
|
|
23
|
+
USER_INPUT: 'user_input',
|
|
24
|
+
CREDENTIAL: 'credential',
|
|
25
|
+
INTERNAL_CONFIG: 'internal_config',
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const SINK = Object.freeze({
|
|
29
|
+
SHELL_EXEC: 'shell_exec',
|
|
30
|
+
NET_FETCH: 'net_fetch',
|
|
31
|
+
FILE_WRITE: 'file_write',
|
|
32
|
+
LOG_OUTPUT: 'log_output',
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const SOURCE_PATTERNS = [
|
|
36
|
+
{ patron: /\.env$|credentials|\.pem$|\.key$/i, labels: [LABEL.SECRET, LABEL.CREDENTIAL] },
|
|
37
|
+
{ patron: /\.ssh\//i, labels: [LABEL.CREDENTIAL] },
|
|
38
|
+
{ patron: /tok[e]n|ap[i].?k[e]y|passw[o]rd|priv[a]te.?k[e]y/i, labels: [LABEL.SECRET] },
|
|
39
|
+
{ patron: /settings\.json|config\.ya?ml|\.claude\//i, labels: [LABEL.INTERNAL_CONFIG] },
|
|
40
|
+
{ patron: /^https?:\/\//i, labels: [LABEL.EXTERNAL_NETWORK] },
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
const BLOCKED_FLOWS = [
|
|
44
|
+
{ sourceLabel: LABEL.SECRET, sink: SINK.SHELL_EXEC, severity: 'CRITICAL', desc: 'Dato sensible en comando shell' },
|
|
45
|
+
{ sourceLabel: LABEL.SECRET, sink: SINK.NET_FETCH, severity: 'CRITICAL', desc: 'Dato sensible enviado a red' },
|
|
46
|
+
{ sourceLabel: LABEL.SECRET, sink: SINK.LOG_OUTPUT, severity: 'HIGH', desc: 'Dato sensible expuesto en output' },
|
|
47
|
+
{ sourceLabel: LABEL.CREDENTIAL, sink: SINK.SHELL_EXEC, severity: 'CRITICAL', desc: 'Credencial en comando shell' },
|
|
48
|
+
{ sourceLabel: LABEL.CREDENTIAL, sink: SINK.NET_FETCH, severity: 'CRITICAL', desc: 'Credencial enviada a red' },
|
|
49
|
+
{ sourceLabel: LABEL.CREDENTIAL, sink: SINK.FILE_WRITE, severity: 'HIGH', desc: 'Credencial escrita a archivo' },
|
|
50
|
+
{ sourceLabel: LABEL.EXTERNAL_NETWORK, sink: SINK.SHELL_EXEC, severity: 'HIGH', desc: 'Datos externos ejecutados como comando' },
|
|
51
|
+
{ sourceLabel: LABEL.EXTERNAL_NETWORK, sink: SINK.FILE_WRITE, severity: 'MEDIUM', desc: 'Datos externos escritos a config' },
|
|
52
|
+
{ sourceLabel: LABEL.INTERNAL_CONFIG, sink: SINK.NET_FETCH, severity: 'HIGH', desc: 'Config interna enviada a red' },
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
const _taintedSources = [];
|
|
56
|
+
const MAX_HISTORY = 100;
|
|
57
|
+
|
|
58
|
+
const tracker = {
|
|
59
|
+
registrarSource(tool, pathOrUrl, labelsOverride) {
|
|
60
|
+
let labels = labelsOverride;
|
|
61
|
+
if (!labels) {
|
|
62
|
+
labels = [];
|
|
63
|
+
for (const { patron, labels: sl } of SOURCE_PATTERNS) {
|
|
64
|
+
if (patron.test(pathOrUrl)) labels.push(...sl);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (labels.length === 0) return;
|
|
68
|
+
labels = [...new Set(labels)];
|
|
69
|
+
_taintedSources.push({ tool, path: pathOrUrl, labels, timestamp: Date.now() });
|
|
70
|
+
if (_taintedSources.length > MAX_HISTORY) _taintedSources.splice(0, _taintedSources.length - MAX_HISTORY);
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
verificarSink(tool, target) {
|
|
74
|
+
let sink;
|
|
75
|
+
if (/^Bash$/i.test(tool)) sink = SINK.SHELL_EXEC;
|
|
76
|
+
else if (/^WebFetch$/i.test(tool)) sink = SINK.NET_FETCH;
|
|
77
|
+
else if (/^Write|Edit$/i.test(tool)) sink = SINK.FILE_WRITE;
|
|
78
|
+
else return null;
|
|
79
|
+
|
|
80
|
+
const recientes = _taintedSources.slice(-20);
|
|
81
|
+
const ahora = Date.now();
|
|
82
|
+
const VENTANA_MS = 15 * 60 * 1000;
|
|
83
|
+
|
|
84
|
+
for (const source of recientes) {
|
|
85
|
+
if (ahora - source.timestamp > VENTANA_MS) continue;
|
|
86
|
+
for (const label of source.labels) {
|
|
87
|
+
const flow = BLOCKED_FLOWS.find(f => f.sourceLabel === label && f.sink === sink);
|
|
88
|
+
if (flow) {
|
|
89
|
+
return {
|
|
90
|
+
bloqueado: true,
|
|
91
|
+
severity: flow.severity,
|
|
92
|
+
descripcion: flow.desc,
|
|
93
|
+
source: { tool: source.tool, path: source.path, labels: source.labels },
|
|
94
|
+
sink: { tool, target: (target || '').substring(0, 100) },
|
|
95
|
+
flujo: source.tool + '(' + source.path + ') [' + label + '] -> ' + tool + '(' + (target || '').substring(0, 50) + ')',
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return null;
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
historial() { return [..._taintedSources]; },
|
|
104
|
+
reset() { _taintedSources.length = 0; },
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
module.exports = { tracker, LABEL, SINK, BLOCKED_FLOWS };
|