@saulwade/swl-ses 2.3.1 → 2.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +241 -240
- package/README.md +597 -597
- package/agentes/_intent-spec.md +73 -73
- package/agentes/_propose-step.md +90 -90
- package/agentes/accesibilidad-wcag-swl.md +690 -690
- package/agentes/arquitecto-swl.md +267 -267
- package/agentes/auto-evolucion-swl.md +908 -908
- package/agentes/backend-api-swl.md +472 -472
- package/agentes/backend-csharp-swl.md +420 -420
- package/agentes/backend-go-swl.md +390 -390
- package/agentes/backend-java-swl.md +281 -281
- package/agentes/backend-node-swl.md +479 -479
- package/agentes/backend-python-swl.md +605 -605
- package/agentes/backend-rust-swl.md +364 -364
- package/agentes/backend-workers-swl.md +482 -482
- package/agentes/cloud-infra-swl.md +509 -509
- package/agentes/consolidador-swl.md +541 -541
- package/agentes/datos-swl.md +605 -605
- package/agentes/depurador-swl.md +352 -352
- package/agentes/devops-ci-swl.md +400 -400
- package/agentes/disenador-ui-swl.md +569 -569
- package/agentes/documentador-swl.md +345 -345
- package/agentes/frontend-angular-swl.md +621 -621
- package/agentes/frontend-css-swl.md +716 -716
- package/agentes/frontend-react-swl.md +692 -692
- package/agentes/frontend-swl.md +496 -496
- package/agentes/frontend-tailwind-swl.md +826 -826
- package/agentes/gh-fix-ci-swl.md +275 -275
- package/agentes/implementador-swl.md +328 -328
- package/agentes/investigador-swl.md +432 -432
- package/agentes/investigador-ux-swl.md +505 -505
- package/agentes/llm-apps-swl.md +278 -278
- package/agentes/migrador-swl.md +442 -442
- package/agentes/mobile-android-swl.md +511 -511
- package/agentes/mobile-cross-swl.md +541 -541
- package/agentes/mobile-ios-swl.md +502 -502
- package/agentes/mobile-testing-swl.md +302 -302
- package/agentes/nemesis-auditor-swl.md +285 -285
- package/agentes/notificador-swl.md +918 -918
- package/agentes/observabilidad-swl.md +438 -438
- package/agentes/orquestador-swl.md +1053 -1026
- package/agentes/pagos-swl.md +310 -310
- package/agentes/perfilador-usuario-swl.md +321 -321
- package/agentes/planificador-swl.md +399 -399
- package/agentes/producto-prd-swl.md +589 -589
- package/agentes/red-team-swl.md +218 -218
- package/agentes/release-manager-swl.md +590 -590
- package/agentes/rendimiento-swl.md +713 -713
- package/agentes/resolutor-build-swl.md +245 -245
- package/agentes/revisor-angular-swl.md +278 -278
- package/agentes/revisor-codigo-swl.md +538 -505
- package/agentes/revisor-csharp-swl.md +264 -264
- package/agentes/revisor-go-swl.md +259 -259
- package/agentes/revisor-java-swl.md +257 -257
- package/agentes/revisor-kotlin-swl.md +273 -273
- package/agentes/revisor-nextjs-swl.md +281 -281
- package/agentes/revisor-php-swl.md +271 -271
- package/agentes/revisor-react-swl.md +278 -278
- package/agentes/revisor-rust-swl.md +346 -346
- package/agentes/revisor-seguridad-swl.md +399 -399
- package/agentes/revisor-swift-swl.md +268 -268
- package/agentes/revisor-typescript-swl.md +346 -346
- package/agentes/sre-swl.md +291 -291
- package/agentes/tdd-qa-swl.md +393 -393
- package/comandos/swl/contexto.md +0 -2
- package/comandos/swl/deuda-codigo.md +97 -0
- package/habilidades/angular-moderno/SKILL.md +5 -1
- package/habilidades/contenedores-docker/SKILL.md +3 -1
- package/habilidades/ejecutar-task-iterativo/SKILL.md +278 -278
- package/habilidades/harness-claude-code/SKILL.md +4 -4
- package/habilidades/harness-claude-code/recursos/disciplina-harness-regla.md +6 -5
- package/habilidades/legacy-code-rescue/SKILL.md +2 -1
- package/habilidades/meta-skills-estandar/SKILL.md +1 -1
- package/habilidades/meta-skills-estandar/recursos/skills-as-agents.md +2 -2
- package/habilidades/postgresql-experto/SKILL.md +3 -1
- package/habilidades/prevencion-sobreingenieria/SKILL.md +70 -92
- package/habilidades/prevencion-sobreingenieria/recursos/soluciones-nativas.md +166 -0
- package/habilidades/prevencion-sobreingenieria/recursos/variables-residuales-post-refactor.md +85 -0
- package/hooks/audit-trail.js +4 -4
- package/hooks/calidad-pre-commit.js +15 -11
- package/hooks/captura-acciones-post.js +3 -2
- package/hooks/captura-acciones-session.js +3 -2
- package/hooks/contexto-iteracion.js +2 -1
- package/hooks/contexto-subagente.js +68 -0
- package/hooks/guardrail-modelo.js +13 -3
- package/hooks/inyeccion-contexto.js +12 -12
- package/hooks/registro-turnos.js +5 -4
- package/hooks/spec-gate.js +2 -1
- package/hooks/sugerir-contribuir.js +2 -1
- package/hooks/sugerir-regenerar-inventario.js +3 -2
- package/hooks/tdd-gate.js +2 -1
- package/llms.txt +3 -3
- package/manifiestos/canonical-hashes.json +667 -10
- package/manifiestos/hook-profiles.json +0 -2
- package/manifiestos/hooks-config.json +469 -477
- package/manifiestos/invariantes-criticos.json +30 -0
- package/manifiestos/modulos.json +1390 -1390
- package/manifiestos/skills-lock.json +24 -24
- package/package.json +93 -93
- package/plugin.json +369 -371
- package/schemas/agent-frontmatter.schema.json +3 -1
- package/scripts/instalador.js +4 -1
- package/scripts/lib/expandir-targets.js +71 -71
- package/scripts/lib/preservar-usuario.js +39 -5
- package/scripts/lib/toml-merge.js +204 -204
- package/scripts/mcp-server/auth.js +105 -105
- package/scripts/mcp-server/cache.js +106 -106
- package/scripts/validar.js +1 -1
- package/scripts/verificar-release.js +51 -0
- package/hooks/lib/context-compressor.js +0 -657
- package/hooks/linea-estado.js +0 -328
- package/hooks/monitor-contexto.js +0 -373
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
"oneOf": [
|
|
52
52
|
{
|
|
53
53
|
"enum": [
|
|
54
|
+
"fable",
|
|
54
55
|
"opus",
|
|
55
56
|
"sonnet",
|
|
56
57
|
"haiku",
|
|
@@ -61,13 +62,14 @@
|
|
|
61
62
|
"pattern": "^claude-(opus|sonnet|haiku|fable)-[0-9]+(-[0-9]+){0,2}$"
|
|
62
63
|
}
|
|
63
64
|
],
|
|
64
|
-
"description": "Modelo principal del agente. Se recomienda alias corto (opus/sonnet/haiku), que resuelve al modelo vigente de la familia y se auto-actualiza; 'inherit' para sub-agentes; o nombre completo tipo 'claude-opus-4-8', 'claude-sonnet-5', 'claude-haiku-4-5-20251001'."
|
|
65
|
+
"description": "Modelo principal del agente. Se recomienda alias corto (fable/opus/sonnet/haiku), que resuelve al modelo vigente de la familia y se auto-actualiza; 'inherit' para sub-agentes; o nombre completo tipo 'claude-fable-5', 'claude-opus-4-8', 'claude-sonnet-5', 'claude-haiku-4-5-20251001'."
|
|
65
66
|
},
|
|
66
67
|
"modeloAlterno": {
|
|
67
68
|
"type": "string",
|
|
68
69
|
"oneOf": [
|
|
69
70
|
{
|
|
70
71
|
"enum": [
|
|
72
|
+
"fable",
|
|
71
73
|
"opus",
|
|
72
74
|
"sonnet",
|
|
73
75
|
"haiku",
|
package/scripts/instalador.js
CHANGED
|
@@ -26,6 +26,7 @@ const { obtenerTransformador } = require('./lib/transformadores');
|
|
|
26
26
|
const {
|
|
27
27
|
esProtegido,
|
|
28
28
|
crearBackup,
|
|
29
|
+
rutaBackupSegura,
|
|
29
30
|
registrarBackup,
|
|
30
31
|
limpiarBackups,
|
|
31
32
|
mergeInstintos,
|
|
@@ -1245,7 +1246,9 @@ function instalarArchivo(archivo, rutas, runtime, opciones = {}) {
|
|
|
1245
1246
|
// ALTO (nemesis O6): respaldar el DIRECTORIO COMPLETO (recursos incluidos)
|
|
1246
1247
|
// antes de sobrescribir — el usuario pudo modificar recursos/ aunque no
|
|
1247
1248
|
// tocara el SKILL.md. Overwrite controlado en-bloque (no fall-through).
|
|
1248
|
-
|
|
1249
|
+
// rutaBackupSegura contiene el backup dentro del dir versionado incluso
|
|
1250
|
+
// cuando destino está fuera del cwd (install --global — bug 2026-07-03).
|
|
1251
|
+
const backupDir = rutaBackupSegura(process.cwd(), versionBackup, destino);
|
|
1249
1252
|
let backupOk = false;
|
|
1250
1253
|
try { copiarDirectorio(destino, backupDir); backupOk = true; } catch (e) { console.log(` ⚠ Backup de directorio falló (${e.message})`); }
|
|
1251
1254
|
if (!backupOk) {
|
|
@@ -1,71 +1,71 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Expansión de --target (CSV) y --all-runtimes para multi-target install.
|
|
5
|
-
*
|
|
6
|
-
* ADR-0019 Sub-fase 2.5.
|
|
7
|
-
*
|
|
8
|
-
* Extraído de bin/swl-ses.js para permitir tests unitarios. NUNCA debe tener
|
|
9
|
-
* side effects fuera de logging — solo manipula la lista de strings.
|
|
10
|
-
*
|
|
11
|
-
* @module scripts/lib/expandir-targets
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
const { listarRuntimesInstalables, RUNTIMES } = require('./detectar-runtime');
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Expande las opciones del CLI a un array de target IDs.
|
|
18
|
-
*
|
|
19
|
-
* Reglas:
|
|
20
|
-
* - `all_runtimes` tiene prioridad sobre `target`. Si ambos vienen, se loggea
|
|
21
|
-
* aviso (vía `logger.warn`) y se usa `all_runtimes`.
|
|
22
|
-
* - `target='a,b,c'` → ['a','b','c'].
|
|
23
|
-
* - `target='claude'` → ['claude'].
|
|
24
|
-
* - Sin nada → ['claude'] (default histórico, backward-compat).
|
|
25
|
-
* - Duplicados se deduplican preservando el primer orden.
|
|
26
|
-
* - Targets desconocidos se omiten con aviso (logger.warn).
|
|
27
|
-
*
|
|
28
|
-
* @param {object} opciones - Objeto con `target` (string|undefined) y `all_runtimes` (bool).
|
|
29
|
-
* @param {object} [logger] - Sustituible para tests. Default console.
|
|
30
|
-
* @returns {{ targets: string[], omitidos: string[], errores: string[] }}
|
|
31
|
-
*/
|
|
32
|
-
function expandirTargets(opciones, logger) {
|
|
33
|
-
const log = logger || console;
|
|
34
|
-
const omitidos = [];
|
|
35
|
-
const errores = [];
|
|
36
|
-
|
|
37
|
-
let candidatos;
|
|
38
|
-
if (opciones.all_runtimes) {
|
|
39
|
-
if (opciones.target && typeof opciones.target === 'string') {
|
|
40
|
-
log.warn && log.warn('[expandir-targets] --all-runtimes tiene prioridad sobre --target; se ignora --target.');
|
|
41
|
-
}
|
|
42
|
-
candidatos = listarRuntimesInstalables();
|
|
43
|
-
} else if (typeof opciones.target === 'string' && opciones.target.includes(',')) {
|
|
44
|
-
candidatos = opciones.target.split(',').map(s => s.trim()).filter(Boolean);
|
|
45
|
-
} else if (typeof opciones.target === 'string' && opciones.target.length > 0) {
|
|
46
|
-
candidatos = [opciones.target.trim()];
|
|
47
|
-
} else {
|
|
48
|
-
candidatos = ['claude']; // backward-compat
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const visto = new Set();
|
|
52
|
-
const targets = [];
|
|
53
|
-
for (const t of candidatos) {
|
|
54
|
-
if (visto.has(t)) continue;
|
|
55
|
-
visto.add(t);
|
|
56
|
-
if (!RUNTIMES[t]) {
|
|
57
|
-
log.warn && log.warn(`[expandir-targets] Target desconocido omitido: "${t}". Disponibles: ${Object.keys(RUNTIMES).join(', ')}`);
|
|
58
|
-
omitidos.push(t);
|
|
59
|
-
continue;
|
|
60
|
-
}
|
|
61
|
-
targets.push(t);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
if (targets.length === 0) {
|
|
65
|
-
errores.push('Ningún target válido tras expansión.');
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
return { targets, omitidos, errores };
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
module.exports = { expandirTargets };
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Expansión de --target (CSV) y --all-runtimes para multi-target install.
|
|
5
|
+
*
|
|
6
|
+
* ADR-0019 Sub-fase 2.5.
|
|
7
|
+
*
|
|
8
|
+
* Extraído de bin/swl-ses.js para permitir tests unitarios. NUNCA debe tener
|
|
9
|
+
* side effects fuera de logging — solo manipula la lista de strings.
|
|
10
|
+
*
|
|
11
|
+
* @module scripts/lib/expandir-targets
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const { listarRuntimesInstalables, RUNTIMES } = require('./detectar-runtime');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Expande las opciones del CLI a un array de target IDs.
|
|
18
|
+
*
|
|
19
|
+
* Reglas:
|
|
20
|
+
* - `all_runtimes` tiene prioridad sobre `target`. Si ambos vienen, se loggea
|
|
21
|
+
* aviso (vía `logger.warn`) y se usa `all_runtimes`.
|
|
22
|
+
* - `target='a,b,c'` → ['a','b','c'].
|
|
23
|
+
* - `target='claude'` → ['claude'].
|
|
24
|
+
* - Sin nada → ['claude'] (default histórico, backward-compat).
|
|
25
|
+
* - Duplicados se deduplican preservando el primer orden.
|
|
26
|
+
* - Targets desconocidos se omiten con aviso (logger.warn).
|
|
27
|
+
*
|
|
28
|
+
* @param {object} opciones - Objeto con `target` (string|undefined) y `all_runtimes` (bool).
|
|
29
|
+
* @param {object} [logger] - Sustituible para tests. Default console.
|
|
30
|
+
* @returns {{ targets: string[], omitidos: string[], errores: string[] }}
|
|
31
|
+
*/
|
|
32
|
+
function expandirTargets(opciones, logger) {
|
|
33
|
+
const log = logger || console;
|
|
34
|
+
const omitidos = [];
|
|
35
|
+
const errores = [];
|
|
36
|
+
|
|
37
|
+
let candidatos;
|
|
38
|
+
if (opciones.all_runtimes) {
|
|
39
|
+
if (opciones.target && typeof opciones.target === 'string') {
|
|
40
|
+
log.warn && log.warn('[expandir-targets] --all-runtimes tiene prioridad sobre --target; se ignora --target.');
|
|
41
|
+
}
|
|
42
|
+
candidatos = listarRuntimesInstalables();
|
|
43
|
+
} else if (typeof opciones.target === 'string' && opciones.target.includes(',')) {
|
|
44
|
+
candidatos = opciones.target.split(',').map(s => s.trim()).filter(Boolean);
|
|
45
|
+
} else if (typeof opciones.target === 'string' && opciones.target.length > 0) {
|
|
46
|
+
candidatos = [opciones.target.trim()];
|
|
47
|
+
} else {
|
|
48
|
+
candidatos = ['claude']; // backward-compat
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const visto = new Set();
|
|
52
|
+
const targets = [];
|
|
53
|
+
for (const t of candidatos) {
|
|
54
|
+
if (visto.has(t)) continue;
|
|
55
|
+
visto.add(t);
|
|
56
|
+
if (!RUNTIMES[t]) {
|
|
57
|
+
log.warn && log.warn(`[expandir-targets] Target desconocido omitido: "${t}". Disponibles: ${Object.keys(RUNTIMES).join(', ')}`);
|
|
58
|
+
omitidos.push(t);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
targets.push(t);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (targets.length === 0) {
|
|
65
|
+
errores.push('Ningún target válido tras expansión.');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return { targets, omitidos, errores };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
module.exports = { expandirTargets };
|
|
@@ -34,7 +34,7 @@ const path = require('path');
|
|
|
34
34
|
* - /swl:evolucionar → skills auto-generados en habilidades/
|
|
35
35
|
* - /swl:crear-skill → skills nuevos en habilidades/
|
|
36
36
|
* - session-store → .planning/sessions/*.json
|
|
37
|
-
* -
|
|
37
|
+
* - /swl:compactar → .planning/COMPACTACION.md
|
|
38
38
|
* - cron-store → .planning/cron/
|
|
39
39
|
* - gateway → .planning/gateway-sessions.json
|
|
40
40
|
* - agent-comms → .planning/comms/
|
|
@@ -181,6 +181,41 @@ function dirBackup(baseDir, version) {
|
|
|
181
181
|
return path.join(baseDir, '.planning', 'backups', `v${version}`);
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
+
/**
|
|
185
|
+
* Subruta segura de un archivo DENTRO del directorio de backups.
|
|
186
|
+
*
|
|
187
|
+
* Un destino fuera de baseDir (install --global: ~/.claude, ~/.openclaude)
|
|
188
|
+
* produce un path.relative con segmentos '..' que ESCAPAN del directorio
|
|
189
|
+
* versionado: `.planning/backups/v2.3.1/../../.claude/...` colapsa a
|
|
190
|
+
* `.planning/.claude/...` (bug 2026-07-03: 1147 backups globales aterrizaron
|
|
191
|
+
* fuera del dir de backups y aparecieron como archivos nuevos en el repo).
|
|
192
|
+
* Se neutralizan '..', '.' y dos puntos de unidad preservando la estructura.
|
|
193
|
+
*
|
|
194
|
+
* @param {string} baseDir - Raíz del proyecto.
|
|
195
|
+
* @param {string} rutaAbsoluta - Ruta absoluta del archivo a respaldar.
|
|
196
|
+
* @returns {string} Subruta relativa contenida (sin '..').
|
|
197
|
+
*/
|
|
198
|
+
function subrutaBackupSegura(baseDir, rutaAbsoluta) {
|
|
199
|
+
return path.relative(baseDir, rutaAbsoluta)
|
|
200
|
+
.split(/[\\/]/)
|
|
201
|
+
.filter(s => s && s !== '..' && s !== '.')
|
|
202
|
+
.map(s => s.replace(/:/g, ''))
|
|
203
|
+
.join(path.sep);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Ruta completa de backup para un archivo, garantizada dentro del
|
|
208
|
+
* directorio versionado de backups.
|
|
209
|
+
*
|
|
210
|
+
* @param {string} baseDir - Raíz del proyecto.
|
|
211
|
+
* @param {string} versionAnterior - Versión que se está sobreescribiendo.
|
|
212
|
+
* @param {string} rutaAbsoluta - Ruta absoluta del archivo a respaldar.
|
|
213
|
+
* @returns {string} Ruta absoluta del backup.
|
|
214
|
+
*/
|
|
215
|
+
function rutaBackupSegura(baseDir, versionAnterior, rutaAbsoluta) {
|
|
216
|
+
return path.join(dirBackup(baseDir, versionAnterior), subrutaBackupSegura(baseDir, rutaAbsoluta));
|
|
217
|
+
}
|
|
218
|
+
|
|
184
219
|
/**
|
|
185
220
|
* Crea un backup de un archivo antes de sobreescribirlo.
|
|
186
221
|
*
|
|
@@ -195,10 +230,8 @@ function crearBackup(baseDir, rutaAbsoluta, versionAnterior) {
|
|
|
195
230
|
}
|
|
196
231
|
|
|
197
232
|
try {
|
|
198
|
-
const
|
|
199
|
-
const
|
|
200
|
-
const rutaBackup = path.join(backupDir, rutaRelativa);
|
|
201
|
-
const dirPadre = path.dirname(rutaBackup);
|
|
233
|
+
const rutaBackup = rutaBackupSegura(baseDir, versionAnterior, rutaAbsoluta);
|
|
234
|
+
const dirPadre = path.dirname(rutaBackup);
|
|
202
235
|
|
|
203
236
|
if (!fs.existsSync(dirPadre)) {
|
|
204
237
|
fs.mkdirSync(dirPadre, { recursive: true });
|
|
@@ -573,6 +606,7 @@ module.exports = {
|
|
|
573
606
|
|
|
574
607
|
// Nivel 2: Backup
|
|
575
608
|
crearBackup,
|
|
609
|
+
rutaBackupSegura,
|
|
576
610
|
registrarBackup,
|
|
577
611
|
listarBackups,
|
|
578
612
|
restaurarBackup,
|
|
@@ -1,204 +1,204 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Merge mínimo de secciones TOML — zero-deps.
|
|
5
|
-
*
|
|
6
|
-
* Opera sobre el formato concreto de `~/.codex/config.toml` (OpenAI Codex CLI):
|
|
7
|
-
* [mcp_servers.<name>]
|
|
8
|
-
* command = "node"
|
|
9
|
-
* args = ["...", "..."]
|
|
10
|
-
* [mcp_servers.<name>.env]
|
|
11
|
-
* KEY = "value"
|
|
12
|
-
*
|
|
13
|
-
* Reglas:
|
|
14
|
-
* - NUNCA reescribe el archivo entero. Solo agrega o reemplaza la sección
|
|
15
|
-
* `[mcp_servers.<name>]` indicada (incluyendo sub-tabla `.env`).
|
|
16
|
-
* - Si el archivo no existe lo crea con permisos 0600.
|
|
17
|
-
* - Si el archivo tiene comentarios o secciones desconocidas, las preserva.
|
|
18
|
-
* - Bloque "owned" delimitado por marcadores `# SWL-MCP-BEGIN <name>` /
|
|
19
|
-
* `# SWL-MCP-END <name>` para permitir actualizaciones idempotentes.
|
|
20
|
-
*
|
|
21
|
-
* NO soporta:
|
|
22
|
-
* - TOML completo (arrays multilínea complejos, inline tables, etc.)
|
|
23
|
-
* - Multi-section nested tables fuera de `mcp_servers.*`
|
|
24
|
-
*
|
|
25
|
-
* Si en el futuro Codex publica un comando idempotente `codex mcp add --idempotent`
|
|
26
|
-
* podemos delegarle el trabajo y deprecar este helper.
|
|
27
|
-
*
|
|
28
|
-
* @module scripts/lib/toml-merge
|
|
29
|
-
*/
|
|
30
|
-
|
|
31
|
-
const fs = require('fs');
|
|
32
|
-
const path = require('path');
|
|
33
|
-
|
|
34
|
-
const { atomicWriteSync } = require('../../hooks/lib/atomic-write');
|
|
35
|
-
|
|
36
|
-
const BEGIN_PREFIX = '# SWL-MCP-BEGIN';
|
|
37
|
-
const END_PREFIX = '# SWL-MCP-END';
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Serializa un servidor MCP en bloque TOML.
|
|
41
|
-
*
|
|
42
|
-
* @param {string} name - Nombre del server (identificador en config.toml).
|
|
43
|
-
* @param {{ command: string, args?: string[], env?: Record<string,string> }} cfg
|
|
44
|
-
* @returns {string} Bloque TOML con marcadores.
|
|
45
|
-
*/
|
|
46
|
-
function serializarServidor(name, cfg) {
|
|
47
|
-
if (!name || typeof name !== 'string' || !/^[a-zA-Z0-9_-]+$/.test(name)) {
|
|
48
|
-
throw new Error(`Nombre de MCP server inválido: "${name}". Solo [a-zA-Z0-9_-].`);
|
|
49
|
-
}
|
|
50
|
-
if (!cfg || typeof cfg.command !== 'string' || cfg.command.length === 0) {
|
|
51
|
-
throw new Error(`MCP server "${name}" requiere "command" string no vacío.`);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const lineas = [];
|
|
55
|
-
lineas.push(`${BEGIN_PREFIX} ${name}`);
|
|
56
|
-
lineas.push(`[mcp_servers.${name}]`);
|
|
57
|
-
lineas.push(`command = ${tomlString(cfg.command)}`);
|
|
58
|
-
|
|
59
|
-
if (Array.isArray(cfg.args) && cfg.args.length > 0) {
|
|
60
|
-
const items = cfg.args.map(tomlString).join(', ');
|
|
61
|
-
lineas.push(`args = [${items}]`);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
if (cfg.env && typeof cfg.env === 'object' && Object.keys(cfg.env).length > 0) {
|
|
65
|
-
lineas.push('');
|
|
66
|
-
lineas.push(`[mcp_servers.${name}.env]`);
|
|
67
|
-
for (const [k, v] of Object.entries(cfg.env)) {
|
|
68
|
-
if (typeof v !== 'string') {
|
|
69
|
-
throw new Error(`MCP server "${name}" env.${k} debe ser string (recibido: ${typeof v}).`);
|
|
70
|
-
}
|
|
71
|
-
// Solo claves alfanuméricas/_ en env names — TOML restricción razonable
|
|
72
|
-
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(k)) {
|
|
73
|
-
throw new Error(`MCP server "${name}" env key inválido: "${k}".`);
|
|
74
|
-
}
|
|
75
|
-
lineas.push(`${k} = ${tomlString(v)}`);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
lineas.push(`${END_PREFIX} ${name}`);
|
|
80
|
-
return lineas.join('\n');
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Escapa un string para TOML usando comillas dobles.
|
|
85
|
-
* No usa basic-string raw para evitar pitfalls con backslashes.
|
|
86
|
-
*/
|
|
87
|
-
function tomlString(s) {
|
|
88
|
-
// TOML basic string: escape backslash, doblequote, control chars.
|
|
89
|
-
const escapado = String(s)
|
|
90
|
-
.replace(/\\/g, '\\\\')
|
|
91
|
-
.replace(/"/g, '\\"')
|
|
92
|
-
.replace(/\n/g, '\\n')
|
|
93
|
-
.replace(/\r/g, '\\r')
|
|
94
|
-
.replace(/\t/g, '\\t');
|
|
95
|
-
return `"${escapado}"`;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Inserta o actualiza un servidor MCP en `config.toml`.
|
|
100
|
-
*
|
|
101
|
-
* Idempotente: si el bloque ya existe con el mismo contenido, no escribe.
|
|
102
|
-
* Si existe con contenido distinto, lo reemplaza dentro de los marcadores.
|
|
103
|
-
* Si no existe, lo agrega al final del archivo precedido de línea en blanco.
|
|
104
|
-
*
|
|
105
|
-
* @param {string} rutaConfig - Path absoluto a config.toml (típicamente ~/.codex/config.toml).
|
|
106
|
-
* @param {string} name - Nombre del MCP server (clave de [mcp_servers.<name>]).
|
|
107
|
-
* @param {{ command: string, args?: string[], env?: Record<string,string> }} cfg
|
|
108
|
-
* @returns {{ accion: 'creado' | 'agregado' | 'actualizado' | 'sin-cambios', ruta: string }}
|
|
109
|
-
*/
|
|
110
|
-
function upsertMcpServer(rutaConfig, name, cfg) {
|
|
111
|
-
const bloqueNuevo = serializarServidor(name, cfg);
|
|
112
|
-
|
|
113
|
-
let existente = '';
|
|
114
|
-
let existia = false;
|
|
115
|
-
try {
|
|
116
|
-
existente = fs.readFileSync(rutaConfig, 'utf-8');
|
|
117
|
-
existia = true;
|
|
118
|
-
} catch (err) {
|
|
119
|
-
if (err.code !== 'ENOENT') throw err;
|
|
120
|
-
// Archivo no existe — se crea
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
const beginTag = `${BEGIN_PREFIX} ${name}`;
|
|
124
|
-
const endTag = `${END_PREFIX} ${name}`;
|
|
125
|
-
|
|
126
|
-
if (!existia) {
|
|
127
|
-
const dir = path.dirname(rutaConfig);
|
|
128
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
129
|
-
const contenido = `# config.toml de Codex CLI — gestionado parcialmente por swl-ses\n# Bloques delimitados por SWL-MCP-BEGIN/END son regenerables.\n\n${bloqueNuevo}\n`;
|
|
130
|
-
atomicWriteSync(rutaConfig, contenido, 'utf8', { mode: 0o600 });
|
|
131
|
-
return { accion: 'creado', ruta: rutaConfig };
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
const idxBegin = existente.indexOf(beginTag);
|
|
135
|
-
const idxEnd = existente.indexOf(endTag);
|
|
136
|
-
|
|
137
|
-
// Caso 1: no existe el bloque → append al final
|
|
138
|
-
if (idxBegin === -1 || idxEnd === -1) {
|
|
139
|
-
const separador = existente.endsWith('\n') ? '\n' : '\n\n';
|
|
140
|
-
const contenido = existente + separador + bloqueNuevo + '\n';
|
|
141
|
-
atomicWriteSync(rutaConfig, contenido, 'utf8', { mode: 0o600 });
|
|
142
|
-
return { accion: 'agregado', ruta: rutaConfig };
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// Caso 2: existe — comparar contenido
|
|
146
|
-
// Calculamos fin REAL incluyendo el endTag completo.
|
|
147
|
-
const finBloqueExistente = idxEnd + endTag.length;
|
|
148
|
-
const bloqueExistente = existente.slice(idxBegin, finBloqueExistente);
|
|
149
|
-
|
|
150
|
-
if (bloqueExistente === bloqueNuevo) {
|
|
151
|
-
return { accion: 'sin-cambios', ruta: rutaConfig };
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
const contenido = existente.slice(0, idxBegin) + bloqueNuevo + existente.slice(finBloqueExistente);
|
|
155
|
-
atomicWriteSync(rutaConfig, contenido, 'utf8', { mode: 0o600 });
|
|
156
|
-
return { accion: 'actualizado', ruta: rutaConfig };
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* Elimina un bloque MCP server por nombre. Idempotente: si no existe, no hace nada.
|
|
161
|
-
*
|
|
162
|
-
* @param {string} rutaConfig
|
|
163
|
-
* @param {string} name
|
|
164
|
-
* @returns {{ accion: 'eliminado' | 'sin-cambios', ruta: string }}
|
|
165
|
-
*/
|
|
166
|
-
function removeMcpServer(rutaConfig, name) {
|
|
167
|
-
let existente = '';
|
|
168
|
-
try {
|
|
169
|
-
existente = fs.readFileSync(rutaConfig, 'utf-8');
|
|
170
|
-
} catch (err) {
|
|
171
|
-
if (err.code === 'ENOENT') return { accion: 'sin-cambios', ruta: rutaConfig };
|
|
172
|
-
throw err;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
const beginTag = `${BEGIN_PREFIX} ${name}`;
|
|
176
|
-
const endTag = `${END_PREFIX} ${name}`;
|
|
177
|
-
const idxBegin = existente.indexOf(beginTag);
|
|
178
|
-
const idxEnd = existente.indexOf(endTag);
|
|
179
|
-
|
|
180
|
-
if (idxBegin === -1 || idxEnd === -1) {
|
|
181
|
-
return { accion: 'sin-cambios', ruta: rutaConfig };
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// Quitar bloque + posible newline previo y posterior (cosmético)
|
|
185
|
-
let inicio = idxBegin;
|
|
186
|
-
let fin = idxEnd + endTag.length;
|
|
187
|
-
// Comer un \n previo si existe (no comer doble \n para preservar separadores entre secciones)
|
|
188
|
-
if (inicio > 0 && existente[inicio - 1] === '\n') inicio--;
|
|
189
|
-
if (fin < existente.length && existente[fin] === '\n') fin++;
|
|
190
|
-
|
|
191
|
-
const contenido = existente.slice(0, inicio) + existente.slice(fin);
|
|
192
|
-
atomicWriteSync(rutaConfig, contenido, 'utf8', { mode: 0o600 });
|
|
193
|
-
return { accion: 'eliminado', ruta: rutaConfig };
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
module.exports = {
|
|
197
|
-
upsertMcpServer,
|
|
198
|
-
removeMcpServer,
|
|
199
|
-
serializarServidor,
|
|
200
|
-
// Para tests
|
|
201
|
-
_tomlString: tomlString,
|
|
202
|
-
BEGIN_PREFIX,
|
|
203
|
-
END_PREFIX,
|
|
204
|
-
};
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Merge mínimo de secciones TOML — zero-deps.
|
|
5
|
+
*
|
|
6
|
+
* Opera sobre el formato concreto de `~/.codex/config.toml` (OpenAI Codex CLI):
|
|
7
|
+
* [mcp_servers.<name>]
|
|
8
|
+
* command = "node"
|
|
9
|
+
* args = ["...", "..."]
|
|
10
|
+
* [mcp_servers.<name>.env]
|
|
11
|
+
* KEY = "value"
|
|
12
|
+
*
|
|
13
|
+
* Reglas:
|
|
14
|
+
* - NUNCA reescribe el archivo entero. Solo agrega o reemplaza la sección
|
|
15
|
+
* `[mcp_servers.<name>]` indicada (incluyendo sub-tabla `.env`).
|
|
16
|
+
* - Si el archivo no existe lo crea con permisos 0600.
|
|
17
|
+
* - Si el archivo tiene comentarios o secciones desconocidas, las preserva.
|
|
18
|
+
* - Bloque "owned" delimitado por marcadores `# SWL-MCP-BEGIN <name>` /
|
|
19
|
+
* `# SWL-MCP-END <name>` para permitir actualizaciones idempotentes.
|
|
20
|
+
*
|
|
21
|
+
* NO soporta:
|
|
22
|
+
* - TOML completo (arrays multilínea complejos, inline tables, etc.)
|
|
23
|
+
* - Multi-section nested tables fuera de `mcp_servers.*`
|
|
24
|
+
*
|
|
25
|
+
* Si en el futuro Codex publica un comando idempotente `codex mcp add --idempotent`
|
|
26
|
+
* podemos delegarle el trabajo y deprecar este helper.
|
|
27
|
+
*
|
|
28
|
+
* @module scripts/lib/toml-merge
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
const fs = require('fs');
|
|
32
|
+
const path = require('path');
|
|
33
|
+
|
|
34
|
+
const { atomicWriteSync } = require('../../hooks/lib/atomic-write');
|
|
35
|
+
|
|
36
|
+
const BEGIN_PREFIX = '# SWL-MCP-BEGIN';
|
|
37
|
+
const END_PREFIX = '# SWL-MCP-END';
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Serializa un servidor MCP en bloque TOML.
|
|
41
|
+
*
|
|
42
|
+
* @param {string} name - Nombre del server (identificador en config.toml).
|
|
43
|
+
* @param {{ command: string, args?: string[], env?: Record<string,string> }} cfg
|
|
44
|
+
* @returns {string} Bloque TOML con marcadores.
|
|
45
|
+
*/
|
|
46
|
+
function serializarServidor(name, cfg) {
|
|
47
|
+
if (!name || typeof name !== 'string' || !/^[a-zA-Z0-9_-]+$/.test(name)) {
|
|
48
|
+
throw new Error(`Nombre de MCP server inválido: "${name}". Solo [a-zA-Z0-9_-].`);
|
|
49
|
+
}
|
|
50
|
+
if (!cfg || typeof cfg.command !== 'string' || cfg.command.length === 0) {
|
|
51
|
+
throw new Error(`MCP server "${name}" requiere "command" string no vacío.`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const lineas = [];
|
|
55
|
+
lineas.push(`${BEGIN_PREFIX} ${name}`);
|
|
56
|
+
lineas.push(`[mcp_servers.${name}]`);
|
|
57
|
+
lineas.push(`command = ${tomlString(cfg.command)}`);
|
|
58
|
+
|
|
59
|
+
if (Array.isArray(cfg.args) && cfg.args.length > 0) {
|
|
60
|
+
const items = cfg.args.map(tomlString).join(', ');
|
|
61
|
+
lineas.push(`args = [${items}]`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (cfg.env && typeof cfg.env === 'object' && Object.keys(cfg.env).length > 0) {
|
|
65
|
+
lineas.push('');
|
|
66
|
+
lineas.push(`[mcp_servers.${name}.env]`);
|
|
67
|
+
for (const [k, v] of Object.entries(cfg.env)) {
|
|
68
|
+
if (typeof v !== 'string') {
|
|
69
|
+
throw new Error(`MCP server "${name}" env.${k} debe ser string (recibido: ${typeof v}).`);
|
|
70
|
+
}
|
|
71
|
+
// Solo claves alfanuméricas/_ en env names — TOML restricción razonable
|
|
72
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(k)) {
|
|
73
|
+
throw new Error(`MCP server "${name}" env key inválido: "${k}".`);
|
|
74
|
+
}
|
|
75
|
+
lineas.push(`${k} = ${tomlString(v)}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
lineas.push(`${END_PREFIX} ${name}`);
|
|
80
|
+
return lineas.join('\n');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Escapa un string para TOML usando comillas dobles.
|
|
85
|
+
* No usa basic-string raw para evitar pitfalls con backslashes.
|
|
86
|
+
*/
|
|
87
|
+
function tomlString(s) {
|
|
88
|
+
// TOML basic string: escape backslash, doblequote, control chars.
|
|
89
|
+
const escapado = String(s)
|
|
90
|
+
.replace(/\\/g, '\\\\')
|
|
91
|
+
.replace(/"/g, '\\"')
|
|
92
|
+
.replace(/\n/g, '\\n')
|
|
93
|
+
.replace(/\r/g, '\\r')
|
|
94
|
+
.replace(/\t/g, '\\t');
|
|
95
|
+
return `"${escapado}"`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Inserta o actualiza un servidor MCP en `config.toml`.
|
|
100
|
+
*
|
|
101
|
+
* Idempotente: si el bloque ya existe con el mismo contenido, no escribe.
|
|
102
|
+
* Si existe con contenido distinto, lo reemplaza dentro de los marcadores.
|
|
103
|
+
* Si no existe, lo agrega al final del archivo precedido de línea en blanco.
|
|
104
|
+
*
|
|
105
|
+
* @param {string} rutaConfig - Path absoluto a config.toml (típicamente ~/.codex/config.toml).
|
|
106
|
+
* @param {string} name - Nombre del MCP server (clave de [mcp_servers.<name>]).
|
|
107
|
+
* @param {{ command: string, args?: string[], env?: Record<string,string> }} cfg
|
|
108
|
+
* @returns {{ accion: 'creado' | 'agregado' | 'actualizado' | 'sin-cambios', ruta: string }}
|
|
109
|
+
*/
|
|
110
|
+
function upsertMcpServer(rutaConfig, name, cfg) {
|
|
111
|
+
const bloqueNuevo = serializarServidor(name, cfg);
|
|
112
|
+
|
|
113
|
+
let existente = '';
|
|
114
|
+
let existia = false;
|
|
115
|
+
try {
|
|
116
|
+
existente = fs.readFileSync(rutaConfig, 'utf-8');
|
|
117
|
+
existia = true;
|
|
118
|
+
} catch (err) {
|
|
119
|
+
if (err.code !== 'ENOENT') throw err;
|
|
120
|
+
// Archivo no existe — se crea
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const beginTag = `${BEGIN_PREFIX} ${name}`;
|
|
124
|
+
const endTag = `${END_PREFIX} ${name}`;
|
|
125
|
+
|
|
126
|
+
if (!existia) {
|
|
127
|
+
const dir = path.dirname(rutaConfig);
|
|
128
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
129
|
+
const contenido = `# config.toml de Codex CLI — gestionado parcialmente por swl-ses\n# Bloques delimitados por SWL-MCP-BEGIN/END son regenerables.\n\n${bloqueNuevo}\n`;
|
|
130
|
+
atomicWriteSync(rutaConfig, contenido, 'utf8', { mode: 0o600 });
|
|
131
|
+
return { accion: 'creado', ruta: rutaConfig };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const idxBegin = existente.indexOf(beginTag);
|
|
135
|
+
const idxEnd = existente.indexOf(endTag);
|
|
136
|
+
|
|
137
|
+
// Caso 1: no existe el bloque → append al final
|
|
138
|
+
if (idxBegin === -1 || idxEnd === -1) {
|
|
139
|
+
const separador = existente.endsWith('\n') ? '\n' : '\n\n';
|
|
140
|
+
const contenido = existente + separador + bloqueNuevo + '\n';
|
|
141
|
+
atomicWriteSync(rutaConfig, contenido, 'utf8', { mode: 0o600 });
|
|
142
|
+
return { accion: 'agregado', ruta: rutaConfig };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Caso 2: existe — comparar contenido
|
|
146
|
+
// Calculamos fin REAL incluyendo el endTag completo.
|
|
147
|
+
const finBloqueExistente = idxEnd + endTag.length;
|
|
148
|
+
const bloqueExistente = existente.slice(idxBegin, finBloqueExistente);
|
|
149
|
+
|
|
150
|
+
if (bloqueExistente === bloqueNuevo) {
|
|
151
|
+
return { accion: 'sin-cambios', ruta: rutaConfig };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const contenido = existente.slice(0, idxBegin) + bloqueNuevo + existente.slice(finBloqueExistente);
|
|
155
|
+
atomicWriteSync(rutaConfig, contenido, 'utf8', { mode: 0o600 });
|
|
156
|
+
return { accion: 'actualizado', ruta: rutaConfig };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Elimina un bloque MCP server por nombre. Idempotente: si no existe, no hace nada.
|
|
161
|
+
*
|
|
162
|
+
* @param {string} rutaConfig
|
|
163
|
+
* @param {string} name
|
|
164
|
+
* @returns {{ accion: 'eliminado' | 'sin-cambios', ruta: string }}
|
|
165
|
+
*/
|
|
166
|
+
function removeMcpServer(rutaConfig, name) {
|
|
167
|
+
let existente = '';
|
|
168
|
+
try {
|
|
169
|
+
existente = fs.readFileSync(rutaConfig, 'utf-8');
|
|
170
|
+
} catch (err) {
|
|
171
|
+
if (err.code === 'ENOENT') return { accion: 'sin-cambios', ruta: rutaConfig };
|
|
172
|
+
throw err;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const beginTag = `${BEGIN_PREFIX} ${name}`;
|
|
176
|
+
const endTag = `${END_PREFIX} ${name}`;
|
|
177
|
+
const idxBegin = existente.indexOf(beginTag);
|
|
178
|
+
const idxEnd = existente.indexOf(endTag);
|
|
179
|
+
|
|
180
|
+
if (idxBegin === -1 || idxEnd === -1) {
|
|
181
|
+
return { accion: 'sin-cambios', ruta: rutaConfig };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Quitar bloque + posible newline previo y posterior (cosmético)
|
|
185
|
+
let inicio = idxBegin;
|
|
186
|
+
let fin = idxEnd + endTag.length;
|
|
187
|
+
// Comer un \n previo si existe (no comer doble \n para preservar separadores entre secciones)
|
|
188
|
+
if (inicio > 0 && existente[inicio - 1] === '\n') inicio--;
|
|
189
|
+
if (fin < existente.length && existente[fin] === '\n') fin++;
|
|
190
|
+
|
|
191
|
+
const contenido = existente.slice(0, inicio) + existente.slice(fin);
|
|
192
|
+
atomicWriteSync(rutaConfig, contenido, 'utf8', { mode: 0o600 });
|
|
193
|
+
return { accion: 'eliminado', ruta: rutaConfig };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
module.exports = {
|
|
197
|
+
upsertMcpServer,
|
|
198
|
+
removeMcpServer,
|
|
199
|
+
serializarServidor,
|
|
200
|
+
// Para tests
|
|
201
|
+
_tomlString: tomlString,
|
|
202
|
+
BEGIN_PREFIX,
|
|
203
|
+
END_PREFIX,
|
|
204
|
+
};
|