refacil-sdd-ai 2.4.0 → 2.6.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/README.md +110 -1
- package/bin/cli.js +124 -3
- package/lib/compact/bash.js +50 -0
- package/lib/compact/rules.js +190 -0
- package/lib/compact/telemetry.js +93 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,7 +7,9 @@ Instala skills para **Claude Code** y **Cursor** que guian al desarrollador por
|
|
|
7
7
|
## Requisitos
|
|
8
8
|
|
|
9
9
|
- **Node.js >= 20.19.0** (requerido por OpenSpec)
|
|
10
|
-
- **Claude Code** o **Cursor**
|
|
10
|
+
- **Claude Code >= 2.1.89** (requerido por el hook `compact-bash` para rewrite silencioso de comandos via `updatedInput`) o **Cursor**
|
|
11
|
+
|
|
12
|
+
`refacil-sdd-ai init` verifica la version de Claude Code y muestra advertencia si es inferior. Con version < 2.1.89 el resto de la metodologia funciona, pero el hook `compact-bash` no tendra efecto (Claude Code ignora `updatedInput` en versiones antiguas).
|
|
11
13
|
|
|
12
14
|
## Instalacion y Setup
|
|
13
15
|
|
|
@@ -53,6 +55,11 @@ refacil-sdd-ai init # Instalar skills, hooks y crear configs en el repo
|
|
|
53
55
|
refacil-sdd-ai update # Actualizar skills y hooks a la ultima version
|
|
54
56
|
refacil-sdd-ai check-update # Verifica si hay una version mas reciente en npm (usado por hook)
|
|
55
57
|
refacil-sdd-ai check-review # Verifica que el review se haya completado (usado por hook)
|
|
58
|
+
refacil-sdd-ai compact-bash # Hook de rewrite de comandos Bash bare (usado por hook, no invocar manual)
|
|
59
|
+
refacil-sdd-ai compact stats # Tabla de rewrites + tokens estimados + USD ahorrado
|
|
60
|
+
refacil-sdd-ai compact disable # Desactiva el hook compact-bash sin desinstalarlo
|
|
61
|
+
refacil-sdd-ai compact enable # Reactiva el hook compact-bash
|
|
62
|
+
refacil-sdd-ai compact clear-log # Limpia ~/.refacil-sdd-ai/compact.log
|
|
56
63
|
refacil-sdd-ai clean # Eliminar skills y remover hooks SDD-AI del repo
|
|
57
64
|
refacil-sdd-ai help # Ver ayuda
|
|
58
65
|
```
|
|
@@ -328,6 +335,7 @@ El paquete instala hooks en `.claude/settings.json` durante `init` y `update`:
|
|
|
328
335
|
| Hook | Evento | Que hace |
|
|
329
336
|
|------|--------|----------|
|
|
330
337
|
| `check-update` | `SessionStart` | Verifica si hay nueva version del paquete en npm y la instala automaticamente. Tambien **sincroniza el bloque `compact-guidance`** en `AGENTS.md` (ver [Eficiencia de tokens](#eficiencia-de-tokens-bloque-auto-gestionado-en-agentsmd)). |
|
|
338
|
+
| `compact-bash` | `PreToolUse` (Bash) | Reescribe **silenciosamente** comandos Bash bare (git/tests/docker logs) a su forma compacta usando `updatedInput`. Sin turnos extra y sin que Claude vea el cambio. Requiere Claude Code >= 2.1.89. |
|
|
331
339
|
| `check-review` | `PreToolUse` (Bash) | Intercepta `git push` y verifica que exista `.review-passed` en cada cambio activo de `openspec/changes/`. Si falta, **bloquea el push** y emite instrucciones para ejecutar `/refacil:review`. El hook no invoca skills por si mismo. |
|
|
332
340
|
|
|
333
341
|
#### Flujo del hook de review
|
|
@@ -378,6 +386,84 @@ openspec/changes/fix-session-timeout-redis/
|
|
|
378
386
|
|
|
379
387
|
El archivo `.review-passed` contiene: veredicto, fecha, resumen, cantidad de hallazgos y si hubo blockers.
|
|
380
388
|
|
|
389
|
+
### Hook `compact-bash` — rewrite silencioso de comandos Bash
|
|
390
|
+
|
|
391
|
+
Segunda capa de reduccion de tokens, **sin costo conversacional**. Antes de que Claude Code ejecute un comando Bash, el hook `compact-bash` inspecciona el comando y, si matchea una regla, lo reescribe usando el campo `updatedInput` del hook. Claude **no ve el cambio**, no hay turno adicional, no hay bloqueo.
|
|
392
|
+
|
|
393
|
+
**Reglas activas (19 reglas)**:
|
|
394
|
+
|
|
395
|
+
Fase 1 — git, tests base, docker logs:
|
|
396
|
+
|
|
397
|
+
| Comando bare | Reescrito a | Ahorro tipico |
|
|
398
|
+
|---|---|---|
|
|
399
|
+
| `git log` | `git log --oneline -20` | ~85% |
|
|
400
|
+
| `git status` | `git status -s` | ~70% |
|
|
401
|
+
| `git diff` (sin args) | `git diff --stat` | ~80% |
|
|
402
|
+
| `git show` | `git show --stat` | ~70% |
|
|
403
|
+
| `docker logs <container>` | `docker logs --tail 100 <container>` | ~80%+ |
|
|
404
|
+
| `npm test` / `yarn test` / `pnpm test` | `… 2>&1 \| tail -80` | ~90% |
|
|
405
|
+
| `jest` | `jest --silent --reporters=summary` | ~85% |
|
|
406
|
+
| `pytest` | `pytest -q` | ~60% |
|
|
407
|
+
|
|
408
|
+
Fase 2A — linters, type checkers, build, sistema:
|
|
409
|
+
|
|
410
|
+
| Comando bare | Reescrito a | Ahorro tipico |
|
|
411
|
+
|---|---|---|
|
|
412
|
+
| `eslint` | `eslint . --format compact --quiet` | ~70% |
|
|
413
|
+
| `eslint <path>` | `eslint <path> --format compact` | ~60% |
|
|
414
|
+
| `biome check` | `biome check --reporter=summary` | ~65% |
|
|
415
|
+
| `tsc` / `npx tsc …` | `… 2>&1 \| head -80` | variable |
|
|
416
|
+
| `prettier --check <path>` | `prettier --check <path> --loglevel warn` | ~50% |
|
|
417
|
+
| `npm audit` | `npm audit 2>&1 \| tail -10` | ~80% |
|
|
418
|
+
| `npm ls` | `npm ls --depth=0` | ~90% |
|
|
419
|
+
| `cargo build` / `cargo test` / `cargo check` | `… --quiet` | ~50% |
|
|
420
|
+
| `go test …` (sin flags) | `… 2>&1 \| tail -80` | ~70% |
|
|
421
|
+
| `mvn test` | `mvn test -q` | ~60% |
|
|
422
|
+
| `./gradlew test` / `gradle test` | `… -q` | ~60% |
|
|
423
|
+
| `ps aux` | `ps -eo pid,pcpu,pmem,comm \| head -30` | ~80% |
|
|
424
|
+
|
|
425
|
+
**Detector de intencion**: si el comando ya tiene flags explicitos (`git log -p`, `git log --all`, `jest --watch`, `docker logs --tail 50 …`), el hook **no interviene**. Tu intencion manda.
|
|
426
|
+
|
|
427
|
+
**Escape mecanismo**: prefija `COMPACT=0` al comando para desactivar el rewrite puntualmente: `COMPACT=0 git log`.
|
|
428
|
+
|
|
429
|
+
**Pipes y redirecciones** en test bare: si ya hay `|` o `>`, el detector no las envuelve de nuevo.
|
|
430
|
+
|
|
431
|
+
**Subcomandos de control**:
|
|
432
|
+
|
|
433
|
+
```bash
|
|
434
|
+
refacil-sdd-ai compact stats # tabla de rewrites + tokens estimados + USD
|
|
435
|
+
refacil-sdd-ai compact disable # desactiva el hook sin desinstalar
|
|
436
|
+
refacil-sdd-ai compact enable # reactiva el hook
|
|
437
|
+
refacil-sdd-ai compact clear-log # limpia ~/.refacil-sdd-ai/compact.log
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
**Telemetria**: cada rewrite genera una linea JSON en `~/.refacil-sdd-ai/compact.log` con timestamp, ruleId y estimacion de tokens ahorrados. El log se usa para el comando `compact stats` que agrega por regla y calcula costo estimado (a razon de $3/MTok input de Sonnet, conservador). La telemetria es local; nada se envia a la nube.
|
|
441
|
+
|
|
442
|
+
**Flujo del hook**:
|
|
443
|
+
|
|
444
|
+
```
|
|
445
|
+
Claude emite "git log"
|
|
446
|
+
│
|
|
447
|
+
▼
|
|
448
|
+
Hook compact-bash recibe tool_input via stdin (JSON)
|
|
449
|
+
│
|
|
450
|
+
▼
|
|
451
|
+
¿Matchea alguna regla y no tiene intent flags?
|
|
452
|
+
│
|
|
453
|
+
├─ SI ──► stdout JSON:
|
|
454
|
+
│ {
|
|
455
|
+
│ "hookSpecificOutput": {
|
|
456
|
+
│ "hookEventName": "PreToolUse",
|
|
457
|
+
│ "permissionDecision": "allow",
|
|
458
|
+
│ "updatedInput": { "command": "git log --oneline -20", ... },
|
|
459
|
+
│ "permissionDecisionReason": "compact-bash: git log → --oneline -20"
|
|
460
|
+
│ }
|
|
461
|
+
│ }
|
|
462
|
+
│ Claude Code ejecuta el comando reescrito. Claude no lo ve.
|
|
463
|
+
│
|
|
464
|
+
└─ NO ──► stdout vacio → Claude Code ejecuta el comando original.
|
|
465
|
+
```
|
|
466
|
+
|
|
381
467
|
### Eficiencia de tokens (bloque auto-gestionado en AGENTS.md)
|
|
382
468
|
|
|
383
469
|
La metodologia SDD-AI genera consumo elevado de contexto (artefactos, specs, prompts de skills). Para compensarlo, `refacil-sdd-ai` mantiene un bloque de guia de eficiencia de tokens dentro de `AGENTS.md` que instruye a la IA sobre como pedir salidas compactas (Read con offset/limit, `git log --oneline`, tests solo con failures, etc.).
|
|
@@ -475,6 +561,29 @@ npm uninstall -g refacil-sdd-ai
|
|
|
475
561
|
- [Claude Code](https://claude.ai/code) — CLI de Anthropic
|
|
476
562
|
- [Cursor](https://cursor.sh) — IDE con IA
|
|
477
563
|
|
|
564
|
+
## Cambios recientes
|
|
565
|
+
|
|
566
|
+
**v2.6.0** — Fase 2 del hook `compact-bash`:
|
|
567
|
+
- 11 reglas nuevas: `eslint`, `biome check`, `tsc`, `prettier --check`, `npm audit`, `npm ls`, `cargo build/test/check`, `go test`, `mvn test`, `gradle test`, `ps aux` (total: 19 reglas)
|
|
568
|
+
- Telemetria local en `~/.refacil-sdd-ai/compact.log`
|
|
569
|
+
- Subcomandos `compact stats | disable | enable | clear-log`
|
|
570
|
+
- `ps-aux` solo se activa en Unix (Linux/Mac); en Windows la regla no interviene
|
|
571
|
+
|
|
572
|
+
**v2.5.0** — Fase 1 del hook `compact-bash`:
|
|
573
|
+
- Hook `PreToolUse` que reescribe silenciosamente comandos Bash bare via `updatedInput` (requiere Claude Code >= 2.1.89)
|
|
574
|
+
- Reglas iniciales: git log/status/diff/show, docker logs, npm/yarn/pnpm test, jest, pytest
|
|
575
|
+
- Escape mecanismo `COMPACT=0 <cmd>`
|
|
576
|
+
|
|
577
|
+
**v2.4.0** — Reduccion de tokens en la metodologia:
|
|
578
|
+
- Boilerplate "Antes de empezar" unificado en 9 skills
|
|
579
|
+
- Validacion de OpenSpec simplificada en `prereqs/SKILL.md`
|
|
580
|
+
- Templates consolidados (`claude-md.md` + `cursorrules.md` → `methodology-guide.md` unico)
|
|
581
|
+
- Compactacion de `review/SKILL.md`, `bug/SKILL.md`, `test/SKILL.md`
|
|
582
|
+
|
|
583
|
+
**v2.3.0** — Bloque `compact-guidance` auto-gestionado en `AGENTS.md`:
|
|
584
|
+
- Hook `SessionStart` sincroniza el bloque en cada sesion
|
|
585
|
+
- Fuente de verdad: `templates/compact-guidance.md`
|
|
586
|
+
|
|
478
587
|
## Licencia
|
|
479
588
|
|
|
480
589
|
MIT
|
package/bin/cli.js
CHANGED
|
@@ -6,6 +6,8 @@ const {
|
|
|
6
6
|
syncCompactGuidance,
|
|
7
7
|
removeCompactGuidance,
|
|
8
8
|
} = require('../lib/compact-guidance');
|
|
9
|
+
const compactBash = require('../lib/compact/bash');
|
|
10
|
+
const compactTelemetry = require('../lib/compact/telemetry');
|
|
9
11
|
|
|
10
12
|
const SKILLS = [
|
|
11
13
|
'setup',
|
|
@@ -128,6 +130,29 @@ function removeSkills() {
|
|
|
128
130
|
return removed;
|
|
129
131
|
}
|
|
130
132
|
|
|
133
|
+
function checkClaudeCodeVersion() {
|
|
134
|
+
const { execSync } = require('child_process');
|
|
135
|
+
try {
|
|
136
|
+
const output = execSync('claude --version 2>&1', {
|
|
137
|
+
encoding: 'utf8',
|
|
138
|
+
timeout: 5000,
|
|
139
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
140
|
+
}).trim();
|
|
141
|
+
const match = output.match(/(\d+)\.(\d+)\.(\d+)/);
|
|
142
|
+
if (!match) return { ok: null, version: null };
|
|
143
|
+
const maj = Number(match[1]);
|
|
144
|
+
const min = Number(match[2]);
|
|
145
|
+
const patch = Number(match[3]);
|
|
146
|
+
const ok =
|
|
147
|
+
maj > 2 ||
|
|
148
|
+
(maj === 2 && min > 1) ||
|
|
149
|
+
(maj === 2 && min === 1 && patch >= 89);
|
|
150
|
+
return { ok, version: `${maj}.${min}.${patch}` };
|
|
151
|
+
} catch (_) {
|
|
152
|
+
return { ok: null, version: null };
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
131
156
|
function checkNodeVersion() {
|
|
132
157
|
const version = process.version; // e.g. v20.19.5
|
|
133
158
|
const major = parseInt(version.split('.')[0].replace('v', ''));
|
|
@@ -174,9 +199,28 @@ function installHook() {
|
|
|
174
199
|
changed = true;
|
|
175
200
|
}
|
|
176
201
|
|
|
177
|
-
// PreToolUse
|
|
202
|
+
// PreToolUse
|
|
178
203
|
if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
|
|
179
204
|
|
|
205
|
+
// compact-bash (must run BEFORE check-review so rewrite is visible to subsequent hooks)
|
|
206
|
+
const hasCompactHook = settings.hooks.PreToolUse.some(
|
|
207
|
+
(h) => h._sdd_compact === true,
|
|
208
|
+
);
|
|
209
|
+
if (!hasCompactHook) {
|
|
210
|
+
settings.hooks.PreToolUse.unshift({
|
|
211
|
+
_sdd_compact: true,
|
|
212
|
+
matcher: 'Bash',
|
|
213
|
+
hooks: [
|
|
214
|
+
{
|
|
215
|
+
type: 'command',
|
|
216
|
+
command: 'refacil-sdd-ai compact-bash',
|
|
217
|
+
},
|
|
218
|
+
],
|
|
219
|
+
});
|
|
220
|
+
changed = true;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// check-review
|
|
180
224
|
const hasReviewHook = settings.hooks.PreToolUse.some(
|
|
181
225
|
(h) => h._sdd_review === true,
|
|
182
226
|
);
|
|
@@ -232,7 +276,7 @@ function uninstallHook() {
|
|
|
232
276
|
if (Array.isArray(settings.hooks.PreToolUse)) {
|
|
233
277
|
const original = settings.hooks.PreToolUse.length;
|
|
234
278
|
settings.hooks.PreToolUse = settings.hooks.PreToolUse.filter(
|
|
235
|
-
(h) => h._sdd_review !== true,
|
|
279
|
+
(h) => h._sdd_review !== true && h._sdd_compact !== true,
|
|
236
280
|
);
|
|
237
281
|
if (settings.hooks.PreToolUse.length !== original) changed = true;
|
|
238
282
|
if (settings.hooks.PreToolUse.length === 0) delete settings.hooks.PreToolUse;
|
|
@@ -362,6 +406,18 @@ function init() {
|
|
|
362
406
|
console.log(` Node.js ${process.version} OK`);
|
|
363
407
|
}
|
|
364
408
|
|
|
409
|
+
// Check Claude Code version (for compact-bash hook)
|
|
410
|
+
const claudeCheck = checkClaudeCodeVersion();
|
|
411
|
+
if (claudeCheck.ok === true) {
|
|
412
|
+
console.log(` Claude Code ${claudeCheck.version} OK`);
|
|
413
|
+
} else if (claudeCheck.ok === false) {
|
|
414
|
+
console.log(`\n ADVERTENCIA: Claude Code ${claudeCheck.version} detectado.`);
|
|
415
|
+
console.log(' El hook compact-bash requiere Claude Code >= 2.1.89 para rewrite silencioso.');
|
|
416
|
+
console.log(' Con version inferior se instala igual pero el rewrite no tendra efecto.');
|
|
417
|
+
console.log(' Actualiza con: npm install -g @anthropic-ai/claude-code\n');
|
|
418
|
+
}
|
|
419
|
+
// ok === null: claude no esta en PATH, silencioso
|
|
420
|
+
|
|
365
421
|
// Install skills
|
|
366
422
|
const count = installSkills();
|
|
367
423
|
console.log(` ${count} skills instaladas en .claude/skills/ y .cursor/skills/`);
|
|
@@ -453,6 +509,59 @@ function clean() {
|
|
|
453
509
|
console.log(' Para eliminar OpenSpec: rm -rf openspec/ .claude/commands/opsx .cursor/commands/opsx\n');
|
|
454
510
|
}
|
|
455
511
|
|
|
512
|
+
// --- Compact subcommands (stats / enable / disable / clear-log) ---
|
|
513
|
+
|
|
514
|
+
function handleCompactSubcommand(sub) {
|
|
515
|
+
switch (sub) {
|
|
516
|
+
case 'stats':
|
|
517
|
+
showCompactStats();
|
|
518
|
+
break;
|
|
519
|
+
case 'disable':
|
|
520
|
+
compactTelemetry.disable();
|
|
521
|
+
console.log(' compact-bash deshabilitado. Reactiva con: refacil-sdd-ai compact enable');
|
|
522
|
+
break;
|
|
523
|
+
case 'enable':
|
|
524
|
+
compactTelemetry.enable();
|
|
525
|
+
console.log(' compact-bash habilitado.');
|
|
526
|
+
break;
|
|
527
|
+
case 'clear-log':
|
|
528
|
+
compactTelemetry.clearLog();
|
|
529
|
+
console.log(' compact.log limpiado.');
|
|
530
|
+
break;
|
|
531
|
+
default:
|
|
532
|
+
console.log('Uso: refacil-sdd-ai compact <stats|disable|enable|clear-log>');
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function showCompactStats() {
|
|
537
|
+
const s = compactTelemetry.stats();
|
|
538
|
+
if (s.totalRewrites === 0) {
|
|
539
|
+
console.log('\n No hay rewrites registrados todavia. El hook compact-bash aun no se ha disparado o esta deshabilitado.\n');
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
const sorted = Object.entries(s.byRule).sort((a, b) => b[1].saved - a[1].saved);
|
|
544
|
+
|
|
545
|
+
console.log(`\n compact-bash stats (${s.totalRewrites} rewrites en total)\n`);
|
|
546
|
+
for (const [id, data] of sorted) {
|
|
547
|
+
const kTokens = (data.saved / 1000).toFixed(1);
|
|
548
|
+
console.log(
|
|
549
|
+
` ${id.padEnd(18)} ${String(data.count).padStart(6)} rewrites ~${kTokens.padStart(7)}k tokens estimados`,
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
const totalK = (s.totalSaved / 1000).toFixed(1);
|
|
553
|
+
const costUsd = ((s.totalSaved / 1_000_000) * 3).toFixed(2);
|
|
554
|
+
console.log(` ${'-'.repeat(62)}`);
|
|
555
|
+
console.log(
|
|
556
|
+
` ${'Total'.padEnd(18)} ${String(s.totalRewrites).padStart(6)} rewrites ~${totalK.padStart(7)}k tokens (~$${costUsd} USD, Sonnet input)`,
|
|
557
|
+
);
|
|
558
|
+
console.log(`\n Log: ${compactTelemetry.LOG_PATH}`);
|
|
559
|
+
if (compactTelemetry.isDisabled()) {
|
|
560
|
+
console.log(' Estado: DESHABILITADO (no se registran nuevos rewrites)');
|
|
561
|
+
}
|
|
562
|
+
console.log('');
|
|
563
|
+
}
|
|
564
|
+
|
|
456
565
|
function help() {
|
|
457
566
|
console.log(`
|
|
458
567
|
refacil-sdd-ai — Metodologia SDD-AI con OpenSpec
|
|
@@ -462,6 +571,12 @@ function help() {
|
|
|
462
571
|
update Re-copia skills (para actualizar a nueva version del paquete)
|
|
463
572
|
check-update Verifica si hay una version mas reciente en npm y sincroniza compact-guidance en AGENTS.md
|
|
464
573
|
check-review Verifica que el review se haya completado (usado por hook PreToolUse)
|
|
574
|
+
compact-bash Reescribe comandos Bash bare para reducir tokens (usado por hook PreToolUse)
|
|
575
|
+
compact Subcomandos del hook compact-bash:
|
|
576
|
+
compact stats - Estadisticas de rewrites y ahorro estimado
|
|
577
|
+
compact disable - Desactiva el rewrite temporalmente
|
|
578
|
+
compact enable - Re-activa el rewrite
|
|
579
|
+
compact clear-log - Borra el log historico
|
|
465
580
|
clean Elimina skills y remueve hooks SDD-AI de .claude/settings.json
|
|
466
581
|
help Muestra esta ayuda
|
|
467
582
|
|
|
@@ -473,7 +588,7 @@ function help() {
|
|
|
473
588
|
|
|
474
589
|
Requisitos:
|
|
475
590
|
- Node.js >= 20.19.0 (requerido por OpenSpec)
|
|
476
|
-
- Claude Code o Cursor
|
|
591
|
+
- Claude Code >= 2.1.89 (requerido por compact-bash para rewrite silencioso) o Cursor
|
|
477
592
|
`);
|
|
478
593
|
}
|
|
479
594
|
|
|
@@ -494,6 +609,12 @@ switch (command) {
|
|
|
494
609
|
case 'check-review':
|
|
495
610
|
checkReview();
|
|
496
611
|
break;
|
|
612
|
+
case 'compact-bash':
|
|
613
|
+
compactBash.run();
|
|
614
|
+
break;
|
|
615
|
+
case 'compact':
|
|
616
|
+
handleCompactSubcommand(process.argv[3]);
|
|
617
|
+
break;
|
|
497
618
|
case 'clean':
|
|
498
619
|
clean();
|
|
499
620
|
break;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const { findRule } = require('./rules');
|
|
3
|
+
const telemetry = require('./telemetry');
|
|
4
|
+
|
|
5
|
+
function run() {
|
|
6
|
+
let input;
|
|
7
|
+
try {
|
|
8
|
+
const stdin = fs.readFileSync(0, 'utf8');
|
|
9
|
+
if (!stdin.trim()) return;
|
|
10
|
+
input = JSON.parse(stdin);
|
|
11
|
+
} catch (_) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (input.tool_name !== 'Bash') return;
|
|
16
|
+
if (telemetry.isDisabled()) return;
|
|
17
|
+
|
|
18
|
+
const origCommand = input.tool_input && input.tool_input.command;
|
|
19
|
+
if (typeof origCommand !== 'string') return;
|
|
20
|
+
|
|
21
|
+
const rule = findRule(origCommand);
|
|
22
|
+
if (!rule) return;
|
|
23
|
+
|
|
24
|
+
let newCommand;
|
|
25
|
+
try {
|
|
26
|
+
newCommand = rule.rewrite(origCommand);
|
|
27
|
+
} catch (_) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (!newCommand || newCommand === origCommand) return;
|
|
32
|
+
|
|
33
|
+
telemetry.logRewrite(rule.id, rule.savedTokensEst);
|
|
34
|
+
|
|
35
|
+
const output = {
|
|
36
|
+
hookSpecificOutput: {
|
|
37
|
+
hookEventName: 'PreToolUse',
|
|
38
|
+
permissionDecision: 'allow',
|
|
39
|
+
permissionDecisionReason: `compact-bash: ${rule.reason}`,
|
|
40
|
+
updatedInput: {
|
|
41
|
+
...input.tool_input,
|
|
42
|
+
command: newCommand,
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
process.stdout.write(JSON.stringify(output));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
module.exports = { run };
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
function hasFlagAfterBase(cmd, baseTokens) {
|
|
2
|
+
const tokens = cmd.trim().split(/\s+/);
|
|
3
|
+
return tokens.slice(baseTokens).some((t) => t.startsWith('-'));
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
function hasPipeOrRedirect(cmd) {
|
|
7
|
+
return /[|><]/.test(cmd);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const RULES = [
|
|
11
|
+
// --- Fase 1: git / tests / docker logs ---
|
|
12
|
+
{
|
|
13
|
+
id: 'git-log',
|
|
14
|
+
match: (cmd) =>
|
|
15
|
+
/^\s*git\s+log(\s|$)/.test(cmd) && !hasFlagAfterBase(cmd, 2),
|
|
16
|
+
rewrite: (cmd) => cmd.replace(/^(\s*git\s+log)/, '$1 --oneline -20'),
|
|
17
|
+
reason: 'git log → --oneline -20',
|
|
18
|
+
savedTokensEst: 850,
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
id: 'git-status',
|
|
22
|
+
match: (cmd) =>
|
|
23
|
+
/^\s*git\s+status(\s|$)/.test(cmd) && !hasFlagAfterBase(cmd, 2),
|
|
24
|
+
rewrite: (cmd) => cmd.replace(/^(\s*git\s+status)/, '$1 -s'),
|
|
25
|
+
reason: 'git status → -s',
|
|
26
|
+
savedTokensEst: 120,
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
id: 'git-diff',
|
|
30
|
+
match: (cmd) => /^\s*git\s+diff\s*$/.test(cmd),
|
|
31
|
+
rewrite: (cmd) => cmd.replace(/^(\s*git\s+diff)\s*$/, '$1 --stat'),
|
|
32
|
+
reason: 'git diff → --stat',
|
|
33
|
+
savedTokensEst: 400,
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: 'git-show',
|
|
37
|
+
match: (cmd) =>
|
|
38
|
+
/^\s*git\s+show(\s|$)/.test(cmd) && !hasFlagAfterBase(cmd, 2),
|
|
39
|
+
rewrite: (cmd) => cmd.replace(/^(\s*git\s+show)/, '$1 --stat'),
|
|
40
|
+
reason: 'git show → --stat',
|
|
41
|
+
savedTokensEst: 200,
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
id: 'docker-logs',
|
|
45
|
+
match: (cmd) => {
|
|
46
|
+
if (!/^\s*docker\s+logs(\s|$)/.test(cmd)) return false;
|
|
47
|
+
if (/\s--tail\b/.test(cmd)) return false;
|
|
48
|
+
if (/\s-n\s+\d/.test(cmd)) return false;
|
|
49
|
+
if (/\s--since\b/.test(cmd)) return false;
|
|
50
|
+
return true;
|
|
51
|
+
},
|
|
52
|
+
rewrite: (cmd) => cmd.replace(/^(\s*docker\s+logs)/, '$1 --tail 100'),
|
|
53
|
+
reason: 'docker logs → --tail 100',
|
|
54
|
+
savedTokensEst: 1500,
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
id: 'pkg-test',
|
|
58
|
+
match: (cmd) => /^\s*(npm|yarn|pnpm)\s+(test|t)\s*$/.test(cmd),
|
|
59
|
+
rewrite: (cmd) => `${cmd.trim()} 2>&1 | tail -80`,
|
|
60
|
+
reason: 'test bare → tail -80',
|
|
61
|
+
savedTokensEst: 2400,
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: 'jest-bare',
|
|
65
|
+
match: (cmd) => /^\s*(npx\s+)?jest\s*$/.test(cmd),
|
|
66
|
+
rewrite: (cmd) =>
|
|
67
|
+
cmd.replace(/^(\s*(?:npx\s+)?jest)\s*$/, '$1 --silent --reporters=summary'),
|
|
68
|
+
reason: 'jest → silent summary',
|
|
69
|
+
savedTokensEst: 1800,
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
id: 'pytest-bare',
|
|
73
|
+
match: (cmd) => /^\s*pytest\s*$/.test(cmd),
|
|
74
|
+
rewrite: (cmd) => cmd.replace(/^(\s*pytest)\s*$/, '$1 -q'),
|
|
75
|
+
reason: 'pytest → -q',
|
|
76
|
+
savedTokensEst: 600,
|
|
77
|
+
},
|
|
78
|
+
// --- Fase 2A: linters / type checkers / build ---
|
|
79
|
+
{
|
|
80
|
+
id: 'eslint',
|
|
81
|
+
match: (cmd) => /^\s*eslint(\s+[^-]\S*)*\s*$/.test(cmd),
|
|
82
|
+
rewrite: (cmd) => {
|
|
83
|
+
const tokens = cmd.trim().split(/\s+/);
|
|
84
|
+
if (tokens.length === 1) {
|
|
85
|
+
return 'eslint . --format compact --quiet';
|
|
86
|
+
}
|
|
87
|
+
return `${cmd.trim()} --format compact`;
|
|
88
|
+
},
|
|
89
|
+
reason: 'eslint → --format compact',
|
|
90
|
+
savedTokensEst: 700,
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
id: 'biome-check',
|
|
94
|
+
match: (cmd) =>
|
|
95
|
+
/^\s*biome\s+check(\s|$)/.test(cmd) && !/--reporter\b/.test(cmd),
|
|
96
|
+
rewrite: (cmd) =>
|
|
97
|
+
cmd.replace(/^(\s*biome\s+check)/, '$1 --reporter=summary'),
|
|
98
|
+
reason: 'biome check → --reporter=summary',
|
|
99
|
+
savedTokensEst: 500,
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
id: 'tsc',
|
|
103
|
+
match: (cmd) => {
|
|
104
|
+
if (!/^\s*(npx\s+)?tsc(\s|$)/.test(cmd)) return false;
|
|
105
|
+
if (/(^|\s)--watch\b|(^|\s)-w\b/.test(cmd)) return false;
|
|
106
|
+
if (hasPipeOrRedirect(cmd)) return false;
|
|
107
|
+
return true;
|
|
108
|
+
},
|
|
109
|
+
rewrite: (cmd) => `${cmd.trim()} 2>&1 | head -80`,
|
|
110
|
+
reason: 'tsc → head -80',
|
|
111
|
+
savedTokensEst: 1200,
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
id: 'prettier-check',
|
|
115
|
+
match: (cmd) =>
|
|
116
|
+
/^\s*prettier\s+--check\b/.test(cmd) && !/--loglevel\b/.test(cmd),
|
|
117
|
+
rewrite: (cmd) =>
|
|
118
|
+
cmd.replace(/^(\s*prettier\s+--check)/, '$1 --loglevel warn'),
|
|
119
|
+
reason: 'prettier --check → --loglevel warn',
|
|
120
|
+
savedTokensEst: 300,
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
id: 'npm-audit',
|
|
124
|
+
match: (cmd) => /^\s*npm\s+audit\s*$/.test(cmd),
|
|
125
|
+
rewrite: (cmd) => `${cmd.trim()} 2>&1 | tail -10`,
|
|
126
|
+
reason: 'npm audit → tail -10',
|
|
127
|
+
savedTokensEst: 900,
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
id: 'npm-ls',
|
|
131
|
+
match: (cmd) => /^\s*npm\s+ls\s*$/.test(cmd),
|
|
132
|
+
rewrite: (cmd) => cmd.replace(/^(\s*npm\s+ls)/, '$1 --depth=0'),
|
|
133
|
+
reason: 'npm ls → --depth=0',
|
|
134
|
+
savedTokensEst: 700,
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
id: 'cargo-bare',
|
|
138
|
+
match: (cmd) => /^\s*cargo\s+(build|test|check)\s*$/.test(cmd),
|
|
139
|
+
rewrite: (cmd) => `${cmd.trim()} --quiet`,
|
|
140
|
+
reason: 'cargo → --quiet',
|
|
141
|
+
savedTokensEst: 400,
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
id: 'go-test',
|
|
145
|
+
match: (cmd) => {
|
|
146
|
+
if (!/^\s*go\s+test\b/.test(cmd)) return false;
|
|
147
|
+
const rest = cmd.trim().substring('go test'.length);
|
|
148
|
+
if (/\s-\S/.test(rest)) return false;
|
|
149
|
+
if (hasPipeOrRedirect(cmd)) return false;
|
|
150
|
+
return true;
|
|
151
|
+
},
|
|
152
|
+
rewrite: (cmd) => `${cmd.trim()} 2>&1 | tail -80`,
|
|
153
|
+
reason: 'go test → tail -80',
|
|
154
|
+
savedTokensEst: 1500,
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
id: 'mvn-test',
|
|
158
|
+
match: (cmd) => /^\s*mvn\s+test\s*$/.test(cmd),
|
|
159
|
+
rewrite: (cmd) => `${cmd.trim()} -q`,
|
|
160
|
+
reason: 'mvn test → -q',
|
|
161
|
+
savedTokensEst: 1800,
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
id: 'gradle-test',
|
|
165
|
+
match: (cmd) => /^\s*(\.\/gradlew|gradle)\s+test\s*$/.test(cmd),
|
|
166
|
+
rewrite: (cmd) => `${cmd.trim()} -q`,
|
|
167
|
+
reason: 'gradle test → -q',
|
|
168
|
+
savedTokensEst: 1500,
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
id: 'ps-aux',
|
|
172
|
+
// Unix-only: en Windows `ps` mapea a PowerShell Get-Process y no entiende estos flags
|
|
173
|
+
match: (cmd) =>
|
|
174
|
+
process.platform !== 'win32' && /^\s*ps\s+aux\s*$/.test(cmd),
|
|
175
|
+
rewrite: () => 'ps -eo pid,pcpu,pmem,comm | head -30',
|
|
176
|
+
reason: 'ps aux → compact columns + head -30',
|
|
177
|
+
savedTokensEst: 800,
|
|
178
|
+
},
|
|
179
|
+
];
|
|
180
|
+
|
|
181
|
+
function findRule(cmd) {
|
|
182
|
+
if (typeof cmd !== 'string' || !cmd.trim()) return null;
|
|
183
|
+
if (/\bCOMPACT=0\b/.test(cmd)) return null;
|
|
184
|
+
for (const rule of RULES) {
|
|
185
|
+
if (rule.match(cmd)) return rule;
|
|
186
|
+
}
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
module.exports = { RULES, findRule };
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
|
|
5
|
+
const HOME_DIR = path.join(os.homedir(), '.refacil-sdd-ai');
|
|
6
|
+
const LOG_PATH = path.join(HOME_DIR, 'compact.log');
|
|
7
|
+
const DISABLED_PATH = path.join(HOME_DIR, 'disabled');
|
|
8
|
+
|
|
9
|
+
function ensureDir() {
|
|
10
|
+
try {
|
|
11
|
+
fs.mkdirSync(HOME_DIR, { recursive: true });
|
|
12
|
+
} catch (_) {}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function isDisabled() {
|
|
16
|
+
return fs.existsSync(DISABLED_PATH);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function logRewrite(ruleId, savedTokensEst) {
|
|
20
|
+
try {
|
|
21
|
+
ensureDir();
|
|
22
|
+
const line =
|
|
23
|
+
JSON.stringify({
|
|
24
|
+
ts: new Date().toISOString(),
|
|
25
|
+
ruleId,
|
|
26
|
+
savedTokensEst: savedTokensEst || 0,
|
|
27
|
+
}) + '\n';
|
|
28
|
+
fs.appendFileSync(LOG_PATH, line);
|
|
29
|
+
} catch (_) {
|
|
30
|
+
// Telemetry must never break the hook
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function readLog() {
|
|
35
|
+
try {
|
|
36
|
+
const content = fs.readFileSync(LOG_PATH, 'utf8');
|
|
37
|
+
return content
|
|
38
|
+
.split('\n')
|
|
39
|
+
.filter(Boolean)
|
|
40
|
+
.map((line) => {
|
|
41
|
+
try {
|
|
42
|
+
return JSON.parse(line);
|
|
43
|
+
} catch (_) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
.filter(Boolean);
|
|
48
|
+
} catch (_) {
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function stats() {
|
|
54
|
+
const entries = readLog();
|
|
55
|
+
const byRule = {};
|
|
56
|
+
let totalSaved = 0;
|
|
57
|
+
for (const e of entries) {
|
|
58
|
+
if (!byRule[e.ruleId]) byRule[e.ruleId] = { count: 0, saved: 0 };
|
|
59
|
+
byRule[e.ruleId].count++;
|
|
60
|
+
byRule[e.ruleId].saved += e.savedTokensEst || 0;
|
|
61
|
+
totalSaved += e.savedTokensEst || 0;
|
|
62
|
+
}
|
|
63
|
+
return { byRule, totalSaved, totalRewrites: entries.length };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function disable() {
|
|
67
|
+
ensureDir();
|
|
68
|
+
fs.writeFileSync(DISABLED_PATH, new Date().toISOString());
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function enable() {
|
|
72
|
+
try {
|
|
73
|
+
fs.unlinkSync(DISABLED_PATH);
|
|
74
|
+
} catch (_) {}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function clearLog() {
|
|
78
|
+
try {
|
|
79
|
+
fs.unlinkSync(LOG_PATH);
|
|
80
|
+
} catch (_) {}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
module.exports = {
|
|
84
|
+
HOME_DIR,
|
|
85
|
+
LOG_PATH,
|
|
86
|
+
DISABLED_PATH,
|
|
87
|
+
isDisabled,
|
|
88
|
+
logRewrite,
|
|
89
|
+
stats,
|
|
90
|
+
disable,
|
|
91
|
+
enable,
|
|
92
|
+
clearLog,
|
|
93
|
+
};
|
package/package.json
CHANGED