@saulwade/swl-ses 1.4.1 → 1.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.
- package/CLAUDE.md +1 -1
- package/README.md +1 -1
- package/agentes/nemesis-auditor-swl.md +161 -161
- package/bin/swl-mcp-server.js +187 -187
- package/comandos/swl/.evolved.json +22 -22
- package/comandos/swl/contribuir.md +233 -233
- package/comandos/swl/nemesis.md +122 -122
- package/gateway/lib/event-channel.js +191 -191
- package/habilidades/backend-production-resilience/SKILL.md +288 -288
- package/habilidades/benchmark-memoria/SKILL.md +186 -186
- package/habilidades/diagrama-arquitectura/assets/template.html +276 -276
- package/habilidades/doubt-driven-review/SKILL.md +171 -171
- package/habilidades/doubt-driven-review/recursos/EXAMPLES.md +130 -130
- package/habilidades/eval-framework/SKILL.md +212 -212
- package/habilidades/feynman-auditor-swl/SKILL.md +123 -123
- package/habilidades/feynman-auditor-swl/recursos/preguntas-language-agnostic.md +108 -108
- package/habilidades/harness-claude-code/SKILL.md +299 -299
- package/habilidades/infra-github-actions/SKILL.md +166 -166
- package/habilidades/legacy-code-rescue/SKILL.md +267 -267
- package/habilidades/manejo-errores/.evolved.json +8 -8
- package/habilidades/meta-skills-estandar/recursos/convencion-examples.md +93 -93
- package/habilidades/meta-skills-estandar/recursos/skills-as-agents.md +163 -163
- package/habilidades/patrones-python/SKILL.md +229 -229
- package/habilidades/patrones-python/recursos/patrones-avanzados.md +469 -469
- package/habilidades/planear-fase/SKILL.md +319 -319
- package/habilidades/release-semver/.evolved.json +8 -8
- package/habilidades/state-inconsistency-auditor-swl/SKILL.md +166 -166
- package/habilidades/state-inconsistency-auditor-swl/recursos/coupled-state-patterns.md +147 -147
- package/habilidades/testing-python/SKILL.md +340 -340
- package/habilidades/web-fetcher-routing/SKILL.md +75 -75
- package/hooks/claudemd-bloat-detector.js +161 -161
- package/hooks/lib/agent-routing.js +107 -107
- package/hooks/lib/auto-consolidator.js +335 -335
- package/hooks/lib/error-classifier.js +308 -308
- package/hooks/lib/merkle-audit.js +96 -96
- package/hooks/lib/provenance-tracker.js +191 -191
- package/hooks/lib/rate-limit-tracker.js +253 -253
- package/hooks/lib/resource-quota.js +122 -122
- package/hooks/lib/retry-jitter.js +165 -165
- package/hooks/lib/security-net.js +201 -201
- package/hooks/lib/skill-auditor.js +588 -588
- package/hooks/lib/sync-status.js +228 -228
- package/hooks/lib/taint-tracker.js +107 -107
- package/hooks/lib/text-similarity.js +241 -241
- package/hooks/lib/toon-compressor.js +245 -245
- package/hooks/registro-turnos.js +209 -209
- package/hooks/sugerir-regenerar-inventario.js +170 -170
- package/hooks/validar-formato-post-subagente.js +140 -140
- package/hooks/validar-memoria-hook.js +218 -218
- package/instintos/prompt-appendices.yaml +57 -57
- package/manifiestos/agent-output-schemas.json +57 -57
- package/manifiestos/modulos.json +11 -6
- package/manifiestos/perfiles.json +2 -1
- package/manifiestos/skills-lock.json +1114 -1114
- package/package.json +1 -1
- package/plantillas/auditor-veto-template.md +105 -105
- package/plantillas/github-workflows/README.md +47 -47
- package/plantillas/github-workflows/release-please.yml +44 -44
- package/plantillas/github-workflows/swl-ci.yml +107 -107
- package/plantillas/github-workflows/swl-security.yml +51 -51
- package/plugin.json +9 -1
- package/reglas/analisis-previo-tareas-grandes.md +172 -172
- package/reglas/arreglar-al-detectar.md +147 -147
- package/reglas/fragmentos-compartidos.md +152 -152
- package/reglas/harness-claude-code.md +213 -213
- package/reglas/usar-context7.md +226 -226
- package/schemas/diary-entry.schema.json +80 -80
- package/scripts/audit-tools/audit-history.js +330 -330
- package/scripts/audit-tools/bundle-tracker.js +290 -290
- package/scripts/audit-tools/canary-monitor.js +352 -352
- package/scripts/audit-tools/code-profiler.js +605 -605
- package/scripts/audit-tools/dep-doctor.js +320 -320
- package/scripts/audit-tools/env-validator.js +206 -206
- package/scripts/audit-tools/lib/fs-walk.js +48 -48
- package/scripts/audit-tools/lib/output.js +23 -23
- package/scripts/audit-tools/migration-checker.js +392 -392
- package/scripts/audit-tools/pentest-scanner.js +1436 -1436
- package/scripts/benchmark-memoria.js +167 -167
- package/scripts/configurar-branch-protection.js +418 -418
- package/scripts/detectar-aprendizajes-duplicados.js +151 -151
- package/scripts/field-report.js +199 -199
- package/scripts/generar-checklists-consolidados.js +273 -273
- package/scripts/generar-inventario.js +420 -420
- package/scripts/generar-matriz-lenguajes.js +271 -271
- package/scripts/lib/artefactos-python.js +43 -43
- package/scripts/lib/benchmark-metrics.js +160 -160
- package/scripts/lib/budget-enforcer.js +252 -252
- package/scripts/lib/configurar-ci.js +380 -380
- package/scripts/lib/contadores-inventario.js +217 -217
- package/scripts/lib/detectar-stack-detallado.js +307 -307
- package/scripts/lib/diary-entry.js +234 -234
- package/scripts/lib/eval-metrics-store.js +218 -218
- package/scripts/lib/eval-quality.js +171 -171
- package/scripts/lib/eval-schemas.js +144 -144
- package/scripts/lib/eval-self-correct.js +106 -106
- package/scripts/lib/eval-validator.js +185 -185
- package/scripts/lib/jaccard-similarity.js +98 -98
- package/scripts/lib/longmemeval-runner.js +125 -125
- package/scripts/lib/manifiestos.js +42 -1
- package/scripts/lib/npm-version.js +261 -261
- package/scripts/lib/paquetes-conocidos.js +50 -50
- package/scripts/lib/prompt-builder.js +264 -264
- package/scripts/lib/rrf-fusion.js +175 -175
- package/scripts/lib/scoring-instintos.js +277 -277
- package/scripts/lib/semantic-search.js +252 -252
- package/scripts/limpiar-artefactos-python.js +131 -131
- package/scripts/mcp-server/README.md +128 -128
- package/scripts/mcp-server/handlers.js +206 -206
- package/scripts/migrar-csv-a-array.js +168 -168
- package/scripts/migrar-fase-dominio.js +201 -201
- package/scripts/publicar.js +511 -511
- package/scripts/run-eval.js +141 -141
- package/scripts/validar-manifest.js +231 -195
- package/scripts/validar-userland-vacio.js +110 -110
package/hooks/registro-turnos.js
CHANGED
|
@@ -1,209 +1,209 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Hook: registro-turnos.js
|
|
6
|
-
* Tipo: PostToolUse (todas las herramientas)
|
|
7
|
-
*
|
|
8
|
-
* Persiste cada tool call (turno) en .planning/sessions/{session_id}/turns.jsonl
|
|
9
|
-
* para auditoría granular y futuro replay/fork de sesiones.
|
|
10
|
-
*
|
|
11
|
-
* Patrón inspirado en `temp/claudian-main/src/core/bootstrap/SessionStorage.ts`
|
|
12
|
-
* (plugin Obsidian Claudian, Yishen Tu, MIT). Diferencias respecto al
|
|
13
|
-
* original:
|
|
14
|
-
* - Claudian persiste SessionMetadata + transcripts en JSONL del provider.
|
|
15
|
-
* SWL persiste turn-by-turn en formato propio compatible con
|
|
16
|
-
* .planning/sessions/.
|
|
17
|
-
* - Sanitización agresiva de entrada/salida antes de persistir
|
|
18
|
-
* (riesgo PII/secretos en argumentos de tools).
|
|
19
|
-
*
|
|
20
|
-
* Estado: OPT-IN. Inactivo por defecto. Activar con SWL_REGISTRO_TURNOS=1.
|
|
21
|
-
*
|
|
22
|
-
* Razones del default OFF:
|
|
23
|
-
* - Volumen alto: cada tool call genera entrada (decenas por sesión).
|
|
24
|
-
* - Riesgo PII: tool_input de Bash/Write/Edit puede contener tokens,
|
|
25
|
-
* contraseñas pegadas accidentalmente, paths privados.
|
|
26
|
-
* - El usuario debe decidir explícitamente activarlo en su entorno.
|
|
27
|
-
*
|
|
28
|
-
* Schema de cada turno (JSONL append-only):
|
|
29
|
-
* {
|
|
30
|
-
* ts: ISO timestamp,
|
|
31
|
-
* session_id: string,
|
|
32
|
-
* tool: string,
|
|
33
|
-
* input_resumen: string, // preview ≤ 200 chars, secrets sanitizados
|
|
34
|
-
* output_resumen: string, // preview ≤ 500 chars, secrets sanitizados
|
|
35
|
-
* duracion_ms?: number,
|
|
36
|
-
* agente?: string, // si tool=Agent, subagent_type
|
|
37
|
-
* error?: boolean
|
|
38
|
-
* }
|
|
39
|
-
*
|
|
40
|
-
* NUNCA persiste:
|
|
41
|
-
* - tool_input completo (truncado a 200 chars)
|
|
42
|
-
* - tool_response completo (truncado a 500 chars)
|
|
43
|
-
* - Patrones que parezcan tokens/secretos (sanitizados a [REDACTED])
|
|
44
|
-
*
|
|
45
|
-
* Variables de entorno:
|
|
46
|
-
* SWL_REGISTRO_TURNOS=1 - activa el hook (default: OFF)
|
|
47
|
-
* SWL_REGISTRO_TURNOS_LIMITE - máximo turns por sesión (default: 1000)
|
|
48
|
-
*
|
|
49
|
-
* Helper de inspección:
|
|
50
|
-
* /swl:sesiones --turnos=<session_id>
|
|
51
|
-
*
|
|
52
|
-
* @see hooks/lib/atomic-write.js
|
|
53
|
-
* @see hooks/lib/privacy-filter.js (si existe; fallback inline si no)
|
|
54
|
-
*/
|
|
55
|
-
|
|
56
|
-
const fs = require('fs');
|
|
57
|
-
const path = require('path');
|
|
58
|
-
|
|
59
|
-
const LIMITE_TURNOS = parseInt(process.env.SWL_REGISTRO_TURNOS_LIMITE || '1000', 10);
|
|
60
|
-
const MAX_INPUT_CHARS = 200;
|
|
61
|
-
const MAX_OUTPUT_CHARS = 500;
|
|
62
|
-
const DIR_BASE = path.join(process.cwd(), '.planning', 'sessions');
|
|
63
|
-
|
|
64
|
-
// Patrones de sanitización conservadores: tokens largos, llaves API, JWT,
|
|
65
|
-
// password en URLs, paths con .env. Si aparece un match, se reemplaza con
|
|
66
|
-
// [REDACTED].
|
|
67
|
-
const PATRONES_REDACT = [
|
|
68
|
-
/\b[A-Za-z0-9_-]{32,}\b/g, // tokens largos
|
|
69
|
-
/(?:sk|pk|key|token|secret|password|pwd|api[_-]?key)["':\s=]+[A-Za-z0-9_\-./]{8,}/gi,
|
|
70
|
-
/\bey[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}\b/g, // JWT
|
|
71
|
-
/https?:\/\/[^:]+:[^@\s]+@/g, // basic auth en URL
|
|
72
|
-
/-----BEGIN [A-Z ]+-----[\s\S]*?-----END [A-Z ]+-----/g, // PEM blocks
|
|
73
|
-
];
|
|
74
|
-
|
|
75
|
-
function sanitizar(texto) {
|
|
76
|
-
if (!texto) return '';
|
|
77
|
-
let r = String(texto);
|
|
78
|
-
for (const re of PATRONES_REDACT) {
|
|
79
|
-
r = r.replace(re, '[REDACTED]');
|
|
80
|
-
}
|
|
81
|
-
return r;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function truncar(texto, max) {
|
|
85
|
-
const s = sanitizar(texto);
|
|
86
|
-
if (s.length <= max) return s;
|
|
87
|
-
return s.slice(0, max - 3) + '...';
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function ensureDir(dir) {
|
|
91
|
-
try { fs.mkdirSync(dir, { recursive: true }); } catch { /* ignore */ }
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function contarTurnos(rutaArchivo) {
|
|
95
|
-
try {
|
|
96
|
-
if (!fs.existsSync(rutaArchivo)) return 0;
|
|
97
|
-
const contenido = fs.readFileSync(rutaArchivo, 'utf8');
|
|
98
|
-
return contenido.split(/\r?\n/).filter(Boolean).length;
|
|
99
|
-
} catch {
|
|
100
|
-
return 0;
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function extraerInfoTurno(data) {
|
|
105
|
-
const toolName = String(data.tool_name || data.tool?.name || '');
|
|
106
|
-
if (!toolName) return null;
|
|
107
|
-
|
|
108
|
-
const sessionId = String(data.session_id || 'default').slice(0, 64);
|
|
109
|
-
|
|
110
|
-
const toolInput = data.tool_input || data.tool?.input || {};
|
|
111
|
-
const toolResponse = data.tool_response || data.tool_result || {};
|
|
112
|
-
|
|
113
|
-
// Resumen de input: serializar y truncar
|
|
114
|
-
let inputStr = '';
|
|
115
|
-
try {
|
|
116
|
-
inputStr = typeof toolInput === 'string'
|
|
117
|
-
? toolInput
|
|
118
|
-
: JSON.stringify(toolInput);
|
|
119
|
-
} catch { inputStr = '[unserializable]'; }
|
|
120
|
-
|
|
121
|
-
// Resumen de output: extraer campos comunes
|
|
122
|
-
let outputStr = '';
|
|
123
|
-
try {
|
|
124
|
-
if (typeof toolResponse === 'string') {
|
|
125
|
-
outputStr = toolResponse;
|
|
126
|
-
} else if (toolResponse && typeof toolResponse === 'object') {
|
|
127
|
-
const campos = ['output', 'content', 'stdout', 'text', 'result', 'message', 'error'];
|
|
128
|
-
for (const c of campos) {
|
|
129
|
-
if (typeof toolResponse[c] === 'string') {
|
|
130
|
-
outputStr = toolResponse[c];
|
|
131
|
-
break;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
if (!outputStr) outputStr = JSON.stringify(toolResponse);
|
|
135
|
-
}
|
|
136
|
-
} catch { outputStr = '[unserializable]'; }
|
|
137
|
-
|
|
138
|
-
const esError = !!(toolResponse && (toolResponse.is_error || toolResponse.status === 'failed'));
|
|
139
|
-
|
|
140
|
-
const turno = {
|
|
141
|
-
ts: new Date().toISOString(),
|
|
142
|
-
session_id: sessionId,
|
|
143
|
-
tool: toolName,
|
|
144
|
-
input_resumen: truncar(inputStr, MAX_INPUT_CHARS),
|
|
145
|
-
output_resumen: truncar(outputStr, MAX_OUTPUT_CHARS),
|
|
146
|
-
};
|
|
147
|
-
|
|
148
|
-
if (data.duration_ms || toolResponse.duration_ms) {
|
|
149
|
-
turno.duracion_ms = data.duration_ms || toolResponse.duration_ms;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
if (toolName === 'Agent') {
|
|
153
|
-
const subType = toolInput.subagent_type || toolInput.type;
|
|
154
|
-
if (subType) turno.agente = String(subType).slice(0, 60);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
if (esError) turno.error = true;
|
|
158
|
-
|
|
159
|
-
return turno;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// Solo registrar listeners de stdin en modo CLI. Cuando se importa como
|
|
163
|
-
// módulo (tests), registrar listeners mantiene el event loop vivo
|
|
164
|
-
// indefinidamente porque stdin nunca emite 'end' en ese contexto, lo que
|
|
165
|
-
// hace que la suite de tests cuelgue hasta el timeout. El hook real
|
|
166
|
-
// invocado por Claude Code siempre se ejecuta como require.main.
|
|
167
|
-
//
|
|
168
|
-
// El opt-in via SWL_REGISTRO_TURNOS también se evalúa aquí adentro: si
|
|
169
|
-
// estuviera en el top-level del archivo, importar el módulo desde un
|
|
170
|
-
// test sin la env var mataría al test runner via process.exit(0).
|
|
171
|
-
if (require.main === module) {
|
|
172
|
-
if (process.env.SWL_REGISTRO_TURNOS !== '1') {
|
|
173
|
-
// OPT-IN: hook inactivo por default. Salir silencioso sin leer stdin.
|
|
174
|
-
process.exit(0);
|
|
175
|
-
}
|
|
176
|
-
let inputRaw = '';
|
|
177
|
-
process.stdin.on('data', chunk => { inputRaw += chunk; });
|
|
178
|
-
|
|
179
|
-
process.stdin.on('end', () => {
|
|
180
|
-
try {
|
|
181
|
-
const data = JSON.parse(inputRaw);
|
|
182
|
-
const turno = extraerInfoTurno(data);
|
|
183
|
-
if (!turno) return;
|
|
184
|
-
|
|
185
|
-
const dirSesion = path.join(DIR_BASE, turno.session_id);
|
|
186
|
-
ensureDir(dirSesion);
|
|
187
|
-
const rutaArchivo = path.join(dirSesion, 'turns.jsonl');
|
|
188
|
-
|
|
189
|
-
if (contarTurnos(rutaArchivo) >= LIMITE_TURNOS) {
|
|
190
|
-
// Hard cap: no seguir creciendo. El usuario decide rotar/limpiar.
|
|
191
|
-
return;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
fs.appendFileSync(rutaArchivo, JSON.stringify(turno) + '\n', 'utf8');
|
|
195
|
-
} catch (err) {
|
|
196
|
-
// Hook nunca bloquea Claude Code, pero deja una traza diagnóstica
|
|
197
|
-
// para no perder regresiones silenciosas (ver feedback memoria
|
|
198
|
-
// sobre errores silenciados).
|
|
199
|
-
process.stderr.write(`[registro-turnos] error interno: ${String(err.message || err).slice(0, 120)}\n`);
|
|
200
|
-
}
|
|
201
|
-
});
|
|
202
|
-
} else {
|
|
203
|
-
module.exports = {
|
|
204
|
-
extraerInfoTurno,
|
|
205
|
-
sanitizar,
|
|
206
|
-
truncar,
|
|
207
|
-
_internals: { PATRONES_REDACT, MAX_INPUT_CHARS, MAX_OUTPUT_CHARS, LIMITE_TURNOS },
|
|
208
|
-
};
|
|
209
|
-
}
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Hook: registro-turnos.js
|
|
6
|
+
* Tipo: PostToolUse (todas las herramientas)
|
|
7
|
+
*
|
|
8
|
+
* Persiste cada tool call (turno) en .planning/sessions/{session_id}/turns.jsonl
|
|
9
|
+
* para auditoría granular y futuro replay/fork de sesiones.
|
|
10
|
+
*
|
|
11
|
+
* Patrón inspirado en `temp/claudian-main/src/core/bootstrap/SessionStorage.ts`
|
|
12
|
+
* (plugin Obsidian Claudian, Yishen Tu, MIT). Diferencias respecto al
|
|
13
|
+
* original:
|
|
14
|
+
* - Claudian persiste SessionMetadata + transcripts en JSONL del provider.
|
|
15
|
+
* SWL persiste turn-by-turn en formato propio compatible con
|
|
16
|
+
* .planning/sessions/.
|
|
17
|
+
* - Sanitización agresiva de entrada/salida antes de persistir
|
|
18
|
+
* (riesgo PII/secretos en argumentos de tools).
|
|
19
|
+
*
|
|
20
|
+
* Estado: OPT-IN. Inactivo por defecto. Activar con SWL_REGISTRO_TURNOS=1.
|
|
21
|
+
*
|
|
22
|
+
* Razones del default OFF:
|
|
23
|
+
* - Volumen alto: cada tool call genera entrada (decenas por sesión).
|
|
24
|
+
* - Riesgo PII: tool_input de Bash/Write/Edit puede contener tokens,
|
|
25
|
+
* contraseñas pegadas accidentalmente, paths privados.
|
|
26
|
+
* - El usuario debe decidir explícitamente activarlo en su entorno.
|
|
27
|
+
*
|
|
28
|
+
* Schema de cada turno (JSONL append-only):
|
|
29
|
+
* {
|
|
30
|
+
* ts: ISO timestamp,
|
|
31
|
+
* session_id: string,
|
|
32
|
+
* tool: string,
|
|
33
|
+
* input_resumen: string, // preview ≤ 200 chars, secrets sanitizados
|
|
34
|
+
* output_resumen: string, // preview ≤ 500 chars, secrets sanitizados
|
|
35
|
+
* duracion_ms?: number,
|
|
36
|
+
* agente?: string, // si tool=Agent, subagent_type
|
|
37
|
+
* error?: boolean
|
|
38
|
+
* }
|
|
39
|
+
*
|
|
40
|
+
* NUNCA persiste:
|
|
41
|
+
* - tool_input completo (truncado a 200 chars)
|
|
42
|
+
* - tool_response completo (truncado a 500 chars)
|
|
43
|
+
* - Patrones que parezcan tokens/secretos (sanitizados a [REDACTED])
|
|
44
|
+
*
|
|
45
|
+
* Variables de entorno:
|
|
46
|
+
* SWL_REGISTRO_TURNOS=1 - activa el hook (default: OFF)
|
|
47
|
+
* SWL_REGISTRO_TURNOS_LIMITE - máximo turns por sesión (default: 1000)
|
|
48
|
+
*
|
|
49
|
+
* Helper de inspección:
|
|
50
|
+
* /swl:sesiones --turnos=<session_id>
|
|
51
|
+
*
|
|
52
|
+
* @see hooks/lib/atomic-write.js
|
|
53
|
+
* @see hooks/lib/privacy-filter.js (si existe; fallback inline si no)
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
const fs = require('fs');
|
|
57
|
+
const path = require('path');
|
|
58
|
+
|
|
59
|
+
const LIMITE_TURNOS = parseInt(process.env.SWL_REGISTRO_TURNOS_LIMITE || '1000', 10);
|
|
60
|
+
const MAX_INPUT_CHARS = 200;
|
|
61
|
+
const MAX_OUTPUT_CHARS = 500;
|
|
62
|
+
const DIR_BASE = path.join(process.cwd(), '.planning', 'sessions');
|
|
63
|
+
|
|
64
|
+
// Patrones de sanitización conservadores: tokens largos, llaves API, JWT,
|
|
65
|
+
// password en URLs, paths con .env. Si aparece un match, se reemplaza con
|
|
66
|
+
// [REDACTED].
|
|
67
|
+
const PATRONES_REDACT = [
|
|
68
|
+
/\b[A-Za-z0-9_-]{32,}\b/g, // tokens largos
|
|
69
|
+
/(?:sk|pk|key|token|secret|password|pwd|api[_-]?key)["':\s=]+[A-Za-z0-9_\-./]{8,}/gi,
|
|
70
|
+
/\bey[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}\b/g, // JWT
|
|
71
|
+
/https?:\/\/[^:]+:[^@\s]+@/g, // basic auth en URL
|
|
72
|
+
/-----BEGIN [A-Z ]+-----[\s\S]*?-----END [A-Z ]+-----/g, // PEM blocks
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
function sanitizar(texto) {
|
|
76
|
+
if (!texto) return '';
|
|
77
|
+
let r = String(texto);
|
|
78
|
+
for (const re of PATRONES_REDACT) {
|
|
79
|
+
r = r.replace(re, '[REDACTED]');
|
|
80
|
+
}
|
|
81
|
+
return r;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function truncar(texto, max) {
|
|
85
|
+
const s = sanitizar(texto);
|
|
86
|
+
if (s.length <= max) return s;
|
|
87
|
+
return s.slice(0, max - 3) + '...';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function ensureDir(dir) {
|
|
91
|
+
try { fs.mkdirSync(dir, { recursive: true }); } catch { /* ignore */ }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function contarTurnos(rutaArchivo) {
|
|
95
|
+
try {
|
|
96
|
+
if (!fs.existsSync(rutaArchivo)) return 0;
|
|
97
|
+
const contenido = fs.readFileSync(rutaArchivo, 'utf8');
|
|
98
|
+
return contenido.split(/\r?\n/).filter(Boolean).length;
|
|
99
|
+
} catch {
|
|
100
|
+
return 0;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function extraerInfoTurno(data) {
|
|
105
|
+
const toolName = String(data.tool_name || data.tool?.name || '');
|
|
106
|
+
if (!toolName) return null;
|
|
107
|
+
|
|
108
|
+
const sessionId = String(data.session_id || 'default').slice(0, 64);
|
|
109
|
+
|
|
110
|
+
const toolInput = data.tool_input || data.tool?.input || {};
|
|
111
|
+
const toolResponse = data.tool_response || data.tool_result || {};
|
|
112
|
+
|
|
113
|
+
// Resumen de input: serializar y truncar
|
|
114
|
+
let inputStr = '';
|
|
115
|
+
try {
|
|
116
|
+
inputStr = typeof toolInput === 'string'
|
|
117
|
+
? toolInput
|
|
118
|
+
: JSON.stringify(toolInput);
|
|
119
|
+
} catch { inputStr = '[unserializable]'; }
|
|
120
|
+
|
|
121
|
+
// Resumen de output: extraer campos comunes
|
|
122
|
+
let outputStr = '';
|
|
123
|
+
try {
|
|
124
|
+
if (typeof toolResponse === 'string') {
|
|
125
|
+
outputStr = toolResponse;
|
|
126
|
+
} else if (toolResponse && typeof toolResponse === 'object') {
|
|
127
|
+
const campos = ['output', 'content', 'stdout', 'text', 'result', 'message', 'error'];
|
|
128
|
+
for (const c of campos) {
|
|
129
|
+
if (typeof toolResponse[c] === 'string') {
|
|
130
|
+
outputStr = toolResponse[c];
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (!outputStr) outputStr = JSON.stringify(toolResponse);
|
|
135
|
+
}
|
|
136
|
+
} catch { outputStr = '[unserializable]'; }
|
|
137
|
+
|
|
138
|
+
const esError = !!(toolResponse && (toolResponse.is_error || toolResponse.status === 'failed'));
|
|
139
|
+
|
|
140
|
+
const turno = {
|
|
141
|
+
ts: new Date().toISOString(),
|
|
142
|
+
session_id: sessionId,
|
|
143
|
+
tool: toolName,
|
|
144
|
+
input_resumen: truncar(inputStr, MAX_INPUT_CHARS),
|
|
145
|
+
output_resumen: truncar(outputStr, MAX_OUTPUT_CHARS),
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
if (data.duration_ms || toolResponse.duration_ms) {
|
|
149
|
+
turno.duracion_ms = data.duration_ms || toolResponse.duration_ms;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (toolName === 'Agent') {
|
|
153
|
+
const subType = toolInput.subagent_type || toolInput.type;
|
|
154
|
+
if (subType) turno.agente = String(subType).slice(0, 60);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (esError) turno.error = true;
|
|
158
|
+
|
|
159
|
+
return turno;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Solo registrar listeners de stdin en modo CLI. Cuando se importa como
|
|
163
|
+
// módulo (tests), registrar listeners mantiene el event loop vivo
|
|
164
|
+
// indefinidamente porque stdin nunca emite 'end' en ese contexto, lo que
|
|
165
|
+
// hace que la suite de tests cuelgue hasta el timeout. El hook real
|
|
166
|
+
// invocado por Claude Code siempre se ejecuta como require.main.
|
|
167
|
+
//
|
|
168
|
+
// El opt-in via SWL_REGISTRO_TURNOS también se evalúa aquí adentro: si
|
|
169
|
+
// estuviera en el top-level del archivo, importar el módulo desde un
|
|
170
|
+
// test sin la env var mataría al test runner via process.exit(0).
|
|
171
|
+
if (require.main === module) {
|
|
172
|
+
if (process.env.SWL_REGISTRO_TURNOS !== '1') {
|
|
173
|
+
// OPT-IN: hook inactivo por default. Salir silencioso sin leer stdin.
|
|
174
|
+
process.exit(0);
|
|
175
|
+
}
|
|
176
|
+
let inputRaw = '';
|
|
177
|
+
process.stdin.on('data', chunk => { inputRaw += chunk; });
|
|
178
|
+
|
|
179
|
+
process.stdin.on('end', () => {
|
|
180
|
+
try {
|
|
181
|
+
const data = JSON.parse(inputRaw);
|
|
182
|
+
const turno = extraerInfoTurno(data);
|
|
183
|
+
if (!turno) return;
|
|
184
|
+
|
|
185
|
+
const dirSesion = path.join(DIR_BASE, turno.session_id);
|
|
186
|
+
ensureDir(dirSesion);
|
|
187
|
+
const rutaArchivo = path.join(dirSesion, 'turns.jsonl');
|
|
188
|
+
|
|
189
|
+
if (contarTurnos(rutaArchivo) >= LIMITE_TURNOS) {
|
|
190
|
+
// Hard cap: no seguir creciendo. El usuario decide rotar/limpiar.
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
fs.appendFileSync(rutaArchivo, JSON.stringify(turno) + '\n', 'utf8');
|
|
195
|
+
} catch (err) {
|
|
196
|
+
// Hook nunca bloquea Claude Code, pero deja una traza diagnóstica
|
|
197
|
+
// para no perder regresiones silenciosas (ver feedback memoria
|
|
198
|
+
// sobre errores silenciados).
|
|
199
|
+
process.stderr.write(`[registro-turnos] error interno: ${String(err.message || err).slice(0, 120)}\n`);
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
} else {
|
|
203
|
+
module.exports = {
|
|
204
|
+
extraerInfoTurno,
|
|
205
|
+
sanitizar,
|
|
206
|
+
truncar,
|
|
207
|
+
_internals: { PATRONES_REDACT, MAX_INPUT_CHARS, MAX_OUTPUT_CHARS, LIMITE_TURNOS },
|
|
208
|
+
};
|
|
209
|
+
}
|