chocolatito-code 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +66 -0
- package/dist/agent/loop.js +129 -36
- package/dist/agent/salidaDelAgente.d.ts +43 -0
- package/dist/agent/salidaDelAgente.js +53 -0
- package/dist/config/nodeVersion.d.ts +13 -0
- package/dist/config/nodeVersion.js +26 -0
- package/dist/index.js +22 -7
- package/dist/mcp/client.d.ts +28 -0
- package/dist/mcp/client.js +62 -8
- package/dist/mcp/ide.d.ts +76 -0
- package/dist/mcp/ide.js +160 -0
- package/dist/mcp/manager.js +9 -0
- package/dist/tools/browserExtension.js +35 -4
- package/dist/tools/definitions.js +9 -2
- package/dist/tools/grepSearch.js +111 -23
- package/dist/tools/runner.js +1 -1
- package/dist/tools/writeFile.d.ts +16 -1
- package/dist/tools/writeFile.js +22 -1
- package/dist/ui/interrupt.js +3 -2
- package/dist/ui/keyboardGuard.d.ts +21 -0
- package/dist/ui/keyboardGuard.js +30 -1
- package/dist/ui/pegados.d.ts +50 -0
- package/dist/ui/pegados.js +83 -0
- package/dist/ui/prompt.d.ts +5 -0
- package/dist/ui/prompt.js +101 -16
- package/dist/ui/salida.d.ts +10 -0
- package/dist/ui/salida.js +50 -0
- package/extension/background.js +182 -11
- package/extension/content.js +446 -48
- package/extension/fuentes/Chocolatito-Marca.ttf +0 -0
- package/extension/iconos/128.png +0 -0
- package/extension/iconos/16.png +0 -0
- package/extension/iconos/32.png +0 -0
- package/extension/iconos/48.png +0 -0
- package/extension/iconos/logotipo.png +0 -0
- package/extension/manifest.json +26 -3
- package/extension/popup.html +16 -6
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -85,6 +85,55 @@ Si repite tres veces la misma llamada con el mismo resultado, se para y pregunta
|
|
|
85
85
|
seguir, o le dices que cambie de enfoque. Sin terminal (CI, `chocolatito "haz X"`) se corta
|
|
86
86
|
solo, para que un bucle que nadie está mirando no se coma la cuota.
|
|
87
87
|
|
|
88
|
+
## 🧩 VSCode
|
|
89
|
+
|
|
90
|
+
Si tienes la extensión instalada y abres `chocolatito` en la terminal integrada, **se conecta
|
|
91
|
+
solo**. No hay nada que configurar.
|
|
92
|
+
|
|
93
|
+
El truco está al revés de lo que parece: la extensión es un **servidor MCP** y el agente es el
|
|
94
|
+
cliente. Cada ventana de VSCode levanta un servidor en `127.0.0.1` y deja una ficha en
|
|
95
|
+
`~/.chocolatito/ide/` con su puerto, su token y la carpeta que tiene abierta. El agente busca la
|
|
96
|
+
ficha cuya carpeta contiene su directorio de trabajo.
|
|
97
|
+
|
|
98
|
+
Lo que gana el agente (lo único que no puede tener por su cuenta):
|
|
99
|
+
|
|
100
|
+
| Herramienta | Para qué |
|
|
101
|
+
| --- | --- |
|
|
102
|
+
| `diagnosticos` | Los errores del servidor de lenguaje. Los subrayados rojos, sin esperar a compilar. |
|
|
103
|
+
| `seleccion_actual` | Qué archivo tienes abierto y qué has seleccionado. Hace que «arregla esto» signifique algo. |
|
|
104
|
+
| `archivos_abiertos` | Las pestañas abiertas, en orden. |
|
|
105
|
+
| `mostrar_diff` | Abre el visor de diferencias nativo. No escribe nada: es para que lo veas antes. |
|
|
106
|
+
|
|
107
|
+
**Con varias ventanas abiertas se elige la correcta**, no la primera: gana la carpeta más
|
|
108
|
+
específica que contenga el directorio de trabajo, y si ninguna lo contiene **no se conecta a
|
|
109
|
+
nada**. Conectarse a la ventana de otro proyecto sería peor que no conectarse: los diffs saldrían
|
|
110
|
+
en el sitio equivocado.
|
|
111
|
+
|
|
112
|
+
El servidor escucha solo en `127.0.0.1` y exige un token de 32 bytes que va en la ficha, dentro de
|
|
113
|
+
tu carpeta personal. Sin eso, cualquier programa de la máquina podría manejar tu editor.
|
|
114
|
+
|
|
115
|
+
Para apagarlo: `CHOCOLATITO_SIN_EDITOR=1`, o la opción `chocolatito.puente` en VSCode.
|
|
116
|
+
|
|
117
|
+
### Instalarla
|
|
118
|
+
|
|
119
|
+
Mientras no esté en el marketplace, se instala desde el paquete:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
cd extension-vscode
|
|
123
|
+
npx @vscode/vsce package --allow-missing-repository --skip-license
|
|
124
|
+
code --install-extension chocolatito-code-vscode-0.1.1.vsix
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
**Con VSCode cerrado**, y reiniciándolo después. Copiar la carpeta a mano a `~/.vscode/extensions`
|
|
128
|
+
**no funciona**: falta el `.vsixmanifest` y VSCode la lista pero no la carga nunca, sin dar ningún
|
|
129
|
+
error. Para desarrollar, `F5` sobre la carpeta abre una ventana con la extensión cargada al vuelo.
|
|
130
|
+
|
|
131
|
+
En la barra de estado, abajo a la derecha, pone **Chocolatito** con el estado del puente. Si algo
|
|
132
|
+
falla —sin carpeta abierta, puente apagado, puerto ocupado— lo dice ahí, y al pulsarlo se abre el
|
|
133
|
+
detalle.
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
88
137
|
## 🔌 MCP (Model Context Protocol)
|
|
89
138
|
|
|
90
139
|
Conecta cualquier servidor MCP y sus herramientas quedan disponibles para el agente.
|
|
@@ -141,6 +190,23 @@ solo lectura de verdad: ni comandos, ni clics, ni escrituras.
|
|
|
141
190
|
|
|
142
191
|
`--yes` aprueba todo sin preguntar: es para CI, no para el día a día.
|
|
143
192
|
|
|
193
|
+
## 📋 Pegar texto largo
|
|
194
|
+
|
|
195
|
+
Pegar un log de 300 líneas ya no mete 300 líneas en el recuadro. Se guarda aparte y en el prompt
|
|
196
|
+
queda una marca corta:
|
|
197
|
+
|
|
198
|
+
```
|
|
199
|
+
❯ arregla esto [pegado #1 · 347 lineas]
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
**El modelo recibe el texto entero**, no la marca: al enviar se sustituye por lo que pegaste. Y un
|
|
203
|
+
solo `backspace` con el cursor detrás de la marca se la lleva completa — lo que ocupa una línea se
|
|
204
|
+
borra como una línea.
|
|
205
|
+
|
|
206
|
+
Por debajo de 5 líneas o 400 caracteres se pega normal, entero y a la vista.
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
144
210
|
## ⌨️ Escribir mientras trabaja
|
|
145
211
|
|
|
146
212
|
No hace falta esperar a que termine. Lo que escribas durante una generación aparece bajo
|
package/dist/agent/loop.js
CHANGED
|
@@ -15,6 +15,7 @@ import { PermissionManager, commandSignature, isDangerousCommand } from "../conf
|
|
|
15
15
|
import { askToolPermission } from "../ui/permissionPrompt.js";
|
|
16
16
|
import { DynamicSpinner } from "../ui/spinner.js";
|
|
17
17
|
import { globalUndoManager } from "./undoManager.js";
|
|
18
|
+
import { salida } from "./salidaDelAgente.js";
|
|
18
19
|
import { InterruptWatcher, isAbortError } from "../ui/interrupt.js";
|
|
19
20
|
import { HookManager } from "../hooks/manager.js";
|
|
20
21
|
import { registerToolGate, autorizarHerramienta, notificarPostHerramienta } from "./toolGate.js";
|
|
@@ -33,6 +34,15 @@ const HERRAMIENTAS_QUE_ESCRIBEN = new Set([
|
|
|
33
34
|
]);
|
|
34
35
|
/** Cortes por longitud seguidos que se toleran antes de rendirse. */
|
|
35
36
|
const MAX_CORTES_POR_LONGITUD = 2;
|
|
37
|
+
/**
|
|
38
|
+
* Respuestas vacias seguidas que se toleran antes de rendirse.
|
|
39
|
+
*
|
|
40
|
+
* Dos, no una: una respuesta vacia suelta es casi siempre un hipo del motor y
|
|
41
|
+
* el reintento la resuelve sin que el usuario se entere. Tres seguidas ya no es
|
|
42
|
+
* un hipo, y seguir reintentando en silencio es lo que hacia que pareciera que
|
|
43
|
+
* el agente te ignora.
|
|
44
|
+
*/
|
|
45
|
+
const MAX_RESPUESTAS_VACIAS = 1;
|
|
36
46
|
/** Mensajes recientes que la compactacion no resume nunca. */
|
|
37
47
|
const COLA_INTACTA = 4;
|
|
38
48
|
/**
|
|
@@ -66,8 +76,43 @@ function parsearArgumentos(bruto) {
|
|
|
66
76
|
/** Lo que se le dice al modelo cuando sus argumentos llegaron a medias. */
|
|
67
77
|
function mensajeDeArgumentosRotos(herramienta) {
|
|
68
78
|
return (`No se ejecuto "${herramienta}": los argumentos llegaron incompletos, seguramente porque la ` +
|
|
69
|
-
`respuesta se corto por longitud. No se ha hecho nada.
|
|
70
|
-
|
|
79
|
+
`respuesta se corto por longitud. No se ha hecho nada.
|
|
80
|
+
|
|
81
|
+
` +
|
|
82
|
+
`Si era un archivo largo, hazlo por partes: una primera llamada a write_file con el principio ` +
|
|
83
|
+
`y las siguientes con modo="anadir" para ir pegando el resto. Cada trozo, por debajo de unas ` +
|
|
84
|
+
`400 lineas.`);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Deja una llamada a herramienta en un estado que la API acepte.
|
|
88
|
+
*
|
|
89
|
+
* EL FALLO QUE CIERRA ESTO
|
|
90
|
+
*
|
|
91
|
+
* Cuando la respuesta se corta a mitad de un `write_file` con un HTML largo
|
|
92
|
+
* dentro, los argumentos llegan asi:
|
|
93
|
+
*
|
|
94
|
+
* {"path":"index.html","content":"<!doctype html><html><head><style>body{ma
|
|
95
|
+
*
|
|
96
|
+
* La herramienta ya no se ejecutaba -eso se arreglo antes-, pero la llamada se
|
|
97
|
+
* guardaba en el historial TAL CUAL, y el historial se reenvia entero en cada
|
|
98
|
+
* peticion. La API valida que los argumentos sean JSON, no lo son, y contesta
|
|
99
|
+
* 400. Desde ese momento la sesion queda inservible: todo lo que escriba el
|
|
100
|
+
* usuario falla igual, la unica salida es /clear, y nadie lo sabe.
|
|
101
|
+
*
|
|
102
|
+
* Se vio en una sesion real, y encima el 400 se llevo por delante los cuatro
|
|
103
|
+
* huecos de peticion de la licencia, asi que al fallo se le sumo un "Tienes 4
|
|
104
|
+
* peticiones en curso" de dos minutos y medio.
|
|
105
|
+
*
|
|
106
|
+
* Guardando "{}" el historial sigue siendo valido y la pareja llamada/respuesta
|
|
107
|
+
* cuadra. Al modelo no se le oculta nada: el resultado de esa herramienta ya le
|
|
108
|
+
* dice que sus argumentos llegaron a medias y que no se hizo nada.
|
|
109
|
+
*/
|
|
110
|
+
function sanearLlamada(tc) {
|
|
111
|
+
const bruto = tc?.function?.arguments;
|
|
112
|
+
const texto = typeof bruto === "string" ? bruto.trim() : "";
|
|
113
|
+
if (texto && parsearArgumentos(texto).ok)
|
|
114
|
+
return tc;
|
|
115
|
+
return { ...tc, function: { ...tc.function, arguments: "{}" } };
|
|
71
116
|
}
|
|
72
117
|
export class AgentLoop {
|
|
73
118
|
client;
|
|
@@ -223,7 +268,7 @@ export class AgentLoop {
|
|
|
223
268
|
const proyectado = antes > 0 ? Math.round(tokensActuales * (despues / antes)) : tokensActuales;
|
|
224
269
|
this.lastTurnTokens = proyectado;
|
|
225
270
|
if (proyectado < COMPACT_THRESHOLD_TOKENS * 0.8) {
|
|
226
|
-
|
|
271
|
+
salida.linea(chalk.hex("#D97757")("\n⚡ [Auto-Compact]: ") +
|
|
227
272
|
chalk.gray(`Liberados ~${(antes - despues).toLocaleString()} tokens de salidas de herramientas antiguas. ` +
|
|
228
273
|
"La conversacion se conserva entera.\n"));
|
|
229
274
|
return true;
|
|
@@ -278,7 +323,7 @@ export class AgentLoop {
|
|
|
278
323
|
// Queda una estimacion hasta la siguiente respuesta de la API: mejor eso
|
|
279
324
|
// que arrastrar la cifra vieja, que dispararia otra compactacion seguida.
|
|
280
325
|
this.lastTurnTokens = estimarTokens(this.messages);
|
|
281
|
-
|
|
326
|
+
salida.linea(chalk.hex("#D97757")("\n⚡ [Auto-Compact]: ") +
|
|
282
327
|
chalk.gray(`Contexto optimizado automáticamente (${this.messages.length} mensajes limpios) para mantener alta velocidad y mínimo consumo de API.\n`));
|
|
283
328
|
return true;
|
|
284
329
|
}
|
|
@@ -286,7 +331,7 @@ export class AgentLoop {
|
|
|
286
331
|
spinner.stop();
|
|
287
332
|
// El catch mudo era medio fallo: si el resumen no sale, el turno sigue con
|
|
288
333
|
// el contexto entero y el error de despues llega sin explicacion.
|
|
289
|
-
|
|
334
|
+
salida.linea(chalk.gray(` (no se pudo compactar el contexto: ${err?.message || String(err)})`));
|
|
290
335
|
return false;
|
|
291
336
|
}
|
|
292
337
|
}
|
|
@@ -307,7 +352,7 @@ export class AgentLoop {
|
|
|
307
352
|
if (!cmd)
|
|
308
353
|
return null;
|
|
309
354
|
if (reflexiones >= MAX_REFLEXIONES) {
|
|
310
|
-
|
|
355
|
+
salida.linea(chalk.yellow(`\n⚠ ${mensajeDeRendicion(cmd.comando)}\n`));
|
|
311
356
|
return null;
|
|
312
357
|
}
|
|
313
358
|
const spinner = new DynamicSpinner();
|
|
@@ -320,13 +365,13 @@ export class AgentLoop {
|
|
|
320
365
|
spinner.stop();
|
|
321
366
|
}
|
|
322
367
|
if (resultado.ok) {
|
|
323
|
-
|
|
368
|
+
salida.linea(chalk.gray(` ✔ Verificado con ${cmd.comando}\n`));
|
|
324
369
|
// Lo verificado deja de estar pendiente: si el modelo vuelve a editar,
|
|
325
370
|
// se vuelve a comprobar; si no, no se comprueba dos veces lo mismo.
|
|
326
371
|
this.archivosTocados.clear();
|
|
327
372
|
return null;
|
|
328
373
|
}
|
|
329
|
-
|
|
374
|
+
salida.linea(chalk.yellow(`\n⚠ La verificacion fallo (${cmd.comando}). Se lo devuelvo al agente.\n`));
|
|
330
375
|
return mensajeDeVerificacion(cmd.comando, resultado.salida, reflexiones + 1);
|
|
331
376
|
}
|
|
332
377
|
/**
|
|
@@ -415,7 +460,7 @@ export class AgentLoop {
|
|
|
415
460
|
const cuota = checkQuota();
|
|
416
461
|
if (!cuota.allowed) {
|
|
417
462
|
const corte = mensajeDeCorte(cuota);
|
|
418
|
-
|
|
463
|
+
salida.linea(chalk.yellow(`\n⏸ ${corte}`) +
|
|
419
464
|
chalk.gray("\n Tu trabajo y tu contexto siguen aqui: al liberarse, continuas donde lo dejaste.\n"));
|
|
420
465
|
return parada("cuota", corte);
|
|
421
466
|
}
|
|
@@ -438,6 +483,8 @@ export class AgentLoop {
|
|
|
438
483
|
let reflexiones = 0;
|
|
439
484
|
/** Veces seguidas que la respuesta se corto por longitud sin hacer nada. */
|
|
440
485
|
let cortesPorLongitud = 0;
|
|
486
|
+
/** Veces seguidas que el turno acabo sin una sola letra que ensenar. */
|
|
487
|
+
let respuestasVacias = 0;
|
|
441
488
|
// Se limpia aqui: lo que se edito en el turno anterior ya se verifico (o no
|
|
442
489
|
// se iba a verificar nunca).
|
|
443
490
|
this.archivosTocados.clear();
|
|
@@ -479,7 +526,7 @@ export class AgentLoop {
|
|
|
479
526
|
}, { signal }), {
|
|
480
527
|
onRetry: (attempt, delay, reason) => {
|
|
481
528
|
spinner.stop();
|
|
482
|
-
|
|
529
|
+
salida.linea(chalk.yellow(`\n⟳ El motor respondio ${reason}. Reintento ${attempt} en ${(delay / 1000).toFixed(1)}s...`));
|
|
483
530
|
spinner.start();
|
|
484
531
|
},
|
|
485
532
|
});
|
|
@@ -488,7 +535,7 @@ export class AgentLoop {
|
|
|
488
535
|
spinner.stop();
|
|
489
536
|
watcher.stop();
|
|
490
537
|
if (isAbortError(err) || watcher.interrupted) {
|
|
491
|
-
|
|
538
|
+
salida.linea(chalk.yellow("\n⏸ Interrumpido. Dime como seguir."));
|
|
492
539
|
return parada("interrumpido");
|
|
493
540
|
}
|
|
494
541
|
// El contexto no cabe. Reintentar la misma peticion es imposible por
|
|
@@ -497,7 +544,7 @@ export class AgentLoop {
|
|
|
497
544
|
// sola vez: si tras compactar sigue sin caber, el problema es otro.
|
|
498
545
|
if (isContextOverflow(err) && !yaCompactadoPorDesbordamiento) {
|
|
499
546
|
yaCompactadoPorDesbordamiento = true;
|
|
500
|
-
|
|
547
|
+
salida.linea(chalk.yellow("\n⚠ La peticion no cabia en el contexto. Compactando y reintentando..."));
|
|
501
548
|
const activo = this.messages[0]?.role === "system" ? this.messages[0].content : "";
|
|
502
549
|
await this.compactHistory(activo);
|
|
503
550
|
iterations--; // este intento no llego al modelo: no cuenta como vuelta
|
|
@@ -508,8 +555,8 @@ export class AgentLoop {
|
|
|
508
555
|
// hace que el usuario piense que el producto esta roto.
|
|
509
556
|
const claro = explicarErrorDelMotor(err);
|
|
510
557
|
if (claro) {
|
|
511
|
-
|
|
512
|
-
⏸ ${claro}
|
|
558
|
+
salida.linea(chalk.yellow(`
|
|
559
|
+
⏸ ${claro}
|
|
513
560
|
`));
|
|
514
561
|
return parada(motivoDeParadaDelError(err), claro);
|
|
515
562
|
}
|
|
@@ -589,13 +636,13 @@ export class AgentLoop {
|
|
|
589
636
|
if (content.trim()) {
|
|
590
637
|
this.messages.push({ role: "assistant", content });
|
|
591
638
|
}
|
|
592
|
-
|
|
639
|
+
salida.linea(chalk.yellow("\n⏸ Interrumpido. Dime como seguir."));
|
|
593
640
|
return parada("interrumpido");
|
|
594
641
|
}
|
|
595
642
|
const claroStream = explicarErrorDelMotor(streamErr);
|
|
596
643
|
if (claroStream) {
|
|
597
|
-
|
|
598
|
-
⏸ ${claroStream}
|
|
644
|
+
salida.linea(chalk.yellow(`
|
|
645
|
+
⏸ ${claroStream}
|
|
599
646
|
`));
|
|
600
647
|
return parada(motivoDeParadaDelError(streamErr), claroStream);
|
|
601
648
|
}
|
|
@@ -610,7 +657,7 @@ export class AgentLoop {
|
|
|
610
657
|
watcher.stop();
|
|
611
658
|
if (content.trim())
|
|
612
659
|
this.messages.push({ role: "assistant", content });
|
|
613
|
-
|
|
660
|
+
salida.linea(chalk.yellow("\n⏸ Interrumpido antes de ejecutar las herramientas. Dime como seguir."));
|
|
614
661
|
return parada("interrumpido");
|
|
615
662
|
}
|
|
616
663
|
// Filter valid accumulated tool calls
|
|
@@ -630,19 +677,20 @@ export class AgentLoop {
|
|
|
630
677
|
if (cortesPorLongitud > MAX_CORTES_POR_LONGITUD) {
|
|
631
678
|
const aviso = `La respuesta se corto por longitud ${cortesPorLongitud} veces seguidas sin llegar a ` +
|
|
632
679
|
`hacer nada. Prueba a pedirlo por partes.`;
|
|
633
|
-
|
|
680
|
+
salida.linea(chalk.yellow(`\n ⚠ ${aviso}\n`));
|
|
634
681
|
if (content.trim())
|
|
635
682
|
this.messages.push({ role: "assistant", content });
|
|
636
683
|
return parada("limite-iteraciones", aviso);
|
|
637
684
|
}
|
|
638
|
-
|
|
685
|
+
salida.linea(chalk.yellow("\n ⚠ La respuesta se corto por longitud antes de hacer nada. Pidiendole que vaya al grano...\n"));
|
|
639
686
|
if (content.trim())
|
|
640
687
|
this.messages.push({ role: "assistant", content });
|
|
641
688
|
this.messages.push({
|
|
642
689
|
role: "user",
|
|
643
690
|
content: "Tu respuesta anterior se corto porque agoto el presupuesto de salida mientras razonabas, " +
|
|
644
691
|
"y no llegaste a usar ninguna herramienta: no se ha hecho nada todavia. Deja de planificar " +
|
|
645
|
-
"y actua ya. Si lo que hay que escribir es largo,
|
|
692
|
+
"y actua ya. Si lo que hay que escribir es largo, empieza con write_file y sigue con " +
|
|
693
|
+
'write_file en modo="anadir", en trozos de menos de 400 lineas.',
|
|
646
694
|
});
|
|
647
695
|
// Sin esto se queda un vigilante de teclado vivo por cada corte, y cada
|
|
648
696
|
// uno vuelve a apuntarse como oyente: la misma tecla se procesa tantas
|
|
@@ -655,7 +703,10 @@ export class AgentLoop {
|
|
|
655
703
|
this.messages.push({
|
|
656
704
|
role: "assistant",
|
|
657
705
|
content: content || null,
|
|
658
|
-
|
|
706
|
+
// Sanear NO es cosmetico: unos argumentos cortados guardados tal cual
|
|
707
|
+
// hacen que la API conteste 400 en TODAS las peticiones siguientes, y
|
|
708
|
+
// la sesion queda inservible hasta un /clear. Ver sanearLlamada().
|
|
709
|
+
tool_calls: toolCalls.map(sanearLlamada),
|
|
659
710
|
});
|
|
660
711
|
const toolResults = [];
|
|
661
712
|
// Las llamadas independientes de solo lectura se lanzan a la vez. Cuando
|
|
@@ -733,7 +784,7 @@ export class AgentLoop {
|
|
|
733
784
|
toolResults.forEach((r) => this.messages.push(r));
|
|
734
785
|
watcher.stop();
|
|
735
786
|
if (watcher.interrupted) {
|
|
736
|
-
|
|
787
|
+
salida.linea(chalk.yellow("\n⏸ Interrumpido. Dime como seguir."));
|
|
737
788
|
return parada("interrumpido");
|
|
738
789
|
}
|
|
739
790
|
continue;
|
|
@@ -781,7 +832,7 @@ export class AgentLoop {
|
|
|
781
832
|
fnArgs = { ...fnArgs, ...pre.updatedInput };
|
|
782
833
|
}
|
|
783
834
|
if (pre.systemMessage) {
|
|
784
|
-
|
|
835
|
+
salida.linea(chalk.yellow(`\n⚑ ${pre.systemMessage}`));
|
|
785
836
|
}
|
|
786
837
|
// Modo plan: bloqueo duro, no un dialogo. Lo que el modo promete es
|
|
787
838
|
// solo lectura, asi que aqui no hay nada que preguntar.
|
|
@@ -848,7 +899,7 @@ export class AgentLoop {
|
|
|
848
899
|
// El systemMessage de PreToolUse si se imprimia y el de PostToolUse
|
|
849
900
|
// no: el mismo campo, del mismo hook, se veia o no segun el evento.
|
|
850
901
|
if (post.systemMessage) {
|
|
851
|
-
|
|
902
|
+
salida.linea(chalk.yellow(`\n⚑ ${post.systemMessage}`));
|
|
852
903
|
}
|
|
853
904
|
// El dialogo del bucle usa stdin, igual que el de permisos: hay que
|
|
854
905
|
// soltarle el terminal al vigilante de Esc. Pero solo si de verdad se
|
|
@@ -859,7 +910,7 @@ export class AgentLoop {
|
|
|
859
910
|
toolResults.push({
|
|
860
911
|
role: "tool",
|
|
861
912
|
tool_call_id: tc.id,
|
|
862
|
-
content: post.additionalContext ? `${texto}\n\n
|
|
913
|
+
content: post.additionalContext ? `${texto}\n\n
|
|
863
914
|
[Hook PostToolUse]: ${post.additionalContext}` : texto,
|
|
864
915
|
});
|
|
865
916
|
}
|
|
@@ -867,20 +918,62 @@ export class AgentLoop {
|
|
|
867
918
|
toolResults.forEach((r) => this.messages.push(r));
|
|
868
919
|
watcher.stop();
|
|
869
920
|
if (watcher.interrupted) {
|
|
870
|
-
|
|
921
|
+
salida.linea(chalk.yellow("\n⏸ Interrumpido. Dime como seguir."));
|
|
871
922
|
return parada("interrumpido");
|
|
872
923
|
}
|
|
873
924
|
continue;
|
|
874
925
|
}
|
|
875
926
|
watcher.stop();
|
|
876
|
-
//
|
|
877
|
-
|
|
878
|
-
|
|
927
|
+
// EL TURNO ACABA SIN UNA SOLA LETRA QUE ENSENAR
|
|
928
|
+
//
|
|
929
|
+
// Salio de un caso real: cuatro ordenes seguidas ("puedes conectarte a mi
|
|
930
|
+
// vscode?", "si?", "puedes?", "a mi vscode") contestadas todas con esto:
|
|
931
|
+
//
|
|
932
|
+
// 💭 penso 1s
|
|
933
|
+
// ⚡ 4.8s · 0 tokens salida · $0.00000 USD · 100% contexto libre
|
|
934
|
+
//
|
|
935
|
+
// Ni un caracter en pantalla, ni un error, ni un aviso. Desde fuera es
|
|
936
|
+
// identico a que el agente te ignore, y el usuario reescribe la orden
|
|
937
|
+
// pensando que no llego. Encima cada una de esas cuatro se llevo por
|
|
938
|
+
// delante un hueco de peticion en el proxy, asi que la quinta choco con
|
|
939
|
+
// "Tienes 4 peticiones en curso": el silencio no solo confunde, ademas
|
|
940
|
+
// deja al usuario sin servicio.
|
|
941
|
+
//
|
|
942
|
+
// Es el mismo agujero que el corte por longitud, en su otra cara: el
|
|
943
|
+
// codigo no distingue "el modelo termino" de "no vino nada". Sin
|
|
944
|
+
// herramientas y sin texto, el bucle daba el turno por hecho, imprimia el
|
|
945
|
+
// pie y volvia al prompt.
|
|
946
|
+
if (!content.trim()) {
|
|
947
|
+
respuestasVacias++;
|
|
948
|
+
// Se reintenta UNA vez, no mas, y aqui la moderacion importa: cada
|
|
949
|
+
// intento consume un hueco de peticion de la licencia, y solo hay
|
|
950
|
+
// cuatro. Insistir con un motor que no contesta es la forma mas rapida
|
|
951
|
+
// de quedarse sin ninguno.
|
|
952
|
+
if (respuestasVacias > MAX_RESPUESTAS_VACIAS) {
|
|
953
|
+
const aviso = `El motor devolvio ${respuestasVacias} respuestas vacias seguidas. ` +
|
|
954
|
+
"No es tu conexion ni tu orden: no esta llegando nada del modelo. " +
|
|
955
|
+
"Espera un momento y reintenta, o /clear si venias de una conversacion larga.";
|
|
956
|
+
salida.linea(chalk.yellow(`\n ⚠ ${aviso}\n`));
|
|
957
|
+
return parada("error-motor", aviso);
|
|
958
|
+
}
|
|
959
|
+
salida.linea(chalk.gray("\n ⚠ El motor no devolvio nada. Reintentando una vez…\n"));
|
|
879
960
|
this.messages.push({
|
|
880
|
-
role: "
|
|
881
|
-
content
|
|
961
|
+
role: "user",
|
|
962
|
+
content: "Tu respuesta anterior llego vacia: ni texto ni llamadas a herramienta. " +
|
|
963
|
+
"El usuario no ha visto absolutamente nada. Contesta a lo que te pidio, " +
|
|
964
|
+
"aunque sea para decir que no puedes hacerlo o que no lo has entendido.",
|
|
882
965
|
});
|
|
966
|
+
continue;
|
|
883
967
|
}
|
|
968
|
+
// Una respuesta con contenido cierra la racha: lo que cuenta son las
|
|
969
|
+
// vacias SEGUIDAS, no las de toda la sesion.
|
|
970
|
+
respuestasVacias = 0;
|
|
971
|
+
// If text content was printed, ensure newline and record assistant message
|
|
972
|
+
salida.crudo("\n\n");
|
|
973
|
+
this.messages.push({
|
|
974
|
+
role: "assistant",
|
|
975
|
+
content,
|
|
976
|
+
});
|
|
884
977
|
// El modelo da el turno por cerrado. Si toco archivos y el proyecto tiene
|
|
885
978
|
// verificacion automatica activada, se comprueba ANTES de creerselo: un
|
|
886
979
|
// error de tipos que nadie mira se queda en el disco y los turnos
|
|
@@ -914,19 +1007,19 @@ export class AgentLoop {
|
|
|
914
1007
|
const tras = checkQuota();
|
|
915
1008
|
renderFooter(formattedCost, turnCompletionTokens, turnCachedTokens, this.currentEspectro.name, durationMs, contextPercent, quotaBadge(tras));
|
|
916
1009
|
if (auxiliar.total > 0) {
|
|
917
|
-
|
|
1010
|
+
salida.linea(chalk.gray(` + $${auxiliar.total.toFixed(4)} USD fuera del bucle (${auxiliar.rutas}), ya contados en la cuota.\n`));
|
|
918
1011
|
}
|
|
919
1012
|
// Aviso al acercarse, no solo al chocar: quedarse a medias sin previo aviso
|
|
920
1013
|
// es lo que hace que la gente cancele.
|
|
921
1014
|
if (tras.allowed && tras.spentWeekUsd >= tras.config.weeklyBudgetUsd * 0.8) {
|
|
922
1015
|
const queda = Math.max(0, tras.config.weeklyBudgetUsd - tras.spentWeekUsd);
|
|
923
|
-
|
|
1016
|
+
salida.linea(chalk.yellow(` ⚠ Te queda poca cuota semanal (${((queda / tras.config.weeklyBudgetUsd) * 100).toFixed(0)}%).\n`));
|
|
924
1017
|
}
|
|
925
1018
|
if (!completado) {
|
|
926
1019
|
// Se agotaron las iteraciones con trabajo a medias. En interactivo se ve
|
|
927
1020
|
// porque el ultimo mensaje queda cortado; en CI no lo ve nadie.
|
|
928
1021
|
const aviso = `El turno se agoto en el limite de ${this.maxIterations} iteraciones sin cerrar la tarea.`;
|
|
929
|
-
|
|
1022
|
+
salida.linea(chalk.yellow(` ⚠ ${aviso}\n`));
|
|
930
1023
|
return parada("limite-iteraciones", aviso);
|
|
931
1024
|
}
|
|
932
1025
|
return parada("completado");
|
|
@@ -948,7 +1041,7 @@ export class AgentLoop {
|
|
|
948
1041
|
const command = typeof fnArgs?.command === "string" ? fnArgs.command : "";
|
|
949
1042
|
if (command) {
|
|
950
1043
|
if (isDangerousCommand(command)) {
|
|
951
|
-
|
|
1044
|
+
salida.linea(chalk.gray(" (este comando es destructivo: se seguira preguntando cada vez, no se puede recordar)\n"));
|
|
952
1045
|
return;
|
|
953
1046
|
}
|
|
954
1047
|
const firma = commandSignature(command);
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Por donde habla el bucle del agente.
|
|
3
|
+
*
|
|
4
|
+
* POR QUE
|
|
5
|
+
*
|
|
6
|
+
* El bucle escribia con `console.log` en diecisiete sitios. Mientras el unico
|
|
7
|
+
* sitio donde se ve al agente sea una terminal eso no molesta, pero ata el
|
|
8
|
+
* motor a su pantalla: para poner el agente en un panel de VSCode, en una web o
|
|
9
|
+
* en una app, hay que arrancarle esos diecisiete `console.log` uno a uno, y
|
|
10
|
+
* cada uno es una ocasion de romper algo.
|
|
11
|
+
*
|
|
12
|
+
* Con esto en medio, el bucle deja de saber donde acaba lo que dice. La
|
|
13
|
+
* terminal pasa a ser UNA implementacion, no LA implementacion.
|
|
14
|
+
*
|
|
15
|
+
* POR QUE RECIBE EL TEXTO YA COLOREADO
|
|
16
|
+
*
|
|
17
|
+
* Es a proposito, y es lo que hace que este cambio no pueda romper nada: lo que
|
|
18
|
+
* el bucle manda es exactamente la misma cadena que antes le pasaba a
|
|
19
|
+
* `console.log`, colores incluidos. La salida por terminal queda byte a byte
|
|
20
|
+
* igual que ayer, y las 538 pruebas siguen valiendo como red.
|
|
21
|
+
*
|
|
22
|
+
* Un panel que quiera pintarlo a su manera quita los codigos de color con
|
|
23
|
+
* `sinColores()` y se queda con el texto. Cuando haga falta distinguir un aviso
|
|
24
|
+
* de una nota, se anaden metodos con significado; hoy no hace falta y adivinarlo
|
|
25
|
+
* seria inventar trabajo.
|
|
26
|
+
*/
|
|
27
|
+
export interface SalidaDelAgente {
|
|
28
|
+
/** Un mensaje suelto, con su salto de linea al final. Equivale a console.log. */
|
|
29
|
+
linea(texto: string): void;
|
|
30
|
+
/** Escritura sin salto, para separar bloques. Equivale a process.stdout.write. */
|
|
31
|
+
crudo(texto: string): void;
|
|
32
|
+
}
|
|
33
|
+
/** Lo que usa el bucle. No sabe ni le importa quien esta al otro lado. */
|
|
34
|
+
export declare const salida: SalidaDelAgente;
|
|
35
|
+
/**
|
|
36
|
+
* Cambia a donde habla el agente. Con `null` vuelve a la terminal.
|
|
37
|
+
*
|
|
38
|
+
* Lo usara el panel de VSCode. Devolver siempre a la terminal al terminar: un
|
|
39
|
+
* agente que habla a un panel cerrado no habla con nadie.
|
|
40
|
+
*/
|
|
41
|
+
export declare function dirigirSalidaDelAgente(destino: SalidaDelAgente | null): void;
|
|
42
|
+
/** Quita los codigos de color, para quien pinte el texto a su manera. */
|
|
43
|
+
export declare function sinColores(texto: string): string;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Por donde habla el bucle del agente.
|
|
3
|
+
*
|
|
4
|
+
* POR QUE
|
|
5
|
+
*
|
|
6
|
+
* El bucle escribia con `console.log` en diecisiete sitios. Mientras el unico
|
|
7
|
+
* sitio donde se ve al agente sea una terminal eso no molesta, pero ata el
|
|
8
|
+
* motor a su pantalla: para poner el agente en un panel de VSCode, en una web o
|
|
9
|
+
* en una app, hay que arrancarle esos diecisiete `console.log` uno a uno, y
|
|
10
|
+
* cada uno es una ocasion de romper algo.
|
|
11
|
+
*
|
|
12
|
+
* Con esto en medio, el bucle deja de saber donde acaba lo que dice. La
|
|
13
|
+
* terminal pasa a ser UNA implementacion, no LA implementacion.
|
|
14
|
+
*
|
|
15
|
+
* POR QUE RECIBE EL TEXTO YA COLOREADO
|
|
16
|
+
*
|
|
17
|
+
* Es a proposito, y es lo que hace que este cambio no pueda romper nada: lo que
|
|
18
|
+
* el bucle manda es exactamente la misma cadena que antes le pasaba a
|
|
19
|
+
* `console.log`, colores incluidos. La salida por terminal queda byte a byte
|
|
20
|
+
* igual que ayer, y las 538 pruebas siguen valiendo como red.
|
|
21
|
+
*
|
|
22
|
+
* Un panel que quiera pintarlo a su manera quita los codigos de color con
|
|
23
|
+
* `sinColores()` y se queda con el texto. Cuando haga falta distinguir un aviso
|
|
24
|
+
* de una nota, se anaden metodos con significado; hoy no hace falta y adivinarlo
|
|
25
|
+
* seria inventar trabajo.
|
|
26
|
+
*/
|
|
27
|
+
const A_LA_TERMINAL = {
|
|
28
|
+
linea: (texto) => {
|
|
29
|
+
console.log(texto);
|
|
30
|
+
},
|
|
31
|
+
crudo: (texto) => {
|
|
32
|
+
process.stdout.write(texto);
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
let actual = A_LA_TERMINAL;
|
|
36
|
+
/** Lo que usa el bucle. No sabe ni le importa quien esta al otro lado. */
|
|
37
|
+
export const salida = {
|
|
38
|
+
linea: (texto) => actual.linea(texto),
|
|
39
|
+
crudo: (texto) => actual.crudo(texto),
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Cambia a donde habla el agente. Con `null` vuelve a la terminal.
|
|
43
|
+
*
|
|
44
|
+
* Lo usara el panel de VSCode. Devolver siempre a la terminal al terminar: un
|
|
45
|
+
* agente que habla a un panel cerrado no habla con nadie.
|
|
46
|
+
*/
|
|
47
|
+
export function dirigirSalidaDelAgente(destino) {
|
|
48
|
+
actual = destino ?? A_LA_TERMINAL;
|
|
49
|
+
}
|
|
50
|
+
/** Quita los codigos de color, para quien pinte el texto a su manera. */
|
|
51
|
+
export function sinColores(texto) {
|
|
52
|
+
return String(texto).replace(/\x1b\[[0-9;]*m/g, "");
|
|
53
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Un Node viejo se dice claro, no se descubre a base de errores raros.
|
|
3
|
+
*
|
|
4
|
+
* `engines` en el package.json solo hace que npm suelte un aviso que nadie lee:
|
|
5
|
+
* la instalacion termina bien y el fallo aparece despues, al arrancar, con un
|
|
6
|
+
* "fetch is not defined" o un error de sintaxis dentro de una dependencia. El
|
|
7
|
+
* que acaba de pagar da por hecho que el producto esta roto, y no le falta
|
|
8
|
+
* razon desde donde el lo ve.
|
|
9
|
+
*
|
|
10
|
+
* Se comprueba antes que nada: antes de la licencia, antes de leer la
|
|
11
|
+
* configuracion y antes de importar nada que pueda reventar por esto mismo.
|
|
12
|
+
*/
|
|
13
|
+
export declare function comprobarNode(version?: string): string | null;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** La version de Node mas antigua con la que esto funciona. */
|
|
2
|
+
const NODE_MINIMO = 20;
|
|
3
|
+
/**
|
|
4
|
+
* Un Node viejo se dice claro, no se descubre a base de errores raros.
|
|
5
|
+
*
|
|
6
|
+
* `engines` en el package.json solo hace que npm suelte un aviso que nadie lee:
|
|
7
|
+
* la instalacion termina bien y el fallo aparece despues, al arrancar, con un
|
|
8
|
+
* "fetch is not defined" o un error de sintaxis dentro de una dependencia. El
|
|
9
|
+
* que acaba de pagar da por hecho que el producto esta roto, y no le falta
|
|
10
|
+
* razon desde donde el lo ve.
|
|
11
|
+
*
|
|
12
|
+
* Se comprueba antes que nada: antes de la licencia, antes de leer la
|
|
13
|
+
* configuracion y antes de importar nada que pueda reventar por esto mismo.
|
|
14
|
+
*/
|
|
15
|
+
export function comprobarNode(version = process.version) {
|
|
16
|
+
const mayor = Number.parseInt(String(version).replace(/^v/, "").split(".")[0], 10);
|
|
17
|
+
// Si no se entiende la version, no es motivo para no dejar trabajar a nadie.
|
|
18
|
+
if (!Number.isFinite(mayor))
|
|
19
|
+
return null;
|
|
20
|
+
if (mayor >= NODE_MINIMO)
|
|
21
|
+
return null;
|
|
22
|
+
return (`Chocolatito Code necesita Node ${NODE_MINIMO} o superior, y estas con ${version}.\n\n` +
|
|
23
|
+
` Descargalo en https://nodejs.org (la version LTS vale).\n` +
|
|
24
|
+
` Despues, vuelve a instalarlo: npm i -g chocolatito-code\n\n` +
|
|
25
|
+
`No es un problema de tu licencia ni de tu conexion.`);
|
|
26
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -29,6 +29,8 @@ import { releaseKeyboard } from "./ui/interrupt.js";
|
|
|
29
29
|
import { takeQueued, resetTypeAhead } from "./ui/typeAhead.js";
|
|
30
30
|
import { mcpManager } from "./mcp/manager.js";
|
|
31
31
|
import { comprobarActualizacion } from "./config/updater.js";
|
|
32
|
+
import { comprobarNode } from "./config/nodeVersion.js";
|
|
33
|
+
import { imprimirSobreElPrompt } from "./ui/prompt.js";
|
|
32
34
|
/**
|
|
33
35
|
* Version leida del package.json instalado, no escrita a mano: una constante
|
|
34
36
|
* duplicada aqui se queda vieja en cuanto se publica una version nueva.
|
|
@@ -178,6 +180,15 @@ async function dispararStop(hooks, datos) {
|
|
|
178
180
|
return null;
|
|
179
181
|
}
|
|
180
182
|
async function main() {
|
|
183
|
+
// Lo primero de todo: un Node viejo se dice claro, no se descubre a base de
|
|
184
|
+
// errores raros dentro de una dependencia. Ver config/nodeVersion.ts.
|
|
185
|
+
const nodeViejo = comprobarNode();
|
|
186
|
+
if (nodeViejo) {
|
|
187
|
+
console.error(chalk.red(`
|
|
188
|
+
${nodeViejo}
|
|
189
|
+
`));
|
|
190
|
+
process.exit(EXIT_USO);
|
|
191
|
+
}
|
|
181
192
|
const rawArgs = process.argv.slice(2);
|
|
182
193
|
if (handleFlags(rawArgs))
|
|
183
194
|
return;
|
|
@@ -237,9 +248,13 @@ async function main() {
|
|
|
237
248
|
// Si tardan, el primer turno los espera; el usuario ya puede escribir.
|
|
238
249
|
const mcpReady = mcpManager.loadAll().then((resumen) => {
|
|
239
250
|
if (resumen) {
|
|
240
|
-
console.log
|
|
241
|
-
|
|
242
|
-
|
|
251
|
+
// Con `console.log` esto se escribia ENCIMA del marco del prompt y lo
|
|
252
|
+
// destrozaba: los servidores tardan, y para cuando contestan el prompt ya
|
|
253
|
+
// esta dibujado. Se vio asi en una sesion real del usuario:
|
|
254
|
+
// ✔ ide (http): 4 herramientasb modo · esc cancelar
|
|
255
|
+
imprimirSobreElPrompt(chalk.hex("#D97757")("\n🔌 Servidores MCP:") +
|
|
256
|
+
`\n${resumen}\n` +
|
|
257
|
+
chalk.gray(` ${mcpManager.toolCount} herramientas MCP disponibles.\n`));
|
|
243
258
|
}
|
|
244
259
|
return resumen;
|
|
245
260
|
});
|
|
@@ -256,11 +271,11 @@ async function main() {
|
|
|
256
271
|
})
|
|
257
272
|
.then((aviso) => {
|
|
258
273
|
avisoDeActualizacion = aviso;
|
|
259
|
-
//
|
|
260
|
-
//
|
|
261
|
-
//
|
|
274
|
+
// Llega tarde a proposito, y para entonces el prompt ya suele estar
|
|
275
|
+
// dibujado: por eso NO se usa console.log, que lo pisaria. Ver
|
|
276
|
+
// imprimirSobreElPrompt en ui/prompt.ts.
|
|
262
277
|
if (aviso)
|
|
263
|
-
|
|
278
|
+
imprimirSobreElPrompt(chalk.gray(`\n ⬆ ${aviso}\n`));
|
|
264
279
|
return aviso;
|
|
265
280
|
})
|
|
266
281
|
.catch(() => "");
|