chocolatito-code 1.6.8 → 1.6.10

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 CHANGED
@@ -1,4 +1,4 @@
1
- # 🦊 Chocolatito Code (v1.0.0)
1
+ # 🦊 Chocolatito Code
2
2
 
3
3
  > **Agente Autónomo de Programación para la Terminal**, desarrollado por **Chocolatito** con la arquitectura del **Motor Espectro**.
4
4
 
@@ -200,6 +200,77 @@ Configuración compatible con Claude Code en `.mcp.json` (ver `.mcp.json.ejemplo
200
200
 
201
201
  Las herramientas aparecen como `mcp__<servidor>__<herramienta>`. Usa `/mcp` para ver el estado.
202
202
 
203
+ ## 🎒 Kits: móntatelo a tu manera (y pásaselo a otro)
204
+
205
+ Chocolatito se puede enseñar. Cuatro cosas cambian cómo trabaja, y hasta ahora vivían
206
+ cada una en su carpeta:
207
+
208
+ | | |
209
+ | --- | --- |
210
+ | **Skills** | procedimientos que sabe hacer, y que carga solo cuando hacen falta |
211
+ | **Reglas** | cómo quieres que trabaje: tu tono, tus formatos, tus manías |
212
+ | **MCP** | los programas de fuera a los que se conecta |
213
+ | **Memoria** | lo que tiene que saber de ti desde el minuto uno |
214
+
215
+ Un **kit** es una carpeta con las cuatro dentro. Se instala de una vez, se quita de una
216
+ vez, y se le pasa a otro por WhatsApp.
217
+
218
+ ```bash
219
+ /kit # qué tienes puesto
220
+ /kit instalar ./kits/diseno # poner uno
221
+ /kit crear mi-kit # empaquetar lo tuyo para repartirlo
222
+ /kit quitar diseno # quitarlo entero, sin restos
223
+ ```
224
+
225
+ ### Qué lleva dentro
226
+
227
+ ```
228
+ mi-kit/
229
+ kit.json nombre, título, descripción ← lo único obligatorio
230
+ skills/ una carpeta por skill, con su SKILL.md
231
+ reglas.md cómo trabajar contigo
232
+ mcp.json los servidores que necesita
233
+ memoria/ lo que tiene que saber de entrada
234
+ ```
235
+
236
+ Instalar **no pisa nada sin decírtelo**: lo que choca con algo tuyo se salta y se te
237
+ nombra (`--forzar` si de verdad quieres reemplazarlo). Los skills entran con el nombre
238
+ del kit delante (`diseno--lote-de-imagenes`), así dos kits pueden traer uno que se llame
239
+ igual, y quitar un kit no se lleva por delante lo que escribiste tú.
240
+
241
+ Y cuando empaquetas lo tuyo con `/kit crear`, **tu memoria no viaja dentro** salvo que lo
242
+ pidas con `--con-memoria`: ahí hay nombres, rutas y a qué te dedicas, y eso no se manda
243
+ sin querer.
244
+
245
+ ### Enseñarle algo nuevo: `/aprende`
246
+
247
+ Esta es la parte que de verdad lo hace tuyo. Cuando acabéis de resolver algo que se va a
248
+ repetir:
249
+
250
+ ```
251
+ /aprende
252
+ /aprende exportar los assets del cliente como los pide siempre
253
+ ```
254
+
255
+ Escribe un skill con **los pasos que funcionaron de verdad** —los comandos exactos, las
256
+ rutas, lo que salió mal por el camino— y lo guarda en `~/.chocolatito/skills`. La próxima
257
+ vez basta con nombrarlo. Repítelo un mes y tendrás un Chocolatito que trabaja como tú;
258
+ mételo en un kit y tu equipo entero trabaja así.
259
+
260
+ ### El kit de diseño, de ejemplo
261
+
262
+ Viene uno hecho, y no es de programación: `kits/diseno` trae lotes de imágenes con
263
+ ImageMagick, las medidas de todas las redes (con las zonas seguras de historias, que es
264
+ lo que nadie recuerda), y variantes de una misma pieza desde un CSV —cuarenta
265
+ certificados con su nombre, sin una sola errata—.
266
+
267
+ ```bash
268
+ /kit instalar ./kits/diseno
269
+ ```
270
+
271
+ Sirve de plantilla: cópialo, cambia las reglas por las tuyas y ya tienes el kit de tu
272
+ oficio.
273
+
203
274
  ## 🪝 Hooks
204
275
 
205
276
  Reglas propias que el agente no puede saltarse, en `.chocolatito/settings.json`:
@@ -278,6 +349,16 @@ Antes de hacer un commit o desplegar cambios, corre `/audit` o `/review`:
278
349
  - **Limpieza de depuración:** localiza sentencias residuales como `console.log`, `debugger;` o `print()`.
279
350
  - **Sugerencia de Conventional Commits:** genera automáticamente la propuesta de commit semántico según los archivos modificados (ej. `feat(tools): ...`, `test: ...`).
280
351
 
352
+ Y se le puede decir dónde callarse. En una herramienta de terminal, `console.log` no es
353
+ un olvido: es la salida del programa. Las rutas que pongas en un `.chocolatitoauditignore`
354
+ —una por línea, `#` para comentar— dejan de salir en la sección de calidad, y el informe
355
+ dice cuántas calló, para que nadie se encuentre el fichero un año después sin saberlo.
356
+
357
+ **Lo que ese fichero NO puede callar: un secreto.** Una clave, un `.env` o una clave
358
+ privada se avisan siempre, esté la ruta ignorada o no. Una herramienta que se deja
359
+ configurar para callarse una credencial es peor que no tenerla, porque da la
360
+ tranquilidad sin el aviso.
361
+
281
362
  ---
282
363
 
283
364
  ## 📌 Widget de Tareas en Vivo y Selector /resume
@@ -4,6 +4,7 @@ import { execa } from "execa";
4
4
  import { RULES_FILE_NAME } from "../config/constants.js";
5
5
  import { construirMapa } from "./repoMap.js";
6
6
  import { nombreDelSistema } from "../config/plataforma.js";
7
+ import { leerReglasGlobales } from "../kits/manager.js";
7
8
  /** Tokens que puede ocupar el mapa dentro del prompt de sistema. */
8
9
  const PRESUPUESTO_MAPA = 4_000;
9
10
  /**
@@ -120,6 +121,16 @@ export async function getProjectContext(cwd = process.cwd()) {
120
121
  rulesContent = modularText;
121
122
  }
122
123
  }
124
+ // Reglas globales del usuario (~/.chocolatito/reglas/*.md), que es donde
125
+ // aterrizan las de los kits. Van DELANTE de las del proyecto a proposito: son
126
+ // como trabajas tu siempre, y lo que diga el repositorio que tienes abierto
127
+ // manda por encima de eso cuando los dos hablan de lo mismo.
128
+ const reglasGlobales = leerReglasGlobales();
129
+ if (reglasGlobales) {
130
+ rulesContent = rulesContent?.trim()
131
+ ? `${reglasGlobales}\n\n${rulesContent.trim()}`
132
+ : reglasGlobales;
133
+ }
123
134
  // 2. Git branch & status detection
124
135
  let gitBranch = null;
125
136
  try {
package/dist/index.js CHANGED
@@ -32,6 +32,7 @@ import { HookManager } from "./hooks/manager.js";
32
32
  import { SessionManager } from "./sessions/manager.js";
33
33
  import { prepararRestauracion, formatearOpcionesDeSesion } from "./sessions/resume.js";
34
34
  import { auditGitDiff, formatAuditReport } from "./tools/gitAudit.js";
35
+ import { instalarKit, listarKits, quitarKit, crearKit } from "./kits/manager.js";
35
36
  import { globalUndoManager } from "./agent/undoManager.js";
36
37
  import { setAutoApprove } from "./ui/permissionPrompt.js";
37
38
  import { onModeChange, getMode, modeAnnouncement } from "./ui/modes.js";
@@ -615,6 +616,91 @@ ${sessionData}
615
616
  console.log("");
616
617
  continue;
617
618
  }
619
+ if (trimmed === "/kit" || trimmed.startsWith("/kit ")) {
620
+ const partes = trimmed.split(/s+/).slice(1);
621
+ const accion = (partes[0] || "").toLowerCase();
622
+ const resto = partes.slice(1).filter((p) => !p.startsWith("--")).join(" ").trim().replace(/^["']|["']$/g, "");
623
+ if (!accion || accion === "lista" || accion === "listar") {
624
+ const kits = listarKits();
625
+ console.log(chalk.bold.hex("#D97757")("\n🎒 KITS INSTALADOS:"));
626
+ if (kits.length === 0) {
627
+ console.log(chalk.gray(" Ninguno todavia.\n"));
628
+ console.log(chalk.gray(" Un kit es una carpeta con skills, reglas, servidores MCP y memoria"));
629
+ console.log(chalk.gray(" dentro. Sirve para montarte tu Chocolatito y pasarselo a otro.\n"));
630
+ console.log(` ${chalk.white("/kit instalar <carpeta>")} ${chalk.gray("pone uno")}`);
631
+ console.log(` ${chalk.white("/kit crear <nombre>")} ${chalk.gray("empaqueta lo que ya tienes")}\n`);
632
+ }
633
+ else {
634
+ for (const k of kits) {
635
+ const cuenta = (tipo) => (k.piezas || []).filter((p) => p.tipo === tipo).length;
636
+ const trozos = [
637
+ cuenta("skill") ? `${cuenta("skill")} skills` : "",
638
+ cuenta("reglas") ? "reglas" : "",
639
+ cuenta("mcp") ? `${cuenta("mcp")} MCP` : "",
640
+ cuenta("memoria") ? `${cuenta("memoria")} notas` : "",
641
+ ].filter(Boolean);
642
+ console.log(` ${chalk.hex("#A855F7")("•")} ${chalk.bold.white(k.titulo || k.nombre)} ${chalk.gray("(" + k.nombre + ")")}`);
643
+ if (k.descripcion)
644
+ console.log(` ${chalk.gray(k.descripcion)}`);
645
+ console.log(` ${chalk.gray(trozos.join(" · ") || "sin piezas")}`);
646
+ }
647
+ console.log("");
648
+ }
649
+ continue;
650
+ }
651
+ if (accion === "instalar" || accion === "poner") {
652
+ if (!resto) {
653
+ console.log(chalk.yellow("\n Dime la carpeta: /kit instalar ./kits/diseno\n"));
654
+ continue;
655
+ }
656
+ const carpeta = path.isAbsolute(resto) ? resto : path.resolve(agent.currentCwd, resto);
657
+ const res = instalarKit(carpeta, { forzar: partes.includes("--forzar") });
658
+ if (!res.ok) {
659
+ console.log(chalk.red(`\n ${res.error}\n`));
660
+ continue;
661
+ }
662
+ console.log(chalk.green(`\n ✔ Kit "${res.kit?.titulo || res.kit?.nombre}" instalado.`));
663
+ for (const p of res.instaladas)
664
+ console.log(` ${chalk.gray("+")} ${p.tipo} ${chalk.white(p.nombre)}`);
665
+ for (const sa of res.saltadas)
666
+ console.log(` ${chalk.yellow("·")} ${chalk.gray("se salto " + sa)}`);
667
+ for (const av of res.avisos)
668
+ console.log(` ${chalk.yellow("!")} ${chalk.gray(av)}`);
669
+ console.log(chalk.gray("\n Los skills ya se pueden nombrar. Las reglas entran en la proxima sesion.\n"));
670
+ continue;
671
+ }
672
+ if (accion === "quitar" || accion === "borrar") {
673
+ const res = quitarKit(resto);
674
+ if (!res.ok) {
675
+ console.log(chalk.red(`\n ${res.error}\n`));
676
+ }
677
+ else {
678
+ console.log(chalk.green(`\n ✔ Kit "${resto}" quitado.`));
679
+ for (const q of res.quitadas)
680
+ console.log(` ${chalk.gray("-")} ${q}`);
681
+ console.log("");
682
+ }
683
+ continue;
684
+ }
685
+ if (accion === "crear" || accion === "empaquetar") {
686
+ const nombre = resto || "mi-kit";
687
+ const destino = path.resolve(agent.currentCwd, "kits", nombre);
688
+ const res = crearKit(destino, { nombre, titulo: nombre }, { incluirMemoria: partes.includes("--con-memoria") });
689
+ if (!res.ok) {
690
+ console.log(chalk.red(`\n ${res.error}\n`));
691
+ continue;
692
+ }
693
+ console.log(chalk.green(`\n ✔ Kit empaquetado en ${destino}`));
694
+ for (const i of res.incluidas)
695
+ console.log(` ${chalk.gray("+")} ${i}`);
696
+ if (res.incluidas.length === 0)
697
+ console.log(chalk.gray(" (de momento vacio: crea skills o reglas y vuelve)"));
698
+ console.log(chalk.gray("\n Tu memoria NO va dentro salvo que pongas --con-memoria.\n"));
699
+ continue;
700
+ }
701
+ console.log(chalk.yellow("\n /kit · /kit instalar <carpeta> · /kit crear <nombre> · /kit quitar <nombre>\n"));
702
+ continue;
703
+ }
618
704
  if (trimmed === "/skills") {
619
705
  const skills = skillManager.scanSkills();
620
706
  console.log(chalk.bold.hex("#D97757")("\n🧩 SKILLS DISPONIBLES (Carga diferida):"));
@@ -0,0 +1,134 @@
1
+ /**
2
+ * KITS: ARMAR TU PROPIO CHOCOLATITO, Y REGALARSELO A OTRO
3
+ *
4
+ * DE DONDE SALE
5
+ *
6
+ * Todas las piezas para que esto no sea solo una herramienta de programadores ya
7
+ * estaban puestas, cada una por su lado:
8
+ *
9
+ * - los skills se buscan en cinco carpetas (skills/manager.ts:22)
10
+ * - los MCP se leen de cuatro sitios (mcp/manager.ts:39)
11
+ * - las reglas salen de CHOCOLATITO.md o .chocolatitorules (agent/context.ts)
12
+ * - la memoria vive en ~/.chocolatito/memory
13
+ *
14
+ * Lo que faltaba era la capa de encima: una forma de mover las CUATRO a la vez.
15
+ * Hoy, para dejar a alguien el Chocolatito que tu te has montado, hay que
16
+ * explicarle cuatro carpetas distintas por WhatsApp. Nadie hace eso.
17
+ *
18
+ * QUE ES UN KIT
19
+ *
20
+ * Una carpeta con las cuatro cosas dentro y un `kit.json` que la nombra:
21
+ *
22
+ * mi-kit/
23
+ * kit.json nombre, titulo, descripcion, autor
24
+ * skills/ una carpeta por skill, con su SKILL.md
25
+ * reglas.md como quieres que trabaje (tono, formatos, manias)
26
+ * mcp.json los servidores MCP que necesita
27
+ * memoria/ lo que tiene que saber de ti desde el minuto uno
28
+ *
29
+ * Todo es opcional menos `kit.json`. Un kit que solo trae reglas es un kit.
30
+ *
31
+ * TRES DECISIONES QUE NO SON OBVIAS
32
+ *
33
+ * 1. INSTALAR NO PISA NADA SIN DECIRLO. Un kit que llega de fuera no puede
34
+ * tapar un skill que ya usas ni cambiarte un servidor MCP por otro en
35
+ * silencio. Lo que choca se salta y se dice; con `forzar` se reemplaza.
36
+ *
37
+ * 2. LOS SKILLS SE INSTALAN CON EL NOMBRE DEL KIT DELANTE (`diseno--lote`).
38
+ * Asi dos kits pueden traer un skill que se llama igual, y sobre todo: se
39
+ * puede QUITAR un kit entero sin llevarse por delante lo que escribiste tu.
40
+ *
41
+ * 3. QUEDA CONSTANCIA DE QUIEN PUSO QUE. `~/.chocolatito/kits/<nombre>.json`
42
+ * guarda la lista exacta de lo que se instalo. Sin eso, desinstalar es
43
+ * adivinar, y adivinar borrando archivos ajenos es justo lo que no se hace.
44
+ */
45
+ export interface KitManifiesto {
46
+ nombre: string;
47
+ titulo?: string;
48
+ descripcion?: string;
49
+ autor?: string;
50
+ version?: string;
51
+ /** Programas que el kit da por instalados. Solo se avisa; no se instala nada. */
52
+ requiere?: string[];
53
+ }
54
+ export interface PiezaInstalada {
55
+ tipo: "skill" | "reglas" | "mcp" | "memoria";
56
+ nombre: string;
57
+ ruta?: string;
58
+ }
59
+ export interface ResultadoInstalacion {
60
+ ok: boolean;
61
+ kit?: KitManifiesto;
62
+ instaladas: PiezaInstalada[];
63
+ saltadas: string[];
64
+ avisos: string[];
65
+ error?: string;
66
+ }
67
+ export interface KitInstalado extends KitManifiesto {
68
+ instaladoEl: string;
69
+ origen: string;
70
+ piezas: PiezaInstalada[];
71
+ }
72
+ export declare function raizChocolatito(home?: string): string;
73
+ export declare function carpetaDeSkills(home?: string): string;
74
+ export declare function carpetaDeReglas(home?: string): string;
75
+ export declare function carpetaDeKits(home?: string): string;
76
+ export declare function rutaDeMcp(home?: string): string;
77
+ export declare function carpetaDeMemoria(home?: string): string;
78
+ /** Lee y valida el kit.json de una carpeta. */
79
+ export declare function leerManifiesto(carpetaKit: string): KitManifiesto | null;
80
+ /**
81
+ * Instala un kit desde una carpeta.
82
+ *
83
+ * `home` se inyecta para poder probar esto sin tocar el ordenador de nadie.
84
+ */
85
+ export declare function instalarKit(carpetaKit: string, opciones?: {
86
+ home?: string;
87
+ forzar?: boolean;
88
+ }): ResultadoInstalacion;
89
+ /** Los kits instalados, por orden de instalacion. */
90
+ export declare function listarKits(home?: string): KitInstalado[];
91
+ /**
92
+ * Quita un kit: borra SOLO lo que consta en su registro.
93
+ *
94
+ * Un servidor MCP que el usuario edito a mano despues de instalarlo se queda:
95
+ * el kit lo trajo, pero la version que hay ahora es suya.
96
+ */
97
+ export declare function quitarKit(nombre: string, home?: string): {
98
+ ok: boolean;
99
+ quitadas: string[];
100
+ error?: string;
101
+ };
102
+ /**
103
+ * Empaqueta lo que ya tienes puesto en una carpeta que se puede pasar a otro.
104
+ *
105
+ * Se copia lo TUYO -lo que hay en ~/.chocolatito- y no lo que trajo otro kit:
106
+ * los skills con `--` en el nombre vienen de un kit ajeno y no son tuyos para
107
+ * repartirlos.
108
+ */
109
+ export declare function crearKit(destino: string, manifiesto: KitManifiesto, opciones?: {
110
+ home?: string;
111
+ incluirMemoria?: boolean;
112
+ }): {
113
+ ok: boolean;
114
+ incluidas: string[];
115
+ error?: string;
116
+ };
117
+ /**
118
+ * Guarda un skill nuevo. Es lo que hay detras de "aprende esto".
119
+ *
120
+ * Un skill es un SKILL.md con dos lineas de cabecera que el buscador ya sabe
121
+ * leer (skills/manager.ts:parseFrontmatter). Se escribe entero aqui para que
122
+ * el modelo no tenga que acordarse del formato ni pueda equivocarse con el.
123
+ */
124
+ export declare function guardarSkill(nombre: string, descripcion: string, contenido: string, opciones?: {
125
+ home?: string;
126
+ cwd?: string;
127
+ ambito?: "global" | "proyecto";
128
+ }): {
129
+ ok: boolean;
130
+ ruta?: string;
131
+ error?: string;
132
+ };
133
+ /** Las reglas globales del usuario, que los kits tambien alimentan. */
134
+ export declare function leerReglasGlobales(home?: string): string;
@@ -0,0 +1,329 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import os from "node:os";
4
+ const NOMBRE_VALIDO = /^[a-z0-9][a-z0-9_-]{0,48}$/i;
5
+ export function raizChocolatito(home = os.homedir()) {
6
+ return path.join(home, ".chocolatito");
7
+ }
8
+ export function carpetaDeSkills(home) {
9
+ return path.join(raizChocolatito(home), "skills");
10
+ }
11
+ export function carpetaDeReglas(home) {
12
+ return path.join(raizChocolatito(home), "reglas");
13
+ }
14
+ export function carpetaDeKits(home) {
15
+ return path.join(raizChocolatito(home), "kits");
16
+ }
17
+ export function rutaDeMcp(home) {
18
+ return path.join(raizChocolatito(home), "mcp.json");
19
+ }
20
+ export function carpetaDeMemoria(home) {
21
+ return path.join(raizChocolatito(home), "memory");
22
+ }
23
+ function asegurar(dir) {
24
+ if (!fs.existsSync(dir))
25
+ fs.mkdirSync(dir, { recursive: true });
26
+ return dir;
27
+ }
28
+ function leerJson(ruta) {
29
+ try {
30
+ return JSON.parse(fs.readFileSync(ruta, "utf-8"));
31
+ }
32
+ catch {
33
+ return null;
34
+ }
35
+ }
36
+ /** Lee y valida el kit.json de una carpeta. */
37
+ export function leerManifiesto(carpetaKit) {
38
+ const manifiesto = leerJson(path.join(carpetaKit, "kit.json"));
39
+ if (!manifiesto || typeof manifiesto.nombre !== "string")
40
+ return null;
41
+ if (!NOMBRE_VALIDO.test(manifiesto.nombre))
42
+ return null;
43
+ return manifiesto;
44
+ }
45
+ function copiarArchivo(origen, destino) {
46
+ asegurar(path.dirname(destino));
47
+ fs.copyFileSync(origen, destino);
48
+ }
49
+ function copiarCarpeta(origen, destino) {
50
+ asegurar(destino);
51
+ for (const entrada of fs.readdirSync(origen, { withFileTypes: true })) {
52
+ const de = path.join(origen, entrada.name);
53
+ const a = path.join(destino, entrada.name);
54
+ if (entrada.isDirectory())
55
+ copiarCarpeta(de, a);
56
+ else if (entrada.isFile())
57
+ fs.copyFileSync(de, a);
58
+ }
59
+ }
60
+ /**
61
+ * Instala un kit desde una carpeta.
62
+ *
63
+ * `home` se inyecta para poder probar esto sin tocar el ordenador de nadie.
64
+ */
65
+ export function instalarKit(carpetaKit, opciones = {}) {
66
+ const { home, forzar = false } = opciones;
67
+ const instaladas = [];
68
+ const saltadas = [];
69
+ const avisos = [];
70
+ if (!fs.existsSync(carpetaKit) || !fs.statSync(carpetaKit).isDirectory()) {
71
+ return { ok: false, instaladas, saltadas, avisos, error: `No existe la carpeta ${carpetaKit}.` };
72
+ }
73
+ const kit = leerManifiesto(carpetaKit);
74
+ if (!kit) {
75
+ return {
76
+ ok: false,
77
+ instaladas,
78
+ saltadas,
79
+ avisos,
80
+ error: `En ${carpetaKit} no hay un kit.json valido (hace falta al menos {"nombre": "..."}).`,
81
+ };
82
+ }
83
+ // ---------------------------------------------------------------- skills
84
+ const dirSkillsKit = path.join(carpetaKit, "skills");
85
+ if (fs.existsSync(dirSkillsKit)) {
86
+ const destinoBase = asegurar(carpetaDeSkills(home));
87
+ for (const entrada of fs.readdirSync(dirSkillsKit, { withFileTypes: true })) {
88
+ const esCarpetaConSkill = entrada.isDirectory() && fs.existsSync(path.join(dirSkillsKit, entrada.name, "SKILL.md"));
89
+ const esArchivoMd = entrada.isFile() && entrada.name.endsWith(".md");
90
+ if (!esCarpetaConSkill && !esArchivoMd)
91
+ continue;
92
+ const nombreSkill = esArchivoMd ? entrada.name.replace(/\.md$/, "") : entrada.name;
93
+ const destino = path.join(destinoBase, `${kit.nombre}--${nombreSkill}`);
94
+ if (fs.existsSync(destino) && !forzar) {
95
+ saltadas.push(`skill ${kit.nombre}--${nombreSkill} (ya estaba; usa forzar para reemplazar)`);
96
+ continue;
97
+ }
98
+ if (esCarpetaConSkill) {
99
+ copiarCarpeta(path.join(dirSkillsKit, entrada.name), destino);
100
+ }
101
+ else {
102
+ copiarArchivo(path.join(dirSkillsKit, entrada.name), path.join(destino, "SKILL.md"));
103
+ }
104
+ instaladas.push({ tipo: "skill", nombre: `${kit.nombre}--${nombreSkill}`, ruta: destino });
105
+ }
106
+ }
107
+ // ---------------------------------------------------------------- reglas
108
+ const reglasKit = path.join(carpetaKit, "reglas.md");
109
+ if (fs.existsSync(reglasKit)) {
110
+ const destino = path.join(asegurar(carpetaDeReglas(home)), `${kit.nombre}.md`);
111
+ if (fs.existsSync(destino) && !forzar) {
112
+ saltadas.push(`reglas de ${kit.nombre} (ya estaban)`);
113
+ }
114
+ else {
115
+ copiarArchivo(reglasKit, destino);
116
+ instaladas.push({ tipo: "reglas", nombre: `${kit.nombre}.md`, ruta: destino });
117
+ }
118
+ }
119
+ // ------------------------------------------------------------------- mcp
120
+ const mcpKit = leerJson(path.join(carpetaKit, "mcp.json"));
121
+ if (mcpKit?.mcpServers && Object.keys(mcpKit.mcpServers).length > 0) {
122
+ const destino = rutaDeMcp(home);
123
+ const actual = leerJson(destino) || {};
124
+ const servidores = { ...(actual.mcpServers || {}) };
125
+ for (const [nombre, cfg] of Object.entries(mcpKit.mcpServers)) {
126
+ if (servidores[nombre] && !forzar) {
127
+ saltadas.push(`servidor MCP "${nombre}" (ya tenias uno con ese nombre)`);
128
+ continue;
129
+ }
130
+ servidores[nombre] = cfg;
131
+ instaladas.push({ tipo: "mcp", nombre, ruta: destino });
132
+ }
133
+ asegurar(path.dirname(destino));
134
+ fs.writeFileSync(destino, JSON.stringify({ ...actual, mcpServers: servidores }, null, 2), "utf-8");
135
+ }
136
+ // --------------------------------------------------------------- memoria
137
+ const dirMemoriaKit = path.join(carpetaKit, "memoria");
138
+ if (fs.existsSync(dirMemoriaKit)) {
139
+ const destinoBase = asegurar(carpetaDeMemoria(home));
140
+ for (const entrada of fs.readdirSync(dirMemoriaKit, { withFileTypes: true })) {
141
+ if (!entrada.isFile() || !entrada.name.endsWith(".md"))
142
+ continue;
143
+ if (entrada.name.toUpperCase() === "MEMORY.MD")
144
+ continue; // el indice es del usuario
145
+ const destino = path.join(destinoBase, `${kit.nombre}--${entrada.name}`);
146
+ if (fs.existsSync(destino) && !forzar) {
147
+ saltadas.push(`nota de memoria ${entrada.name} (ya estaba)`);
148
+ continue;
149
+ }
150
+ copiarArchivo(path.join(dirMemoriaKit, entrada.name), destino);
151
+ instaladas.push({ tipo: "memoria", nombre: entrada.name, ruta: destino });
152
+ }
153
+ }
154
+ // Lo que el kit da por instalado en el sistema. No se instala nada solo:
155
+ // bajar programas a la maquina de alguien porque lo pide un fichero de fuera
156
+ // es exactamente lo que no debe hacer un instalador de kits.
157
+ if (kit.requiere?.length) {
158
+ avisos.push(`Este kit da por hecho que tienes: ${kit.requiere.join(", ")}.`);
159
+ }
160
+ const registro = {
161
+ ...kit,
162
+ instaladoEl: new Date().toISOString(),
163
+ origen: path.resolve(carpetaKit),
164
+ piezas: instaladas,
165
+ };
166
+ const dirKits = asegurar(carpetaDeKits(home));
167
+ fs.writeFileSync(path.join(dirKits, `${kit.nombre}.json`), JSON.stringify(registro, null, 2), "utf-8");
168
+ return { ok: true, kit, instaladas, saltadas, avisos };
169
+ }
170
+ /** Los kits instalados, por orden de instalacion. */
171
+ export function listarKits(home) {
172
+ const dir = carpetaDeKits(home);
173
+ if (!fs.existsSync(dir))
174
+ return [];
175
+ const kits = [];
176
+ for (const archivo of fs.readdirSync(dir)) {
177
+ if (!archivo.endsWith(".json"))
178
+ continue;
179
+ const k = leerJson(path.join(dir, archivo));
180
+ if (k?.nombre)
181
+ kits.push(k);
182
+ }
183
+ return kits.sort((a, b) => (a.instaladoEl || "").localeCompare(b.instaladoEl || ""));
184
+ }
185
+ /**
186
+ * Quita un kit: borra SOLO lo que consta en su registro.
187
+ *
188
+ * Un servidor MCP que el usuario edito a mano despues de instalarlo se queda:
189
+ * el kit lo trajo, pero la version que hay ahora es suya.
190
+ */
191
+ export function quitarKit(nombre, home) {
192
+ const registroPath = path.join(carpetaDeKits(home), `${nombre}.json`);
193
+ const registro = leerJson(registroPath);
194
+ if (!registro)
195
+ return { ok: false, quitadas: [], error: `No hay ningun kit instalado con el nombre "${nombre}".` };
196
+ const quitadas = [];
197
+ for (const pieza of registro.piezas || []) {
198
+ try {
199
+ if (pieza.tipo === "mcp") {
200
+ const destino = rutaDeMcp(home);
201
+ const actual = leerJson(destino);
202
+ if (actual?.mcpServers && actual.mcpServers[pieza.nombre]) {
203
+ delete actual.mcpServers[pieza.nombre];
204
+ fs.writeFileSync(destino, JSON.stringify(actual, null, 2), "utf-8");
205
+ quitadas.push(`servidor MCP ${pieza.nombre}`);
206
+ }
207
+ continue;
208
+ }
209
+ if (pieza.ruta && fs.existsSync(pieza.ruta)) {
210
+ fs.rmSync(pieza.ruta, { recursive: true, force: true });
211
+ quitadas.push(`${pieza.tipo} ${pieza.nombre}`);
212
+ }
213
+ }
214
+ catch { }
215
+ }
216
+ try {
217
+ fs.rmSync(registroPath, { force: true });
218
+ }
219
+ catch { }
220
+ return { ok: true, quitadas };
221
+ }
222
+ /**
223
+ * Empaqueta lo que ya tienes puesto en una carpeta que se puede pasar a otro.
224
+ *
225
+ * Se copia lo TUYO -lo que hay en ~/.chocolatito- y no lo que trajo otro kit:
226
+ * los skills con `--` en el nombre vienen de un kit ajeno y no son tuyos para
227
+ * repartirlos.
228
+ */
229
+ export function crearKit(destino, manifiesto, opciones = {}) {
230
+ const { home, incluirMemoria = false } = opciones;
231
+ if (!NOMBRE_VALIDO.test(manifiesto.nombre || "")) {
232
+ return { ok: false, incluidas: [], error: "El nombre del kit solo admite letras, numeros, guion y guion bajo." };
233
+ }
234
+ const incluidas = [];
235
+ asegurar(destino);
236
+ fs.writeFileSync(path.join(destino, "kit.json"), JSON.stringify({ version: "1.0.0", ...manifiesto }, null, 2), "utf-8");
237
+ const dirSkills = carpetaDeSkills(home);
238
+ if (fs.existsSync(dirSkills)) {
239
+ for (const entrada of fs.readdirSync(dirSkills, { withFileTypes: true })) {
240
+ if (!entrada.isDirectory() || entrada.name.includes("--"))
241
+ continue;
242
+ copiarCarpeta(path.join(dirSkills, entrada.name), path.join(destino, "skills", entrada.name));
243
+ incluidas.push(`skill ${entrada.name}`);
244
+ }
245
+ }
246
+ const dirReglas = carpetaDeReglas(home);
247
+ if (fs.existsSync(dirReglas)) {
248
+ const propias = fs
249
+ .readdirSync(dirReglas)
250
+ .filter((a) => a.endsWith(".md") && !listarKits(home).some((k) => `${k.nombre}.md` === a));
251
+ if (propias.length > 0) {
252
+ const texto = propias
253
+ .map((a) => fs.readFileSync(path.join(dirReglas, a), "utf-8").trim())
254
+ .join("\n\n");
255
+ fs.writeFileSync(path.join(destino, "reglas.md"), `${texto}\n`, "utf-8");
256
+ incluidas.push("reglas");
257
+ }
258
+ }
259
+ const mcp = leerJson(rutaDeMcp(home));
260
+ if (mcp?.mcpServers && Object.keys(mcp.mcpServers).length > 0) {
261
+ fs.writeFileSync(path.join(destino, "mcp.json"), JSON.stringify({ mcpServers: mcp.mcpServers }, null, 2), "utf-8");
262
+ incluidas.push(`${Object.keys(mcp.mcpServers).length} servidores MCP`);
263
+ }
264
+ // La memoria NO va por defecto, y es a proposito: ahi dentro hay cosas tuyas
265
+ // -nombres, rutas, a que te dedicas- que no pensabas repartir al mandar un kit.
266
+ if (incluirMemoria) {
267
+ const dirMemoria = carpetaDeMemoria(home);
268
+ if (fs.existsSync(dirMemoria)) {
269
+ for (const archivo of fs.readdirSync(dirMemoria)) {
270
+ if (!archivo.endsWith(".md") || archivo.toUpperCase() === "MEMORY.MD")
271
+ continue;
272
+ copiarArchivo(path.join(dirMemoria, archivo), path.join(destino, "memoria", archivo));
273
+ incluidas.push(`memoria ${archivo}`);
274
+ }
275
+ }
276
+ }
277
+ return { ok: true, incluidas };
278
+ }
279
+ /**
280
+ * Guarda un skill nuevo. Es lo que hay detras de "aprende esto".
281
+ *
282
+ * Un skill es un SKILL.md con dos lineas de cabecera que el buscador ya sabe
283
+ * leer (skills/manager.ts:parseFrontmatter). Se escribe entero aqui para que
284
+ * el modelo no tenga que acordarse del formato ni pueda equivocarse con el.
285
+ */
286
+ export function guardarSkill(nombre, descripcion, contenido, opciones = {}) {
287
+ const limpio = (nombre || "").trim().toLowerCase().replace(/\s+/g, "-");
288
+ if (!NOMBRE_VALIDO.test(limpio)) {
289
+ return { ok: false, error: "El nombre del skill solo admite letras, numeros, guion y guion bajo." };
290
+ }
291
+ if (!descripcion?.trim()) {
292
+ return { ok: false, error: "Un skill sin descripcion no se encuentra nunca: es lo que se lee para elegirlo." };
293
+ }
294
+ if (!contenido?.trim()) {
295
+ return { ok: false, error: "Falta el contenido del skill." };
296
+ }
297
+ const ambito = opciones.ambito || "global";
298
+ const base = ambito === "proyecto"
299
+ ? path.join(opciones.cwd || process.cwd(), ".skills")
300
+ : carpetaDeSkills(opciones.home);
301
+ const carpeta = asegurar(path.join(base, limpio));
302
+ const ruta = path.join(carpeta, "SKILL.md");
303
+ const texto = `---\nname: ${limpio}\ndescription: ${descripcion.trim().replace(/\s+/g, " ")}\n---\n\n${contenido.trim()}\n`;
304
+ try {
305
+ fs.writeFileSync(ruta, texto, "utf-8");
306
+ return { ok: true, ruta };
307
+ }
308
+ catch (err) {
309
+ return { ok: false, error: err?.message || String(err) };
310
+ }
311
+ }
312
+ /** Las reglas globales del usuario, que los kits tambien alimentan. */
313
+ export function leerReglasGlobales(home) {
314
+ const dir = carpetaDeReglas(home);
315
+ if (!fs.existsSync(dir))
316
+ return "";
317
+ const trozos = [];
318
+ for (const archivo of fs.readdirSync(dir).sort()) {
319
+ if (!archivo.endsWith(".md"))
320
+ continue;
321
+ try {
322
+ const texto = fs.readFileSync(path.join(dir, archivo), "utf-8").trim();
323
+ if (texto)
324
+ trozos.push(`[~/.chocolatito/reglas/${archivo}]:\n${texto}`);
325
+ }
326
+ catch { }
327
+ }
328
+ return trozos.join("\n\n");
329
+ }
@@ -50,6 +50,38 @@ export const TOOLS = [
50
50
  },
51
51
  },
52
52
  },
53
+ {
54
+ type: "function",
55
+ function: {
56
+ name: "save_skill",
57
+ description: "Guarda un procedimiento aprendido como skill reutilizable, para repetirlo en sesiones futuras con solo nombrarlo. " +
58
+ "Usalo cuando el usuario diga 'aprende esto', 'recuerda como se hace' o cuando acabes de resolver algo que se va a repetir. " +
59
+ "Escribe los PASOS que funcionaron, con los comandos exactos, no un resumen de lo que paso.",
60
+ parameters: {
61
+ type: "object",
62
+ properties: {
63
+ nombre: {
64
+ type: "string",
65
+ description: "Nombre corto en minusculas y con guiones (ej. 'exportar-para-instagram').",
66
+ },
67
+ descripcion: {
68
+ type: "string",
69
+ description: "Una linea que diga CUANDO usar este skill. Es lo unico que se ve al elegirlo, asi que tiene que describir la situacion, no el contenido.",
70
+ },
71
+ contenido: {
72
+ type: "string",
73
+ description: "Las instrucciones completas en Markdown: pasos, comandos exactos, avisos y ejemplos.",
74
+ },
75
+ ambito: {
76
+ type: "string",
77
+ enum: ["global", "proyecto"],
78
+ description: "'global' (~/.chocolatito/skills, sirve en todas partes) o 'proyecto' (.skills/ de esta carpeta). Por defecto global.",
79
+ },
80
+ },
81
+ required: ["nombre", "descripcion", "contenido"],
82
+ },
83
+ },
84
+ },
53
85
  {
54
86
  type: "function",
55
87
  function: {
@@ -19,6 +19,8 @@ export interface AuditReport {
19
19
  secrets: AuditFinding[];
20
20
  debugLogs: AuditFinding[];
21
21
  residualLogs: AuditFinding[];
22
+ /** Cuantas sentencias se callaron por `.chocolatitoauditignore`. Se dice siempre. */
23
+ debugSilenciados: number;
22
24
  sensitiveFiles: string[];
23
25
  suggestedCommit: string;
24
26
  rawDiff: string;
@@ -35,7 +37,32 @@ export declare const DEBUG_PATTERNS: Array<{
35
37
  export declare function esDocumentacion(archivo: string): boolean;
36
38
  /** Quita los trozos entre comillas invertidas: `asi` y ``asi``. */
37
39
  export declare function sinCitasEnLinea(texto: string): string;
38
- export declare function auditDiffText(rawDiff: string, changedFiles?: string[]): AuditReport;
40
+ /**
41
+ * SE PUEDE CALLAR EL RUIDO, NUNCA UN SECRETO
42
+ *
43
+ * En un programa de terminal, `console.log` NO es un resto de depuracion: es la
44
+ * interfaz. Este repositorio tiene ocho en `src/index.ts` que son justo eso -el
45
+ * enlace de `--servir`, el aviso de que solo escucha en esta maquina- y salian
46
+ * los ocho en cada auditoria. Ocho avisos que hay que ignorar a mano cada vez
47
+ * son ocho razones para dejar de leer la seccion entera.
48
+ *
49
+ * Adivinar cual es interfaz y cual es un olvido no se puede hacer desde fuera:
50
+ * es la misma llamada. Asi que lo dice el proyecto, en un `.chocolatitoauditignore`
51
+ * al lado del `.gitignore`, una ruta o un patron por linea, y `#` para comentar.
52
+ *
53
+ * Con un limite que no se negocia: **esto solo calla la seccion de calidad**. Un
54
+ * secreto, un archivo sensible o una clave privada se avisan siempre, este la
55
+ * ruta ignorada o no. Una herramienta que se puede configurar para callarse una
56
+ * credencial es peor que no tenerla, porque da la tranquilidad sin el aviso.
57
+ *
58
+ * Y lo que se calla se cuenta: el informe dice cuantos silencio, para que nadie
59
+ * descubra el fichero un año despues sin saber que estaba puesto.
60
+ */
61
+ export declare const ARCHIVO_DE_IGNORADOS = ".chocolatitoauditignore";
62
+ export declare function leerPatronesIgnorados(cwd: string): string[];
63
+ /** Un patron de `.chocolatitoauditignore` contra una ruta del diff. */
64
+ export declare function rutaIgnorada(archivo: string, patrones: string[]): boolean;
65
+ export declare function auditDiffText(rawDiff: string, changedFiles?: string[], patronesIgnorados?: string[]): AuditReport;
39
66
  export declare function auditGitDiff(cwd?: string, mockDiff?: string): Promise<AuditReport>;
40
67
  export declare function suggestConventionalCommit(files: string[], _insertions: number, _deletions: number): string;
41
68
  export declare function formatAuditReport(report: AuditReport): string;
@@ -1,3 +1,5 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
1
3
  import chalk from "chalk";
2
4
  import { ejecutarGit } from "./gitTools.js";
3
5
  export const SECRET_PATTERNS = [
@@ -16,7 +18,11 @@ export const SENSITIVE_FILE_PATTERN = /(?:^|[/\\])(\.env(?:\.[a-zA-Z0-9_-]+)?|id
16
18
  export const DEBUG_PATTERNS = [
17
19
  { name: "console.log", pattern: /\bconsole\.log\s*\(/ },
18
20
  { name: "console.debug", pattern: /\bconsole\.debug\s*\(/ },
19
- { name: "debugger", pattern: /\bdebugger\b/ },
21
+ // `debugger` como PALABRA sale tambien dentro de una frase: el propio aviso
22
+ // de esta herramienta dice "ni console.log ni debugger" y se denunciaba solo.
23
+ // Un `debugger` de verdad es una sentencia: lleva punto y coma, o esta sola en
24
+ // su linea. Eso es lo que se busca.
25
+ { name: "debugger", pattern: /(?:^|[;{}])\s*debugger\s*(?:;|$)/ },
20
26
  { name: "print()", pattern: /(?<![\w.])print\s*\(/ },
21
27
  { name: "var_dump", pattern: /\bvar_dump\s*\(/ },
22
28
  { name: "dd()", pattern: /(?<![\w.])dd\s*\(/ },
@@ -60,10 +66,79 @@ export function esDocumentacion(archivo) {
60
66
  export function sinCitasEnLinea(texto) {
61
67
  return texto.replace(/``[^`]*``/g, " ").replace(/`[^`]*`/g, " ");
62
68
  }
63
- export function auditDiffText(rawDiff, changedFiles = []) {
69
+ /**
70
+ * SE PUEDE CALLAR EL RUIDO, NUNCA UN SECRETO
71
+ *
72
+ * En un programa de terminal, `console.log` NO es un resto de depuracion: es la
73
+ * interfaz. Este repositorio tiene ocho en `src/index.ts` que son justo eso -el
74
+ * enlace de `--servir`, el aviso de que solo escucha en esta maquina- y salian
75
+ * los ocho en cada auditoria. Ocho avisos que hay que ignorar a mano cada vez
76
+ * son ocho razones para dejar de leer la seccion entera.
77
+ *
78
+ * Adivinar cual es interfaz y cual es un olvido no se puede hacer desde fuera:
79
+ * es la misma llamada. Asi que lo dice el proyecto, en un `.chocolatitoauditignore`
80
+ * al lado del `.gitignore`, una ruta o un patron por linea, y `#` para comentar.
81
+ *
82
+ * Con un limite que no se negocia: **esto solo calla la seccion de calidad**. Un
83
+ * secreto, un archivo sensible o una clave privada se avisan siempre, este la
84
+ * ruta ignorada o no. Una herramienta que se puede configurar para callarse una
85
+ * credencial es peor que no tenerla, porque da la tranquilidad sin el aviso.
86
+ *
87
+ * Y lo que se calla se cuenta: el informe dice cuantos silencio, para que nadie
88
+ * descubra el fichero un año despues sin saber que estaba puesto.
89
+ */
90
+ export const ARCHIVO_DE_IGNORADOS = ".chocolatitoauditignore";
91
+ export function leerPatronesIgnorados(cwd) {
92
+ try {
93
+ const crudo = fs.readFileSync(path.join(cwd, ARCHIVO_DE_IGNORADOS), "utf-8");
94
+ return crudo
95
+ .split(/\r?\n/)
96
+ .map((l) => l.trim())
97
+ .filter((l) => l.length > 0 && !l.startsWith("#"));
98
+ }
99
+ catch {
100
+ return [];
101
+ }
102
+ }
103
+ /** Un patron de `.chocolatitoauditignore` contra una ruta del diff. */
104
+ export function rutaIgnorada(archivo, patrones) {
105
+ if (!archivo || patrones.length === 0)
106
+ return false;
107
+ const ruta = archivo.replace(/\\/g, "/").replace(/^\.\//, "");
108
+ const base = ruta.split("/").pop() || ruta;
109
+ for (const crudo of patrones) {
110
+ const patron = crudo.replace(/\\/g, "/").replace(/^\.\//, "");
111
+ // Una carpeta: "src/ui/" tapa todo lo que hay dentro.
112
+ if (patron.endsWith("/")) {
113
+ if (ruta.startsWith(patron))
114
+ return true;
115
+ continue;
116
+ }
117
+ // "**" cruza barras, "*" no: src/*.ts no baja a src/ui/marco.ts, src/**/*.ts si.
118
+ const comoRegex = patron
119
+ .split("**")
120
+ .map((trozo) => trozo
121
+ .replace(/[.+^$(){}|[\]\\]/g, (m) => "\\" + m)
122
+ .replace(/\*/g, "[^/]*")
123
+ .replace(/\?/g, "[^/]"))
124
+ .join(".*");
125
+ const re = new RegExp(`^${comoRegex}$`);
126
+ if (re.test(ruta))
127
+ return true;
128
+ // Un patron sin barras ("*.min.js") mira tambien solo el nombre.
129
+ if (!patron.includes("/") && re.test(base))
130
+ return true;
131
+ // Una carpeta escrita sin barra final ("src/ui") tapa igual lo de dentro.
132
+ if (ruta.startsWith(`${patron}/`))
133
+ return true;
134
+ }
135
+ return false;
136
+ }
137
+ export function auditDiffText(rawDiff, changedFiles = [], patronesIgnorados = []) {
64
138
  const secrets = [];
65
139
  const debugLogs = [];
66
140
  const sensitiveFiles = [];
141
+ let debugSilenciados = 0;
67
142
  for (const f of changedFiles) {
68
143
  if (SENSITIVE_FILE_PATTERN.test(f) && !sensitiveFiles.includes(f)) {
69
144
  sensitiveFiles.push(f);
@@ -119,9 +194,16 @@ export function auditDiffText(rawDiff, changedFiles = []) {
119
194
  });
120
195
  }
121
196
  }
197
+ const silenciado = rutaIgnorada(currentFile || "", patronesIgnorados);
122
198
  for (const dp of DEBUG_PATTERNS) {
123
199
  if (enDocumentacion)
124
200
  break;
201
+ if (silenciado) {
202
+ // Se cuenta aunque no se liste: callar no es esconder.
203
+ if (dp.pattern.test(addedContent))
204
+ debugSilenciados++;
205
+ continue;
206
+ }
125
207
  if (dp.pattern.test(addedContent)) {
126
208
  debugLogs.push({
127
209
  type: "debug",
@@ -156,6 +238,7 @@ export function auditDiffText(rawDiff, changedFiles = []) {
156
238
  secrets,
157
239
  debugLogs,
158
240
  residualLogs: debugLogs,
241
+ debugSilenciados,
159
242
  sensitiveFiles,
160
243
  suggestedCommit,
161
244
  rawDiff,
@@ -174,6 +257,7 @@ export async function auditGitDiff(cwd = process.cwd(), mockDiff) {
174
257
  secrets: [],
175
258
  debugLogs: [],
176
259
  residualLogs: [],
260
+ debugSilenciados: 0,
177
261
  sensitiveFiles: [],
178
262
  suggestedCommit: "chore: update project",
179
263
  rawDiff: statusRes.error,
@@ -188,6 +272,7 @@ export async function auditGitDiff(cwd = process.cwd(), mockDiff) {
188
272
  secrets: [],
189
273
  debugLogs: [],
190
274
  residualLogs: [],
275
+ debugSilenciados: 0,
191
276
  sensitiveFiles: [],
192
277
  suggestedCommit: "chore: no pending changes",
193
278
  rawDiff: "",
@@ -205,7 +290,7 @@ export async function auditGitDiff(cwd = process.cwd(), mockDiff) {
205
290
  diffRes = await ejecutarGit(["diff"], cwd);
206
291
  }
207
292
  const rawDiff = diffRes.ok ? diffRes.stdout : "";
208
- return auditDiffText(rawDiff, changedFiles);
293
+ return auditDiffText(rawDiff, changedFiles, leerPatronesIgnorados(cwd));
209
294
  }
210
295
  export function suggestConventionalCommit(files, _insertions, _deletions) {
211
296
  if (files.length === 0)
@@ -268,8 +353,15 @@ export function formatAuditReport(report) {
268
353
  if (report.debugLogs.length > 5) {
269
354
  lines.push(chalk.gray(` ... y ${report.debugLogs.length - 5} más.`));
270
355
  }
356
+ if (report.debugSilenciados > 0) {
357
+ lines.push(chalk.gray(` (${report.debugSilenciados} más en rutas de ${ARCHIVO_DE_IGNORADOS}, sin listar.)`));
358
+ }
271
359
  lines.push("");
272
360
  }
361
+ else if (report.debugSilenciados > 0) {
362
+ lines.push(chalk.green(" ✔ Calidad: Sin rastros nuevos de console.log ni debugger") +
363
+ chalk.gray(` (${report.debugSilenciados} en rutas de ${ARCHIVO_DE_IGNORADOS}, sin listar).`));
364
+ }
273
365
  else {
274
366
  lines.push(chalk.green(" ✔ Calidad: Sin rastros de console.log ni debugger en líneas nuevas."));
275
367
  }
@@ -26,6 +26,7 @@ import { validateFileSyntax } from "../agent/syntaxValidator.js";
26
26
  import { fileTracker } from "../agent/fileTracker.js";
27
27
  import { mcpManager } from "../mcp/manager.js";
28
28
  import { taskManager } from "./backgroundTask.js";
29
+ import { guardarSkill } from "../kits/manager.js";
29
30
  import fs from "node:fs";
30
31
  import path from "node:path";
31
32
  /**
@@ -82,6 +83,18 @@ export async function executeToolCall(name, args, cwd = process.cwd(), apiKey, m
82
83
  }
83
84
  break;
84
85
  }
86
+ case "save_skill": {
87
+ // Aprender algo no puede depender de que el modelo se acuerde del
88
+ // formato del SKILL.md: la cabecera la escribe guardarSkill().
89
+ const res = guardarSkill(args.nombre, args.descripcion, args.contenido, {
90
+ cwd,
91
+ ambito: args.ambito === "proyecto" ? "proyecto" : "global",
92
+ });
93
+ rawResult = res.ok
94
+ ? `Skill "${args.nombre}" guardado en ${res.ruta}. Ya se puede usar por su nombre en cualquier sesion.`
95
+ : `No se pudo guardar el skill: ${res.error}`;
96
+ break;
97
+ }
85
98
  case "save_memory":
86
99
  memoryManager.setCwd(cwd);
87
100
  rawResult = memoryManager.saveMemory(args.topic, args.fact, args.scope);
@@ -30,6 +30,8 @@ export const SLASH_COMMANDS = [
30
30
  { name: "/plan", description: "Alternar Modo Plan (solo lectura e investigación previa)" },
31
31
  { name: "/resume", description: "Reanudar una sesión anterior guardada", args: "[id]" },
32
32
  { name: "/skills", description: "Ver skills disponibles y cómo usarlos" },
33
+ { name: "/kit", description: "Montar tu Chocolatito: instalar, crear o quitar kits (skills + reglas + MCP)", args: "[instalar|crear|quitar] [nombre]" },
34
+ { name: "/aprende", description: "Guardar lo que acabamos de hacer como skill reutilizable", args: "[en pocas palabras]" },
33
35
  { name: "/memory", description: "Consultar la memoria persistente transversal (~/.chocolatito/memory/)" },
34
36
  { name: "/effort", description: "Regular potencia del motor (1.0, 1.5, 2.0, 2.5)", args: "[nivel]" },
35
37
  { name: "/goal", description: "Ejecutar meta compleja autónoma con auto-corrección", args: "<meta>" },
@@ -2,7 +2,7 @@
2
2
  "manifest_version": 3,
3
3
  "minimum_chrome_version": "102",
4
4
  "name": "Chocolatito Code",
5
- "version": "1.6.8",
5
+ "version": "1.6.10",
6
6
  "description": "Deja que Chocolatito Code trabaje dentro de tu Chrome, en segundo plano y con tus sesiones ya iniciadas.",
7
7
  "permissions": [
8
8
  "tabs",
@@ -0,0 +1,8 @@
1
+ {
2
+ "nombre": "diseno",
3
+ "titulo": "Diseño gráfico",
4
+ "descripcion": "Lotes de imágenes, exportación para redes y variantes de una misma pieza. Para quien trabaja con archivos, no con código.",
5
+ "autor": "Chocolatito",
6
+ "version": "1.0.0",
7
+ "requiere": ["ImageMagick (magick)", "ffmpeg"]
8
+ }
@@ -0,0 +1,26 @@
1
+ # Cómo trabajar conmigo
2
+
3
+ No soy programador. No me expliques el código que vas a escribir ni me pegues
4
+ scripts para que los revise: dime qué vas a hacer con mis archivos, en una línea,
5
+ y hazlo.
6
+
7
+ ## Lo que no se toca
8
+
9
+ - **Nunca sobreescribas un original.** Todo lo que proceses va a una carpeta
10
+ nueva (`salida/`, `export/`, `2048/`), y si esa carpeta ya existe, pregunta.
11
+ - Antes de un lote de más de veinte archivos, **haz uno solo y enséñamelo.** Si
12
+ ese está bien, sigue con el resto sin volver a preguntar.
13
+ - Si un archivo da error, sáltalo y sigue. Al final dime cuáles fallaron. No
14
+ pares el lote entero por uno roto.
15
+
16
+ ## Cómo enseñarme el resultado
17
+
18
+ Dime la ruta de la carpeta y cuántos archivos salieron, con su peso. Si algo
19
+ cambió de proporción o se recortó, avísame: eso es lo que se ve mal luego.
20
+
21
+ ## Palabras que uso
22
+
23
+ - "para web" = 72 ppp, sRGB, y comprimido de verdad (que pese poco).
24
+ - "para imprenta" = 300 ppp, sin tocar el color, sin recomprimir.
25
+ - "cuadrado" = recorte centrado, no deformar. Nunca estires una imagen.
26
+ - "limpio" = sin fondo, PNG con transparencia.
@@ -0,0 +1,56 @@
1
+ ---
2
+ name: exportar-para-redes
3
+ description: Usar cuando haya que sacar una misma pieza en los tamaños de Instagram, TikTok, YouTube, LinkedIn o historias — el clásico "dame esto en todos los formatos".
4
+ ---
5
+
6
+ # Exportar para redes
7
+
8
+ Las medidas, que son lo que nadie se acuerda:
9
+
10
+ | Sitio | Medida | Notas |
11
+ | --- | --- | --- |
12
+ | Instagram feed | 1080 × 1350 | El vertical 4:5 ocupa más pantalla que el cuadrado |
13
+ | Instagram cuadrado | 1080 × 1080 | |
14
+ | Historias / Reels / TikTok | 1080 × 1920 | 9:16 |
15
+ | YouTube miniatura | 1280 × 720 | Menos de 2 MB |
16
+ | YouTube Short | 1080 × 1920 | |
17
+ | LinkedIn | 1200 × 627 | |
18
+ | X / Twitter | 1600 × 900 | |
19
+ | Portada de Facebook | 1640 × 856 | |
20
+
21
+ ## Imagen
22
+
23
+ Todas de una vez, sin deformar nada:
24
+
25
+ ```bash
26
+ for m in 1080x1350 1080x1080 1080x1920 1280x720; do
27
+ magick original.jpg -resize ${m}^ -gravity center -extent $m -quality 88 "redes/${m}.jpg"
28
+ done
29
+ ```
30
+
31
+ **La zona segura importa más que la medida.** En historias y Reels, la interfaz
32
+ se come unos 250 px arriba y 350 abajo: si el texto va ahí, queda tapado por el
33
+ nombre de la cuenta o los botones. Cuando recortes a 9:16 desde algo horizontal,
34
+ dilo — casi siempre hay que recolocar el texto, y eso no lo arregla el recorte.
35
+
36
+ ## Vídeo
37
+
38
+ Vertical desde un horizontal, recortando por el centro:
39
+
40
+ ```bash
41
+ ffmpeg -i entrada.mp4 -vf "crop=ih*9/16:ih,scale=1080:1920" -c:a copy salida.mp4
42
+ ```
43
+
44
+ Con desenfoque a los lados en vez de recortar (se pierde menos):
45
+
46
+ ```bash
47
+ ffmpeg -i entrada.mp4 -lavfi "[0:v]scale=1080:-1,boxblur=20[fondo];[0:v]scale=1080:-1[primero];[fondo][primero]overlay=(W-w)/2:(H-h)/2" -c:a copy salida.mp4
48
+ ```
49
+
50
+ Límites que hacen que te rechacen la subida: TikTok y Reels, **menos de 90 s**
51
+ para que no baje la calidad; miniatura de YouTube, **menos de 2 MB**.
52
+
53
+ ## Al terminar
54
+
55
+ Enseña la carpeta con cada archivo y su peso, y avisa de cualquiera que se haya
56
+ recortado de forma que pueda comerse algo importante.
@@ -0,0 +1,72 @@
1
+ ---
2
+ name: lote-de-imagenes
3
+ description: Usar cuando haya que procesar muchas imágenes de una carpeta a la vez — redimensionar, recortar, convertir de formato, comprimir para web, renombrar en serie o quitar el fondo.
4
+ ---
5
+
6
+ # Lote de imágenes
7
+
8
+ Un lote es cualquier cosa que el usuario diría como "a todas las de esta
9
+ carpeta". La herramienta es **ImageMagick** (`magick`), y el trabajo va por
10
+ `run_command`.
11
+
12
+ ## Antes de empezar, siempre
13
+
14
+ 1. Comprueba que ImageMagick está: `magick -version`. Si no está, dilo y para —
15
+ en Windows se instala con `winget install ImageMagick.ImageMagick`.
16
+ 2. Cuenta lo que hay: `magick identify -format "%f %wx%h\n" *.jpg | wc -l`.
17
+ 3. **Crea la carpeta de salida.** Nunca escribas encima del original.
18
+ 4. **Haz el primero y enséñalo.** Solo entonces sigue con el resto.
19
+
20
+ ## Recetas que funcionan
21
+
22
+ **Redimensionar cabiendo dentro de una caja** (no deforma nunca):
23
+
24
+ ```bash
25
+ magick mogrify -path salida/ -resize 2048x2048\> -quality 85 *.jpg
26
+ ```
27
+
28
+ El `\>` es la parte importante: encoge lo que es más grande y **no agranda** lo
29
+ que ya es pequeño, que es lo que arruina una foto.
30
+
31
+ **Cuadrado recortando por el centro**, para una cuadrícula de Instagram:
32
+
33
+ ```bash
34
+ magick mogrify -path salida/ -resize 1080x1080^ -gravity center -extent 1080x1080 *.jpg
35
+ ```
36
+
37
+ `^` llena la caja y `-extent` corta lo que sobra. Sin `^` quedan bandas.
38
+
39
+ **Comprimir para web de verdad** (esto es lo que baja el peso, no el tamaño):
40
+
41
+ ```bash
42
+ magick mogrify -path web/ -strip -interlace Plane -sampling-factor 4:2:0 -quality 82 *.jpg
43
+ ```
44
+
45
+ `-strip` quita los metadatos (a veces la mitad del archivo). Para WebP, que pesa
46
+ un tercio: `-format webp -quality 80`.
47
+
48
+ **A PNG con transparencia, quitando un fondo liso:**
49
+
50
+ ```bash
51
+ magick entrada.jpg -fuzz 12% -transparent white salida.png
52
+ ```
53
+
54
+ Sube el `-fuzz` si el fondo no es un blanco perfecto. Para fondos complejos esto
55
+ no vale: hace falta una herramienta de recorte por IA, dilo y no lo intentes.
56
+
57
+ **Renombrar en serie con numeración:**
58
+
59
+ ```bash
60
+ magick mogrify -path salida/ -format jpg *.HEIC # y de paso convierte
61
+ ```
62
+
63
+ ## Al terminar
64
+
65
+ Di cuántos salieron, cuánto pesaban antes y cuánto ahora:
66
+
67
+ ```bash
68
+ du -sh original/ salida/
69
+ ```
70
+
71
+ Y nombra los que fallaron, si hubo alguno. No hace falta explicar por qué falló
72
+ cada uno: la ruta y "no se pudo" basta.
@@ -0,0 +1,59 @@
1
+ ---
2
+ name: variantes-de-pieza
3
+ description: Usar cuando haya que hacer muchas versiones de una misma pieza cambiando el texto, el color o el idioma — banners por producto, certificados con nombres, carteles por ciudad, tarjetas por persona.
4
+ ---
5
+
6
+ # Variantes de una misma pieza
7
+
8
+ El encargo clásico: "la misma pieza, pero con estos cuarenta nombres". A mano son
9
+ dos horas; aquí son dos minutos y **ninguna errata**, porque el texto sale del
10
+ archivo de datos y no de copiar y pegar.
11
+
12
+ ## Cómo se hace
13
+
14
+ 1. **Pide los datos en una tabla.** Un CSV con una columna por cosa que cambia
15
+ (`nombre,cargo,ciudad`). Si el usuario los tiene en Excel, que exporte a CSV;
16
+ si los tiene sueltos, escríbelos tú en un CSV y enséñaselo antes de seguir.
17
+ 2. **La plantilla, en SVG.** Es texto plano, así que sustituir es trivial y el
18
+ resultado sale nítido a cualquier tamaño. Si el usuario trae un PNG o un JPG
19
+ de fondo, se incrusta dentro del SVG y el texto va encima.
20
+ 3. **Una variante primero.** Enséñala. Ahí se ven el tipo de letra, el encuadre y
21
+ si el texto largo se sale.
22
+ 4. El resto de golpe.
23
+
24
+ ## Plantilla y sustitución
25
+
26
+ ```svg
27
+ <svg xmlns="http://www.w3.org/2000/svg" width="1080" height="1080">
28
+ <image href="fondo.jpg" width="1080" height="1080"/>
29
+ <text x="540" y="600" text-anchor="middle" font-family="Inter, sans-serif"
30
+ font-size="72" font-weight="700" fill="#ffffff">{{NOMBRE}}</text>
31
+ </svg>
32
+ ```
33
+
34
+ Genera un SVG por fila sustituyendo `{{NOMBRE}}`, y conviértelos:
35
+
36
+ ```bash
37
+ magick -density 300 salida/variante-01.svg salida/variante-01.png
38
+ ```
39
+
40
+ Usa `create_image` con SVG cuando la pieza sea sencilla, y `run_command` con un
41
+ script cuando sean muchas.
42
+
43
+ ## Lo que se rompe siempre
44
+
45
+ - **El texto largo se sale.** Mira el nombre más largo del CSV ANTES de lanzar el
46
+ lote y ajusta el tamaño de letra a ese, no al primero.
47
+ - **Las tildes y las ñ.** El SVG va en UTF-8; si salen cuadraditos, es la fuente.
48
+ - **La fuente de marca.** Si no está instalada, el sistema pone otra y todas las
49
+ piezas salen mal a la vez. Compruébalo en la primera: `magick -list font | grep -i <fuente>`.
50
+ - **Nombres de archivo con acentos o barras** rompen el lote. Numera: `pieza-01`,
51
+ `pieza-02`, y aparte una tabla que diga cuál es cuál.
52
+
53
+ ## Al terminar
54
+
55
+ Una hoja de contactos para revisarlo todo de un vistazo:
56
+
57
+ ```bash
58
+ magick montage salida/*.png -tile 5x -geometry 300x300+8+8 revision.jpg
59
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chocolatito-code",
3
- "version": "1.6.8",
3
+ "version": "1.6.10",
4
4
  "description": "Agente autónomo de programación para la terminal, con control real del ordenador y del navegador. Desarrollado por Chocolatito.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -10,6 +10,7 @@
10
10
  "dist",
11
11
  "extension",
12
12
  "skills",
13
+ "kits",
13
14
  "LICENSE",
14
15
  "README.md",
15
16
  "COMPUTER-USE.md",