@saulwade/swl-ses 2.3.1 → 2.4.2

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 (123) hide show
  1. package/CLAUDE.md +242 -240
  2. package/README.md +597 -597
  3. package/agentes/_intent-spec.md +73 -73
  4. package/agentes/_propose-step.md +90 -90
  5. package/agentes/accesibilidad-wcag-swl.md +690 -690
  6. package/agentes/arquitecto-swl.md +267 -267
  7. package/agentes/auto-evolucion-swl.md +908 -908
  8. package/agentes/backend-api-swl.md +472 -472
  9. package/agentes/backend-csharp-swl.md +420 -420
  10. package/agentes/backend-go-swl.md +390 -390
  11. package/agentes/backend-java-swl.md +281 -281
  12. package/agentes/backend-node-swl.md +479 -479
  13. package/agentes/backend-python-swl.md +605 -605
  14. package/agentes/backend-rust-swl.md +364 -364
  15. package/agentes/backend-workers-swl.md +482 -482
  16. package/agentes/cloud-infra-swl.md +509 -509
  17. package/agentes/consolidador-swl.md +541 -541
  18. package/agentes/datos-swl.md +605 -605
  19. package/agentes/depurador-swl.md +352 -352
  20. package/agentes/devops-ci-swl.md +400 -400
  21. package/agentes/disenador-ui-swl.md +569 -569
  22. package/agentes/documentador-swl.md +345 -345
  23. package/agentes/frontend-angular-swl.md +621 -621
  24. package/agentes/frontend-css-swl.md +716 -716
  25. package/agentes/frontend-react-swl.md +692 -692
  26. package/agentes/frontend-swl.md +496 -496
  27. package/agentes/frontend-tailwind-swl.md +826 -826
  28. package/agentes/gh-fix-ci-swl.md +275 -275
  29. package/agentes/implementador-swl.md +328 -328
  30. package/agentes/investigador-swl.md +432 -432
  31. package/agentes/investigador-ux-swl.md +505 -505
  32. package/agentes/llm-apps-swl.md +278 -278
  33. package/agentes/migrador-swl.md +442 -442
  34. package/agentes/mobile-android-swl.md +511 -511
  35. package/agentes/mobile-cross-swl.md +541 -541
  36. package/agentes/mobile-ios-swl.md +502 -502
  37. package/agentes/mobile-testing-swl.md +302 -302
  38. package/agentes/nemesis-auditor-swl.md +285 -285
  39. package/agentes/notificador-swl.md +918 -918
  40. package/agentes/observabilidad-swl.md +438 -438
  41. package/agentes/orquestador-swl.md +1053 -1026
  42. package/agentes/pagos-swl.md +310 -310
  43. package/agentes/perfilador-usuario-swl.md +321 -321
  44. package/agentes/planificador-swl.md +399 -399
  45. package/agentes/producto-prd-swl.md +589 -589
  46. package/agentes/red-team-swl.md +218 -218
  47. package/agentes/release-manager-swl.md +590 -590
  48. package/agentes/rendimiento-swl.md +713 -713
  49. package/agentes/resolutor-build-swl.md +245 -245
  50. package/agentes/revisor-angular-swl.md +278 -278
  51. package/agentes/revisor-codigo-swl.md +538 -505
  52. package/agentes/revisor-csharp-swl.md +264 -264
  53. package/agentes/revisor-go-swl.md +259 -259
  54. package/agentes/revisor-java-swl.md +257 -257
  55. package/agentes/revisor-kotlin-swl.md +273 -273
  56. package/agentes/revisor-nextjs-swl.md +281 -281
  57. package/agentes/revisor-php-swl.md +271 -271
  58. package/agentes/revisor-react-swl.md +278 -278
  59. package/agentes/revisor-rust-swl.md +346 -346
  60. package/agentes/revisor-seguridad-swl.md +399 -399
  61. package/agentes/revisor-swift-swl.md +268 -268
  62. package/agentes/revisor-typescript-swl.md +346 -346
  63. package/agentes/sre-swl.md +291 -291
  64. package/agentes/tdd-qa-swl.md +393 -393
  65. package/comandos/swl/contexto.md +0 -2
  66. package/comandos/swl/deuda-codigo.md +97 -0
  67. package/habilidades/angular-moderno/SKILL.md +5 -1
  68. package/habilidades/contenedores-docker/SKILL.md +3 -1
  69. package/habilidades/ejecutar-task-iterativo/SKILL.md +278 -278
  70. package/habilidades/harness-claude-code/SKILL.md +5 -4
  71. package/habilidades/harness-claude-code/recursos/disciplina-harness-regla.md +6 -5
  72. package/habilidades/legacy-code-rescue/SKILL.md +2 -1
  73. package/habilidades/meta-skills-estandar/SKILL.md +1 -1
  74. package/habilidades/meta-skills-estandar/recursos/skills-as-agents.md +2 -2
  75. package/habilidades/postgresql-experto/SKILL.md +3 -1
  76. package/habilidades/prevencion-sobreingenieria/SKILL.md +70 -92
  77. package/habilidades/prevencion-sobreingenieria/recursos/soluciones-nativas.md +166 -0
  78. package/habilidades/prevencion-sobreingenieria/recursos/variables-residuales-post-refactor.md +85 -0
  79. package/habilidades/tdd-workflow/SKILL.md +3 -1
  80. package/hooks/audit-trail.js +4 -4
  81. package/hooks/calidad-pre-commit.js +15 -11
  82. package/hooks/captura-acciones-post.js +3 -2
  83. package/hooks/captura-acciones-session.js +3 -2
  84. package/hooks/contexto-iteracion.js +2 -1
  85. package/hooks/contexto-subagente.js +68 -0
  86. package/hooks/guardrail-modelo.js +13 -3
  87. package/hooks/inyeccion-contexto.js +12 -12
  88. package/hooks/registro-turnos.js +5 -4
  89. package/hooks/spec-gate.js +2 -1
  90. package/hooks/sugerir-contribuir.js +2 -1
  91. package/hooks/sugerir-regenerar-inventario.js +3 -2
  92. package/hooks/tdd-gate.js +2 -1
  93. package/llms.txt +3 -3
  94. package/manifiestos/canonical-hashes.json +995 -10
  95. package/manifiestos/hook-profiles.json +0 -2
  96. package/manifiestos/hooks-config.json +469 -477
  97. package/manifiestos/invariantes-criticos.json +30 -0
  98. package/manifiestos/modulos.json +1390 -1390
  99. package/manifiestos/skills-lock.json +24 -24
  100. package/package.json +93 -93
  101. package/plugin.json +369 -371
  102. package/reglas/arreglar-al-detectar.md +16 -0
  103. package/reglas/pruebas.md +7 -3
  104. package/schemas/agent-frontmatter.schema.json +3 -1
  105. package/scripts/actualizar.js +253 -254
  106. package/scripts/doctor.js +25 -17
  107. package/scripts/instalador.js +4 -1
  108. package/scripts/lib/detectar-runtime.js +58 -0
  109. package/scripts/lib/expandir-targets.js +71 -71
  110. package/scripts/lib/hooks-settings.js +40 -3
  111. package/scripts/lib/preservar-usuario.js +39 -5
  112. package/scripts/lib/toml-merge.js +204 -204
  113. package/scripts/mcp-server/auth.js +105 -105
  114. package/scripts/mcp-server/cache.js +106 -106
  115. package/scripts/tui/pantallas/inspect.js +175 -173
  116. package/scripts/tui/pantallas/uninstall-wizard.js +210 -208
  117. package/scripts/tui/pantallas/update-wizard.js +234 -232
  118. package/scripts/tui/pantallas/welcome.js +189 -187
  119. package/scripts/validar.js +1 -1
  120. package/scripts/verificar-release.js +51 -0
  121. package/hooks/lib/context-compressor.js +0 -657
  122. package/hooks/linea-estado.js +0 -328
  123. package/hooks/monitor-contexto.js +0 -373
@@ -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
+ };
@@ -1,105 +1,105 @@
1
- 'use strict';
2
-
3
- /**
4
- * Auth opt-in para swl-mcp-server v1.0.0 (ADR-0019 Sub-fase 3).
5
- *
6
- * El stub experimental v0.1.x corría sin auth. Cualquier proceso con acceso al
7
- * stdio podía leer toda la memoria SWL. Esa decisión era aceptable solo en
8
- * single-user local — no en multi-usuario, no expuesto a red, no compartido.
9
- *
10
- * v1.0.0 introduce auth **opt-in**:
11
- * - Si `SWL_MCP_API_KEY` NO está definida en el entorno del server, el comportamiento
12
- * es idéntico al stub (sin auth). Compatibilidad total con clientes existentes.
13
- * - Si `SWL_MCP_API_KEY` ESTÁ definida, cada `tools/call` debe incluir
14
- * `params._auth = "<api-key>"`. Sin el token o con token incorrecto, se devuelve
15
- * `-32001 Unauthorized`.
16
- *
17
- * Decisión deliberada de NO usar header `Authorization: Bearer ...`:
18
- * - El protocolo MCP sobre stdio NO transporta headers HTTP.
19
- * - Inventar un campo en `initialize.capabilities` específico para swl-ses
20
- * rompería la spec del protocolo.
21
- * - `_auth` como campo en `params` es el patrón de menor fricción.
22
- *
23
- * Limitaciones aceptadas:
24
- * - Token estático sin rotación automática — el operador rota manualmente
25
- * cambiando la env var del proceso.
26
- * - Sin scopes (read/write) porque v1 sigue siendo read-only.
27
- *
28
- * @module scripts/mcp-server/auth
29
- */
30
-
31
- const ENV_VAR_NAME = 'SWL_MCP_API_KEY';
32
- const ERROR_CODE_UNAUTHORIZED = -32001;
33
- const ERROR_CODE_FORBIDDEN = -32002;
34
-
35
- /**
36
- * Construye un validador de auth desde el entorno actual.
37
- *
38
- * Pattern de "construct once, use many": leer la env var una sola vez al arranque
39
- * y devolver una función pura que valida cada request. Evita race conditions con
40
- * tests que mutan process.env.
41
- *
42
- * @param {object} [opciones] - { env: NodeJS.ProcessEnv } sustituible para tests.
43
- * @returns {{ requerida: boolean, validar: (request: object) => { ok: boolean, code?: number, message?: string } }}
44
- */
45
- function construirValidador(opciones = {}) {
46
- const env = opciones.env || process.env;
47
- const apiKey = env[ENV_VAR_NAME];
48
- const requerida = typeof apiKey === 'string' && apiKey.length > 0;
49
-
50
- return {
51
- requerida,
52
- validar: (request) => {
53
- if (!requerida) return { ok: true };
54
- // Solo validar tools/call — initialize, ping, tools/list son metadata pública.
55
- const metodo = request && request.method;
56
- if (metodo !== 'tools/call') return { ok: true };
57
-
58
- const params = request.params || {};
59
- const tokenCliente = params._auth;
60
-
61
- if (typeof tokenCliente !== 'string' || tokenCliente.length === 0) {
62
- return {
63
- ok: false,
64
- code: ERROR_CODE_UNAUTHORIZED,
65
- message: 'swl-mcp-server requiere autenticación: SWL_MCP_API_KEY está configurada en el server. El cliente debe enviar params._auth con el API key.',
66
- };
67
- }
68
- if (!comparacionConstante(tokenCliente, apiKey)) {
69
- return {
70
- ok: false,
71
- code: ERROR_CODE_FORBIDDEN,
72
- message: 'Token inválido.',
73
- };
74
- }
75
- return { ok: true };
76
- },
77
- };
78
- }
79
-
80
- /**
81
- * Comparación de strings en tiempo constante para evitar timing attacks
82
- * sobre el API key. Implementación zero-deps simple.
83
- *
84
- * Si las longitudes difieren, igual se itera la longitud máxima para no filtrar
85
- * información sobre el largo del secreto vía duración de la comparación.
86
- */
87
- function comparacionConstante(a, b) {
88
- if (typeof a !== 'string' || typeof b !== 'string') return false;
89
- const len = Math.max(a.length, b.length);
90
- let diff = a.length ^ b.length;
91
- for (let i = 0; i < len; i++) {
92
- const ca = i < a.length ? a.charCodeAt(i) : 0;
93
- const cb = i < b.length ? b.charCodeAt(i) : 0;
94
- diff |= ca ^ cb;
95
- }
96
- return diff === 0;
97
- }
98
-
99
- module.exports = {
100
- construirValidador,
101
- comparacionConstante,
102
- ENV_VAR_NAME,
103
- ERROR_CODE_UNAUTHORIZED,
104
- ERROR_CODE_FORBIDDEN,
105
- };
1
+ 'use strict';
2
+
3
+ /**
4
+ * Auth opt-in para swl-mcp-server v1.0.0 (ADR-0019 Sub-fase 3).
5
+ *
6
+ * El stub experimental v0.1.x corría sin auth. Cualquier proceso con acceso al
7
+ * stdio podía leer toda la memoria SWL. Esa decisión era aceptable solo en
8
+ * single-user local — no en multi-usuario, no expuesto a red, no compartido.
9
+ *
10
+ * v1.0.0 introduce auth **opt-in**:
11
+ * - Si `SWL_MCP_API_KEY` NO está definida en el entorno del server, el comportamiento
12
+ * es idéntico al stub (sin auth). Compatibilidad total con clientes existentes.
13
+ * - Si `SWL_MCP_API_KEY` ESTÁ definida, cada `tools/call` debe incluir
14
+ * `params._auth = "<api-key>"`. Sin el token o con token incorrecto, se devuelve
15
+ * `-32001 Unauthorized`.
16
+ *
17
+ * Decisión deliberada de NO usar header `Authorization: Bearer ...`:
18
+ * - El protocolo MCP sobre stdio NO transporta headers HTTP.
19
+ * - Inventar un campo en `initialize.capabilities` específico para swl-ses
20
+ * rompería la spec del protocolo.
21
+ * - `_auth` como campo en `params` es el patrón de menor fricción.
22
+ *
23
+ * Limitaciones aceptadas:
24
+ * - Token estático sin rotación automática — el operador rota manualmente
25
+ * cambiando la env var del proceso.
26
+ * - Sin scopes (read/write) porque v1 sigue siendo read-only.
27
+ *
28
+ * @module scripts/mcp-server/auth
29
+ */
30
+
31
+ const ENV_VAR_NAME = 'SWL_MCP_API_KEY';
32
+ const ERROR_CODE_UNAUTHORIZED = -32001;
33
+ const ERROR_CODE_FORBIDDEN = -32002;
34
+
35
+ /**
36
+ * Construye un validador de auth desde el entorno actual.
37
+ *
38
+ * Pattern de "construct once, use many": leer la env var una sola vez al arranque
39
+ * y devolver una función pura que valida cada request. Evita race conditions con
40
+ * tests que mutan process.env.
41
+ *
42
+ * @param {object} [opciones] - { env: NodeJS.ProcessEnv } sustituible para tests.
43
+ * @returns {{ requerida: boolean, validar: (request: object) => { ok: boolean, code?: number, message?: string } }}
44
+ */
45
+ function construirValidador(opciones = {}) {
46
+ const env = opciones.env || process.env;
47
+ const apiKey = env[ENV_VAR_NAME];
48
+ const requerida = typeof apiKey === 'string' && apiKey.length > 0;
49
+
50
+ return {
51
+ requerida,
52
+ validar: (request) => {
53
+ if (!requerida) return { ok: true };
54
+ // Solo validar tools/call — initialize, ping, tools/list son metadata pública.
55
+ const metodo = request && request.method;
56
+ if (metodo !== 'tools/call') return { ok: true };
57
+
58
+ const params = request.params || {};
59
+ const tokenCliente = params._auth;
60
+
61
+ if (typeof tokenCliente !== 'string' || tokenCliente.length === 0) {
62
+ return {
63
+ ok: false,
64
+ code: ERROR_CODE_UNAUTHORIZED,
65
+ message: 'swl-mcp-server requiere autenticación: SWL_MCP_API_KEY está configurada en el server. El cliente debe enviar params._auth con el API key.',
66
+ };
67
+ }
68
+ if (!comparacionConstante(tokenCliente, apiKey)) {
69
+ return {
70
+ ok: false,
71
+ code: ERROR_CODE_FORBIDDEN,
72
+ message: 'Token inválido.',
73
+ };
74
+ }
75
+ return { ok: true };
76
+ },
77
+ };
78
+ }
79
+
80
+ /**
81
+ * Comparación de strings en tiempo constante para evitar timing attacks
82
+ * sobre el API key. Implementación zero-deps simple.
83
+ *
84
+ * Si las longitudes difieren, igual se itera la longitud máxima para no filtrar
85
+ * información sobre el largo del secreto vía duración de la comparación.
86
+ */
87
+ function comparacionConstante(a, b) {
88
+ if (typeof a !== 'string' || typeof b !== 'string') return false;
89
+ const len = Math.max(a.length, b.length);
90
+ let diff = a.length ^ b.length;
91
+ for (let i = 0; i < len; i++) {
92
+ const ca = i < a.length ? a.charCodeAt(i) : 0;
93
+ const cb = i < b.length ? b.charCodeAt(i) : 0;
94
+ diff |= ca ^ cb;
95
+ }
96
+ return diff === 0;
97
+ }
98
+
99
+ module.exports = {
100
+ construirValidador,
101
+ comparacionConstante,
102
+ ENV_VAR_NAME,
103
+ ERROR_CODE_UNAUTHORIZED,
104
+ ERROR_CODE_FORBIDDEN,
105
+ };