chocolatito-code 1.6.9 → 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 +71 -0
- package/dist/agent/context.js +11 -0
- package/dist/index.js +86 -0
- package/dist/kits/manager.d.ts +134 -0
- package/dist/kits/manager.js +329 -0
- package/dist/tools/definitions.js +32 -0
- package/dist/tools/runner.js +13 -0
- package/dist/ui/comandos.js +2 -0
- package/extension/manifest.json +1 -1
- package/kits/diseno/kit.json +8 -0
- package/kits/diseno/reglas.md +26 -0
- package/kits/diseno/skills/exportar-para-redes/SKILL.md +56 -0
- package/kits/diseno/skills/lote-de-imagenes/SKILL.md +72 -0
- package/kits/diseno/skills/variantes-de-pieza/SKILL.md +59 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -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`:
|
package/dist/agent/context.js
CHANGED
|
@@ -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: {
|
package/dist/tools/runner.js
CHANGED
|
@@ -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);
|
package/dist/ui/comandos.js
CHANGED
|
@@ -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>" },
|
package/extension/manifest.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"manifest_version": 3,
|
|
3
3
|
"minimum_chrome_version": "102",
|
|
4
4
|
"name": "Chocolatito Code",
|
|
5
|
-
"version": "1.6.
|
|
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.
|
|
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",
|