@saulwade/swl-ses 1.4.2 → 1.5.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 +209 -208
- 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/meta-skills-estandar/SKILL.md +22 -1
- package/habilidades/node-experto/SKILL.md +13 -2
- package/habilidades/protocolo-revision-swl/SKILL.md +276 -0
- package/habilidades/tdd-workflow/SKILL.md +33 -4
- package/habilidades/verificar-trabajo/SKILL.md +54 -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 +62 -8
- 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,128 +1,170 @@
|
|
|
1
|
-
# swl-mcp-server
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
## Qué hace
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
1
|
+
# swl-mcp-server v1.0.0
|
|
2
|
+
|
|
3
|
+
Servidor MCP de solo lectura para exponer la memoria de swl-ses
|
|
4
|
+
(aprendizajes, sesiones, instintos) a clientes MCP externos como Cursor,
|
|
5
|
+
Codex CLI y Gemini CLI.
|
|
6
|
+
|
|
7
|
+
Promovido de stub experimental (v0.1.x) a v1.0.0 en ADR-0019 Sub-fase 3.
|
|
8
|
+
|
|
9
|
+
## Qué hace
|
|
10
|
+
|
|
11
|
+
Expone 5 endpoints sobre stdio JSON-RPC:
|
|
12
|
+
|
|
13
|
+
1. **`swl_memory_search`** — búsqueda hybrid sobre memoria SWL
|
|
14
|
+
(aprendizajes + sesiones + instintos) usando `hooks/lib/memory-search`
|
|
15
|
+
con RRF fusion.
|
|
16
|
+
2. **`swl_aprendizajes_recientes`** — últimos N aprendizajes de
|
|
17
|
+
`.planning/APRENDIZAJES.md`.
|
|
18
|
+
3. **`swl_instintos_activos`** — instintos con `effective_confidence ≥
|
|
19
|
+
umbral`.
|
|
20
|
+
4. **`swl_list_skills`** (Sub-fase 9 v1.5.0) — lista skills SWL
|
|
21
|
+
disponibles con nombre + descripción del frontmatter. Útil para
|
|
22
|
+
descubrir conocimiento operacional antes de invocar uno.
|
|
23
|
+
5. **`swl_invoke_skill`** (Sub-fase 9 v1.5.0) — devuelve el SKILL.md
|
|
24
|
+
completo de un skill por nombre. Para clientes MCP que no cargan
|
|
25
|
+
skills filesystem nativamente (Codex `--local`, Gemini CLI, otros) —
|
|
26
|
+
el cliente recibe el cuerpo del SKILL.md como texto y lo usa como
|
|
27
|
+
contexto en su próxima llamada.
|
|
28
|
+
|
|
29
|
+
El server lee el estado file-based de swl-ses tal como existe en `cwd`
|
|
30
|
+
(o el directorio especificado por `SWL_MCP_BASE_DIR`). NO escribe — solo
|
|
31
|
+
lectura. NO ejecuta scripts referenciados desde un skill.
|
|
32
|
+
|
|
33
|
+
### Resolución del directorio de skills
|
|
34
|
+
|
|
35
|
+
Para `swl_list_skills` y `swl_invoke_skill`, el server busca skills en
|
|
36
|
+
el primer directorio que exista, en este orden:
|
|
37
|
+
|
|
38
|
+
1. `<baseDir>/habilidades/` — repo SWL como project root.
|
|
39
|
+
2. `<baseDir>/.claude/skills/` — proyecto consumidor con SWL instalado
|
|
40
|
+
en Claude Code.
|
|
41
|
+
3. `<baseDir>/.cursor/skills/` — proyecto con SWL instalado en Cursor.
|
|
42
|
+
|
|
43
|
+
## Features v1.0.0
|
|
44
|
+
|
|
45
|
+
| Feature | Cómo activar | Default |
|
|
46
|
+
|---|---|---|
|
|
47
|
+
| Auth opt-in (Bearer en `params._auth`) | `SWL_MCP_API_KEY=<token>` en el env del server | sin auth (backward-compat) |
|
|
48
|
+
| Caching mtime-based con TTL | Siempre activo | 60 segundos |
|
|
49
|
+
| Override TTL del cache | `SWL_MCP_CACHE_TTL_MS=<ms>` | 60000 |
|
|
50
|
+
| Telemetría JSONL por call | `SWL_MCP_METRICS=1` | sin telemetría |
|
|
51
|
+
| Schema versioning por handler | Siempre presente en `tools/list` | `1.0.0` |
|
|
52
|
+
|
|
53
|
+
## Cómo arrancar (testing)
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
# Smoke standalone
|
|
57
|
+
echo '{"jsonrpc":"2.0","id":1,"method":"initialize"}' | node bin/swl-mcp-server.js
|
|
58
|
+
|
|
59
|
+
# Output esperado (sin auth):
|
|
60
|
+
# {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2024-11-05","capabilities":{...},"serverInfo":{"name":"swl-mcp-server","version":"1.0.0","authRequired":false,"telemetryEnabled":false}}}
|
|
61
|
+
|
|
62
|
+
# Con auth opt-in
|
|
63
|
+
SWL_MCP_API_KEY="secret-123" node bin/swl-mcp-server.js
|
|
64
|
+
|
|
65
|
+
# Cliente debe enviar params._auth:
|
|
66
|
+
echo '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"_auth":"secret-123","name":"swl_aprendizajes_recientes","arguments":{"limit":5}}}' | SWL_MCP_API_KEY=secret-123 node bin/swl-mcp-server.js
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Configuración en clientes MCP
|
|
70
|
+
|
|
71
|
+
### Cursor (autoconfig disponible)
|
|
72
|
+
|
|
73
|
+
Desde v1.5.0 el instalador puede registrar el server automáticamente:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
npx @saulwade/swl-ses@latest install --target cursor --with-mcp
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Esto genera `.cursor/mcp.json` (per-proyecto) o `~/.cursor/mcp.json`
|
|
80
|
+
(con `--global`) preservando otros mcpServers configurados.
|
|
81
|
+
|
|
82
|
+
Configuración manual:
|
|
83
|
+
|
|
84
|
+
```json
|
|
85
|
+
{
|
|
86
|
+
"mcpServers": {
|
|
87
|
+
"swl-memory": {
|
|
88
|
+
"command": "node",
|
|
89
|
+
"args": ["/path/to/swl-ses/bin/swl-mcp-server.js"],
|
|
90
|
+
"env": {
|
|
91
|
+
"SWL_MCP_BASE_DIR": "/path/to/proyecto",
|
|
92
|
+
"SWL_MCP_API_KEY": "opcional-token-secreto"
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Codex CLI (autoconfig disponible)
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
npx @saulwade/swl-ses@latest install --target codex --global --with-mcp
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Esto inserta `[mcp_servers.swl-memory]` en `~/.codex/config.toml`
|
|
106
|
+
preservando otros servers configurados. Verificar con `codex mcp list`.
|
|
107
|
+
|
|
108
|
+
### Gemini CLI
|
|
109
|
+
|
|
110
|
+
Manualmente similar a Cursor — el server expone stdio JSON-RPC estándar.
|
|
111
|
+
|
|
112
|
+
### Claude Code (NO necesario)
|
|
113
|
+
|
|
114
|
+
Claude Code ya tiene acceso directo a los archivos de swl-ses dentro de
|
|
115
|
+
su propio runtime. NO usar el MCP server desde Claude Code en el mismo
|
|
116
|
+
proyecto — sería redundante.
|
|
117
|
+
|
|
118
|
+
## Cambios v0.1.x → v1.0.0
|
|
119
|
+
|
|
120
|
+
| Aspecto | v0.1.x (stub) | v1.0.0 |
|
|
121
|
+
|---|---|---|
|
|
122
|
+
| Auth | sin auth | opt-in con `SWL_MCP_API_KEY` + Bearer en `params._auth` |
|
|
123
|
+
| Caching | sin caching | mtime-based con TTL configurable |
|
|
124
|
+
| Telemetría | logs stderr | JSONL en `.planning/evolucion/mcp-metrics.jsonl` (opt-in) |
|
|
125
|
+
| Schema versioning | implícito | `_schemaVersion` en cada tool de `tools/list` |
|
|
126
|
+
| Tests | smoke manual | 38 tests unitarios (`tests/mcp-server/*.test.js`) |
|
|
127
|
+
| Estado | "NO usar en producción" | apto para los proyectos del usuario |
|
|
128
|
+
|
|
129
|
+
## Backward compatibility
|
|
130
|
+
|
|
131
|
+
v1.0.0 es 100% backward-compatible con clientes que conectan sin auth.
|
|
132
|
+
Si `SWL_MCP_API_KEY` no está set en el env del server, el comportamiento
|
|
133
|
+
es idéntico al stub v0.1.x — no se requiere ningún cambio en clientes
|
|
134
|
+
configurados antes.
|
|
135
|
+
|
|
136
|
+
## Diseño futuro (cuando aparezca demanda real)
|
|
137
|
+
|
|
138
|
+
- **HTTP transport opcional**: además de stdio, ofrecer servidor HTTP/SSE
|
|
139
|
+
con TLS y CORS configurable.
|
|
140
|
+
- **Handlers de escritura** (`swl:memory:write`, `swl:instintos:write`)
|
|
141
|
+
fuera de scope v1.0 — requerirían scopes en el token y validación
|
|
142
|
+
semántica de cada mutación.
|
|
143
|
+
- **Rate limiting** por API key cuando aparezca un caso multi-cliente.
|
|
144
|
+
- **OpenTelemetry** para latencias p95 distribuidas si el server se
|
|
145
|
+
consume desde múltiples editores en paralelo.
|
|
146
|
+
|
|
147
|
+
## Estado de seguridad
|
|
148
|
+
|
|
149
|
+
- ✓ Read-only — no ejecuta código ni modifica archivos.
|
|
150
|
+
- ✓ Auth opt-in con comparación constante para defender contra timing
|
|
151
|
+
attacks.
|
|
152
|
+
- ✓ Caching con invalidación correcta (mtime + TTL).
|
|
153
|
+
- ✓ Telemetría con error silencioso — nunca rompe el server.
|
|
154
|
+
- ✗ `baseDir` no se valida contra patrón "proyecto SWL válido". Un
|
|
155
|
+
cliente con acceso al stdio puede apuntarlo a cualquier directorio
|
|
156
|
+
con `APRENDIZAJES.md`. Mitigación: el operador configura el server
|
|
157
|
+
con `SWL_MCP_BASE_DIR` explícito.
|
|
158
|
+
- ✗ Sin límite de tamaño de query — un cliente malicioso podría enviar
|
|
159
|
+
una query gigante. Mitigación parcial: `limit` está clampeado a 50/100.
|
|
160
|
+
|
|
161
|
+
Para casos de uso single-user en máquinas confiables del operador, los
|
|
162
|
+
puntos restantes son aceptables. Para uso multi-usuario o exposición a
|
|
163
|
+
red, evaluar primero el alcance del riesgo y considerar las features
|
|
164
|
+
de "diseño futuro" arriba.
|
|
165
|
+
|
|
166
|
+
## Referencias
|
|
167
|
+
|
|
168
|
+
- ADR-0019 Sub-fase 3: `.planning/adrs/0019-integracion-codex-y-cursor-completa.md`.
|
|
169
|
+
- Tests: `tests/mcp-server/*.test.js` (auth, cache, telemetry, rutear).
|
|
170
|
+
- Configuración Cursor: `docs/MCP-SERVER-CURSOR-NOTAS.md`.
|
|
@@ -0,0 +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
|
+
};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Cache mtime-based para handlers del swl-mcp-server v1.0.0 (ADR-0019 Sub-fase 3).
|
|
5
|
+
*
|
|
6
|
+
* El stub experimental leía disco en cada call (latencia medida ~10ms con
|
|
7
|
+
* APRENDIZAJES.md de 1172 líneas). Para datasets más grandes (>10k líneas) o
|
|
8
|
+
* múltiples clientes concurrentes, el caching reduce sustancialmente la carga.
|
|
9
|
+
*
|
|
10
|
+
* Patrón:
|
|
11
|
+
* - Key = path absoluto del archivo.
|
|
12
|
+
* - Value = { mtime, contenido, parsed }.
|
|
13
|
+
* - Invalidación: si fs.stat().mtime > value.mtime → recargar y reparsear.
|
|
14
|
+
* - TTL opcional vía `SWL_MCP_CACHE_TTL_MS` (default 60000 ms): aunque mtime no
|
|
15
|
+
* haya cambiado, recargar tras TTL para defensa contra clock skew o ediciones
|
|
16
|
+
* que no actualizan mtime (raras pero posibles en filesystems exóticos).
|
|
17
|
+
*
|
|
18
|
+
* Zero-deps. NO usar para escritura — sigue read-only.
|
|
19
|
+
*
|
|
20
|
+
* @module scripts/mcp-server/cache
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const fs = require('fs');
|
|
24
|
+
|
|
25
|
+
const DEFAULT_TTL_MS = 60 * 1000;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Crea una instancia de cache mtime-based.
|
|
29
|
+
*
|
|
30
|
+
* @param {object} [opciones]
|
|
31
|
+
* @param {number} [opciones.ttlMs] - TTL en milisegundos. Default 60000.
|
|
32
|
+
* @param {object} [opciones.env] - Para leer SWL_MCP_CACHE_TTL_MS en tests.
|
|
33
|
+
* @returns {{ get: Function, invalidate: Function, stats: Function, _store: Map }}
|
|
34
|
+
*/
|
|
35
|
+
function crearCache(opciones = {}) {
|
|
36
|
+
const env = opciones.env || process.env;
|
|
37
|
+
const ttlMs = opciones.ttlMs !== undefined
|
|
38
|
+
? opciones.ttlMs
|
|
39
|
+
: (parseInt(env.SWL_MCP_CACHE_TTL_MS, 10) || DEFAULT_TTL_MS);
|
|
40
|
+
|
|
41
|
+
const store = new Map();
|
|
42
|
+
const stats = { hits: 0, misses: 0, invalidations: 0 };
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Obtiene el valor cacheado para `path`. Si el archivo cambió (mtime) o el TTL
|
|
46
|
+
* expiró, vuelve a leer y parsear con `parser(contenido, path)`.
|
|
47
|
+
*
|
|
48
|
+
* @param {string} ruta - Path absoluto del archivo a cachear.
|
|
49
|
+
* @param {(contenido: string, ruta: string) => any} parser - Función que transforma
|
|
50
|
+
* el contenido del archivo en el objeto a cachear. DEBE ser pura.
|
|
51
|
+
* @returns {{ data: any, hit: boolean } | null} null si el archivo no existe.
|
|
52
|
+
*/
|
|
53
|
+
function get(ruta, parser) {
|
|
54
|
+
let stat;
|
|
55
|
+
try {
|
|
56
|
+
stat = fs.statSync(ruta);
|
|
57
|
+
} catch (err) {
|
|
58
|
+
if (err.code === 'ENOENT') return null;
|
|
59
|
+
throw err;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const mtimeMs = stat.mtimeMs;
|
|
63
|
+
const ahora = Date.now();
|
|
64
|
+
const cached = store.get(ruta);
|
|
65
|
+
|
|
66
|
+
if (cached && cached.mtimeMs === mtimeMs && (ahora - cached.cargadoEn) < ttlMs) {
|
|
67
|
+
stats.hits++;
|
|
68
|
+
return { data: cached.data, hit: true };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (cached) stats.invalidations++;
|
|
72
|
+
else stats.misses++;
|
|
73
|
+
|
|
74
|
+
const contenido = fs.readFileSync(ruta, 'utf-8');
|
|
75
|
+
const data = parser(contenido, ruta);
|
|
76
|
+
store.set(ruta, { mtimeMs, cargadoEn: ahora, data });
|
|
77
|
+
return { data, hit: false };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Invalida una entrada o todo el cache.
|
|
82
|
+
* @param {string} [ruta] - Si se omite, vacía todo.
|
|
83
|
+
*/
|
|
84
|
+
function invalidate(ruta) {
|
|
85
|
+
if (ruta === undefined) {
|
|
86
|
+
const n = store.size;
|
|
87
|
+
store.clear();
|
|
88
|
+
stats.invalidations += n;
|
|
89
|
+
return n;
|
|
90
|
+
}
|
|
91
|
+
if (store.delete(ruta)) stats.invalidations++;
|
|
92
|
+
return 0;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
get,
|
|
97
|
+
invalidate,
|
|
98
|
+
stats: () => ({ ...stats, size: store.size, ttlMs }),
|
|
99
|
+
_store: store, // exposed para tests
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
module.exports = {
|
|
104
|
+
crearCache,
|
|
105
|
+
DEFAULT_TTL_MS,
|
|
106
|
+
};
|