@saulwade/swl-ses 1.4.2 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +3 -3
- package/README.md +561 -560
- package/bin/swl-mcp-server.js +214 -187
- package/bin/swl-ses.js +74 -0
- package/comandos/swl/ejecutar-fase.md +33 -4
- package/comandos/swl/metricas.md +72 -0
- package/habilidades/discutir-fase/SKILL.md +50 -2
- package/habilidades/ejecutar-task-iterativo/SKILL.md +278 -0
- package/habilidades/protocolo-revision-swl/SKILL.md +276 -0
- package/habilidades/verificar-trabajo/SKILL.md +49 -5
- package/manifiestos/modulos.json +1321 -1267
- package/package.json +3 -3
- package/plugin.json +351 -351
- package/scripts/derivar-feature-list.js +489 -0
- package/scripts/doctor.js +31 -4
- package/scripts/instalador.js +56 -5
- package/scripts/lib/detectar-runtime.js +75 -9
- package/scripts/lib/estado.js +13 -1
- package/scripts/lib/expandir-targets.js +71 -0
- package/scripts/lib/parsear-opciones.js +3 -0
- package/scripts/lib/toml-merge.js +204 -0
- package/scripts/lib/transformadores/base.js +43 -9
- package/scripts/lib/transformadores/codex.js +375 -115
- package/scripts/lib/transformadores/cursor.js +359 -0
- package/scripts/lib/transformadores/index.js +2 -0
- package/scripts/mcp-server/README.md +170 -128
- package/scripts/mcp-server/auth.js +105 -0
- package/scripts/mcp-server/cache.js +106 -0
- package/scripts/mcp-server/handlers.js +190 -10
- package/scripts/mcp-server/telemetry.js +78 -0
|
@@ -81,19 +81,29 @@ const RUNTIMES = {
|
|
|
81
81
|
tiposSoportados: ['agentes', 'habilidades', 'comandos', 'reglas', 'hooks'],
|
|
82
82
|
},
|
|
83
83
|
codex: {
|
|
84
|
+
// ADR-0019 Sub-fase 1 → ampliada en Sub-fase 8 (mismo v1.5.0).
|
|
85
|
+
// Codex CLI lee:
|
|
86
|
+
// - AGENTS.md jerárquico (project + ~/.codex/AGENTS.md global)
|
|
87
|
+
// - ${CODEX_HOME:-$HOME/.codex}/skills/<name>/SKILL.md (skills propias)
|
|
88
|
+
// - ~/.codex/config.toml con [mcp_servers.NAME] (MCP)
|
|
89
|
+
// - hooks.json con PreToolUse/PostToolUse/Stop (NO mapeado por SWL — formato distinto)
|
|
90
|
+
// Codex NO expone agents/ individual desde filesystem — los agentes SWL
|
|
91
|
+
// van consolidados en AGENTS.md como tabla referencial.
|
|
84
92
|
nombre: 'Codex CLI',
|
|
85
93
|
global: path.join(os.homedir(), '.codex'),
|
|
86
94
|
local: '.codex',
|
|
87
|
-
archivosConfig: [],
|
|
88
|
-
dirAgentes:
|
|
89
|
-
dirHabilidades:
|
|
90
|
-
dirComandos: null,
|
|
91
|
-
dirReglas: null,
|
|
92
|
-
formatoAgente: '
|
|
93
|
-
soportaHooks:
|
|
94
|
-
|
|
95
|
+
archivosConfig: ['config.toml'],
|
|
96
|
+
dirAgentes: 'agents', // ~/.codex/agents/<name>.toml — Sub-fase 11 v1.5.0 (formato TOML)
|
|
97
|
+
dirHabilidades: 'skills', // path real `~/.agents/skills/` resuelto por transformador (Sub-fase 10)
|
|
98
|
+
dirComandos: null, // Codex no soporta slash custom filesystem
|
|
99
|
+
dirReglas: null, // consolidado en AGENTS.md (Codex rules son Starlark, no portable)
|
|
100
|
+
formatoAgente: 'toml', // Sub-fase 11: cada agente es un .toml individual
|
|
101
|
+
soportaHooks: true, // Sub-fase 10: ~/.codex/hooks.json (6 eventos)
|
|
102
|
+
hookConfig: 'hooks.json',
|
|
103
|
+
soporte: 'completo',
|
|
95
104
|
archivoPrincipal: 'AGENTS.md',
|
|
96
|
-
tiposSoportados: ['agentes', 'reglas'],
|
|
105
|
+
tiposSoportados: ['agentes', 'habilidades', 'reglas', 'hooks'],
|
|
106
|
+
notas: 'Agentes en ~/.codex/agents/<name>.toml (formato TOML — Sub-fase 11). Skills en ~/.agents/skills/ (path oficial OpenAI — Sub-fase 10). Hooks en ~/.codex/hooks.json. MCP server en ~/.codex/config.toml. AGENTS.md sigue conteniendo tabla referencial como índice.',
|
|
97
107
|
},
|
|
98
108
|
gemini: {
|
|
99
109
|
nombre: 'Gemini CLI',
|
|
@@ -111,6 +121,34 @@ const RUNTIMES = {
|
|
|
111
121
|
archivoPrincipal: 'GEMINI.md',
|
|
112
122
|
tiposSoportados: ['agentes', 'habilidades', 'comandos', 'reglas', 'hooks'],
|
|
113
123
|
},
|
|
124
|
+
cursor: {
|
|
125
|
+
// ADR-0019 Sub-fase 2 → refinada en Sub-fase 7 (mismo v1.5.0).
|
|
126
|
+
// Tras verificar docs oficiales de Cursor (cursor.com/es/docs/{subagents,skills,rules,mcp}):
|
|
127
|
+
// Cursor SÍ soporta subagents y skills filesystem nativamente.
|
|
128
|
+
// - `.cursor/agents/<name>.md` con frontmatter Claude-compatible
|
|
129
|
+
// (name, description, model=inherit, readonly, is_background).
|
|
130
|
+
// Legacy: lee `.claude/agents/`, `.codex/agents/`.
|
|
131
|
+
// - `.cursor/skills/<name>/SKILL.md` (mismo formato Anthropic Skills).
|
|
132
|
+
// Legacy: lee `.agents/skills/`, `.claude/skills/`, `.codex/skills/`.
|
|
133
|
+
// - `.cursor/rules/*.mdc` con frontmatter description/alwaysApply/globs.
|
|
134
|
+
// - `.cursor/mcp.json` o `~/.cursor/mcp.json` para MCP servers.
|
|
135
|
+
// - NO soporta nativamente: slash commands custom, hooks (PreToolUse, etc).
|
|
136
|
+
nombre: 'Cursor',
|
|
137
|
+
global: path.join(os.homedir(), '.cursor'),
|
|
138
|
+
local: '.cursor',
|
|
139
|
+
archivosConfig: ['mcp.json'],
|
|
140
|
+
dirAgentes: 'agents', // .cursor/agents/<name>.md
|
|
141
|
+
dirHabilidades: 'skills', // .cursor/skills/<name>/SKILL.md
|
|
142
|
+
dirComandos: null, // Cursor no tiene slash custom filesystem (skills cumplen ese rol)
|
|
143
|
+
dirReglas: 'rules', // .cursor/rules/*.mdc
|
|
144
|
+
formatoAgente: 'markdown-frontmatter',
|
|
145
|
+
soportaHooks: true, // Sub-fase 10: .cursor/hooks.json (17 eventos)
|
|
146
|
+
hookConfig: 'hooks.json',
|
|
147
|
+
soporte: 'completo',
|
|
148
|
+
archivoPrincipal: null,
|
|
149
|
+
tiposSoportados: ['agentes', 'habilidades', 'reglas', 'hooks'],
|
|
150
|
+
notas: 'Subagents en .cursor/agents/, skills en .cursor/skills/<name>/SKILL.md, reglas en .cursor/rules/*.mdc, hooks en .cursor/hooks.json (17 eventos camelCase), MCP en .cursor/mcp.json. Slash commands custom: usar skills (se invocan con /<name>).',
|
|
151
|
+
},
|
|
114
152
|
};
|
|
115
153
|
|
|
116
154
|
/**
|
|
@@ -269,6 +307,33 @@ function detectarInstalacionesDuales(runtimeId, opciones = {}) {
|
|
|
269
307
|
};
|
|
270
308
|
}
|
|
271
309
|
|
|
310
|
+
/**
|
|
311
|
+
* Lista los IDs de runtimes "instalables" — es decir, los que tienen
|
|
312
|
+
* transformador disponible y soporte declarado (completo o parcial).
|
|
313
|
+
*
|
|
314
|
+
* Se usa para expandir `--all-runtimes` en multi-target install (ADR-0019 Sub-fase 2.5).
|
|
315
|
+
*
|
|
316
|
+
* Excluye alias (openclaude comparte el dir .claude/ con claude — incluirlo en
|
|
317
|
+
* --all-runtimes produciría doble instalación al mismo destino).
|
|
318
|
+
*
|
|
319
|
+
* Orden estable: el orden de la lista importa porque define el orden de
|
|
320
|
+
* ejecución de --all-runtimes. Si un target falla los siguientes igual se
|
|
321
|
+
* intentan (ADR-0019 punto 9: atomicidad por target, no rollback).
|
|
322
|
+
*
|
|
323
|
+
* @returns {string[]} Lista de runtime IDs ordenada de "más usado" a "menos usado".
|
|
324
|
+
*/
|
|
325
|
+
function listarRuntimesInstalables() {
|
|
326
|
+
return [
|
|
327
|
+
'claude', // Soporte completo, primer ciudadano
|
|
328
|
+
'cursor', // Soporte parcial — reglas + MCP
|
|
329
|
+
'codex', // Soporte parcial — AGENTS.md + MCP (v1.5.0+)
|
|
330
|
+
'opencode', // Soporte completo
|
|
331
|
+
'gemini', // Soporte completo
|
|
332
|
+
'copilot', // Soporte parcial — agentes consolidados
|
|
333
|
+
// 'openclaude' intencionalmente excluido — comparte dir con claude
|
|
334
|
+
];
|
|
335
|
+
}
|
|
336
|
+
|
|
272
337
|
module.exports = {
|
|
273
338
|
RUNTIMES,
|
|
274
339
|
detectarRuntimes,
|
|
@@ -276,4 +341,5 @@ module.exports = {
|
|
|
276
341
|
obtenerRuntime,
|
|
277
342
|
calcularRutas,
|
|
278
343
|
resumenDeteccion,
|
|
344
|
+
listarRuntimesInstalables,
|
|
279
345
|
};
|
package/scripts/lib/estado.js
CHANGED
|
@@ -21,7 +21,17 @@ try {
|
|
|
21
21
|
const NOMBRE_ESTADO = '.swl-install-state.json';
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
|
-
* Crea un nuevo registro de estado de instalación
|
|
24
|
+
* Crea un nuevo registro de estado de instalación.
|
|
25
|
+
*
|
|
26
|
+
* Persiste el contexto de filtrado de reglas-lenguajes (allLangs, stackInstalado)
|
|
27
|
+
* para que doctor verifique conteos contra el filtro real usado al instalar,
|
|
28
|
+
* no contra un nuevo filtro calculado desde el cwd actual del doctor.
|
|
29
|
+
*
|
|
30
|
+
* Bug histórico v1.4.2: doctor reportaba "reglas: 65 (perfil esperaba 25)" como
|
|
31
|
+
* falso positivo cuando el usuario instalaba globalmente desde un dir con
|
|
32
|
+
* indicadores de lenguaje y luego ejecutaba doctor desde otro dir sin esos
|
|
33
|
+
* indicadores. El recálculo del filtro daba 25 esperadas en lugar de las 65
|
|
34
|
+
* realmente instaladas.
|
|
25
35
|
*/
|
|
26
36
|
function crearEstado(opciones) {
|
|
27
37
|
return {
|
|
@@ -37,6 +47,8 @@ function crearEstado(opciones) {
|
|
|
37
47
|
componentesExternos: [],
|
|
38
48
|
hooksRegistrados: 0,
|
|
39
49
|
settingsModificado: false,
|
|
50
|
+
allLangs: opciones.allLangs === true,
|
|
51
|
+
stackInstalado: Array.isArray(opciones.stackInstalado) ? opciones.stackInstalado : null,
|
|
40
52
|
instaladoEn: new Date().toISOString(),
|
|
41
53
|
actualizadoEn: new Date().toISOString(),
|
|
42
54
|
};
|
|
@@ -0,0 +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 };
|
|
@@ -0,0 +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
|
+
};
|
|
@@ -8,27 +8,61 @@ class TransformadorBase {
|
|
|
8
8
|
this.runtime = runtimeConfig;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
// Parsea frontmatter YAML de un archivo markdown
|
|
11
|
+
// Parsea frontmatter YAML de un archivo markdown.
|
|
12
|
+
// Maneja CRLF (Windows) y LF (Unix). Soporta valores multi-línea con `>` y `|`.
|
|
12
13
|
// Retorna { frontmatter: object, cuerpo: string }
|
|
13
14
|
parsearFrontmatter(contenido) {
|
|
14
|
-
|
|
15
|
+
// Normalizar a LF para que el regex funcione con archivos Windows (CRLF).
|
|
16
|
+
const norm = String(contenido).replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
17
|
+
const match = norm.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
|
|
15
18
|
if (!match) return { frontmatter: {}, cuerpo: contenido };
|
|
16
19
|
|
|
17
20
|
const yamlStr = match[1];
|
|
18
21
|
const cuerpo = match[2];
|
|
19
22
|
const frontmatter = {};
|
|
20
23
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
const lineas = yamlStr.split('\n');
|
|
25
|
+
let i = 0;
|
|
26
|
+
while (i < lineas.length) {
|
|
27
|
+
const linea = lineas[i];
|
|
28
|
+
const idxColon = linea.indexOf(':');
|
|
29
|
+
if (idxColon === -1) { i++; continue; }
|
|
30
|
+
const clave = linea.slice(0, idxColon).trim();
|
|
31
|
+
let valorRaw = linea.slice(idxColon + 1).trim();
|
|
32
|
+
|
|
33
|
+
// Multi-línea con `>` (folded) o `|` (literal):
|
|
34
|
+
// capturar líneas indentadas siguientes y colapsarlas a single-line para `>`,
|
|
35
|
+
// mantener saltos para `|`. Soporta el patrón estándar de YAML en SWL agents.
|
|
36
|
+
if (valorRaw === '>' || valorRaw === '|') {
|
|
37
|
+
const folded = valorRaw === '>';
|
|
38
|
+
const partes = [];
|
|
39
|
+
i++;
|
|
40
|
+
while (i < lineas.length) {
|
|
41
|
+
const sig = lineas[i];
|
|
42
|
+
// Línea no indentada y no vacía → fin del bloque multi-línea.
|
|
43
|
+
if (sig.length > 0 && !/^\s/.test(sig)) break;
|
|
44
|
+
// Línea vacía dentro del bloque → mantener como separador.
|
|
45
|
+
if (sig.trim().length === 0) {
|
|
46
|
+
partes.push('');
|
|
47
|
+
i++;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
partes.push(sig.replace(/^\s+/, ''));
|
|
51
|
+
i++;
|
|
52
|
+
}
|
|
53
|
+
frontmatter[clave] = folded
|
|
54
|
+
? partes.filter(p => p.length > 0).join(' ').trim()
|
|
55
|
+
: partes.join('\n').trim();
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Valor inline simple
|
|
60
|
+
let valor = valorRaw;
|
|
27
61
|
if (valor === 'true') valor = true;
|
|
28
62
|
else if (valor === 'false') valor = false;
|
|
29
|
-
// Numbers
|
|
30
63
|
else if (/^\d+$/.test(valor)) valor = parseInt(valor, 10);
|
|
31
64
|
frontmatter[clave] = valor;
|
|
65
|
+
i++;
|
|
32
66
|
}
|
|
33
67
|
|
|
34
68
|
return { frontmatter, cuerpo };
|