chocolatito-code 1.3.0 → 1.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,7 +3,7 @@ import process from "node:process";
3
3
  import { getAllTools } from "../tools/definitions.js";
4
4
  import { executeToolCall } from "../tools/runner.js";
5
5
  import { TokenTracker } from "./tracker.js";
6
- import { renderToolStart, renderToolSuccess, renderToolError, renderDiff, renderFooter, renderError, } from "../ui/renderer.js";
6
+ import { renderToolStart, renderToolSuccess, renderToolError, renderDiff, renderFooter, renderError, formatToolName, } from "../ui/renderer.js";
7
7
  import { CONTEXT_WINDOW, MAX_OUTPUT_TOKENS, COMPACT_THRESHOLD_TOKENS, } from "../config/constants.js";
8
8
  import { withRetry, isContextOverflow, esDemasiadasEnCurso } from "./retry.js";
9
9
  import { canRunInParallel } from "../tools/safety.js";
@@ -14,7 +14,7 @@ import { checkQuota, recordUsage, mensajeDeCorte, quotaBadge } from "../config/q
14
14
  import { ReasoningStream } from "../ui/reasoningStream.js";
15
15
  import { PermissionManager, commandSignature, isDangerousCommand } from "../config/permissions.js";
16
16
  import { askToolPermission } from "../ui/permissionPrompt.js";
17
- import { DynamicSpinner } from "../ui/spinner.js";
17
+ import { DynamicSpinner, conSpinner } from "../ui/spinner.js";
18
18
  import { globalUndoManager } from "./undoManager.js";
19
19
  import { salida } from "./salidaDelAgente.js";
20
20
  import { InterruptWatcher, isAbortError } from "../ui/interrupt.js";
@@ -727,7 +727,20 @@ export class AgentLoop {
727
727
  // huecos de la licencia durante minutos, y los cortes vienen en rachas:
728
728
  // reintentar es justo como se pasa de perder un hueco a perder los cuatro.
729
729
  // Mas vale que el usuario reescriba una linea.
730
- if (!llegoElFinal && (content.trim().length > 0 || toolCalls.length > 0)) {
730
+ //
731
+ // NO SE EXIGE QUE HAYA LLEGADO TEXTO. La primera version pedia contenido o
732
+ // herramientas, y se le escapaba el caso mas comun: el corte que pasa
733
+ // DESPUES del razonamiento y ANTES de la primera palabra. Eso caia en la
734
+ // rama de "respuesta vacia", que si reintenta, y volvia a gastar huecos:
735
+ //
736
+ // 💭 penso 2s
737
+ // ⚠ El motor no devolvio nada. Reintentando una vez…
738
+ //
739
+ // Sin marca de final, el stream se corto. Da igual cuanto habia llegado.
740
+ // Una respuesta vacia DE VERDAD trae su finish_reason: el motor cerro el
741
+ // turno en regla y simplemente no dijo nada, y eso si merece un reintento
742
+ // porque la conexion estaba bien.
743
+ if (!llegoElFinal) {
731
744
  watcher.stop();
732
745
  // Lo que llego se guarda, pero MARCADO. Sin la marca, en el turno
733
746
  // siguiente el modelo lee su propia frase a medias como algo que decidio
@@ -738,11 +751,20 @@ export class AgentLoop {
738
751
  content: `${content}\n\n[Aqui se corto: la conexion con el motor se cerro a mitad de la respuesta.]`,
739
752
  });
740
753
  }
754
+ // Se dice HASTA DONDE llego, y no por cortesia: es el unico dato que
755
+ // distingue un corte antes de empezar de uno a mitad de frase, y en un
756
+ // pegado del usuario es lo unico con lo que se puede diagnosticar.
741
757
  // Las llamadas a herramienta de un stream cortado NO se ejecutan: sus
742
758
  // argumentos estan tan a medias como el texto.
743
- const aviso = "La respuesta se corto a mitad: la conexion con el motor se cerro sin avisar. " +
744
- "Lo que llego se queda arriba. No lo reintento solo porque cada intento ocupa uno de " +
745
- "los cuatro huecos de tu licencia durante varios minutos; vuelve a pedirmelo y sigo.";
759
+ const hastaDonde = content.trim()
760
+ ? "Lo que llego se queda arriba."
761
+ : reasoning.trim()
762
+ ? "Se corto antes de escribir nada: solo llego el razonamiento."
763
+ : "Se corto antes de que llegara nada.";
764
+ const aviso = "La respuesta se corto: la conexion con el motor se cerro sin decir por que (no llego " +
765
+ `ni el motivo de fin ni el recuento del gasto). ${hastaDonde} ` +
766
+ "No lo reintento solo porque cada intento ocupa uno de los cuatro huecos de tu licencia " +
767
+ "durante varios minutos; vuelve a pedirmelo y sigo.";
746
768
  salida.linea(chalk.yellow(`\n ⚠ ${aviso}\n`));
747
769
  return parada("error-motor", aviso);
748
770
  }
@@ -845,10 +867,14 @@ export class AgentLoop {
845
867
  for (const { tc, args } of autorizadas) {
846
868
  renderToolStart(tc.function.name, getToolSummary(tc.function.name, args));
847
869
  }
848
- const settled = await Promise.all(autorizadas.map(async ({ tc, args, pos }) => {
870
+ // Un solo spinner para el lote entero: uno por herramienta se pisarian
871
+ // entre ellos y el pie parpadearia tantas veces como llamadas haya.
872
+ const settled = await conSpinner(autorizadas.length === 1
873
+ ? formatToolName(autorizadas[0].tc.function.name)
874
+ : `${autorizadas.length} herramientas a la vez`, () => Promise.all(autorizadas.map(async ({ tc, args, pos }) => {
849
875
  const r = await executeToolCall(tc.function.name, args, this.currentCwd, this.apiKey, this.currentEspectro.underlyingModel);
850
876
  return { tc, args, pos, result: r.result };
851
- }));
877
+ })));
852
878
  for (const { tc, args, pos, result } of settled) {
853
879
  renderToolSuccess(tc.function.name, result);
854
880
  const aviso = await notificarPostHerramienta(tc.function.name, args, result, this.currentCwd);
@@ -969,7 +995,10 @@ export class AgentLoop {
969
995
  }
970
996
  const summary = getToolSummary(fnName, fnArgs);
971
997
  renderToolStart(fnName, summary);
972
- const { result, diffText, newCwd } = await executeToolCall(fnName, fnArgs, this.currentCwd, this.apiKey, this.currentEspectro.underlyingModel);
998
+ // El pie se queda puesto mientras dure la herramienta. Un run_command
999
+ // de veinte segundos dejaba la pantalla muda: ni cronometro, ni forma
1000
+ // de saber que seguia vivo, ni donde escribir para encolar algo.
1001
+ const { result, diffText, newCwd } = await conSpinner(formatToolName(fnName), () => executeToolCall(fnName, fnArgs, this.currentCwd, this.apiKey, this.currentEspectro.underlyingModel));
973
1002
  if (newCwd) {
974
1003
  this.currentCwd = newCwd;
975
1004
  }
@@ -1027,6 +1056,12 @@ export class AgentLoop {
1027
1056
  // codigo no distingue "el modelo termino" de "no vino nada". Sin
1028
1057
  // herramientas y sin texto, el bucle daba el turno por hecho, imprimia el
1029
1058
  // pie y volvia al prompt.
1059
+ //
1060
+ // Aqui solo se llega con la marca de final puesta: un stream cortado sale
1061
+ // mucho antes, por su propia rama. Asi que esto es lo que parece -el motor
1062
+ // cerro el turno en regla y no dijo nada- y no un corte disfrazado. Esa
1063
+ // separacion es la que hace que reintentar aqui sea razonable: la conexion
1064
+ // estaba bien, y por eso el reintento no es tirar un hueco a la basura.
1030
1065
  if (!content.trim()) {
1031
1066
  respuestasVacias++;
1032
1067
  // Se reintenta UNA vez, no mas, y aqui la moderacion importa: cada
@@ -1034,13 +1069,14 @@ export class AgentLoop {
1034
1069
  // cuatro. Insistir con un motor que no contesta es la forma mas rapida
1035
1070
  // de quedarse sin ninguno.
1036
1071
  if (respuestasVacias > MAX_RESPUESTAS_VACIAS) {
1037
- const aviso = `El motor devolvio ${respuestasVacias} respuestas vacias seguidas. ` +
1038
- "No es tu conexion ni tu orden: no esta llegando nada del modelo. " +
1039
- "Espera un momento y reintenta, o /clear si venias de una conversacion larga.";
1072
+ const aviso = `El motor cerro el turno en regla ${respuestasVacias} veces seguidas sin una sola ` +
1073
+ "palabra. No es un corte de conexion -eso se avisa aparte y con otras palabras-: la " +
1074
+ "peticion llego y volvio entera, solo que vacia. Espera un momento y reintenta, o " +
1075
+ "/clear si venias de una conversacion larga.";
1040
1076
  salida.linea(chalk.yellow(`\n ⚠ ${aviso}\n`));
1041
1077
  return parada("error-motor", aviso);
1042
1078
  }
1043
- salida.linea(chalk.gray("\n ⚠ El motor no devolvio nada. Reintentando una vez…\n"));
1079
+ salida.linea(chalk.gray("\n ⚠ El motor cerro el turno sin decir nada. Reintentando una vez…\n"));
1044
1080
  this.messages.push({
1045
1081
  role: "user",
1046
1082
  content: "Tu respuesta anterior llego vacia: ni texto ni llamadas a herramienta. " +
@@ -50,6 +50,17 @@ export interface QuotaStatus {
50
50
  config: QuotaConfig;
51
51
  }
52
52
  export declare function checkQuota(ahora?: number): QuotaStatus;
53
+ /**
54
+ * Cuando la ventana corta se vacia DEL TODO, si no se gasta nada mas.
55
+ *
56
+ * `seLiberaEn` dice cuando caduca el gasto mas viejo, que es lo que hace falta
57
+ * para saber cuando se puede volver a trabajar tras un corte. Para ensenar el
58
+ * consumo es otra pregunta: cuando vuelve a cero. Y como la ventana es
59
+ * deslizante, eso es cuando caduca el gasto mas RECIENTE.
60
+ *
61
+ * `null` si no hay nada dentro de la ventana: no hay nada que renovar.
62
+ */
63
+ export declare function ventanaVaciaEn(ahora?: number): Date | null;
53
64
  /** Cuanto falta para la hora indicada, en lenguaje llano. */
54
65
  export declare function faltaPara(fecha: Date, ahora?: number): string;
55
66
  /** Mensaje completo para cuando se agota la cuota. */
@@ -148,6 +148,24 @@ export function checkQuota(ahora = Date.now()) {
148
148
  }
149
149
  return { allowed: true, spentWindowUsd, spentWeekUsd, config };
150
150
  }
151
+ /**
152
+ * Cuando la ventana corta se vacia DEL TODO, si no se gasta nada mas.
153
+ *
154
+ * `seLiberaEn` dice cuando caduca el gasto mas viejo, que es lo que hace falta
155
+ * para saber cuando se puede volver a trabajar tras un corte. Para ensenar el
156
+ * consumo es otra pregunta: cuando vuelve a cero. Y como la ventana es
157
+ * deslizante, eso es cuando caduca el gasto mas RECIENTE.
158
+ *
159
+ * `null` si no hay nada dentro de la ventana: no hay nada que renovar.
160
+ */
161
+ export function ventanaVaciaEn(ahora = Date.now()) {
162
+ const ventanaMs = getQuotaConfig().windowHours * 60 * 60 * 1000;
163
+ const dentro = load().filter((e) => e.t >= ahora - ventanaMs);
164
+ if (dentro.length === 0)
165
+ return null;
166
+ const masReciente = dentro.reduce((max, e) => (e.t > max ? e.t : max), dentro[0].t);
167
+ return new Date(masReciente + ventanaMs);
168
+ }
151
169
  /** Cuanto falta para la hora indicada, en lenguaje llano. */
152
170
  export function faltaPara(fecha, ahora = Date.now()) {
153
171
  const ms = fecha.getTime() - ahora;
package/dist/index.js CHANGED
@@ -5,13 +5,16 @@ import { fileURLToPath } from "node:url";
5
5
  import chalk from "chalk";
6
6
  import { ensureApiKey } from "./config/env.js";
7
7
  import { ensureLicense, licenseStatus, deactivate, getStoredLicense } from "./config/license.js";
8
- import { isManagedMode } from "./config/quota.js";
8
+ import { isManagedMode, checkQuota, ventanaVaciaEn } from "./config/quota.js";
9
+ import { construirInformeDeUso } from "./ui/informeDeUso.js";
9
10
  import { getEngineBaseUrl, DEFAULT_ESPECTRO, ESPECTRO_MODELS, RULES_FILE_NAME } from "./config/constants.js";
10
11
  import { getProjectContext } from "./agent/context.js";
11
12
  import { buildSystemPrompt } from "./prompts/systemPrompt.js";
12
13
  import { AgentLoop } from "./agent/loop.js";
13
14
  import { codigoDeSalida, EXIT_OK, EXIT_FALLO, EXIT_USO, EXIT_LICENCIA } from "./agent/result.js";
14
15
  import { showBanner, animateStartupBanner, showHelp, showEffortMenu } from "./ui/banner.js";
16
+ import { fijarMotor } from "./ui/motorActual.js";
17
+ import { elegirDeLista } from "./ui/selector.js";
15
18
  import { askInteractivePrompt, SLASH_COMMANDS } from "./ui/prompt.js";
16
19
  import { changeDir } from "./tools/changeDir.js";
17
20
  import { gitDiff, gitCommit } from "./tools/gitTools.js";
@@ -184,8 +187,8 @@ async function main() {
184
187
  // errores raros dentro de una dependencia. Ver config/nodeVersion.ts.
185
188
  const nodeViejo = comprobarNode();
186
189
  if (nodeViejo) {
187
- console.error(chalk.red(`
188
- ${nodeViejo}
190
+ console.error(chalk.red(`
191
+ ${nodeViejo}
189
192
  `));
190
193
  process.exit(EXIT_USO);
191
194
  }
@@ -232,6 +235,9 @@ ${nodeViejo}
232
235
  const sessionManager = new SessionManager();
233
236
  const sessionId = `session_${Date.now()}`;
234
237
  let currentModel = DEFAULT_ESPECTRO;
238
+ // La barra de estado lo lee de aqui. Fijarlo al arrancar y no solo al
239
+ // cambiarlo: si no, hasta el primer /effort la barra no dice nada.
240
+ fijarMotor(currentModel.name);
235
241
  let isPlanMode = false;
236
242
  let systemPrompt = buildSystemPrompt(context, currentModel, skillManager, memoryManager, isPlanMode);
237
243
  const agent = new AgentLoop(apiKey, getEngineBaseUrl(), currentModel, systemPrompt);
@@ -399,15 +405,15 @@ ${nodeViejo}
399
405
  .filter((m) => m.role === "user" || m.role === "assistant")
400
406
  .map((m) => `### 👤 ${m.role === "user" ? "Usuario" : "Chocolatito"}\n\n${typeof m.content === "string" ? m.content : JSON.stringify(m.content, null, 2)}\n`)
401
407
  .join("\n---\n\n");
402
- const mdContent = `# 🦊 Bitácora de Sesión — Chocolatito Code
403
- **Fecha:** ${new Date().toLocaleString()}
404
- **Directorio:** \`${agent.currentCwd}\`
405
- **Motor:** ${currentModel.name}
406
- **Inversión Total:** ${agent.tracker.calculateCost(undefined, undefined, undefined, currentModel.underlyingModel).formattedCost}
407
-
408
- ---
409
-
410
- ${sessionData}
408
+ const mdContent = `# 🦊 Bitácora de Sesión — Chocolatito Code
409
+ **Fecha:** ${new Date().toLocaleString()}
410
+ **Directorio:** \`${agent.currentCwd}\`
411
+ **Motor:** ${currentModel.name}
412
+ **Inversión Total:** ${agent.tracker.calculateCost(undefined, undefined, undefined, currentModel.underlyingModel).formattedCost}
413
+
414
+ ---
415
+
416
+ ${sessionData}
411
417
  `;
412
418
  fs.writeFileSync(exportPath, mdContent, "utf-8");
413
419
  console.log(chalk.green(`\n✔ Bitácora exportada exitosamente en: ${chalk.bold.white(exportPath)}\n`));
@@ -428,6 +434,7 @@ ${sessionData}
428
434
  const restauracion = prepararRestauracion(loaded, { cwd: agent.currentCwd, modelId: currentModel.id });
429
435
  agent.currentCwd = restauracion.cwd;
430
436
  currentModel = ESPECTRO_MODELS[restauracion.modelId] || currentModel;
437
+ fijarMotor(currentModel.name);
431
438
  const refreshedContext = await getProjectContext(agent.currentCwd);
432
439
  systemPrompt = buildSystemPrompt(refreshedContext, currentModel, skillManager, memoryManager, isPlanMode);
433
440
  agent.messages = [{ role: "system", content: systemPrompt }, ...restauracion.conversacion];
@@ -500,12 +507,12 @@ ${sessionData}
500
507
  }
501
508
  console.log(chalk.bold.hex("#A855F7")("\n🎯 INICIANDO MODO META AUTÓNOMA (GOAL MODE):"));
502
509
  console.log(chalk.gray(` Objetivo: "${goalText}"\n`));
503
- const autonomousPrompt = `[MODO META AUTÓNOMA ACTIVADO]
504
- Tu objetivo principal es: "${goalText}"
505
- Directivas de ejecución:
506
- 1. Formula un plan de ejecución mental paso a paso.
507
- 2. Ejecuta cada paso utilizando las herramientas correspondientes (creación, edición, ejecución, verificación).
508
- 3. Si algo falla o hay errores, autocorregir inmediatamente y volver a probar.
510
+ const autonomousPrompt = `[MODO META AUTÓNOMA ACTIVADO]
511
+ Tu objetivo principal es: "${goalText}"
512
+ Directivas de ejecución:
513
+ 1. Formula un plan de ejecución mental paso a paso.
514
+ 2. Ejecuta cada paso utilizando las herramientas correspondientes (creación, edición, ejecución, verificación).
515
+ 3. Si algo falla o hay errores, autocorregir inmediatamente y volver a probar.
509
516
  4. No te detengas hasta verificar que el objetivo esté 100% completado con éxito.`;
510
517
  renderUserPrompt(`/goal ${goalText}`);
511
518
  await agent.runTask(autonomousPrompt);
@@ -559,32 +566,63 @@ Directivas de ejecución:
559
566
  }
560
567
  continue;
561
568
  }
562
- if (trimmed === "/cost") {
563
- const { formattedCost, inputCost, outputCost } = agent.tracker.calculateCost(undefined, undefined, undefined, currentModel.underlyingModel);
564
- console.log(chalk.bold.hex("#A855F7")("\n📊 REPORTE DE CONSUMO DE SESIÓN:") +
565
- `\n ${chalk.gray(" Motor activo:")} ${chalk.bold.white(currentModel.name)}` +
566
- `\n ${chalk.gray("• Tokens entrada:")} ${agent.tracker.totalPromptTokens.toLocaleString()} ($${inputCost.toFixed(5)} USD)` +
567
- `\n ${chalk.gray("• Tokens memoria:")} ${agent.tracker.cachedPromptTokens.toLocaleString()} (90% de ahorro en caché)` +
568
- `\n ${chalk.gray("• Tokens salida:")} ${agent.tracker.totalCompletionTokens.toLocaleString()} ($${outputCost.toFixed(5)} USD)` +
569
- `\n ${chalk.gray("• Inversión total:")} ${chalk.bold.green(formattedCost)}\n`);
569
+ // "/cost" sigue funcionando sin anunciarse: el nombre viejo esta en la
570
+ // memoria de los dedos de quien ya lo usaba, y quitarlo de golpe solo
571
+ // ensena un "comando desconocido" a quien no ha hecho nada mal.
572
+ if (trimmed === "/uso" || trimmed === "/cost") {
573
+ // Lo que se ensena es la CUENTA, no la tarifa: quien paga una suscripcion
574
+ // plana no puede hacer nada con "$0.00508", y lo unico que necesita saber
575
+ // es cuanto le queda antes de que esto le pare. Con clave propia es al
576
+ // reves -el dinero es suyo- y el informe lo tiene en cuenta.
577
+ const { formattedCost } = agent.tracker.calculateCost(undefined, undefined, undefined, currentModel.underlyingModel);
578
+ console.log(construirInformeDeUso(checkQuota(), {
579
+ motor: currentModel.name,
580
+ tokensEntrada: agent.tracker.totalPromptTokens,
581
+ tokensSalida: agent.tracker.totalCompletionTokens,
582
+ tokensEnCache: agent.tracker.cachedPromptTokens,
583
+ costeFormateado: formattedCost,
584
+ }, ventanaVaciaEn()));
570
585
  continue;
571
586
  }
572
587
  if (trimmed.startsWith("/effort") || trimmed.startsWith("/espectro") || trimmed === "/model") {
573
588
  const parts = trimmed.split(/\s+/);
574
- const targetId = parts[1]?.trim();
589
+ let targetId = parts[1]?.trim();
590
+ // Sin argumento se ABRE UN MENU, no se imprime una lista.
591
+ //
592
+ // Antes se volcaban las cuatro opciones con console.log y una frase al
593
+ // final que decia "selecciona con /effort 2.0". En pantalla se veia una
594
+ // lista, la gente pulsaba las flechas -que es lo unico razonable- y no
595
+ // pasaba nada.
575
596
  if (!targetId || !ESPECTRO_MODELS[targetId]) {
576
- showEffortMenu(currentModel.id);
577
- }
578
- else {
579
- currentModel = ESPECTRO_MODELS[targetId];
580
- const refreshedContext = await getProjectContext(agent.currentCwd);
581
- const updatedPrompt = buildSystemPrompt(refreshedContext, currentModel, skillManager, memoryManager, isPlanMode);
582
- agent.setEspectroModel(currentModel, updatedPrompt);
583
- console.log(chalk.green(`\n✔ Nivel actualizado a `) +
584
- chalk.bold.hex("#A855F7")(currentModel.name) +
585
- chalk.gray(` (${currentModel.effortLevel})`) +
586
- `\n ${chalk.italic.gray(currentModel.description)}\n`);
597
+ const opciones = Object.values(ESPECTRO_MODELS).map((m) => ({
598
+ id: m.id,
599
+ titulo: `${m.name} ${m.id === currentModel.id ? "· activo" : ""}`.trimEnd(),
600
+ detalle: `Esfuerzo ${m.effortLevel} — ${m.description}`,
601
+ }));
602
+ const indiceActual = Object.values(ESPECTRO_MODELS).findIndex((m) => m.id === currentModel.id);
603
+ const elegido = await elegirDeLista("POTENCIA DEL MOTOR — CHOCOLATITO CODE", opciones, Math.max(0, indiceActual));
604
+ // Sin terminal el menu no puede abrirse: ahi se cae a la lista de
605
+ // siempre, que al menos dice como elegir escribiendolo.
606
+ if (elegido === null) {
607
+ if (!process.stdin.isTTY)
608
+ showEffortMenu(currentModel.id);
609
+ continue;
610
+ }
611
+ targetId = elegido;
587
612
  }
613
+ currentModel = ESPECTRO_MODELS[targetId];
614
+ // Sin esto la barra de abajo sigue anunciando el motor anterior.
615
+ fijarMotor(currentModel.name);
616
+ const refreshedContext = await getProjectContext(agent.currentCwd);
617
+ const updatedPrompt = buildSystemPrompt(refreshedContext, currentModel, skillManager, memoryManager, isPlanMode);
618
+ agent.setEspectroModel(currentModel, updatedPrompt);
619
+ console.log(chalk.green(`
620
+ ✔ Nivel actualizado a `) +
621
+ chalk.bold.hex("#A855F7")(currentModel.name) +
622
+ chalk.gray(` (${currentModel.effortLevel})`) +
623
+ `
624
+ ${chalk.italic.gray(currentModel.description)}
625
+ `);
588
626
  continue;
589
627
  }
590
628
  if (trimmed === "/clear") {