@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
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* handlers.js — Handlers para los 3 endpoints MCP
|
|
4
|
+
* handlers.js — Handlers para los 3 endpoints MCP del swl-mcp-server.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* v1.0.0 (ADR-0019 Sub-fase 3): integra caching mtime-based desde
|
|
7
|
+
* `scripts/mcp-server/cache.js`. Sigue siendo solo lectura.
|
|
8
8
|
*
|
|
9
9
|
* Los handlers leen el estado file-based de swl-ses (APRENDIZAJES.md,
|
|
10
10
|
* .planning/sessions/, instintos/proyecto.yaml) y devuelven datos
|
|
@@ -18,6 +18,11 @@ const path = require('path');
|
|
|
18
18
|
|
|
19
19
|
const memorySearch = require('../../hooks/lib/memory-search');
|
|
20
20
|
const scoringInstintos = require('../lib/scoring-instintos');
|
|
21
|
+
const { crearCache } = require('./cache');
|
|
22
|
+
|
|
23
|
+
// Cache singleton — compartido entre handlers para reducir IO en lecturas repetidas.
|
|
24
|
+
// mtime-based, invalidación automática cuando el archivo cambia. ADR-0019 Sub-fase 3.
|
|
25
|
+
const cache = crearCache();
|
|
21
26
|
|
|
22
27
|
// ── handler: swl_memory_search ────────────────────────────────────────────────
|
|
23
28
|
|
|
@@ -64,18 +69,18 @@ function swlAprendizajesRecientes(baseDir, args = {}) {
|
|
|
64
69
|
: 10;
|
|
65
70
|
|
|
66
71
|
const ruta = path.join(baseDir, '.planning', 'APRENDIZAJES.md');
|
|
67
|
-
|
|
68
|
-
return { error: 'APRENDIZAJES.md no encontrado', results: [] };
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
let contenido;
|
|
72
|
+
let cached;
|
|
72
73
|
try {
|
|
73
|
-
|
|
74
|
+
cached = cache.get(ruta, (contenido) => {
|
|
75
|
+
const bloques = contenido.split(/^## /m).filter(b => b.trim().length > 0);
|
|
76
|
+
return { bloques };
|
|
77
|
+
});
|
|
74
78
|
} catch (err) {
|
|
75
79
|
return { error: 'Error de lectura: ' + err.message, results: [] };
|
|
76
80
|
}
|
|
81
|
+
if (!cached) return { error: 'APRENDIZAJES.md no encontrado', results: [] };
|
|
77
82
|
|
|
78
|
-
const bloques =
|
|
83
|
+
const { bloques } = cached.data;
|
|
79
84
|
// Los más recientes están al FINAL del archivo (append-only por convención)
|
|
80
85
|
const recientes = bloques.slice(-limit).reverse();
|
|
81
86
|
|
|
@@ -159,11 +164,156 @@ function swlInstintosActivos(baseDir, args = {}) {
|
|
|
159
164
|
};
|
|
160
165
|
}
|
|
161
166
|
|
|
167
|
+
// ── handler: swl_list_skills ──────────────────────────────────────────────────
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Lista los skills disponibles (nombre + descripción del frontmatter).
|
|
171
|
+
*
|
|
172
|
+
* Sub-fase 9 (v1.5.0). Útil para clientes MCP que no cargan skills filesystem
|
|
173
|
+
* nativamente y necesitan descubrir qué skills existen antes de invocarlos
|
|
174
|
+
* con swl_invoke_skill.
|
|
175
|
+
*
|
|
176
|
+
* Resolución del directorio de skills:
|
|
177
|
+
* 1. Si baseDir tiene `habilidades/`, usar ese (repo SWL como project root).
|
|
178
|
+
* 2. Si baseDir tiene `.claude/skills/`, usar ese (proyecto consumidor con SWL instalado).
|
|
179
|
+
* 3. Si baseDir tiene `.cursor/skills/`, usar ese.
|
|
180
|
+
* 4. Si nada de eso, devolver error.
|
|
181
|
+
*
|
|
182
|
+
* @param {string} baseDir
|
|
183
|
+
* @param {object} args - { dominio?: string, limit?: number }
|
|
184
|
+
*/
|
|
185
|
+
function swlListSkills(baseDir, args = {}) {
|
|
186
|
+
const dirSkills = resolverDirSkills(baseDir);
|
|
187
|
+
if (!dirSkills) {
|
|
188
|
+
return { error: 'No se encontró directorio de skills (habilidades/, .claude/skills/, .cursor/skills/)', results: [] };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const limit = (typeof args.limit === 'number' && args.limit > 0)
|
|
192
|
+
? Math.min(args.limit, 500) : 200;
|
|
193
|
+
const dominio = typeof args.dominio === 'string' ? args.dominio.toLowerCase() : null;
|
|
194
|
+
|
|
195
|
+
let nombres;
|
|
196
|
+
try {
|
|
197
|
+
nombres = fs.readdirSync(dirSkills, { withFileTypes: true })
|
|
198
|
+
.filter(d => d.isDirectory() && !d.name.startsWith('.'))
|
|
199
|
+
.map(d => d.name);
|
|
200
|
+
} catch (err) {
|
|
201
|
+
return { error: 'Error leyendo directorio: ' + err.message, results: [] };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const results = [];
|
|
205
|
+
for (const nombre of nombres) {
|
|
206
|
+
if (dominio && !nombre.toLowerCase().includes(dominio)) continue;
|
|
207
|
+
const skillMd = path.join(dirSkills, nombre, 'SKILL.md');
|
|
208
|
+
if (!fs.existsSync(skillMd)) continue;
|
|
209
|
+
try {
|
|
210
|
+
const contenido = fs.readFileSync(skillMd, 'utf-8');
|
|
211
|
+
const descMatch = contenido.match(/description:\s*>?\s*\n?\s*(.+?)(?=\n\w+:|\n---)/s);
|
|
212
|
+
const descripcion = descMatch ? descMatch[1].replace(/\s+/g, ' ').trim() : '';
|
|
213
|
+
results.push({
|
|
214
|
+
nombre,
|
|
215
|
+
descripcion: descripcion.length > 300 ? descripcion.slice(0, 297) + '...' : descripcion,
|
|
216
|
+
});
|
|
217
|
+
} catch { /* skill ilegible — saltar */ }
|
|
218
|
+
if (results.length >= limit) break;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return { results, count: results.length, total: nombres.length, dir: dirSkills };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ── handler: swl_invoke_skill ─────────────────────────────────────────────────
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Devuelve el contenido completo de un SKILL.md por nombre.
|
|
228
|
+
*
|
|
229
|
+
* Sub-fase 9 (v1.5.0). Útil para clientes MCP donde el cliente NO carga skills
|
|
230
|
+
* filesystem nativamente (Codex CLI en `--local`, Gemini CLI, otros) pero quiere
|
|
231
|
+
* acceder a conocimiento operacional SWL. El cliente recibe el cuerpo completo
|
|
232
|
+
* del SKILL.md y puede usarlo como contexto en su próxima llamada.
|
|
233
|
+
*
|
|
234
|
+
* Limitaciones aceptadas:
|
|
235
|
+
* - NO ejecuta scripts del skill — solo devuelve el SKILL.md como texto.
|
|
236
|
+
* - Si el skill referencia `recursos/X.md`, el cliente debe pedirlo aparte
|
|
237
|
+
* (no implementamos retrieval recursivo en v1.5.0).
|
|
238
|
+
* - El cuerpo se trunca a 100 KB para no saturar el contexto del cliente.
|
|
239
|
+
*
|
|
240
|
+
* @param {string} baseDir
|
|
241
|
+
* @param {object} args - { nombre: string }
|
|
242
|
+
*/
|
|
243
|
+
function swlInvokeSkill(baseDir, args) {
|
|
244
|
+
if (!args || typeof args.nombre !== 'string' || !args.nombre.trim()) {
|
|
245
|
+
return { error: 'argumento "nombre" (string) requerido' };
|
|
246
|
+
}
|
|
247
|
+
// Sanitización: solo nombres en kebab-case (prevenir path traversal).
|
|
248
|
+
const nombre = args.nombre.trim();
|
|
249
|
+
if (!/^[a-z0-9][a-z0-9-]{0,62}$/i.test(nombre)) {
|
|
250
|
+
return { error: 'Nombre de skill inválido. Solo [a-zA-Z0-9-], 1-63 caracteres.' };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const dirSkills = resolverDirSkills(baseDir);
|
|
254
|
+
if (!dirSkills) {
|
|
255
|
+
return { error: 'No se encontró directorio de skills (habilidades/, .claude/skills/, .cursor/skills/)' };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const skillMd = path.join(dirSkills, nombre, 'SKILL.md');
|
|
259
|
+
if (!fs.existsSync(skillMd)) {
|
|
260
|
+
return { error: `Skill "${nombre}" no encontrado en ${dirSkills}` };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
let contenido;
|
|
264
|
+
try {
|
|
265
|
+
contenido = fs.readFileSync(skillMd, 'utf-8');
|
|
266
|
+
} catch (err) {
|
|
267
|
+
return { error: 'Error leyendo SKILL.md: ' + err.message };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const MAX = 100 * 1024;
|
|
271
|
+
let truncado = false;
|
|
272
|
+
if (Buffer.byteLength(contenido, 'utf-8') > MAX) {
|
|
273
|
+
contenido = contenido.slice(0, MAX) + '\n\n> NOTA: contenido truncado a 100 KB.';
|
|
274
|
+
truncado = true;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Listar recursos disponibles (sin leerlos) para que el cliente sepa qué pedir.
|
|
278
|
+
const recursos = [];
|
|
279
|
+
const dirRecursos = path.join(dirSkills, nombre, 'recursos');
|
|
280
|
+
if (fs.existsSync(dirRecursos)) {
|
|
281
|
+
try {
|
|
282
|
+
const archivos = fs.readdirSync(dirRecursos)
|
|
283
|
+
.filter(f => /\.(md|json|yaml|yml|txt)$/i.test(f));
|
|
284
|
+
recursos.push(...archivos.map(f => `recursos/${f}`));
|
|
285
|
+
} catch { /* sin recursos */ }
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return { nombre, contenido, truncado, recursos, fuente: skillMd };
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Resuelve el directorio de skills en el orden de precedencia:
|
|
293
|
+
* 1. baseDir/habilidades/ (repo SWL como project root)
|
|
294
|
+
* 2. baseDir/.claude/skills/ (proyecto consumidor con SWL instalado en Claude)
|
|
295
|
+
* 3. baseDir/.cursor/skills/ (proyecto con SWL instalado en Cursor)
|
|
296
|
+
*
|
|
297
|
+
* Retorna null si ninguno existe.
|
|
298
|
+
*/
|
|
299
|
+
function resolverDirSkills(baseDir) {
|
|
300
|
+
const candidatos = [
|
|
301
|
+
path.join(baseDir, 'habilidades'),
|
|
302
|
+
path.join(baseDir, '.claude', 'skills'),
|
|
303
|
+
path.join(baseDir, '.cursor', 'skills'),
|
|
304
|
+
];
|
|
305
|
+
for (const dir of candidatos) {
|
|
306
|
+
if (fs.existsSync(dir)) return dir;
|
|
307
|
+
}
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
|
|
162
311
|
// ── exports ───────────────────────────────────────────────────────────────────
|
|
163
312
|
|
|
164
313
|
const HANDLERS = {
|
|
165
314
|
swl_memory_search: {
|
|
166
315
|
description: 'Búsqueda hybrid sobre memoria SWL (aprendizajes + sesiones + instintos) con RRF fusion.',
|
|
316
|
+
schemaVersion: '1.0.0',
|
|
167
317
|
inputSchema: {
|
|
168
318
|
type: 'object',
|
|
169
319
|
properties: {
|
|
@@ -177,6 +327,7 @@ const HANDLERS = {
|
|
|
177
327
|
},
|
|
178
328
|
swl_aprendizajes_recientes: {
|
|
179
329
|
description: 'Últimos N aprendizajes de .planning/APRENDIZAJES.md (más recientes primero).',
|
|
330
|
+
schemaVersion: '1.0.0',
|
|
180
331
|
inputSchema: {
|
|
181
332
|
type: 'object',
|
|
182
333
|
properties: {
|
|
@@ -187,6 +338,7 @@ const HANDLERS = {
|
|
|
187
338
|
},
|
|
188
339
|
swl_instintos_activos: {
|
|
189
340
|
description: 'Instintos con effective_confidence ≥ umbral. Default 0.5.',
|
|
341
|
+
schemaVersion: '1.0.0',
|
|
190
342
|
inputSchema: {
|
|
191
343
|
type: 'object',
|
|
192
344
|
properties: {
|
|
@@ -196,6 +348,30 @@ const HANDLERS = {
|
|
|
196
348
|
},
|
|
197
349
|
handler: swlInstintosActivos,
|
|
198
350
|
},
|
|
351
|
+
swl_list_skills: {
|
|
352
|
+
description: 'Lista skills SWL disponibles (nombre + descripción). Útil para descubrir conocimiento operacional antes de invocar swl_invoke_skill.',
|
|
353
|
+
schemaVersion: '1.0.0',
|
|
354
|
+
inputSchema: {
|
|
355
|
+
type: 'object',
|
|
356
|
+
properties: {
|
|
357
|
+
dominio: { type: 'string', description: 'Filtro substring por nombre (opcional)' },
|
|
358
|
+
limit: { type: 'number', description: 'Máximo (default 200, max 500)' },
|
|
359
|
+
},
|
|
360
|
+
},
|
|
361
|
+
handler: swlListSkills,
|
|
362
|
+
},
|
|
363
|
+
swl_invoke_skill: {
|
|
364
|
+
description: 'Devuelve el SKILL.md completo de un skill SWL por nombre. Para clientes MCP que no cargan skills filesystem nativamente — el cliente recibe el cuerpo y lo usa como contexto.',
|
|
365
|
+
schemaVersion: '1.0.0',
|
|
366
|
+
inputSchema: {
|
|
367
|
+
type: 'object',
|
|
368
|
+
properties: {
|
|
369
|
+
nombre: { type: 'string', description: 'Nombre del skill (kebab-case, ej. fastapi-experto)' },
|
|
370
|
+
},
|
|
371
|
+
required: ['nombre'],
|
|
372
|
+
},
|
|
373
|
+
handler: swlInvokeSkill,
|
|
374
|
+
},
|
|
199
375
|
};
|
|
200
376
|
|
|
201
377
|
module.exports = {
|
|
@@ -203,4 +379,8 @@ module.exports = {
|
|
|
203
379
|
swlMemorySearch,
|
|
204
380
|
swlAprendizajesRecientes,
|
|
205
381
|
swlInstintosActivos,
|
|
382
|
+
swlListSkills,
|
|
383
|
+
swlInvokeSkill,
|
|
384
|
+
// Expuesto para tests — permite invalidar el cache singleton entre runs.
|
|
385
|
+
_cache: cache,
|
|
206
386
|
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Telemetría opt-in del swl-mcp-server v1.0.0 (ADR-0019 Sub-fase 3).
|
|
5
|
+
*
|
|
6
|
+
* Cuando `SWL_MCP_METRICS=1` está activo, cada `tools/call` se registra en
|
|
7
|
+
* `.planning/evolucion/mcp-metrics.jsonl` con:
|
|
8
|
+
* { ts, tool, durationMs, ok, error?, baseDir }
|
|
9
|
+
*
|
|
10
|
+
* Defaults:
|
|
11
|
+
* - SWL_MCP_METRICS no set → no se persiste nada (zero-cost).
|
|
12
|
+
* - Se usa append-only JSONL (alta frecuencia → atomicWriteJSON sería ineficiente).
|
|
13
|
+
* - Si el filesystem no permite escritura (p.ej. baseDir read-only), el error
|
|
14
|
+
* se traga silenciosamente — la telemetría NUNCA debe romper el server.
|
|
15
|
+
*
|
|
16
|
+
* Zero-deps.
|
|
17
|
+
*
|
|
18
|
+
* @module scripts/mcp-server/telemetry
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const fs = require('fs');
|
|
22
|
+
const path = require('path');
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Construye un grabador de telemetría desde el entorno actual.
|
|
26
|
+
*
|
|
27
|
+
* @param {object} [opciones]
|
|
28
|
+
* @param {string} opciones.baseDir - Directorio raíz del proyecto SWL (donde está .planning/).
|
|
29
|
+
* @param {object} [opciones.env] - Sustituible para tests.
|
|
30
|
+
* @returns {{ habilitada: boolean, registrar: Function, ruta: string|null }}
|
|
31
|
+
*/
|
|
32
|
+
function construirTelemetria(opciones = {}) {
|
|
33
|
+
const env = opciones.env || process.env;
|
|
34
|
+
const habilitada = env.SWL_MCP_METRICS === '1' || env.SWL_MCP_METRICS === 'true';
|
|
35
|
+
const baseDir = opciones.baseDir;
|
|
36
|
+
|
|
37
|
+
if (!habilitada || !baseDir) {
|
|
38
|
+
return {
|
|
39
|
+
habilitada: false,
|
|
40
|
+
registrar: () => {}, // noop
|
|
41
|
+
ruta: null,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const dirEvolucion = path.join(baseDir, '.planning', 'evolucion');
|
|
46
|
+
const ruta = path.join(dirEvolucion, 'mcp-metrics.jsonl');
|
|
47
|
+
|
|
48
|
+
// Asegurar que el directorio existe — si falla, deshabilitamos silenciosamente.
|
|
49
|
+
let escribible = true;
|
|
50
|
+
try {
|
|
51
|
+
fs.mkdirSync(dirEvolucion, { recursive: true });
|
|
52
|
+
} catch (err) {
|
|
53
|
+
escribible = false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
habilitada: escribible,
|
|
58
|
+
registrar: (evento) => {
|
|
59
|
+
if (!escribible) return;
|
|
60
|
+
try {
|
|
61
|
+
const linea = JSON.stringify({
|
|
62
|
+
ts: new Date().toISOString(),
|
|
63
|
+
...evento,
|
|
64
|
+
}) + '\n';
|
|
65
|
+
fs.appendFileSync(ruta, linea, { encoding: 'utf-8' });
|
|
66
|
+
} catch {
|
|
67
|
+
// Telemetría nunca debe romper el server — error silencioso.
|
|
68
|
+
// Tras 5 errores consecutivos podríamos deshabilitar, pero no
|
|
69
|
+
// implementamos eso hasta que aparezca como problema real.
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
ruta,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
module.exports = {
|
|
77
|
+
construirTelemetria,
|
|
78
|
+
};
|