@saulwade/swl-ses 1.5.1 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/CLAUDE.md +225 -209
  2. package/README.md +578 -561
  3. package/agentes/arquitecto-swl.md +33 -1
  4. package/agentes/nemesis-auditor-swl.md +59 -19
  5. package/bin/swl-mcp-server.js +214 -214
  6. package/bin/swl-ses.js +49 -7
  7. package/comandos/swl/.evolved.json +22 -22
  8. package/comandos/swl/contribuir.md +233 -233
  9. package/comandos/swl/nemesis.md +230 -56
  10. package/gateway/lib/event-channel.js +191 -191
  11. package/habilidades/backend-production-resilience/SKILL.md +288 -288
  12. package/habilidades/benchmark-memoria/SKILL.md +186 -186
  13. package/habilidades/diagrama-arquitectura/assets/template.html +276 -276
  14. package/habilidades/doubt-driven-review/SKILL.md +171 -171
  15. package/habilidades/doubt-driven-review/recursos/EXAMPLES.md +130 -130
  16. package/habilidades/ejecutar-task-iterativo/SKILL.md +278 -278
  17. package/habilidades/eval-framework/SKILL.md +212 -212
  18. package/habilidades/feynman-auditor-swl/SKILL.md +123 -123
  19. package/habilidades/feynman-auditor-swl/recursos/preguntas-language-agnostic.md +108 -108
  20. package/habilidades/harness-claude-code/SKILL.md +299 -299
  21. package/habilidades/infra-github-actions/SKILL.md +166 -166
  22. package/habilidades/legacy-code-rescue/SKILL.md +267 -267
  23. package/habilidades/manejo-errores/.evolved.json +8 -8
  24. package/habilidades/meta-skills-estandar/SKILL.md +207 -4
  25. package/habilidades/meta-skills-estandar/recursos/convencion-examples.md +93 -93
  26. package/habilidades/meta-skills-estandar/recursos/skills-as-agents.md +163 -163
  27. package/habilidades/nemesis-evaluacion-json/SKILL.md +266 -0
  28. package/habilidades/nemesis-redistribuir/SKILL.md +341 -0
  29. package/habilidades/node-experto/SKILL.md +94 -4
  30. package/habilidades/patrones-python/SKILL.md +229 -229
  31. package/habilidades/patrones-python/recursos/patrones-avanzados.md +469 -469
  32. package/habilidades/planear-fase/SKILL.md +319 -319
  33. package/habilidades/protocolo-revision-swl/SKILL.md +350 -276
  34. package/habilidades/release-semver/.evolved.json +8 -8
  35. package/habilidades/state-inconsistency-auditor-swl/SKILL.md +166 -166
  36. package/habilidades/state-inconsistency-auditor-swl/recursos/coupled-state-patterns.md +147 -147
  37. package/habilidades/tdd-workflow/SKILL.md +121 -4
  38. package/habilidades/testing-python/SKILL.md +340 -340
  39. package/habilidades/web-fetcher-routing/SKILL.md +75 -75
  40. package/hooks/check-update.js +31 -3
  41. package/hooks/claudemd-bloat-detector.js +161 -161
  42. package/hooks/extraccion-aprendizajes.js +11 -0
  43. package/hooks/lib/agent-routing.js +107 -107
  44. package/hooks/lib/auto-consolidator.js +335 -335
  45. package/hooks/lib/error-classifier.js +308 -308
  46. package/hooks/lib/merkle-audit.js +96 -96
  47. package/hooks/lib/provenance-tracker.js +191 -191
  48. package/hooks/lib/rate-limit-tracker.js +253 -253
  49. package/hooks/lib/resource-quota.js +122 -122
  50. package/hooks/lib/retry-jitter.js +165 -165
  51. package/hooks/lib/security-net.js +201 -201
  52. package/hooks/lib/skill-auditor.js +588 -588
  53. package/hooks/lib/sync-status.js +228 -228
  54. package/hooks/lib/taint-tracker.js +107 -107
  55. package/hooks/lib/text-similarity.js +241 -241
  56. package/hooks/lib/toon-compressor.js +245 -245
  57. package/hooks/registro-turnos.js +209 -209
  58. package/hooks/sugerir-regenerar-inventario.js +170 -170
  59. package/hooks/validar-formato-post-subagente.js +140 -140
  60. package/hooks/validar-memoria-hook.js +218 -218
  61. package/instintos/prompt-appendices.yaml +57 -57
  62. package/manifiestos/agent-output-schemas.json +57 -57
  63. package/manifiestos/modulos.json +1324 -1321
  64. package/manifiestos/skills-lock.json +1142 -1114
  65. package/package.json +5 -4
  66. package/plantillas/auditor-veto-template.md +105 -105
  67. package/plantillas/github-workflows/README.md +47 -47
  68. package/plantillas/github-workflows/release-please.yml +44 -44
  69. package/plantillas/github-workflows/swl-ci.yml +107 -107
  70. package/plantillas/github-workflows/swl-security.yml +51 -51
  71. package/plugin.json +355 -351
  72. package/reglas/analisis-previo-tareas-grandes.md +172 -172
  73. package/reglas/arreglar-al-detectar.md +147 -147
  74. package/reglas/fragmentos-compartidos.md +152 -152
  75. package/reglas/harness-claude-code.md +213 -213
  76. package/reglas/registro-componentes-nuevos.md +192 -0
  77. package/reglas/usar-context7.md +226 -226
  78. package/schemas/diary-entry.schema.json +80 -80
  79. package/scripts/actualizar.js +110 -1
  80. package/scripts/audit-tools/audit-history.js +330 -330
  81. package/scripts/audit-tools/bundle-tracker.js +290 -290
  82. package/scripts/audit-tools/canary-monitor.js +352 -352
  83. package/scripts/audit-tools/code-profiler.js +605 -605
  84. package/scripts/audit-tools/dep-doctor.js +320 -320
  85. package/scripts/audit-tools/env-validator.js +206 -206
  86. package/scripts/audit-tools/lib/fs-walk.js +48 -48
  87. package/scripts/audit-tools/lib/output.js +23 -23
  88. package/scripts/audit-tools/migration-checker.js +392 -392
  89. package/scripts/audit-tools/pentest-scanner.js +1436 -1436
  90. package/scripts/benchmark-memoria.js +167 -167
  91. package/scripts/configurar-branch-protection.js +418 -418
  92. package/scripts/derivar-feature-list.js +489 -489
  93. package/scripts/desinstalar.js +105 -24
  94. package/scripts/detectar-aprendizajes-duplicados.js +151 -151
  95. package/scripts/doctor.js +27 -0
  96. package/scripts/field-report.js +199 -199
  97. package/scripts/generar-checklists-consolidados.js +273 -273
  98. package/scripts/generar-inventario.js +420 -420
  99. package/scripts/generar-matriz-lenguajes.js +271 -271
  100. package/scripts/instalador.js +55 -4
  101. package/scripts/lib/artefactos-python.js +43 -43
  102. package/scripts/lib/benchmark-metrics.js +160 -160
  103. package/scripts/lib/budget-enforcer.js +252 -252
  104. package/scripts/lib/configurar-ci.js +380 -380
  105. package/scripts/lib/contadores-inventario.js +217 -217
  106. package/scripts/lib/detectar-stack-detallado.js +307 -307
  107. package/scripts/lib/diary-entry.js +234 -234
  108. package/scripts/lib/eval-metrics-store.js +218 -218
  109. package/scripts/lib/eval-quality.js +171 -171
  110. package/scripts/lib/eval-schemas.js +144 -144
  111. package/scripts/lib/eval-self-correct.js +106 -106
  112. package/scripts/lib/eval-validator.js +185 -185
  113. package/scripts/lib/expandir-targets.js +71 -71
  114. package/scripts/lib/jaccard-similarity.js +98 -98
  115. package/scripts/lib/longmemeval-runner.js +125 -125
  116. package/scripts/lib/mcp_config.py +127 -0
  117. package/scripts/lib/npm-version.js +261 -261
  118. package/scripts/lib/paquetes-conocidos.js +50 -50
  119. package/scripts/lib/parsear-opciones.js +3 -0
  120. package/scripts/lib/prompt-builder.js +264 -264
  121. package/scripts/lib/rrf-fusion.js +175 -175
  122. package/scripts/lib/scoring-instintos.js +277 -277
  123. package/scripts/lib/semantic-search.js +252 -252
  124. package/scripts/lib/toml-merge.js +204 -204
  125. package/scripts/lib/transformadores/codex.js +375 -375
  126. package/scripts/lib/transformadores/cursor.js +359 -359
  127. package/scripts/lib/ui.js +148 -22
  128. package/scripts/limpiar-artefactos-python.js +131 -131
  129. package/scripts/mcp-orchestrator.py +8 -18
  130. package/scripts/mcp-pool-manager.py +12 -23
  131. package/scripts/mcp-server/README.md +170 -170
  132. package/scripts/mcp-server/auth.js +105 -105
  133. package/scripts/mcp-server/cache.js +106 -106
  134. package/scripts/mcp-server/telemetry.js +78 -78
  135. package/scripts/migrar-csv-a-array.js +168 -168
  136. package/scripts/migrar-fase-dominio.js +201 -201
  137. package/scripts/publicar.js +511 -511
  138. package/scripts/run-eval.js +141 -141
  139. package/scripts/tui/componentes/selector-multi.js +189 -0
  140. package/scripts/tui/componentes/selector-unico.js +158 -0
  141. package/scripts/tui/ejecutores.js +375 -0
  142. package/scripts/tui/index.js +162 -0
  143. package/scripts/tui/lib/colores.js +129 -0
  144. package/scripts/tui/lib/render.js +264 -0
  145. package/scripts/tui/lib/teclas.js +113 -0
  146. package/scripts/tui/pantallas/inspect.js +173 -0
  147. package/scripts/tui/pantallas/install-wizard.js +334 -0
  148. package/scripts/tui/pantallas/menu-principal.js +52 -0
  149. package/scripts/tui/pantallas/progreso.js +274 -0
  150. package/scripts/tui/pantallas/resumen.js +132 -0
  151. package/scripts/tui/pantallas/uninstall-wizard.js +208 -0
  152. package/scripts/tui/pantallas/update-wizard.js +232 -0
  153. package/scripts/tui/pantallas/welcome.js +187 -0
  154. package/scripts/validar-userland-vacio.js +110 -110
  155. package/scripts/verificar-docs-vs-codigo.js +425 -0
package/scripts/lib/ui.js CHANGED
@@ -82,6 +82,25 @@ function encabezado(titulo, version) {
82
82
  // Spinner
83
83
  // ---------------------------------------------------------------------------
84
84
 
85
+ // Registro global de spinners activos. Permite que las funciones interactivas
86
+ // (preguntarSiNo, preguntarOpcion, preguntarTexto) pausen cualquier spinner en
87
+ // curso antes de crear un readline, evitando que el setInterval del spinner
88
+ // sobreescriba el prompt con `\r`. Origen: bug observado en `swl-ses update`
89
+ // cuando el instalador hace preguntarSiNo dentro del spinner del actualizador.
90
+ const _spinnersActivos = new Set();
91
+
92
+ function _pausarSpinnersActivos() {
93
+ for (const sp of _spinnersActivos) {
94
+ sp._pausar();
95
+ }
96
+ }
97
+
98
+ function _reanudarSpinnersActivos() {
99
+ for (const sp of _spinnersActivos) {
100
+ sp._reanudar();
101
+ }
102
+ }
103
+
85
104
  function spinner(mensaje) {
86
105
  if (!SOPORTA_COLOR) {
87
106
  // Sin TTY: solo mostrar mensaje estático
@@ -91,49 +110,96 @@ function spinner(mensaje) {
91
110
  exito: (msg) => { console.log(` [OK] ${msg}`); },
92
111
  fallo: (msg) => { console.log(` [ERROR] ${msg}`); },
93
112
  detener: () => {},
113
+ _pausar: () => {},
114
+ _reanudar: () => {},
94
115
  };
95
116
  }
96
117
 
97
118
  let frameIdx = 0;
98
119
  let textoActual = mensaje;
99
120
  let activo = true;
121
+ let pausado = false;
122
+ let intervalo = null;
100
123
 
101
- const intervalo = setInterval(() => {
102
- if (!activo) return;
124
+ function _tick() {
125
+ if (!activo || pausado) return;
103
126
  const frame = SPINNER_FRAMES[frameIdx % SPINNER_FRAMES.length];
104
127
  process.stdout.write(`\r ${color.cyan(frame)} ${textoActual} `);
105
128
  frameIdx++;
106
- }, 80);
129
+ }
130
+
131
+ function _arrancarIntervalo() {
132
+ if (intervalo) return;
133
+ intervalo = setInterval(_tick, 80);
134
+ }
135
+
136
+ function _detenerIntervalo() {
137
+ if (intervalo) {
138
+ clearInterval(intervalo);
139
+ intervalo = null;
140
+ }
141
+ }
107
142
 
108
143
  function limpiarLinea() {
109
144
  process.stdout.write('\r\x1b[2K');
110
145
  }
111
146
 
147
+ _arrancarIntervalo();
148
+
149
+ // Declarado antes del handle para que las funciones de terminación puedan
150
+ // removerlo. Origen: F-1 nemesis iter-1 — sin remove, cada spinner deja un
151
+ // listener muerto en `process` y un loop con N runtimes acumula warnings
152
+ // de MaxListenersExceeded.
153
+ const exitHandler = () => { handle.detener(); };
154
+
112
155
  const handle = {
113
156
  actualizar(msg) {
114
157
  textoActual = msg;
115
158
  },
116
159
  exito(msg) {
117
160
  activo = false;
118
- clearInterval(intervalo);
161
+ _detenerIntervalo();
162
+ _spinnersActivos.delete(handle);
163
+ process.removeListener('exit', exitHandler);
119
164
  limpiarLinea();
120
165
  console.log(` ${color.verde(icono.check)} ${msg}`);
121
166
  },
122
167
  fallo(msg) {
123
168
  activo = false;
124
- clearInterval(intervalo);
169
+ _detenerIntervalo();
170
+ _spinnersActivos.delete(handle);
171
+ process.removeListener('exit', exitHandler);
125
172
  limpiarLinea();
126
173
  console.log(` ${color.rojo(icono.cross)} ${msg}`);
127
174
  },
128
175
  detener() {
129
176
  activo = false;
130
- clearInterval(intervalo);
177
+ _detenerIntervalo();
178
+ _spinnersActivos.delete(handle);
179
+ process.removeListener('exit', exitHandler);
180
+ limpiarLinea();
181
+ },
182
+ _pausar() {
183
+ // Pausa el tick para que un readline pueda renderizar el prompt sin
184
+ // que el spinner lo sobreescriba con `\r`. Limpia la línea para que
185
+ // el prompt empiece en una columna libre.
186
+ if (pausado) return;
187
+ pausado = true;
188
+ _detenerIntervalo();
131
189
  limpiarLinea();
132
190
  },
191
+ _reanudar() {
192
+ // Reanuda el tick tras el cierre del readline. Solo si el spinner sigue
193
+ // lógicamente activo (no fue detenido durante la pausa).
194
+ if (!pausado || !activo) return;
195
+ pausado = false;
196
+ _arrancarIntervalo();
197
+ },
133
198
  };
134
199
 
135
- // Limpieza en caso de que el proceso muera
136
- const exitHandler = () => { handle.detener(); };
200
+ _spinnersActivos.add(handle);
201
+
202
+ // Limpieza si el proceso muere antes de exito/fallo/detener
137
203
  process.once('exit', exitHandler);
138
204
 
139
205
  return handle;
@@ -151,19 +217,36 @@ function preguntarSiNo(mensaje, valorDefault = true) {
151
217
  }
152
218
 
153
219
  const hint = valorDefault ? 'S/n' : 's/N';
220
+ _pausarSpinnersActivos();
154
221
  const rl = readline.createInterface({
155
222
  input: process.stdin,
156
223
  output: process.stdout,
157
224
  });
158
225
 
226
+ // El evento 'close' es la única ruta de finalización. Se garantiza que
227
+ // se dispare incluso si el callback de question no se ejecuta (Ctrl+C,
228
+ // EOF, error en stdin). Esto evita el bug donde los spinners quedaban
229
+ // permanentemente pausados tras un cierre prematuro del readline.
230
+ let valorFinal = valorDefault;
231
+ let resuelto = false;
232
+
233
+ function finalizar() {
234
+ if (resuelto) return;
235
+ resuelto = true;
236
+ _reanudarSpinnersActivos();
237
+ resolve(valorFinal);
238
+ }
239
+
240
+ rl.on('close', finalizar);
241
+
159
242
  rl.question(` ${mensaje} [${hint}] `, (respuesta) => {
160
- rl.close();
161
243
  const r = respuesta.trim().toLowerCase();
162
244
  if (r === '') {
163
- resolve(valorDefault);
245
+ valorFinal = valorDefault;
164
246
  } else {
165
- resolve(r === 's' || r === 'si' || r === 'sí' || r === 'y' || r === 'yes');
247
+ valorFinal = r === 's' || r === 'si' || r === 'sí' || r === 'y' || r === 'yes';
166
248
  }
249
+ rl.close(); // dispara 'close' → finalizar(); idempotente vía `resuelto`
167
250
  });
168
251
  });
169
252
  }
@@ -185,6 +268,11 @@ function preguntarOpcion(titulo, opciones, opts = {}) {
185
268
  return;
186
269
  }
187
270
 
271
+ // Pausar antes de emitir el menú con console.log — si hay spinner activo,
272
+ // un tick a 80ms puede sobreescribir la primera línea del menú. Origen:
273
+ // F-2 nemesis iter-1.
274
+ _pausarSpinnersActivos();
275
+
188
276
  console.log('');
189
277
  console.log(` ${titulo}`);
190
278
  console.log('');
@@ -197,21 +285,36 @@ function preguntarOpcion(titulo, opciones, opts = {}) {
197
285
 
198
286
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
199
287
  const hint = `[1-${opciones.length}, Enter=${opciones[indiceDefault].valor}]`;
288
+
289
+ // Ver preguntarSiNo: 'close' garantiza reanudación incluso si el callback
290
+ // de question no se invoca (Ctrl+C, EOF, error).
291
+ let valorFinal = opciones[indiceDefault].valor;
292
+ let resuelto = false;
293
+
294
+ function finalizar() {
295
+ if (resuelto) return;
296
+ resuelto = true;
297
+ _reanudarSpinnersActivos();
298
+ resolve(valorFinal);
299
+ }
300
+
301
+ rl.on('close', finalizar);
302
+
200
303
  rl.question(` Tu elección ${hint}: `, (respuesta) => {
201
- rl.close();
202
304
  const r = respuesta.trim();
203
305
  if (r === '') {
204
- resolve(opciones[indiceDefault].valor);
205
- return;
206
- }
207
- const n = parseInt(r, 10);
208
- if (Number.isFinite(n) && n >= 1 && n <= opciones.length) {
209
- resolve(opciones[n - 1].valor);
306
+ valorFinal = opciones[indiceDefault].valor;
210
307
  } else {
211
- // Permitir también escribir el valor directo si coincide
212
- const match = opciones.find(o => o.valor === r);
213
- resolve(match ? match.valor : opciones[indiceDefault].valor);
308
+ const n = parseInt(r, 10);
309
+ if (Number.isFinite(n) && n >= 1 && n <= opciones.length) {
310
+ valorFinal = opciones[n - 1].valor;
311
+ } else {
312
+ // Permitir también escribir el valor directo si coincide
313
+ const match = opciones.find(o => o.valor === r);
314
+ valorFinal = match ? match.valor : opciones[indiceDefault].valor;
315
+ }
214
316
  }
317
+ rl.close();
215
318
  });
216
319
  });
217
320
  }
@@ -230,10 +333,26 @@ function preguntarTexto(mensaje, valorDefault = '') {
230
333
  return;
231
334
  }
232
335
  const hint = valorDefault ? ` [${valorDefault}]` : '';
336
+ _pausarSpinnersActivos();
233
337
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
338
+
339
+ // Ver preguntarSiNo: 'close' garantiza reanudación incluso si el callback
340
+ // de question no se invoca (Ctrl+C, EOF, error).
341
+ let valorFinal = valorDefault;
342
+ let resuelto = false;
343
+
344
+ function finalizar() {
345
+ if (resuelto) return;
346
+ resuelto = true;
347
+ _reanudarSpinnersActivos();
348
+ resolve(valorFinal);
349
+ }
350
+
351
+ rl.on('close', finalizar);
352
+
234
353
  rl.question(` ${mensaje}${hint}: `, (respuesta) => {
354
+ valorFinal = respuesta.trim() || valorDefault;
235
355
  rl.close();
236
- resolve(respuesta.trim() || valorDefault);
237
356
  });
238
357
  });
239
358
  }
@@ -245,6 +364,13 @@ function preguntarTexto(mensaje, valorDefault = '') {
245
364
  module.exports = {
246
365
  SOPORTA_COLOR,
247
366
  ES_TTY,
367
+ // Exports internos para tests del fix de race spinner/prompt. NO usar en
368
+ // código de producción — la API estable es spinner() + preguntar*.
369
+ __internalForTesting: {
370
+ spinnersActivos: _spinnersActivos,
371
+ pausarSpinnersActivos: _pausarSpinnersActivos,
372
+ reanudarSpinnersActivos: _reanudarSpinnersActivos,
373
+ },
248
374
  color,
249
375
  icono,
250
376
  formatearPaso,
@@ -1,131 +1,131 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
-
4
- /**
5
- * limpiar-artefactos-python.js — limpia caché Python antes de empaquetar.
6
- *
7
- * Se ejecuta como `prepack` antes de `npm pack` y `npm publish` para evitar
8
- * que artefactos locales (.pyc, __pycache__/) contaminen el tarball publicado.
9
- *
10
- * Reglas de seguridad (defensa en profundidad):
11
- * 1. Aborta si cwd no coincide con la raíz del package.json del repo.
12
- * 2. Profundidad máxima de recursión: 3 niveles desde la raíz.
13
- * 3. Allowlist explícita de directorios a EXCLUIR de la búsqueda
14
- * (node_modules, .git, temp, .planning, respositorios-git, _userland).
15
- * 4. Solo elimina directorios cuyo nombre coincide exactamente con el
16
- * conjunto cerrado: __pycache__, .pytest_cache, .mypy_cache, .ruff_cache.
17
- * 5. Solo elimina archivos sueltos con extensiones .pyc, .pyo.
18
- * 6. En CI no-interactivo respeta el flag SWL_PREPACK_DRY=1 (no borra,
19
- * solo lista).
20
- *
21
- * Exit codes:
22
- * 0 — OK (limpieza ejecutada o nada que limpiar)
23
- * 1 — error de invariante (cwd incorrecto, package.json no encontrado)
24
- */
25
-
26
- const fs = require('fs');
27
- const path = require('path');
28
-
29
- const { NOMBRES_VALIDOS } = require('./lib/paquetes-conocidos');
30
- const {
31
- DIRS_ARTEFACTOS_PYTHON: DIRS_A_LIMPIAR,
32
- EXTS_ARTEFACTOS_PYTHON: EXTS_A_LIMPIAR,
33
- } = require('./lib/artefactos-python');
34
-
35
- const PROFUNDIDAD_MAX = 3;
36
- const DIRS_EXCLUIDOS = new Set([
37
- 'node_modules', '.git', 'temp', '.planning', 'respositorios-git',
38
- '_userland', 'tests', '.github',
39
- ]);
40
-
41
- const dryRun = process.env.SWL_PREPACK_DRY === '1';
42
-
43
- function log(msg) { process.stdout.write(`[prepack] ${msg}\n`); }
44
- function err(msg) { process.stderr.write(`[prepack] ERROR: ${msg}\n`); }
45
-
46
- /**
47
- * Verifica que el cwd actual contiene el package.json del repo swl-ses.
48
- * Esto evita que el script borre directorios en la máquina del usuario si
49
- * algún día se invocara desde un cwd incorrecto (ej. dentro de un tarball
50
- * extraído por npm en .npm/_cacache).
51
- */
52
- function verificarRaiz() {
53
- const cwd = process.cwd();
54
- const pkgPath = path.join(cwd, 'package.json');
55
- if (!fs.existsSync(pkgPath)) {
56
- err(`no existe package.json en cwd: ${cwd}`);
57
- process.exit(1);
58
- }
59
- const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
60
- if (!NOMBRES_VALIDOS.includes(pkg.name)) {
61
- err(`package.json en ${cwd} no corresponde a swl-ses (name: ${pkg.name}). Abortando por seguridad.`);
62
- process.exit(1);
63
- }
64
- return cwd;
65
- }
66
-
67
- /**
68
- * Recorre el árbol desde root con profundidad limitada y elimina los
69
- * artefactos Python detectados. No sigue symlinks.
70
- */
71
- function limpiar(root, profundidad = 0) {
72
- if (profundidad > PROFUNDIDAD_MAX) return { dirs: 0, files: 0 };
73
-
74
- let dirsEliminados = 0;
75
- let filesEliminados = 0;
76
-
77
- let entries;
78
- try {
79
- entries = fs.readdirSync(root, { withFileTypes: true });
80
- } catch (e) {
81
- err(`no se pudo leer ${root}: ${e.message}`);
82
- return { dirs: 0, files: 0 };
83
- }
84
-
85
- for (const entry of entries) {
86
- if (entry.isSymbolicLink()) continue;
87
- const fullPath = path.join(root, entry.name);
88
-
89
- if (entry.isDirectory()) {
90
- if (DIRS_A_LIMPIAR.has(entry.name)) {
91
- if (dryRun) {
92
- log(`[dry-run] borraría dir: ${path.relative(process.cwd(), fullPath)}`);
93
- } else {
94
- fs.rmSync(fullPath, { recursive: true, force: true });
95
- log(`borrado dir: ${path.relative(process.cwd(), fullPath)}`);
96
- }
97
- dirsEliminados++;
98
- } else if (!DIRS_EXCLUIDOS.has(entry.name) && !entry.name.startsWith('.')) {
99
- const sub = limpiar(fullPath, profundidad + 1);
100
- dirsEliminados += sub.dirs;
101
- filesEliminados += sub.files;
102
- }
103
- } else if (entry.isFile()) {
104
- const ext = path.extname(entry.name).toLowerCase();
105
- if (EXTS_A_LIMPIAR.has(ext)) {
106
- if (dryRun) {
107
- log(`[dry-run] borraría archivo: ${path.relative(process.cwd(), fullPath)}`);
108
- } else {
109
- fs.rmSync(fullPath, { force: true });
110
- }
111
- filesEliminados++;
112
- }
113
- }
114
- }
115
-
116
- return { dirs: dirsEliminados, files: filesEliminados };
117
- }
118
-
119
- function main() {
120
- const root = verificarRaiz();
121
- const result = limpiar(root);
122
-
123
- if (result.dirs === 0 && result.files === 0) {
124
- log('sin artefactos Python que limpiar.');
125
- } else {
126
- const accion = dryRun ? 'detectados' : 'eliminados';
127
- log(`${accion}: ${result.dirs} directorios + ${result.files} archivos.`);
128
- }
129
- }
130
-
131
- main();
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * limpiar-artefactos-python.js — limpia caché Python antes de empaquetar.
6
+ *
7
+ * Se ejecuta como `prepack` antes de `npm pack` y `npm publish` para evitar
8
+ * que artefactos locales (.pyc, __pycache__/) contaminen el tarball publicado.
9
+ *
10
+ * Reglas de seguridad (defensa en profundidad):
11
+ * 1. Aborta si cwd no coincide con la raíz del package.json del repo.
12
+ * 2. Profundidad máxima de recursión: 3 niveles desde la raíz.
13
+ * 3. Allowlist explícita de directorios a EXCLUIR de la búsqueda
14
+ * (node_modules, .git, temp, .planning, respositorios-git, _userland).
15
+ * 4. Solo elimina directorios cuyo nombre coincide exactamente con el
16
+ * conjunto cerrado: __pycache__, .pytest_cache, .mypy_cache, .ruff_cache.
17
+ * 5. Solo elimina archivos sueltos con extensiones .pyc, .pyo.
18
+ * 6. En CI no-interactivo respeta el flag SWL_PREPACK_DRY=1 (no borra,
19
+ * solo lista).
20
+ *
21
+ * Exit codes:
22
+ * 0 — OK (limpieza ejecutada o nada que limpiar)
23
+ * 1 — error de invariante (cwd incorrecto, package.json no encontrado)
24
+ */
25
+
26
+ const fs = require('fs');
27
+ const path = require('path');
28
+
29
+ const { NOMBRES_VALIDOS } = require('./lib/paquetes-conocidos');
30
+ const {
31
+ DIRS_ARTEFACTOS_PYTHON: DIRS_A_LIMPIAR,
32
+ EXTS_ARTEFACTOS_PYTHON: EXTS_A_LIMPIAR,
33
+ } = require('./lib/artefactos-python');
34
+
35
+ const PROFUNDIDAD_MAX = 3;
36
+ const DIRS_EXCLUIDOS = new Set([
37
+ 'node_modules', '.git', 'temp', '.planning', 'respositorios-git',
38
+ '_userland', 'tests', '.github',
39
+ ]);
40
+
41
+ const dryRun = process.env.SWL_PREPACK_DRY === '1';
42
+
43
+ function log(msg) { process.stdout.write(`[prepack] ${msg}\n`); }
44
+ function err(msg) { process.stderr.write(`[prepack] ERROR: ${msg}\n`); }
45
+
46
+ /**
47
+ * Verifica que el cwd actual contiene el package.json del repo swl-ses.
48
+ * Esto evita que el script borre directorios en la máquina del usuario si
49
+ * algún día se invocara desde un cwd incorrecto (ej. dentro de un tarball
50
+ * extraído por npm en .npm/_cacache).
51
+ */
52
+ function verificarRaiz() {
53
+ const cwd = process.cwd();
54
+ const pkgPath = path.join(cwd, 'package.json');
55
+ if (!fs.existsSync(pkgPath)) {
56
+ err(`no existe package.json en cwd: ${cwd}`);
57
+ process.exit(1);
58
+ }
59
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
60
+ if (!NOMBRES_VALIDOS.includes(pkg.name)) {
61
+ err(`package.json en ${cwd} no corresponde a swl-ses (name: ${pkg.name}). Abortando por seguridad.`);
62
+ process.exit(1);
63
+ }
64
+ return cwd;
65
+ }
66
+
67
+ /**
68
+ * Recorre el árbol desde root con profundidad limitada y elimina los
69
+ * artefactos Python detectados. No sigue symlinks.
70
+ */
71
+ function limpiar(root, profundidad = 0) {
72
+ if (profundidad > PROFUNDIDAD_MAX) return { dirs: 0, files: 0 };
73
+
74
+ let dirsEliminados = 0;
75
+ let filesEliminados = 0;
76
+
77
+ let entries;
78
+ try {
79
+ entries = fs.readdirSync(root, { withFileTypes: true });
80
+ } catch (e) {
81
+ err(`no se pudo leer ${root}: ${e.message}`);
82
+ return { dirs: 0, files: 0 };
83
+ }
84
+
85
+ for (const entry of entries) {
86
+ if (entry.isSymbolicLink()) continue;
87
+ const fullPath = path.join(root, entry.name);
88
+
89
+ if (entry.isDirectory()) {
90
+ if (DIRS_A_LIMPIAR.has(entry.name)) {
91
+ if (dryRun) {
92
+ log(`[dry-run] borraría dir: ${path.relative(process.cwd(), fullPath)}`);
93
+ } else {
94
+ fs.rmSync(fullPath, { recursive: true, force: true });
95
+ log(`borrado dir: ${path.relative(process.cwd(), fullPath)}`);
96
+ }
97
+ dirsEliminados++;
98
+ } else if (!DIRS_EXCLUIDOS.has(entry.name) && !entry.name.startsWith('.')) {
99
+ const sub = limpiar(fullPath, profundidad + 1);
100
+ dirsEliminados += sub.dirs;
101
+ filesEliminados += sub.files;
102
+ }
103
+ } else if (entry.isFile()) {
104
+ const ext = path.extname(entry.name).toLowerCase();
105
+ if (EXTS_A_LIMPIAR.has(ext)) {
106
+ if (dryRun) {
107
+ log(`[dry-run] borraría archivo: ${path.relative(process.cwd(), fullPath)}`);
108
+ } else {
109
+ fs.rmSync(fullPath, { force: true });
110
+ }
111
+ filesEliminados++;
112
+ }
113
+ }
114
+ }
115
+
116
+ return { dirs: dirsEliminados, files: filesEliminados };
117
+ }
118
+
119
+ function main() {
120
+ const root = verificarRaiz();
121
+ const result = limpiar(root);
122
+
123
+ if (result.dirs === 0 && result.files === 0) {
124
+ log('sin artefactos Python que limpiar.');
125
+ } else {
126
+ const accion = dryRun ? 'detectados' : 'eliminados';
127
+ log(`${accion}: ${result.dirs} directorios + ${result.files} archivos.`);
128
+ }
129
+ }
130
+
131
+ main();
@@ -30,6 +30,10 @@ from datetime import datetime, timezone
30
30
  from pathlib import Path
31
31
  from typing import Any
32
32
 
33
+ # Importar helper compartido sin convertir scripts/ en paquete.
34
+ sys.path.insert(0, str(Path(__file__).resolve().parent / 'lib'))
35
+ from mcp_config import cargar_config_mcp # noqa: E402
36
+
33
37
  # ---------------------------------------------------------------------------
34
38
  # Dependencias — raw mcp SDK
35
39
  # ---------------------------------------------------------------------------
@@ -45,32 +49,18 @@ except ImportError:
45
49
  # Constantes
46
50
  # ---------------------------------------------------------------------------
47
51
 
48
- SETTINGS_CANDIDATES = [
49
- '.claude/settings.local.json',
50
- '.claude/settings.json',
51
- 'mcp-servers.json',
52
- ]
53
-
54
52
  TIMEOUT_S = 12
55
53
  SNAPSHOT_FILE = Path('.planning') / 'mcp-snapshot.json'
56
54
 
57
55
  # ---------------------------------------------------------------------------
58
- # Carga de config (identica a mcp-pool-manager para evitar dependencia circular)
56
+ # Carga de config delega a scripts/lib/mcp_config.py para deep merge
57
+ # unificado con mcp-pool-manager.py. Antes hacia first-wins y rompia el
58
+ # health check cuando settings.local.json contenia overrides parciales.
59
59
  # ---------------------------------------------------------------------------
60
60
 
61
61
 
62
62
  def _cargar_config(cwd: Path, config_path: str | None = None) -> dict:
63
- candidates = [Path(config_path)] if config_path else [cwd / p for p in SETTINGS_CANDIDATES]
64
- for p in candidates:
65
- if p.exists():
66
- try:
67
- data = json.loads(p.read_text(encoding='utf-8'))
68
- servers = data.get('mcpServers', {})
69
- if servers:
70
- return servers
71
- except Exception:
72
- continue
73
- return {}
63
+ return cargar_config_mcp(cwd, config_path)
74
64
 
75
65
 
76
66
  def _build_env(cfg: dict) -> dict | None:
@@ -4,10 +4,11 @@
4
4
  Conecta a servidores MCP configurados en .claude/settings.json y expone
5
5
  sus herramientas para uso por agentes y scripts de swl-ses.
6
6
 
7
- Lee la configuracion de mcpServers en orden de precedencia:
8
- 1. .claude/settings.local.json
9
- 2. .claude/settings.json
10
- 3. mcp-servers.json (config independiente opcional)
7
+ Lee la configuracion fusionando las capas con deep merge (semantica de
8
+ Claude Code): mcp-servers.json -> .claude/settings.json -> .claude/settings.local.json.
9
+ La capa mas profunda sobrescribe por clave; env se mergea por sub-clave.
10
+ Esto permite que settings.local.json contenga overrides parciales (p.ej.
11
+ solo OBSIDIAN_API_KEY) sin perder command/args de la capa compartida.
11
12
 
12
13
  Uso:
13
14
  python scripts/mcp-pool-manager.py list-servers
@@ -30,6 +31,10 @@ import time
30
31
  from pathlib import Path
31
32
  from typing import Any
32
33
 
34
+ # Importar helper compartido sin convertir scripts/ en paquete.
35
+ sys.path.insert(0, str(Path(__file__).resolve().parent / 'lib'))
36
+ from mcp_config import cargar_config_mcp # noqa: E402
37
+
33
38
  # ---------------------------------------------------------------------------
34
39
  # Dependencias — raw mcp SDK (disponible en entornos con Claude Code)
35
40
  # ---------------------------------------------------------------------------
@@ -45,33 +50,17 @@ except ImportError:
45
50
  # Constantes
46
51
  # ---------------------------------------------------------------------------
47
52
 
48
- SETTINGS_CANDIDATES = [
49
- '.claude/settings.local.json',
50
- '.claude/settings.json',
51
- 'mcp-servers.json',
52
- ]
53
-
54
53
  TIMEOUT_CONNECT_S = 10 # segundos maximo para conectar a un servidor
55
54
  TIMEOUT_CALL_S = 30 # segundos maximo para llamar una herramienta
56
55
 
57
56
  # ---------------------------------------------------------------------------
58
- # Carga de configuracion
57
+ # Carga de configuracion — delega a scripts/lib/mcp_config.py
59
58
  # ---------------------------------------------------------------------------
60
59
 
61
60
 
62
61
  def _cargar_config(cwd: Path, config_path: str | None = None) -> dict:
63
- """Carga la seccion mcpServers del primer archivo de config encontrado."""
64
- candidates = [Path(config_path)] if config_path else [cwd / p for p in SETTINGS_CANDIDATES]
65
- for p in candidates:
66
- if p.exists():
67
- try:
68
- data = json.loads(p.read_text(encoding='utf-8'))
69
- servers = data.get('mcpServers', {})
70
- if servers:
71
- return servers
72
- except Exception:
73
- continue
74
- return {}
62
+ """Devuelve los servidores MCP fusionados de todas las capas."""
63
+ return cargar_config_mcp(cwd, config_path)
75
64
 
76
65
 
77
66
  def _get_server(servers: dict, nombre: str) -> dict: