chocolatito-code 1.0.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.
Files changed (122) hide show
  1. package/README.md +155 -0
  2. package/dist/agent/compaction.d.ts +65 -0
  3. package/dist/agent/compaction.js +134 -0
  4. package/dist/agent/context.d.ts +3 -0
  5. package/dist/agent/context.js +35 -0
  6. package/dist/agent/loop.d.ts +91 -2
  7. package/dist/agent/loop.js +642 -91
  8. package/dist/agent/loopDetector.d.ts +57 -0
  9. package/dist/agent/loopDetector.js +0 -0
  10. package/dist/agent/mentions.d.ts +13 -0
  11. package/dist/agent/mentions.js +49 -0
  12. package/dist/agent/repoMap.d.ts +117 -0
  13. package/dist/agent/repoMap.js +560 -0
  14. package/dist/agent/result.d.ts +64 -0
  15. package/dist/agent/result.js +48 -0
  16. package/dist/agent/retry.d.ts +35 -0
  17. package/dist/agent/retry.js +123 -4
  18. package/dist/agent/salidaDelAgente.d.ts +43 -0
  19. package/dist/agent/salidaDelAgente.js +53 -0
  20. package/dist/agent/subagent.js +62 -15
  21. package/dist/agent/syntaxValidator.js +288 -52
  22. package/dist/agent/toolGate.d.ts +58 -0
  23. package/dist/agent/toolGate.js +111 -0
  24. package/dist/agent/tracker.js +4 -7
  25. package/dist/agent/undoManager.d.ts +44 -11
  26. package/dist/agent/undoManager.js +95 -24
  27. package/dist/agent/usageMeter.d.ts +56 -0
  28. package/dist/agent/usageMeter.js +51 -0
  29. package/dist/agent/verifier.d.ts +49 -0
  30. package/dist/agent/verifier.js +158 -0
  31. package/dist/config/constants.js +14 -0
  32. package/dist/config/engine.d.ts +19 -0
  33. package/dist/config/engine.js +35 -3
  34. package/dist/config/license.d.ts +5 -0
  35. package/dist/config/license.js +107 -9
  36. package/dist/config/limits.d.ts +28 -0
  37. package/dist/config/limits.js +51 -0
  38. package/dist/config/nodeVersion.d.ts +13 -0
  39. package/dist/config/nodeVersion.js +26 -0
  40. package/dist/config/permissions.d.ts +76 -1
  41. package/dist/config/permissions.js +379 -23
  42. package/dist/config/quota.js +24 -1
  43. package/dist/config/updater.d.ts +68 -0
  44. package/dist/config/updater.js +215 -0
  45. package/dist/hooks/manager.js +159 -14
  46. package/dist/index.js +240 -37
  47. package/dist/mcp/client.d.ts +50 -0
  48. package/dist/mcp/client.js +155 -19
  49. package/dist/mcp/ide.d.ts +76 -0
  50. package/dist/mcp/ide.js +160 -0
  51. package/dist/mcp/manager.js +9 -0
  52. package/dist/prompts/systemPrompt.js +12 -1
  53. package/dist/sessions/manager.d.ts +12 -0
  54. package/dist/sessions/manager.js +33 -5
  55. package/dist/sessions/resume.d.ts +36 -0
  56. package/dist/sessions/resume.js +43 -0
  57. package/dist/skills/manager.d.ts +1 -0
  58. package/dist/skills/manager.js +9 -0
  59. package/dist/tools/binary.d.ts +46 -0
  60. package/dist/tools/binary.js +100 -0
  61. package/dist/tools/browserCdp.js +13 -1
  62. package/dist/tools/browserExtension.d.ts +5 -0
  63. package/dist/tools/browserExtension.js +237 -22
  64. package/dist/tools/browserSession.d.ts +23 -0
  65. package/dist/tools/browserSession.js +28 -0
  66. package/dist/tools/computerUse.js +33 -1
  67. package/dist/tools/createImage.d.ts +11 -0
  68. package/dist/tools/createImage.js +13 -1
  69. package/dist/tools/definitions.js +17 -4
  70. package/dist/tools/editDiagnosis.d.ts +54 -0
  71. package/dist/tools/editDiagnosis.js +142 -0
  72. package/dist/tools/editFile.d.ts +1 -0
  73. package/dist/tools/editFile.js +118 -62
  74. package/dist/tools/findFiles.d.ts +21 -0
  75. package/dist/tools/findFiles.js +75 -6
  76. package/dist/tools/getSystemInfo.js +15 -7
  77. package/dist/tools/gitTools.d.ts +24 -0
  78. package/dist/tools/gitTools.js +87 -26
  79. package/dist/tools/grepSearch.js +111 -23
  80. package/dist/tools/moveCopyFile.d.ts +18 -1
  81. package/dist/tools/moveCopyFile.js +74 -4
  82. package/dist/tools/patchCascade.d.ts +57 -0
  83. package/dist/tools/patchCascade.js +338 -0
  84. package/dist/tools/runCommand.js +213 -25
  85. package/dist/tools/runner.js +58 -5
  86. package/dist/tools/toolDefsComputer.js +12 -2
  87. package/dist/tools/truncator.d.ts +27 -1
  88. package/dist/tools/truncator.js +117 -11
  89. package/dist/tools/viewFile.js +15 -11
  90. package/dist/tools/visionBridge.js +7 -0
  91. package/dist/tools/webSearch.d.ts +21 -0
  92. package/dist/tools/webSearch.js +125 -14
  93. package/dist/tools/writeFile.d.ts +16 -1
  94. package/dist/tools/writeFile.js +40 -2
  95. package/dist/ui/interrupt.d.ts +4 -1
  96. package/dist/ui/interrupt.js +59 -7
  97. package/dist/ui/keyboardGuard.d.ts +76 -0
  98. package/dist/ui/keyboardGuard.js +94 -0
  99. package/dist/ui/loopPrompt.d.ts +17 -0
  100. package/dist/ui/loopPrompt.js +59 -0
  101. package/dist/ui/pegados.d.ts +50 -0
  102. package/dist/ui/pegados.js +83 -0
  103. package/dist/ui/permissionPrompt.js +84 -2
  104. package/dist/ui/prompt.d.ts +40 -0
  105. package/dist/ui/prompt.js +436 -131
  106. package/dist/ui/reasoningStream.d.ts +8 -0
  107. package/dist/ui/reasoningStream.js +99 -7
  108. package/dist/ui/salida.d.ts +10 -0
  109. package/dist/ui/salida.js +50 -0
  110. package/extension/README.md +95 -74
  111. package/extension/background.js +772 -181
  112. package/extension/content.js +527 -46
  113. package/extension/fuentes/Chocolatito-Marca.ttf +0 -0
  114. package/extension/iconos/128.png +0 -0
  115. package/extension/iconos/16.png +0 -0
  116. package/extension/iconos/32.png +0 -0
  117. package/extension/iconos/48.png +0 -0
  118. package/extension/iconos/logotipo.png +0 -0
  119. package/extension/manifest.json +28 -4
  120. package/extension/popup.html +16 -6
  121. package/extension/popup.js +28 -12
  122. package/package.json +3 -2
@@ -3,6 +3,7 @@ import process from "node:process";
3
3
  import chalk from "chalk";
4
4
  import { cycleMode, modeAnnouncement } from "./modes.js";
5
5
  import { pushTyped, backspaceTyped, commitTyped } from "./typeAhead.js";
6
+ import { dibujar } from "./salida.js";
6
7
  /**
7
8
  * Interrupcion con Esc mientras el agente trabaja.
8
9
  *
@@ -32,6 +33,42 @@ import { pushTyped, backspaceTyped, commitTyped } from "./typeAhead.js";
32
33
  */
33
34
  const activos = new Set();
34
35
  /** Suelta el teclado de cualquier vigilante que lo tenga. Lo llama el prompt. */
36
+ /**
37
+ * Como estaba el terminal ANTES de que ningun vigilante lo tocara.
38
+ *
39
+ * Cada vigilante se guardaba su propio `wasRaw` leyendo el estado en el momento
40
+ * de arrancar. Con un vigilante suelto -y el bucle tiene veinte salidas, basta
41
+ * que una se escape- el siguiente leia el modo crudo que habia dejado el
42
+ * anterior y se lo apuntaba como "lo de antes". Al soltarlos todos, cada uno
43
+ * restauraba su version y ganaba el ultimo: el terminal acababa en un estado que
44
+ * no era el de nadie.
45
+ *
46
+ * Con una sola marca, tomada por el primero y devuelta por el ultimo, el numero
47
+ * de vigilantes deja de importar.
48
+ */
49
+ let modoCrudoOriginal = null;
50
+ /**
51
+ * Hay un prompt dibujado ahora mismo.
52
+ *
53
+ * Un vigilante puede pararse TARDE: el bucle vuelve, el prompt se dibuja y coge
54
+ * el modo crudo, y solo entonces una promesa rezagada del turno anterior llama a
55
+ * su `stop()`. Ese stop restauraba "como estaba antes del turno" —modo cocido—
56
+ * y se lo quitaba al prompt recien abierto. Desde fuera se veia como el fallo
57
+ * que se reporto tres veces: tecleas y tu texto sale en la linea de ayuda, no
58
+ * en el recuadro, porque quien hace el eco es el terminal.
59
+ *
60
+ * Con esta marca, el ultimo vigilante en irse solo devuelve el terminal si no
61
+ * hay nadie esperandolo.
62
+ */
63
+ let promptConElTeclado = false;
64
+ /** Lo llama el prompt al abrirse, despues de coger el modo crudo. */
65
+ export function tomarTecladoParaPrompt() {
66
+ promptConElTeclado = true;
67
+ }
68
+ /** Lo llama el prompt al cerrarse, para que el siguiente turno pueda restaurar. */
69
+ export function devolverTecladoDelPrompt() {
70
+ promptConElTeclado = false;
71
+ }
35
72
  export function releaseKeyboard() {
36
73
  for (const w of Array.from(activos)) {
37
74
  w.stop();
@@ -62,7 +99,11 @@ export class InterruptWatcher {
62
99
  return this.controller.signal;
63
100
  }
64
101
  readline.emitKeypressEvents(process.stdin);
65
- this.wasRaw = process.stdin.isRaw || false;
102
+ // Solo el primero apunta como estaba: los que vengan detras verian el modo
103
+ // crudo que dejo este, no el del usuario.
104
+ if (activos.size === 0)
105
+ modoCrudoOriginal = process.stdin.isRaw || false;
106
+ this.wasRaw = modoCrudoOriginal ?? false;
66
107
  process.stdin.setRawMode(true);
67
108
  process.stdin.resume();
68
109
  this.onKey = (str, key) => {
@@ -72,7 +113,7 @@ export class InterruptWatcher {
72
113
  Boolean(key && key.sequence === "\x1b[Z");
73
114
  if (isShiftTab) {
74
115
  cycleMode();
75
- process.stdout.write(`\r\x1b[2K${modeAnnouncement()}\n`);
116
+ dibujar(`\r\x1b[2K${modeAnnouncement()}\n`);
76
117
  return;
77
118
  }
78
119
  // Ignorar secuencias de escape multi-byte (flechas, inicio, fin, etc.) para que no aborten
@@ -90,7 +131,7 @@ export class InterruptWatcher {
90
131
  const texto = commitTyped();
91
132
  if (texto) {
92
133
  const corto = texto.length > 70 ? `${texto.slice(0, 67)}...` : texto;
93
- process.stdout.write(`\r\x1b[2K${chalk.gray("↳ en cola:")} ${chalk.white(corto)}\n`);
134
+ dibujar(`\r\x1b[2K${chalk.gray("↳ en cola:")} ${chalk.white(corto)}\n`);
94
135
  }
95
136
  return;
96
137
  }
@@ -137,12 +178,23 @@ export class InterruptWatcher {
137
178
  process.stdin.removeListener("keypress", this.onKey);
138
179
  this.onKey = null;
139
180
  }
140
- if (this.listening && process.stdin.setRawMode) {
141
- // Se devuelve el terminal como estaba, o el prompt siguiente queda roto.
142
- process.stdin.setRawMode(this.wasRaw);
143
- }
181
+ const escuchaba = this.listening;
144
182
  this.listening = false;
145
183
  activos.delete(this);
184
+ // El terminal se devuelve cuando se va el ULTIMO, no cada vez que se va
185
+ // uno: si quedan vigilantes vivos, restaurar aqui les quita el teclado a
186
+ // ellos a mitad de turno.
187
+ if (escuchaba && activos.size === 0) {
188
+ if (promptConElTeclado) {
189
+ // Llegamos tarde: el prompt ya esta abierto y el modo crudo es suyo.
190
+ // Solo se olvida la marca, que ya no describe nada.
191
+ modoCrudoOriginal = null;
192
+ }
193
+ else if (process.stdin.setRawMode) {
194
+ process.stdin.setRawMode(modoCrudoOriginal ?? this.wasRaw);
195
+ modoCrudoOriginal = null;
196
+ }
197
+ }
146
198
  }
147
199
  }
148
200
  /** Un error de aborto no es un fallo: es el usuario diciendo "para". */
@@ -0,0 +1,76 @@
1
+ /**
2
+ * El prompt no suelta el teclado mientras esta abierto.
3
+ *
4
+ * EL FALLO QUE CIERRA ESTE ARCHIVO
5
+ *
6
+ * Reportado tres veces desde el uso real, siempre igual: "escribi 2 prompts y
7
+ * al siguiente ya no me deja escribir nada, teclee lo que teclee". La sesion
8
+ * pegada por el usuario tenia la pista buena, escondida en la ultima linea:
9
+ *
10
+ * ? /help para atajos · esc cancelar ⇥ modo normal · Espectro 1.0 (Agil) estaba haciendo y denuevo...
11
+ *
12
+ * Lo que escribia SI aparecia, pero pegado al final de la linea de ayuda en vez
13
+ * de dentro del recuadro. Eso solo pasa de una forma: el modo crudo estaba
14
+ * apagado, asi que quien hacia el eco era PowerShell —en el sitio donde estuviera
15
+ * el cursor— y el prompt no se enteraba de ninguna tecla, porque en modo cocido
16
+ * no llega nada hasta que se pulsa Enter. El teclado no estaba muerto: estaba
17
+ * en manos del terminal.
18
+ *
19
+ * POR QUE NO SE ARREGLA EN EL SITIO QUE LO ROMPE
20
+ *
21
+ * Hay demasiados sitios que lo rompen, y todos con razon:
22
+ *
23
+ * - `InterruptWatcher.stop()` devuelve el modo crudo a como estaba. Si un
24
+ * vigilante se para TARDE —despues de que el prompt ya se haya dibujado—
25
+ * restaura un estado que ya no toca y se lo quita al prompt.
26
+ * - `readline.createInterface(...).close()` hace por dentro `pause()` y
27
+ * `setRawMode(false)`. Lo usan el dialogo de "¿que prefieres que haga?" y la
28
+ * pregunta del detector de bucles.
29
+ * - Cualquier salida del bucle que se escape sin parar lo suyo.
30
+ *
31
+ * Ya hubo dos intentos de arreglarlo apretando tuercas una por una, y el fallo
32
+ * volvio las dos veces, porque basta que UNA de las veinte salidas se escape.
33
+ * Asi que el prompt deja de confiar: mientras esta abierto, comprueba cuatro
34
+ * veces por segundo que el teclado sigue siendo suyo y lo recupera si no.
35
+ *
36
+ * Cuesta cuatro comparaciones cada 250 ms. El fallo costaba cerrar el terminal.
37
+ */
38
+ /** Lo minimo de `process.stdin` que hace falta, para poder probarlo con uno falso. */
39
+ export interface EntradaDeTeclado {
40
+ isTTY?: boolean;
41
+ isRaw?: boolean;
42
+ setRawMode?: (flag: boolean) => unknown;
43
+ isPaused?: () => boolean;
44
+ resume?: () => unknown;
45
+ }
46
+ /**
47
+ * Devuelve el teclado al prompt si alguien se lo ha quitado.
48
+ *
49
+ * Devuelve la lista de lo que ha tenido que reparar. Vacia es lo normal: si
50
+ * alguna vez devuelve algo, hay un fallo de verdad en quien solto el teclado, y
51
+ * por eso `CHOCOLATITO_TECLADO=debug` lo saca por pantalla en vez de callarselo.
52
+ */
53
+ export declare function repararTeclado(entrada: EntradaDeTeclado): string[];
54
+ /** Cada cuanto se comprueba. Cuatro veces por segundo se nota como instantaneo. */
55
+ export declare const MS_ENTRE_COMPROBACIONES = 250;
56
+ /**
57
+ * Vuelve a poner el modo crudo DE VERDAD, no solo en la cuenta de Node.
58
+ *
59
+ * EL AGUJERO QUE TAPA
60
+ *
61
+ * `setRawMode(true)` no siempre llega al terminal. Por debajo, libuv guarda el
62
+ * modo que cree tener y empieza con `if (tty->mode == mode) return 0;`. Asi que
63
+ * si algo cambia el modo de la consola por fuera de Node -un proceso hijo, la
64
+ * propia shell, el terminal- Node sigue creyendo que esta en crudo, y a partir
65
+ * de ahi TODOS los `setRawMode(true)` se convierten en no-op. Incluido el que
66
+ * hace el prompt al abrirse.
67
+ *
68
+ * Se veia asi, y se reporto cuatro veces: el usuario teclea, su texto sale al
69
+ * final de la linea de ayuda -lo esta haciendo el eco del terminal, no el
70
+ * prompt- y el recuadro no se entera de nada. El vigilante tampoco reparaba
71
+ * nada, porque preguntaba a Node y Node decia que todo estaba bien.
72
+ *
73
+ * Pasar por `false` primero rompe esa cache: la segunda llamada ya ve un cambio
74
+ * de verdad y lo aplica sobre la consola.
75
+ */
76
+ export declare function reafirmarModoCrudo(entrada: EntradaDeTeclado): void;
@@ -0,0 +1,94 @@
1
+ /**
2
+ * El prompt no suelta el teclado mientras esta abierto.
3
+ *
4
+ * EL FALLO QUE CIERRA ESTE ARCHIVO
5
+ *
6
+ * Reportado tres veces desde el uso real, siempre igual: "escribi 2 prompts y
7
+ * al siguiente ya no me deja escribir nada, teclee lo que teclee". La sesion
8
+ * pegada por el usuario tenia la pista buena, escondida en la ultima linea:
9
+ *
10
+ * ? /help para atajos · esc cancelar ⇥ modo normal · Espectro 1.0 (Agil) estaba haciendo y denuevo...
11
+ *
12
+ * Lo que escribia SI aparecia, pero pegado al final de la linea de ayuda en vez
13
+ * de dentro del recuadro. Eso solo pasa de una forma: el modo crudo estaba
14
+ * apagado, asi que quien hacia el eco era PowerShell —en el sitio donde estuviera
15
+ * el cursor— y el prompt no se enteraba de ninguna tecla, porque en modo cocido
16
+ * no llega nada hasta que se pulsa Enter. El teclado no estaba muerto: estaba
17
+ * en manos del terminal.
18
+ *
19
+ * POR QUE NO SE ARREGLA EN EL SITIO QUE LO ROMPE
20
+ *
21
+ * Hay demasiados sitios que lo rompen, y todos con razon:
22
+ *
23
+ * - `InterruptWatcher.stop()` devuelve el modo crudo a como estaba. Si un
24
+ * vigilante se para TARDE —despues de que el prompt ya se haya dibujado—
25
+ * restaura un estado que ya no toca y se lo quita al prompt.
26
+ * - `readline.createInterface(...).close()` hace por dentro `pause()` y
27
+ * `setRawMode(false)`. Lo usan el dialogo de "¿que prefieres que haga?" y la
28
+ * pregunta del detector de bucles.
29
+ * - Cualquier salida del bucle que se escape sin parar lo suyo.
30
+ *
31
+ * Ya hubo dos intentos de arreglarlo apretando tuercas una por una, y el fallo
32
+ * volvio las dos veces, porque basta que UNA de las veinte salidas se escape.
33
+ * Asi que el prompt deja de confiar: mientras esta abierto, comprueba cuatro
34
+ * veces por segundo que el teclado sigue siendo suyo y lo recupera si no.
35
+ *
36
+ * Cuesta cuatro comparaciones cada 250 ms. El fallo costaba cerrar el terminal.
37
+ */
38
+ /**
39
+ * Devuelve el teclado al prompt si alguien se lo ha quitado.
40
+ *
41
+ * Devuelve la lista de lo que ha tenido que reparar. Vacia es lo normal: si
42
+ * alguna vez devuelve algo, hay un fallo de verdad en quien solto el teclado, y
43
+ * por eso `CHOCOLATITO_TECLADO=debug` lo saca por pantalla en vez de callarselo.
44
+ */
45
+ export function repararTeclado(entrada) {
46
+ // Sin terminal delante (una tuberia, CI, `chocolatito "tarea"`) no hay modo
47
+ // crudo que defender y forzarlo seria un error.
48
+ if (!entrada || !entrada.isTTY || typeof entrada.setRawMode !== "function")
49
+ return [];
50
+ const reparado = [];
51
+ if (entrada.isRaw !== true) {
52
+ reafirmarModoCrudo(entrada);
53
+ reparado.push("modo crudo");
54
+ }
55
+ // `pause()` corta el flujo sin avisar a nadie: el oyente de `keypress` sigue
56
+ // apuntado y no recibe ni una tecla. Es lo que deja detras un readline al
57
+ // cerrarse.
58
+ if (typeof entrada.isPaused === "function" && entrada.isPaused() && typeof entrada.resume === "function") {
59
+ entrada.resume();
60
+ reparado.push("entrada en pausa");
61
+ }
62
+ return reparado;
63
+ }
64
+ /** Cada cuanto se comprueba. Cuatro veces por segundo se nota como instantaneo. */
65
+ export const MS_ENTRE_COMPROBACIONES = 250;
66
+ /**
67
+ * Vuelve a poner el modo crudo DE VERDAD, no solo en la cuenta de Node.
68
+ *
69
+ * EL AGUJERO QUE TAPA
70
+ *
71
+ * `setRawMode(true)` no siempre llega al terminal. Por debajo, libuv guarda el
72
+ * modo que cree tener y empieza con `if (tty->mode == mode) return 0;`. Asi que
73
+ * si algo cambia el modo de la consola por fuera de Node -un proceso hijo, la
74
+ * propia shell, el terminal- Node sigue creyendo que esta en crudo, y a partir
75
+ * de ahi TODOS los `setRawMode(true)` se convierten en no-op. Incluido el que
76
+ * hace el prompt al abrirse.
77
+ *
78
+ * Se veia asi, y se reporto cuatro veces: el usuario teclea, su texto sale al
79
+ * final de la linea de ayuda -lo esta haciendo el eco del terminal, no el
80
+ * prompt- y el recuadro no se entera de nada. El vigilante tampoco reparaba
81
+ * nada, porque preguntaba a Node y Node decia que todo estaba bien.
82
+ *
83
+ * Pasar por `false` primero rompe esa cache: la segunda llamada ya ve un cambio
84
+ * de verdad y lo aplica sobre la consola.
85
+ */
86
+ export function reafirmarModoCrudo(entrada) {
87
+ if (!entrada || typeof entrada.setRawMode !== "function")
88
+ return;
89
+ try {
90
+ entrada.setRawMode(false);
91
+ }
92
+ catch { }
93
+ entrada.setRawMode(true);
94
+ }
@@ -0,0 +1,17 @@
1
+ import { AvisoDeBucle } from "../agent/loopDetector.js";
2
+ export interface RespuestaDeBucle {
3
+ /** "seguir" deja al agente repetir; "parar" corta esa llamada y le obliga a cambiar. */
4
+ accion: "seguir" | "parar";
5
+ indicacion?: string;
6
+ }
7
+ /**
8
+ * Que hacer cuando el agente lleva tres vueltas haciendo exactamente lo mismo.
9
+ *
10
+ * Sin terminal —CI, `chocolatito "tarea"`, un cron— se PARA. Es lo contrario de
11
+ * lo que hace el dialogo de permisos, que ante la duda deniega para no tocar
12
+ * nada, y el motivo es que aqui el riesgo es el opuesto: no hay nada que
13
+ * proteger del agente, hay una cuota que proteger de un bucle que nadie esta
14
+ * mirando. Parar no mata el turno: le devuelve al modelo un mensaje explicando
15
+ * el atasco, y el turno sigue.
16
+ */
17
+ export declare function preguntarPorBucle(aviso: AvisoDeBucle): Promise<RespuestaDeBucle>;
@@ -0,0 +1,59 @@
1
+ import readline from "node:readline";
2
+ import process from "node:process";
3
+ import chalk from "chalk";
4
+ /**
5
+ * Que hacer cuando el agente lleva tres vueltas haciendo exactamente lo mismo.
6
+ *
7
+ * Sin terminal —CI, `chocolatito "tarea"`, un cron— se PARA. Es lo contrario de
8
+ * lo que hace el dialogo de permisos, que ante la duda deniega para no tocar
9
+ * nada, y el motivo es que aqui el riesgo es el opuesto: no hay nada que
10
+ * proteger del agente, hay una cuota que proteger de un bucle que nadie esta
11
+ * mirando. Parar no mata el turno: le devuelve al modelo un mensaje explicando
12
+ * el atasco, y el turno sigue.
13
+ */
14
+ export async function preguntarPorBucle(aviso) {
15
+ const cabecera = chalk.yellow(`\n↻ Parece un bucle: `) +
16
+ chalk.white(`"${aviso.herramienta}"`) +
17
+ chalk.yellow(` lleva ${aviso.repeticiones} llamadas identicas con el mismo resultado.`);
18
+ if (!process.stdin.isTTY) {
19
+ console.log(cabecera + chalk.gray("\n Sin terminal interactiva: se corta la repeticion para no gastar cuota.\n"));
20
+ return { accion: "parar" };
21
+ }
22
+ console.log(cabecera);
23
+ console.log(chalk.gray(" ¿Le dejo seguir intentandolo, o le digo que cambie de enfoque?\n"));
24
+ return new Promise((resolve) => {
25
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
26
+ // Como en el dialogo de permisos: 'close' llega tambien tras una respuesta
27
+ // normal, y sin guarda el mensaje de corte pisaria lo que el usuario acaba
28
+ // de escribir. Manda quien llegue primero.
29
+ let asentada = false;
30
+ const asentar = (r) => {
31
+ if (asentada)
32
+ return;
33
+ asentada = true;
34
+ resolve(r);
35
+ };
36
+ // Ctrl+C, Ctrl+D o stdin cerrandose significan lo mismo: no hay respuesta.
37
+ // Aqui eso es "para", que es la opcion que no cuesta dinero.
38
+ rl.on("close", () => {
39
+ // readline duerme su entrada al cerrarse (`close()` llama a `pause()`).
40
+ // Aqui estamos a mitad de turno: si se queda dormida, el vigilante que
41
+ // sigue no recibe ni una tecla y el Esc deja de cancelar.
42
+ process.stdin.resume();
43
+ if (asentada)
44
+ return;
45
+ process.stdout.write("\n");
46
+ asentar({ accion: "parar" });
47
+ });
48
+ rl.question(chalk.hex("#A855F7")(" [enter] que cambie de enfoque · [s] dejale seguir · o escribe una indicacion ❯ "), (respuesta) => {
49
+ const texto = respuesta.trim();
50
+ if (texto.toLowerCase() === "s" || texto.toLowerCase() === "si") {
51
+ asentar({ accion: "seguir" });
52
+ }
53
+ else {
54
+ asentar({ accion: "parar", indicacion: texto || undefined });
55
+ }
56
+ rl.close();
57
+ });
58
+ });
59
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Un pegado grande no se mete entero en el recuadro.
3
+ *
4
+ * EL PROBLEMA
5
+ *
6
+ * Pegar un error de compilacion de 300 lineas metia las 300 lineas dentro del
7
+ * prompt. El recuadro crecia hasta llenar la pantalla, el marco se volvia
8
+ * inutil, y para deshacerlo habia que pulsar borrar trescientas veces. Es el
9
+ * caso normal de usar un agente: uno pega un log, una traza, un archivo entero.
10
+ *
11
+ * COMO QUEDA
12
+ *
13
+ * El texto se guarda aparte y en el recuadro solo aparece una marca corta:
14
+ *
15
+ * ❯ arregla esto [pegado #1 · 347 lineas]
16
+ *
17
+ * Al enviar, la marca se sustituye por el texto completo, asi que el modelo
18
+ * recibe exactamente lo que se pego. Y un solo borrar se lleva la marca entera:
19
+ * lo que ocupa una linea se borra como una linea.
20
+ *
21
+ * POR QUE UNA MARCA DE TEXTO Y NO UNA ESTRUCTURA APARTE
22
+ *
23
+ * Porque el buffer del prompt es una cadena y todo lo demas -el cursor, el
24
+ * historial, el recorte del marco, la busqueda de menciones- cuenta caracteres.
25
+ * Meter ahi un objeto obligaria a tocar las seis cosas. Una marca de texto se
26
+ * lleva bien con todo lo que ya funciona.
27
+ */
28
+ /** A partir de aqui, un pegado se guarda aparte en vez de llenar el recuadro. */
29
+ export declare const MINIMO_LINEAS = 5;
30
+ export declare const MINIMO_CARACTERES = 400;
31
+ /** Si merece la pena guardarlo aparte, o es lo bastante corto para verlo entero. */
32
+ export declare function esGrande(texto: string): boolean;
33
+ /** Guarda el texto y devuelve la marca que va en el recuadro. */
34
+ export declare function guardarPegado(texto: string): string;
35
+ /** Devuelve el texto de verdad, con las marcas sustituidas. Es lo que ve el modelo. */
36
+ export declare function expandirPegados(entrada: string): string;
37
+ /**
38
+ * Si justo antes del cursor termina una marca, dice donde empieza.
39
+ *
40
+ * Es lo que hace que un solo borrar se lleve el pegado entero. Sin esto habria
41
+ * que borrar la marca letra a letra, que es un poco menos absurdo que borrar
42
+ * las 347 lineas pero sigue siendo absurdo.
43
+ */
44
+ export declare function marcaQueTerminaEn(buffer: string, cursor: number): {
45
+ inicio: number;
46
+ } | null;
47
+ /** Se olvidan al cerrar el prompt: cada turno empieza con su propia cuenta. */
48
+ export declare function limpiarPegados(): void;
49
+ /** Cuantos hay guardados ahora mismo. Solo para las pruebas. */
50
+ export declare function cuantosPegados(): number;
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Un pegado grande no se mete entero en el recuadro.
3
+ *
4
+ * EL PROBLEMA
5
+ *
6
+ * Pegar un error de compilacion de 300 lineas metia las 300 lineas dentro del
7
+ * prompt. El recuadro crecia hasta llenar la pantalla, el marco se volvia
8
+ * inutil, y para deshacerlo habia que pulsar borrar trescientas veces. Es el
9
+ * caso normal de usar un agente: uno pega un log, una traza, un archivo entero.
10
+ *
11
+ * COMO QUEDA
12
+ *
13
+ * El texto se guarda aparte y en el recuadro solo aparece una marca corta:
14
+ *
15
+ * ❯ arregla esto [pegado #1 · 347 lineas]
16
+ *
17
+ * Al enviar, la marca se sustituye por el texto completo, asi que el modelo
18
+ * recibe exactamente lo que se pego. Y un solo borrar se lleva la marca entera:
19
+ * lo que ocupa una linea se borra como una linea.
20
+ *
21
+ * POR QUE UNA MARCA DE TEXTO Y NO UNA ESTRUCTURA APARTE
22
+ *
23
+ * Porque el buffer del prompt es una cadena y todo lo demas -el cursor, el
24
+ * historial, el recorte del marco, la busqueda de menciones- cuenta caracteres.
25
+ * Meter ahi un objeto obligaria a tocar las seis cosas. Una marca de texto se
26
+ * lleva bien con todo lo que ya funciona.
27
+ */
28
+ /** A partir de aqui, un pegado se guarda aparte en vez de llenar el recuadro. */
29
+ export const MINIMO_LINEAS = 5;
30
+ export const MINIMO_CARACTERES = 400;
31
+ const guardados = new Map();
32
+ let siguiente = 1;
33
+ /** Reconoce una marca. El id es lo unico que hace falta para recuperar el texto. */
34
+ const MARCA = /\[pegado #(\d+) · [^\]]*\]/g;
35
+ function resumen(texto) {
36
+ const lineas = texto.split("\n").length;
37
+ if (lineas > 1)
38
+ return `${lineas.toLocaleString("es")} lineas`;
39
+ return `${texto.length.toLocaleString("es")} caracteres`;
40
+ }
41
+ /** Si merece la pena guardarlo aparte, o es lo bastante corto para verlo entero. */
42
+ export function esGrande(texto) {
43
+ return texto.length >= MINIMO_CARACTERES || texto.split("\n").length > MINIMO_LINEAS;
44
+ }
45
+ /** Guarda el texto y devuelve la marca que va en el recuadro. */
46
+ export function guardarPegado(texto) {
47
+ const id = siguiente++;
48
+ guardados.set(id, texto);
49
+ return `[pegado #${id} · ${resumen(texto)}]`;
50
+ }
51
+ /** Devuelve el texto de verdad, con las marcas sustituidas. Es lo que ve el modelo. */
52
+ export function expandirPegados(entrada) {
53
+ // Una marca de un id que ya no existe se deja tal cual: es texto que el
54
+ // usuario escribio a mano, y borrarselo seria peor que dejarlo.
55
+ return entrada.replace(MARCA, (todo, id) => guardados.get(Number(id)) ?? todo);
56
+ }
57
+ /**
58
+ * Si justo antes del cursor termina una marca, dice donde empieza.
59
+ *
60
+ * Es lo que hace que un solo borrar se lleve el pegado entero. Sin esto habria
61
+ * que borrar la marca letra a letra, que es un poco menos absurdo que borrar
62
+ * las 347 lineas pero sigue siendo absurdo.
63
+ */
64
+ export function marcaQueTerminaEn(buffer, cursor) {
65
+ if (cursor <= 0 || buffer[cursor - 1] !== "]")
66
+ return null;
67
+ const trozo = buffer.slice(0, cursor);
68
+ const inicio = trozo.lastIndexOf("[pegado #");
69
+ if (inicio < 0)
70
+ return null;
71
+ // Que lo que hay entre los corchetes sea de verdad una marca y no un
72
+ // "[pegado #" que el usuario escribiera a mano.
73
+ return /^\[pegado #\d+ · [^\]]*\]$/.test(trozo.slice(inicio)) ? { inicio } : null;
74
+ }
75
+ /** Se olvidan al cerrar el prompt: cada turno empieza con su propia cuenta. */
76
+ export function limpiarPegados() {
77
+ guardados.clear();
78
+ siguiente = 1;
79
+ }
80
+ /** Cuantos hay guardados ahora mismo. Solo para las pruebas. */
81
+ export function cuantosPegados() {
82
+ return guardados.size;
83
+ }
@@ -2,6 +2,7 @@ import readline from "node:readline";
2
2
  import process from "node:process";
3
3
  import chalk from "chalk";
4
4
  import { cycleMode, describeMode, getMode, modeAnnouncement } from "./modes.js";
5
+ import { isOutsideProject, isSvgSource } from "../config/permissions.js";
5
6
  /**
6
7
  * Aprobacion automatica: se activa con --yes / -y o con la variable de entorno
7
8
  * CHOCOLATITO_AUTO_APPROVE. Pensada para modo no interactivo y CI.
@@ -77,9 +78,45 @@ export async function askToolPermission(toolName, args, cwd = process.cwd()) {
77
78
  input: process.stdin,
78
79
  output: process.stdout,
79
80
  });
81
+ // Guarda de resolucion unica: 'close' llega tambien despues de una
82
+ // respuesta normal, y sin ella el mensaje de cancelado pisaria al texto
83
+ // que el usuario acaba de escribir. Manda quien llegue primero.
84
+ let asentada = false;
85
+ const asentar = (texto) => {
86
+ if (asentada)
87
+ return;
88
+ asentada = true;
89
+ resFeedback(texto);
90
+ };
91
+ // Ctrl+C aqui dentro no llama al callback de 'line': readline cierra la
92
+ // interfaz y punto. Sin este oyente la promesa no se asentaba nunca, el
93
+ // `await` de askToolPermission no volvia y el bucle del agente se
94
+ // quedaba vivo pero inerte: ni ejecutaba, ni cancelaba, ni devolvia el
95
+ // prompt. La unica salida era un segundo Ctrl+C, que ya llegaba con el
96
+ // terminal en modo cocido y mataba la sesion entera.
97
+ //
98
+ // Vale igual para Ctrl+D y para stdin cerrandose por su cuenta: todo lo
99
+ // que cierre la interfaz sin linea significa lo mismo, que no hay
100
+ // respuesta, y eso se traduce en cancelar la accion.
101
+ rl.on("close", () => {
102
+ if (asentada)
103
+ return;
104
+ // El cursor se queda a medio renglon, pegado al "❯": se cierra la
105
+ // linea para que lo siguiente no salga encima del prompt.
106
+ process.stdout.write("\n");
107
+ asentar("Cancelado por el usuario con Ctrl+C.");
108
+ });
109
+ // Un readline, al cerrarse, hace por dentro `pause()` sobre su entrada
110
+ // ademas de apagar el modo crudo. Quien venga detras —el vigilante del
111
+ // resto del turno— da por hecho que la entrada fluye, se apunta como
112
+ // oyente y no recibe ni una tecla: el Esc deja de cancelar y lo que se
113
+ // teclee no se encola. Se despierta aqui, que es donde se durmio.
114
+ rl.on("close", () => {
115
+ process.stdin.resume();
116
+ });
80
117
  rl.question(chalk.hex("#A855F7")("\n¿Qué prefieres que haga? ❯ "), (answer) => {
118
+ asentar(answer.trim() || "El usuario canceló la acción sin dar detalles adicionales.");
81
119
  rl.close();
82
- resFeedback(answer.trim() || "El usuario canceló la acción sin dar detalles adicionales.");
83
120
  });
84
121
  });
85
122
  }
@@ -235,6 +272,10 @@ function classify(toolName, args) {
235
272
  return { title: " Controlar tu ordenador", color: "#A855F7" };
236
273
  return { title: " Permiso de ejecución", color: "#D97757" };
237
274
  }
275
+ /** Herramientas que conducen pestanas del navegador del usuario. */
276
+ function esNavegador(toolName) {
277
+ return toolName === "chrome" || toolName === "browser" || toolName === "browser_live";
278
+ }
238
279
  /** Las lineas de detalle, ya recortadas para que quepan dentro del marco. */
239
280
  function details(toolName, args, cwd, width) {
240
281
  const max = width - 8;
@@ -258,14 +299,55 @@ function details(toolName, args, cwd, width) {
258
299
  add(plain, `${chalk.green(`+${mas} líneas`)} ${chalk.red(`-${menos} líneas`)}`);
259
300
  }
260
301
  }
302
+ else if (typeof args.outputPath === "string") {
303
+ // create_image no trae ni `command` ni `path`, asi que el dialogo caia en el
304
+ // caso de abajo y pedia aprobar la palabra "create_image" a secas: ni el
305
+ // destino ni el detalle de que el generatorScript se ejecuta con python.
306
+ add(args.outputPath, chalk.bold.white(args.outputPath));
307
+ const script = typeof args.generatorScript === "string" ? args.generatorScript : "";
308
+ const lineas = script.split("\n").length;
309
+ if (isSvgSource(script)) {
310
+ add(`SVG, ${lineas} líneas`, chalk.gray(`SVG, ${lineas} líneas`));
311
+ }
312
+ else {
313
+ const plain = `ejecuta ${lineas} líneas de Python`;
314
+ add(plain, chalk.hex("#F59E0B")(plain));
315
+ }
316
+ }
317
+ else if (typeof args.sourcePath === "string") {
318
+ // move_copy_file no trae `path` ni `outputPath`, asi que caia en el caso de
319
+ // abajo y el dialogo pedia aprobar la palabra "move_copy_file" a secas. Lo
320
+ // que hay que ver aqui es justo lo contrario: que archivo se pisa.
321
+ add(args.sourcePath, chalk.bold.white(args.sourcePath));
322
+ const destino = String(args.destinationPath ?? "");
323
+ add(`-> ${destino}`, `${chalk.gray("->")} ${chalk.bold.white(destino)}`);
324
+ if (args.overwrite === true) {
325
+ const aviso = "el destino ya existe y su contenido se pierde";
326
+ add(aviso, chalk.hex("#EF4444")(aviso));
327
+ }
328
+ }
261
329
  else if (typeof args.action === "string") {
262
- const extra = args.url || args.appName || args.window || args.text || "";
330
+ // Sobre QUE actua importa tanto como la accion. Un "close_tab" a secas no
331
+ // decia que pestana moria, asi que aprobarlo era aprobar a ciegas.
332
+ const objetivo = args.url || args.appName || args.window || args.text ||
333
+ (typeof args.tabId === "number" ? `pestaña ${args.tabId}` : "") ||
334
+ (typeof args.ref === "number" ? `elemento [${args.ref}]` : "") ||
335
+ "";
336
+ const extra = objetivo || (esNavegador(toolName) ? "en la pestaña de trabajo" : "");
263
337
  const plain = extra ? `${args.action} ${String(extra)}` : String(args.action);
264
338
  add(plain, extra ? `${chalk.bold.white(args.action)} ${chalk.gray(String(extra))}` : chalk.bold.white(args.action));
265
339
  }
266
340
  else {
267
341
  add(toolName, chalk.bold.white(toolName));
268
342
  }
343
+ // Una ruta larga se lee igual venga de donde venga: "C:\Users\yo\proyecto\src"
344
+ // y "C:\Windows\System32" ocupan lo mismo en el marco. Este es justo el
345
+ // dialogo que se aprueba de un vistazo, asi que el motivo por el que aparece
346
+ // va escrito con letras.
347
+ if ([args.path, args.outputPath, args.sourcePath, args.destinationPath].some((r) => isOutsideProject(cwd, r))) {
348
+ const aviso = "fuera del proyecto";
349
+ add(aviso, chalk.hex("#EF4444")(aviso));
350
+ }
269
351
  out.push({ text: "", length: 0 });
270
352
  return out;
271
353
  }
@@ -1,7 +1,47 @@
1
+ /**
2
+ * Imprime sin romper el prompt. Uselo en vez de `console.log` para todo lo que
3
+ * pueda llegar TARDE: conexiones en segundo plano, avisos, temporizadores.
4
+ */
5
+ export declare function imprimirSobreElPrompt(texto: string): void;
1
6
  export interface SlashCommandItem {
2
7
  name: string;
3
8
  description: string;
4
9
  args?: string;
5
10
  }
6
11
  export declare const SLASH_COMMANDS: SlashCommandItem[];
12
+ export interface DropdownItem {
13
+ type: "command" | "file";
14
+ label: string;
15
+ secondary?: string;
16
+ insertText: string;
17
+ }
18
+ export interface PromptFrameOptions {
19
+ cols: number;
20
+ /** Alto de la ventana; sirve para no dibujar un marco que no quepa. */
21
+ rows?: number;
22
+ promptSymbol?: string;
23
+ input?: string;
24
+ cursorIndex?: number;
25
+ items?: DropdownItem[];
26
+ selectedIndex?: number;
27
+ }
28
+ export interface PromptFrame {
29
+ /** Lineas LOGICAS. Cualquiera puede ocupar mas de una fila fisica. */
30
+ lines: string[];
31
+ /** Filas de pantalla que ocupara el marco entero con este ancho. */
32
+ totalRows: number;
33
+ /** Fila fisica del cursor, con 0 en la primera fila del marco. */
34
+ cursorRow: number;
35
+ cursorCol: number;
36
+ /** Primer elemento del desplegable que se muestra (ventana deslizante). */
37
+ dropdownOffset: number;
38
+ }
39
+ /**
40
+ * Arma el marco del prompt y dice exactamente donde queda el cursor.
41
+ *
42
+ * Va aparte de render() por dos razones: se puede probar sin terminal, y la
43
+ * aritmetica de filas se hace UNA vez sobre las mismas lineas que se pintan, en
44
+ * lugar de estimarse por otro lado y descuadrarse.
45
+ */
46
+ export declare function buildPromptFrame(opts: PromptFrameOptions): PromptFrame;
7
47
  export declare function askInteractivePrompt(promptSymbol?: string, cwd?: string): Promise<string>;