@saulwade/swl-ses 1.6.3 → 1.6.5

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 (42) hide show
  1. package/CLAUDE.md +3 -3
  2. package/README.md +2 -2
  3. package/agentes/gh-fix-ci-swl.md +275 -0
  4. package/agentes/nemesis-auditor-swl.md +90 -1
  5. package/comandos/swl/exportar-vault.md +106 -14
  6. package/comandos/swl/nemesis.md +70 -3
  7. package/comandos/swl/release.md +62 -2
  8. package/comandos/swl/salud.md +32 -0
  9. package/comandos/swl/verificar.md +116 -2
  10. package/habilidades/agent-browser/SKILL.md +111 -4
  11. package/habilidades/agent-deep-links/SKILL.md +148 -0
  12. package/habilidades/backend-async-postgres-testing/SKILL.md +215 -0
  13. package/habilidades/backend-error-design/SKILL.md +221 -0
  14. package/habilidades/browser-interaction-patterns/SKILL.md +514 -0
  15. package/habilidades/browser-research-domains/SKILL.md +635 -0
  16. package/habilidades/changelog-generator/SKILL.md +172 -0
  17. package/habilidades/changelog-generator/scripts/parse-commits.js +354 -0
  18. package/habilidades/devsecops-pipeline-security/SKILL.md +3 -0
  19. package/habilidades/fastapi-experto/SKILL.md +49 -4
  20. package/habilidades/harness-claude-code/SKILL.md +4 -1
  21. package/habilidades/postgresql-experto/SKILL.md +80 -4
  22. package/habilidades/proceso-discovery-machote/SKILL.md +157 -0
  23. package/habilidades/proceso-modular-split/SKILL.md +256 -0
  24. package/habilidades/tdd-workflow/SKILL.md +12 -5
  25. package/hooks/extraccion-aprendizajes.js +8 -0
  26. package/hooks/lib/deep-links.js +185 -0
  27. package/hooks/lib/evolution-tracker.js +115 -18
  28. package/hooks/lib/gateway-notify.js +70 -7
  29. package/manifiestos/modulos.json +13 -3
  30. package/manifiestos/skills-lock.json +1247 -1191
  31. package/package.json +3 -3
  32. package/plugin.json +11 -2
  33. package/reglas/arquitectura.md +38 -0
  34. package/reglas/arreglar-al-detectar.md +93 -0
  35. package/reglas/auditorias-documentales-estructurales.md +38 -0
  36. package/reglas/registro-componentes-nuevos.md +14 -0
  37. package/reglas/tests-cleanup.md +220 -0
  38. package/scripts/lib/mcp_config.py +29 -14
  39. package/scripts/mcp-orchestrator.py +153 -131
  40. package/scripts/mcp-pool-manager.py +132 -107
  41. package/scripts/mcp-telemetry.py +139 -120
  42. package/scripts/verificar-release.js +199 -1
@@ -32,8 +32,8 @@ from pathlib import Path
32
32
  from typing import Any
33
33
 
34
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
35
+ sys.path.insert(0, str(Path(__file__).resolve().parent / "lib"))
36
+ from mcp_config import build_stdio_env, cargar_config_mcp # noqa: E402
37
37
 
38
38
  # ---------------------------------------------------------------------------
39
39
  # Dependencias — raw mcp SDK (disponible en entornos con Claude Code)
@@ -42,6 +42,7 @@ try:
42
42
  from mcp import ClientSession
43
43
  from mcp.client.stdio import stdio_client, StdioServerParameters
44
44
  from mcp.client.sse import sse_client
45
+
45
46
  HAS_MCP = True
46
47
  except ImportError:
47
48
  HAS_MCP = False
@@ -50,8 +51,8 @@ except ImportError:
50
51
  # Constantes
51
52
  # ---------------------------------------------------------------------------
52
53
 
53
- TIMEOUT_CONNECT_S = 10 # segundos maximo para conectar a un servidor
54
- TIMEOUT_CALL_S = 30 # segundos maximo para llamar una herramienta
54
+ TIMEOUT_CONNECT_S = 10 # segundos maximo para conectar a un servidor
55
+ TIMEOUT_CALL_S = 30 # segundos maximo para llamar una herramienta
55
56
 
56
57
  # ---------------------------------------------------------------------------
57
58
  # Carga de configuracion — delega a scripts/lib/mcp_config.py
@@ -66,34 +67,33 @@ def _cargar_config(cwd: Path, config_path: str | None = None) -> dict:
66
67
  def _get_server(servers: dict, nombre: str) -> dict:
67
68
  """Retorna la config de un servidor o termina con error."""
68
69
  if nombre not in servers:
69
- sys.stderr.write(f'[mcp-pool] Servidor no encontrado: {nombre}\n')
70
- sys.stderr.write(f'[mcp-pool] Servidores disponibles: {", ".join(servers.keys())}\n')
70
+ sys.stderr.write(f"[mcp-pool] Servidor no encontrado: {nombre}\n")
71
+ sys.stderr.write(
72
+ f'[mcp-pool] Servidores disponibles: {", ".join(servers.keys())}\n'
73
+ )
71
74
  sys.exit(1)
72
75
  return servers[nombre]
73
76
 
77
+
74
78
  # ---------------------------------------------------------------------------
75
79
  # Fabrica de transporte
76
80
  # ---------------------------------------------------------------------------
77
81
 
78
82
 
79
- def _build_env(cfg: dict) -> dict | None:
83
+ def _build_env(cfg: dict) -> dict:
80
84
  """Combina el entorno del proceso con las variables de entorno del servidor."""
81
- extra = cfg.get('env') or {}
82
- if not extra:
83
- return None
84
- merged = dict(os.environ)
85
- merged.update(extra)
86
- return merged
85
+ return build_stdio_env(cfg)
87
86
 
88
87
 
89
88
  def _make_stdio_params(cfg: dict) -> StdioServerParameters:
90
89
  return StdioServerParameters(
91
- command=cfg['command'],
92
- args=cfg.get('args', []),
90
+ command=cfg["command"],
91
+ args=cfg.get("args", []),
93
92
  env=_build_env(cfg),
94
- cwd=cfg.get('cwd'),
93
+ cwd=cfg.get("cwd"),
95
94
  )
96
95
 
96
+
97
97
  # ---------------------------------------------------------------------------
98
98
  # Operaciones MCP (async helpers)
99
99
  # ---------------------------------------------------------------------------
@@ -101,43 +101,51 @@ def _make_stdio_params(cfg: dict) -> StdioServerParameters:
101
101
 
102
102
  async def _list_tools_server(nombre: str, cfg: dict) -> dict:
103
103
  """Conecta a un servidor MCP y lista sus herramientas. Tolera errores."""
104
- resultado: dict = {'server': nombre, 'tools': [], 'error': None, 'duration_ms': 0}
104
+ resultado: dict = {"server": nombre, "tools": [], "error": None, "duration_ms": 0}
105
105
  t0 = time.time()
106
106
  try:
107
- if 'url' in cfg:
108
- transport = sse_client(cfg['url'])
107
+ if "url" in cfg:
108
+ transport = sse_client(cfg["url"])
109
109
  else:
110
110
  transport = stdio_client(_make_stdio_params(cfg))
111
111
 
112
112
  async with transport as (read, write):
113
113
  async with ClientSession(read, write) as session:
114
114
  await asyncio.wait_for(session.initialize(), timeout=TIMEOUT_CONNECT_S)
115
- resp = await asyncio.wait_for(session.list_tools(), timeout=TIMEOUT_CONNECT_S)
116
- resultado['tools'] = [
115
+ resp = await asyncio.wait_for(
116
+ session.list_tools(), timeout=TIMEOUT_CONNECT_S
117
+ )
118
+ resultado["tools"] = [
117
119
  {
118
- 'name': t.name,
119
- 'description': t.description or '',
120
- 'schema': t.inputSchema or {},
120
+ "name": t.name,
121
+ "description": t.description or "",
122
+ "schema": t.inputSchema or {},
121
123
  }
122
124
  for t in (resp.tools or [])
123
125
  ]
124
126
  except asyncio.TimeoutError:
125
- resultado['error'] = f'Timeout al conectar ({TIMEOUT_CONNECT_S}s)'
127
+ resultado["error"] = f"Timeout al conectar ({TIMEOUT_CONNECT_S}s)"
126
128
  except Exception as exc:
127
- resultado['error'] = str(exc)
129
+ resultado["error"] = str(exc)
128
130
  finally:
129
- resultado['duration_ms'] = int((time.time() - t0) * 1000)
131
+ resultado["duration_ms"] = int((time.time() - t0) * 1000)
130
132
  return resultado
131
133
 
132
134
 
133
135
  async def _call_tool_server(nombre: str, cfg: dict, tool: str, args: dict) -> dict:
134
136
  """Llama una herramienta en un servidor MCP."""
135
- resultado: dict = {'server': nombre, 'tool': tool, 'result': None,
136
- 'is_error': False, 'error': None, 'duration_ms': 0}
137
+ resultado: dict = {
138
+ "server": nombre,
139
+ "tool": tool,
140
+ "result": None,
141
+ "is_error": False,
142
+ "error": None,
143
+ "duration_ms": 0,
144
+ }
137
145
  t0 = time.time()
138
146
  try:
139
- if 'url' in cfg:
140
- transport = sse_client(cfg['url'])
147
+ if "url" in cfg:
148
+ transport = sse_client(cfg["url"])
141
149
  else:
142
150
  transport = stdio_client(_make_stdio_params(cfg))
143
151
 
@@ -147,19 +155,20 @@ async def _call_tool_server(nombre: str, cfg: dict, tool: str, args: dict) -> di
147
155
  resp = await asyncio.wait_for(
148
156
  session.call_tool(tool, args), timeout=TIMEOUT_CALL_S
149
157
  )
150
- resultado['is_error'] = bool(getattr(resp, 'isError', False))
151
- resultado['result'] = [
152
- c.text if hasattr(c, 'text') else str(c)
158
+ resultado["is_error"] = bool(getattr(resp, "isError", False))
159
+ resultado["result"] = [
160
+ c.text if hasattr(c, "text") else str(c)
153
161
  for c in (resp.content or [])
154
162
  ]
155
163
  except asyncio.TimeoutError:
156
- resultado['error'] = f'Timeout al llamar herramienta ({TIMEOUT_CALL_S}s)'
164
+ resultado["error"] = f"Timeout al llamar herramienta ({TIMEOUT_CALL_S}s)"
157
165
  except Exception as exc:
158
- resultado['error'] = str(exc)
166
+ resultado["error"] = str(exc)
159
167
  finally:
160
- resultado['duration_ms'] = int((time.time() - t0) * 1000)
168
+ resultado["duration_ms"] = int((time.time() - t0) * 1000)
161
169
  return resultado
162
170
 
171
+
163
172
  # ---------------------------------------------------------------------------
164
173
  # Subcomandos
165
174
  # ---------------------------------------------------------------------------
@@ -169,94 +178,104 @@ async def cmd_list_servers(servers: dict, as_json: bool) -> None:
169
178
  out = sys.stdout.write
170
179
  if as_json:
171
180
  payload = [
172
- {'name': n, 'transport': 'http' if 'url' in c else 'stdio',
173
- 'command': c.get('url') or f"{c.get('command', '?')} {' '.join(c.get('args', []))}"}
181
+ {
182
+ "name": n,
183
+ "transport": "http" if "url" in c else "stdio",
184
+ "command": c.get("url")
185
+ or f"{c.get('command', '?')} {' '.join(c.get('args', []))}",
186
+ }
174
187
  for n, c in servers.items()
175
188
  ]
176
- out(json.dumps(payload, ensure_ascii=False, indent=2) + '\n')
189
+ out(json.dumps(payload, ensure_ascii=False, indent=2) + "\n")
177
190
  return
178
- out(f'Servidores MCP configurados ({len(servers)}):\n')
179
- out('-' * 60 + '\n')
191
+ out(f"Servidores MCP configurados ({len(servers)}):\n")
192
+ out("-" * 60 + "\n")
180
193
  for nombre, cfg in servers.items():
181
- if 'url' in cfg:
194
+ if "url" in cfg:
182
195
  detalle = f"HTTP {cfg['url']}"
183
196
  else:
184
197
  detalle = f"stdio {cfg.get('command', '?')} {' '.join(cfg.get('args', []))}"
185
- out(f' {nombre:<28} {detalle}\n')
198
+ out(f" {nombre:<28} {detalle}\n")
186
199
 
187
200
 
188
- async def cmd_list_tools(servers: dict, server_filter: str | None, as_json: bool) -> None:
201
+ async def cmd_list_tools(
202
+ servers: dict, server_filter: str | None, as_json: bool
203
+ ) -> None:
189
204
  out = sys.stdout.write
190
205
  targets = {server_filter: servers[server_filter]} if server_filter else servers
191
- tareas = [_list_tools_server(n, c) for n, c in targets.items()]
206
+ tareas = [_list_tools_server(n, c) for n, c in targets.items()]
192
207
  resultados = await asyncio.gather(*tareas)
193
208
 
194
209
  if as_json:
195
- out(json.dumps(resultados, ensure_ascii=False, indent=2) + '\n')
210
+ out(json.dumps(resultados, ensure_ascii=False, indent=2) + "\n")
196
211
  return
197
212
 
198
213
  for r in resultados:
199
- if r['error']:
214
+ if r["error"]:
200
215
  out(f'\n[{r["server"]}] ERROR: {r["error"]}\n')
201
216
  continue
202
- out(f'\n[{r["server"]}] {len(r["tools"])} herramientas ({r["duration_ms"]}ms):\n')
203
- for t in r['tools']:
204
- desc = t['description']
217
+ out(
218
+ f'\n[{r["server"]}] {len(r["tools"])} herramientas ({r["duration_ms"]}ms):\n'
219
+ )
220
+ for t in r["tools"]:
221
+ desc = t["description"]
205
222
  if len(desc) > 80:
206
- desc = desc[:77] + '...'
223
+ desc = desc[:77] + "..."
207
224
  out(f' - {t["name"]:<32} {desc}\n')
208
225
 
209
226
 
210
227
  async def cmd_ping(servers: dict, server_filter: str | None, as_json: bool) -> None:
211
228
  out = sys.stdout.write
212
- targets = {server_filter: servers[server_filter]} if server_filter else servers
213
- tareas = [_list_tools_server(n, c) for n, c in targets.items()]
229
+ targets = {server_filter: servers[server_filter]} if server_filter else servers
230
+ tareas = [_list_tools_server(n, c) for n, c in targets.items()]
214
231
  resultados = await asyncio.gather(*tareas)
215
232
 
216
233
  pings = [
217
234
  {
218
- 'server': r['server'],
219
- 'estado': 'OK' if not r['error'] else 'ERROR',
220
- 'herramientas': len(r['tools']),
221
- 'duration_ms': r['duration_ms'],
222
- 'error': r['error'],
235
+ "server": r["server"],
236
+ "estado": "OK" if not r["error"] else "ERROR",
237
+ "herramientas": len(r["tools"]),
238
+ "duration_ms": r["duration_ms"],
239
+ "error": r["error"],
223
240
  }
224
241
  for r in resultados
225
242
  ]
226
243
 
227
244
  if as_json:
228
- out(json.dumps(pings, ensure_ascii=False, indent=2) + '\n')
245
+ out(json.dumps(pings, ensure_ascii=False, indent=2) + "\n")
229
246
  return
230
247
 
231
- out('\nPing a servidores MCP:\n')
232
- out('-' * 64 + '\n')
248
+ out("\nPing a servidores MCP:\n")
249
+ out("-" * 64 + "\n")
233
250
  for p in pings:
234
- estado = p['estado']
235
- ms = p['duration_ms']
236
- tools = p['herramientas']
237
- sufijo = f' ({p["error"]})' if p['error'] else ''
251
+ estado = p["estado"]
252
+ ms = p["duration_ms"]
253
+ tools = p["herramientas"]
254
+ sufijo = f' ({p["error"]})' if p["error"] else ""
238
255
  out(f' {p["server"]:<28} {estado:<6} {tools:>3} tools {ms:>5}ms{sufijo}\n')
239
256
 
240
257
 
241
- async def cmd_call_tool(servers: dict, server_name: str, tool: str,
242
- tool_args: dict, as_json: bool) -> None:
258
+ async def cmd_call_tool(
259
+ servers: dict, server_name: str, tool: str, tool_args: dict, as_json: bool
260
+ ) -> None:
243
261
  out = sys.stdout.write
244
262
  cfg = _get_server(servers, server_name)
245
- r = await _call_tool_server(server_name, cfg, tool, tool_args)
263
+ r = await _call_tool_server(server_name, cfg, tool, tool_args)
246
264
 
247
265
  if as_json:
248
- out(json.dumps(r, ensure_ascii=False, indent=2) + '\n')
266
+ out(json.dumps(r, ensure_ascii=False, indent=2) + "\n")
249
267
  return
250
268
 
251
- if r['error']:
269
+ if r["error"]:
252
270
  out(f'ERROR ({r["duration_ms"]}ms): {r["error"]}\n')
253
271
  return
254
- if r['is_error']:
272
+ if r["is_error"]:
255
273
  out(f'Tool error ({r["duration_ms"]}ms):\n')
256
274
  else:
257
275
  out(f'Resultado ({r["duration_ms"]}ms):\n')
258
- for linea in (r['result'] or []):
259
- out(f'{linea}\n')
276
+ for linea in r["result"] or []:
277
+ out(f"{linea}\n")
278
+
260
279
 
261
280
  # ---------------------------------------------------------------------------
262
281
  # CLI
@@ -264,78 +283,84 @@ async def cmd_call_tool(servers: dict, server_name: str, tool: str,
264
283
 
265
284
 
266
285
  def main() -> None:
267
- if hasattr(sys.stdout, 'reconfigure'):
268
- sys.stdout.reconfigure(encoding='utf-8', errors='replace')
286
+ if hasattr(sys.stdout, "reconfigure"):
287
+ sys.stdout.reconfigure(encoding="utf-8", errors="replace")
269
288
 
270
289
  if not HAS_MCP:
271
290
  sys.stderr.write(
272
- '[mcp-pool] ERROR: la libreria mcp no esta instalada.\n'
273
- '[mcp-pool] Ejecuta: pip install mcp\n'
291
+ "[mcp-pool] ERROR: la libreria mcp no esta instalada.\n"
292
+ "[mcp-pool] Ejecuta: pip install mcp\n"
274
293
  )
275
294
  sys.exit(1)
276
295
 
277
296
  parser = argparse.ArgumentParser(
278
- description='Gestor de conexiones MCP para swl-ses'
297
+ description="Gestor de conexiones MCP para swl-ses"
279
298
  )
280
- parser.add_argument('--json', action='store_true', help='Salida en formato JSON')
281
- parser.add_argument('--config', default=None, help='Ruta al archivo de config MCP')
282
- parser.add_argument('--cwd', default='.', help='Directorio raiz del proyecto')
299
+ parser.add_argument("--json", action="store_true", help="Salida en formato JSON")
300
+ parser.add_argument("--config", default=None, help="Ruta al archivo de config MCP")
301
+ parser.add_argument("--cwd", default=".", help="Directorio raiz del proyecto")
283
302
 
284
- sub = parser.add_subparsers(dest='cmd')
303
+ sub = parser.add_subparsers(dest="cmd")
285
304
 
286
- sub.add_parser('list-servers', help='Lista servidores MCP configurados')
305
+ sub.add_parser("list-servers", help="Lista servidores MCP configurados")
287
306
 
288
- p_tools = sub.add_parser('list-tools', help='Lista herramientas de servidor(es)')
289
- p_tools.add_argument('servidor', nargs='?', help='Nombre del servidor (opcional)')
307
+ p_tools = sub.add_parser("list-tools", help="Lista herramientas de servidor(es)")
308
+ p_tools.add_argument("servidor", nargs="?", help="Nombre del servidor (opcional)")
290
309
 
291
- p_ping = sub.add_parser('ping', help='Verifica conectividad de servidor(es)')
292
- p_ping.add_argument('servidor', nargs='?', help='Nombre del servidor (opcional)')
310
+ p_ping = sub.add_parser("ping", help="Verifica conectividad de servidor(es)")
311
+ p_ping.add_argument("servidor", nargs="?", help="Nombre del servidor (opcional)")
293
312
 
294
- p_call = sub.add_parser('call-tool', help='Llama una herramienta MCP')
295
- p_call.add_argument('servidor', help='Nombre del servidor')
296
- p_call.add_argument('herramienta', help='Nombre de la herramienta')
297
- p_call.add_argument('args', nargs='?', default='{}',
298
- help='Argumentos JSON de la herramienta (default: {})')
313
+ p_call = sub.add_parser("call-tool", help="Llama una herramienta MCP")
314
+ p_call.add_argument("servidor", help="Nombre del servidor")
315
+ p_call.add_argument("herramienta", help="Nombre de la herramienta")
316
+ p_call.add_argument(
317
+ "args",
318
+ nargs="?",
319
+ default="{}",
320
+ help="Argumentos JSON de la herramienta (default: {})",
321
+ )
299
322
 
300
- args = parser.parse_args()
301
- cwd = Path(args.cwd).resolve()
323
+ args = parser.parse_args()
324
+ cwd = Path(args.cwd).resolve()
302
325
  servers = _cargar_config(cwd, args.config)
303
326
 
304
327
  if not servers:
305
328
  sys.stderr.write(
306
- '[mcp-pool] No se encontraron servidores MCP configurados.\n'
329
+ "[mcp-pool] No se encontraron servidores MCP configurados.\n"
307
330
  '[mcp-pool] Verifica la clave "mcpServers" en .claude/settings.json\n'
308
331
  )
309
332
  sys.exit(1)
310
333
 
311
- as_json = bool(getattr(args, 'json', False))
334
+ as_json = bool(getattr(args, "json", False))
312
335
 
313
- if args.cmd == 'list-servers':
336
+ if args.cmd == "list-servers":
314
337
  asyncio.run(cmd_list_servers(servers, as_json))
315
338
 
316
- elif args.cmd == 'list-tools':
317
- servidor = getattr(args, 'servidor', None)
339
+ elif args.cmd == "list-tools":
340
+ servidor = getattr(args, "servidor", None)
318
341
  if servidor:
319
- _get_server(servers, servidor) # valida que exista
342
+ _get_server(servers, servidor) # valida que exista
320
343
  asyncio.run(cmd_list_tools(servers, servidor, as_json))
321
344
 
322
- elif args.cmd == 'ping':
323
- servidor = getattr(args, 'servidor', None)
345
+ elif args.cmd == "ping":
346
+ servidor = getattr(args, "servidor", None)
324
347
  if servidor:
325
348
  _get_server(servers, servidor)
326
349
  asyncio.run(cmd_ping(servers, servidor, as_json))
327
350
 
328
- elif args.cmd == 'call-tool':
351
+ elif args.cmd == "call-tool":
329
352
  try:
330
353
  tool_args = json.loads(args.args)
331
354
  except json.JSONDecodeError:
332
- sys.stderr.write(f'[mcp-pool] Argumentos JSON invalidos: {args.args}\n')
355
+ sys.stderr.write(f"[mcp-pool] Argumentos JSON invalidos: {args.args}\n")
333
356
  sys.exit(1)
334
- asyncio.run(cmd_call_tool(servers, args.servidor, args.herramienta, tool_args, as_json))
357
+ asyncio.run(
358
+ cmd_call_tool(servers, args.servidor, args.herramienta, tool_args, as_json)
359
+ )
335
360
 
336
361
  else:
337
362
  parser.print_help()
338
363
 
339
364
 
340
- if __name__ == '__main__':
365
+ if __name__ == "__main__":
341
366
  main()