chocolatito-code 1.6.13 → 1.6.15
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/COMPUTER-USE.md +37 -0
- package/README.md +29 -3
- package/dist/agent/historialDeCambios.d.ts +77 -0
- package/dist/agent/historialDeCambios.js +220 -0
- package/dist/agent/salidaDelAgente.d.ts +9 -0
- package/dist/agent/salidaDelAgente.js +19 -0
- package/dist/agent/undoManager.d.ts +21 -0
- package/dist/agent/undoManager.js +85 -5
- package/dist/config/permissions.js +4 -0
- package/dist/index.js +216 -27
- package/dist/prompts/systemPrompt.js +15 -11
- package/dist/servidor/archivos.d.ts +69 -0
- package/dist/servidor/archivos.js +156 -0
- package/dist/servidor/captura.d.ts +9 -0
- package/dist/servidor/captura.js +48 -0
- package/dist/servidor/fondo.d.ts +85 -0
- package/dist/servidor/fondo.js +134 -0
- package/dist/servidor/marca.d.ts +14 -0
- package/dist/servidor/marca.js +14 -0
- package/dist/servidor/pagina.d.ts +0 -26
- package/dist/servidor/pagina.js +337 -10
- package/dist/servidor/puente.d.ts +13 -0
- package/dist/servidor/puente.js +11 -0
- package/dist/servidor/servidor.d.ts +76 -2
- package/dist/servidor/servidor.js +245 -13
- package/dist/tools/computerUse.d.ts +11 -1
- package/dist/tools/computerUse.js +376 -34
- package/dist/tools/toolDefsComputer.js +39 -14
- package/dist/tools/visionBridge.d.ts +4 -2
- package/dist/tools/visionBridge.js +40 -15
- package/dist/tools/win/hostScript.js +300 -29
- package/dist/ui/comandos.js +1 -0
- package/dist/ui/ink/App.js +23 -3
- package/dist/ui/ink/Prompt.d.ts +5 -1
- package/dist/ui/ink/Prompt.js +92 -4
- package/dist/ui/ink/control.d.ts +2 -0
- package/dist/ui/ink/control.js +15 -0
- package/dist/ui/ink/montarApp.d.ts +0 -19
- package/dist/ui/ink/montarApp.js +3 -0
- package/dist/ui/permissionPrompt.js +44 -9
- package/dist/ui/pieFijo.js +5 -0
- package/dist/ui/prompt.js +6 -0
- package/dist/ui/renderer.d.ts +1 -0
- package/dist/ui/renderer.js +27 -2
- package/dist/ui/selector.js +5 -0
- package/dist/ui/spinner.d.ts +1 -0
- package/dist/ui/spinner.js +6 -0
- package/extension/manifest.json +1 -1
- package/package.json +1 -1
package/COMPUTER-USE.md
CHANGED
|
@@ -482,3 +482,40 @@ Es ambiguo y no voy a elegir por ti: usa list_windows y pasa un titulo mas concr
|
|
|
482
482
|
|
|
483
483
|
Elegir a ciegas entre candidatos empatados no es una comodidad: es cómo se acaba
|
|
484
484
|
mandando una pulsación destructiva a la ventana equivocada.
|
|
485
|
+
|
|
486
|
+
---
|
|
487
|
+
|
|
488
|
+
## 9. Nivel Google Project Astra & OpenAI Operator: Percepción Multimodal y Flujos Autónomos
|
|
489
|
+
|
|
490
|
+
Para acercar Chocolatito Code al nivel de **Google Project Astra** y **OpenAI Operator**, se incorporaron capacidades avanzadas de percepción multimodal y ejecución compuesta:
|
|
491
|
+
|
|
492
|
+
### 9.1 Cuadrícula de Coordenadas Visuales (`grid: true`)
|
|
493
|
+
Cuando una interfaz carece de árbol de accesibilidad (lienzos HTML5/WebGL, motores de videojuegos, apps Qt/Flutter o ventanas remotas), los modelos multimodales a menudo sufren desviaciones de 30 a 80 píxeles.
|
|
494
|
+
Al pasar `grid: true` (o en `find_element` visual), el host dibuja una cuadrícula milimétrica semitransparente con marcadores de coordenadas directas `(x,y)` cada 200px. El modelo de visión lee el marcador visual exacto, logrando una precisión de localización superior al 98%.
|
|
495
|
+
|
|
496
|
+
### 9.2 Encadenamiento de Acciones Compuestas (`action: "sequence"`)
|
|
497
|
+
Reducir los viajes de ida y vuelta al modelo es esencial para la agilidad. En vez de pagar 4 turnos (20–30s) para hacer clic en un buscador, escribir una consulta, presionar Enter y esperar el cambio, se pasa una sola acción:
|
|
498
|
+
```json
|
|
499
|
+
{
|
|
500
|
+
"action": "sequence",
|
|
501
|
+
"steps": [
|
|
502
|
+
{ "action": "ui_click", "ref": 4 },
|
|
503
|
+
{ "action": "type", "text": "chocolatito code" },
|
|
504
|
+
{ "action": "key", "keys": "Return" },
|
|
505
|
+
{ "action": "wait_change" }
|
|
506
|
+
]
|
|
507
|
+
}
|
|
508
|
+
```
|
|
509
|
+
Se ejecuta en subsegundos en la máquina local y devuelve el resultado consolidado junto con el estado posterior de la ventana.
|
|
510
|
+
|
|
511
|
+
### 9.3 Detección de Cambios Visuales (`wait_visual_change`)
|
|
512
|
+
Las aplicaciones que dibujan mediante DirectX, OpenGL, streaming o canvas web no disparan eventos de Windows UI Automation. `wait_visual_change` realiza un muestreo de píxeles de alta velocidad (128x128) y detecta transiciones en la pantalla, ventana o región específica sin consumir CPU.
|
|
513
|
+
|
|
514
|
+
### 9.4 Inspección de Alta Resolución por Región (`region: [x1, y1, x2, y2]`)
|
|
515
|
+
Permite recortar e inspeccionar un área crítica (iconos de bandeja, barras de estado, mensajes de error diminutos) en resolución nativa 1:1 sin sobrepasar el límite de tamaño de imagen para visión.
|
|
516
|
+
|
|
517
|
+
### 9.5 Auto-Recuperación Resiliente en `ui_click` y `ui_type`
|
|
518
|
+
Si un control de interfaz personalizada (WinUI 3, WPF o Electron) no implementa `InvokePattern` o `TogglePattern`, `ui_click` no falla: obtiene automáticamente el centroide de su rectángulo delimitador (`ui_rect`) y envía un clic de segundo plano (`bg_click`). Igualmente, `ui_type` apoya con enfoque automático por clic antes de teclear.
|
|
519
|
+
|
|
520
|
+
### 9.6 Coordenadas Normalizadas (0..1000 y 0.0..1.0)
|
|
521
|
+
Soporte nativo para coordenadas normalizadas en escala 0..1000 estilo OpenAI Operator y Anthropic, adaptándose automáticamente a monitores 1080p, 2K, 4K y configuraciones multimonitor.
|
package/README.md
CHANGED
|
@@ -94,7 +94,10 @@ teclado.
|
|
|
94
94
|
|
|
95
95
|
```bash
|
|
96
96
|
chocolatito --servir # solo desde esta máquina
|
|
97
|
-
chocolatito --servir-red # también desde el móvil o la tablet de tu red
|
|
97
|
+
chocolatito --servir-red # también desde el móvil o la tablet de tu red
|
|
98
|
+
chocolatito --servir-red --fondo # y suelta el terminal: puedes cerrar la ventana
|
|
99
|
+
chocolatito --estado # ¿sigue viva?
|
|
100
|
+
chocolatito --parar # cerrarla
|
|
98
101
|
```
|
|
99
102
|
|
|
100
103
|
Imprime un enlace con un token dentro. Ese enlace **es** la llave: quien lo tenga entero
|
|
@@ -109,12 +112,22 @@ JavaScript de Node. Por el navegador no hace falta instalar nada.
|
|
|
109
112
|
| --- | --- |
|
|
110
113
|
| `--puerto N` | Otro puerto (por defecto 4700). |
|
|
111
114
|
| Se reconecta solo | Un móvil pierde la red a cada rato; la sesión no se pierde con ella. |
|
|
112
|
-
| Los permisos | Salen en el móvil, con los mismos botones que en el terminal. |
|
|
115
|
+
| Los permisos | Salen en el móvil, con los mismos botones que en el terminal. |
|
|
116
|
+
| Archivos | Arrastra, pega del portapapeles o pulsa `+`. Caen en `entradas/` y el agente los recibe con su ruta. |
|
|
117
|
+
| Imágenes | Lo que el agente exporta se **ve** en la página, no se anuncia con una ruta. |
|
|
118
|
+
| Te avisa | Cuando pide permiso o cuando termina: notificación, vibración y un punto en la pestaña. Solo si no estás mirando la página. |
|
|
119
|
+
| Y te lo llevas | **⇩ Guardar** bajo cada resultado: la pieza, el ZIP o el PDF van al teléfono sin pasar por el ordenador. |
|
|
113
120
|
|
|
114
121
|
**Manda el navegador.** Con `--servir`, lo que escribas en el terminal ya no se lee: el
|
|
115
122
|
terminal queda de monitor. Es a propósito — dos entradas peleándose por el mismo teclado
|
|
116
123
|
es de donde salen los cuelgues raros.
|
|
117
124
|
|
|
125
|
+
**Desde fuera de tu red**, con un túnel: `cloudflared tunnel --url http://127.0.0.1:4700` y
|
|
126
|
+
después `chocolatito --servir --tunel <el enlace que te dé>`. Esa bandera autoriza **ese**
|
|
127
|
+
dominio y solo ese; sin ella el servidor lo rechaza, que es lo que tiene que hacer con un
|
|
128
|
+
nombre de dominio cualquiera. Ojo: ese enlace sale a internet y lo único que lo protege es
|
|
129
|
+
el token. Ciérralo al acabar.
|
|
130
|
+
|
|
118
131
|
**Lo que NO hace, a propósito:** no hay HTTPS ni túnel propio. Para entrar desde fuera de
|
|
119
132
|
tu casa, usa Tailscale o `cloudflared`, que ya existen y están auditados. Un túnel casero
|
|
120
133
|
es la parte fácil de una responsabilidad muy grande.
|
|
@@ -339,7 +352,20 @@ No hace falta esperar a que termine. Lo que escribas durante una generación apa
|
|
|
339
352
|
el spinner y, al pulsar Enter, queda en cola: el agente acaba su turno y lo toma como
|
|
340
353
|
siguiente orden, sin que tengas que volver a teclearlo.
|
|
341
354
|
|
|
342
|
-
`Esc` pausa el turno en curso. `Ctrl+C` una vez interrumpe; dos veces cierra la sesión.
|
|
355
|
+
`Esc` pausa el turno en curso. `Ctrl+C` una vez interrumpe; dos veces cierra la sesión.
|
|
356
|
+
|
|
357
|
+
---
|
|
358
|
+
|
|
359
|
+
## ⏪ Volver atrás (`/undo` y `/rewind`)
|
|
360
|
+
|
|
361
|
+
Antes de tocar un archivo, Chocolatito guarda una copia. `/undo` deshace el último turno y
|
|
362
|
+
`/rewind` enseña la lista para elegir hasta dónde volver.
|
|
363
|
+
|
|
364
|
+
**Y sobrevive a cerrar la sesión**, que es cuando de verdad hace falta: uno no deshace treinta
|
|
365
|
+
segundos después, sino al día siguiente, al abrir el proyecto y ver que falta algo. Se guarda
|
|
366
|
+
en `.chocolatito/cambios/` de tu proyecto, con los últimos 20 turnos, y con un `.gitignore`
|
|
367
|
+
dentro para que no acabe en tu repositorio. Los archivos de más de 25 MB no se copian: se te
|
|
368
|
+
avisa en el momento, en vez de prometerte una vuelta atrás que no existe.
|
|
343
369
|
|
|
344
370
|
---
|
|
345
371
|
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { Checkpoint, FileSnapshot } from "./undoManager.js";
|
|
2
|
+
/**
|
|
3
|
+
* EL HISTORIAL DE CAMBIOS, EN DISCO
|
|
4
|
+
*
|
|
5
|
+
* DE DONDE SALE
|
|
6
|
+
*
|
|
7
|
+
* `/undo` estaba entero en memoria: `private checkpoints: Checkpoint[] = []`.
|
|
8
|
+
* O sea que la red de seguridad de un agente que edita y borra archivos solo
|
|
9
|
+
* duraba lo que durase la ventana del terminal. Cierras, o se cierra sola, y ya
|
|
10
|
+
* no hay vuelta atras de nada de lo que hizo.
|
|
11
|
+
*
|
|
12
|
+
* Y es al reves de como se usa: el momento en que uno quiere deshacer no es
|
|
13
|
+
* treinta segundos despues -ahi todavia estas mirando-, es al dia siguiente,
|
|
14
|
+
* cuando abres el proyecto y ves que falta algo.
|
|
15
|
+
*
|
|
16
|
+
* DONDE SE GUARDA
|
|
17
|
+
*
|
|
18
|
+
* En `<proyecto>/.chocolatito/cambios/`, al lado de la memoria del proyecto y
|
|
19
|
+
* no en el home: lo que se guarda son archivos DE ESE proyecto, y si el
|
|
20
|
+
* proyecto se borra o se mueve, su historial se va con el. Mezclarlos todos en
|
|
21
|
+
* el home dejaria copias de codigo de clientes distintos en el mismo cajon.
|
|
22
|
+
*
|
|
23
|
+
* QUE SE GUARDA, Y QUE NO
|
|
24
|
+
*
|
|
25
|
+
* Un turno es una carpeta: `meta.json` con la lista de rutas y un archivo por
|
|
26
|
+
* cada copia, numerado. Los bytes se guardan CRUDOS, sin codificar: por aqui
|
|
27
|
+
* pasan imagenes y videos, y un mp4 convertido a texto vuelve corrupto -ya pasó
|
|
28
|
+
* una vez con /undo y utf-8-.
|
|
29
|
+
*
|
|
30
|
+
* Con dos topes, porque esto crece en el disco de otro:
|
|
31
|
+
*
|
|
32
|
+
* - TAMANO POR ARCHIVO. Copiar un video de 500 MB en cada turno que lo toque
|
|
33
|
+
* llena el disco sin que nadie lo pida. Por encima del tope consta como
|
|
34
|
+
* "sin copia", que es exactamente lo que es, y se avisa en el momento.
|
|
35
|
+
* - CUANTOS TURNOS SE GUARDAN. Los mas viejos se tiran enteros. Veinte es lo
|
|
36
|
+
* que cabe en la cabeza de alguien que intenta acordarse de que hizo ayer.
|
|
37
|
+
*/
|
|
38
|
+
/** Por encima de esto no se copia: se anota que no hay copia y se dice. */
|
|
39
|
+
export declare const TOPE_POR_ARCHIVO: number;
|
|
40
|
+
/** Turnos que se conservan. Mas atras, nadie se acuerda de que paso. */
|
|
41
|
+
export declare const TURNOS_GUARDADOS = 20;
|
|
42
|
+
export declare const CARPETA: string;
|
|
43
|
+
export interface TurnoGuardado {
|
|
44
|
+
id: string;
|
|
45
|
+
/** ISO, no la hora local: esto se lee al dia siguiente y desde otra zona. */
|
|
46
|
+
cuando: string;
|
|
47
|
+
descripcion: string;
|
|
48
|
+
archivos: Array<{
|
|
49
|
+
ruta: string;
|
|
50
|
+
tipo: FileSnapshot["kind"];
|
|
51
|
+
/** Nombre del blob dentro de la carpeta del turno, si hay copia. */
|
|
52
|
+
copia?: string;
|
|
53
|
+
motivo?: string;
|
|
54
|
+
bytes?: number;
|
|
55
|
+
}>;
|
|
56
|
+
}
|
|
57
|
+
export declare function carpetaDeCambios(cwd: string): string;
|
|
58
|
+
/**
|
|
59
|
+
* Guarda un turno. Devuelve lo que NO se pudo copiar, para poder decirlo.
|
|
60
|
+
*
|
|
61
|
+
* No lanza nunca: si el disco esta lleno o la carpeta es de solo lectura, el
|
|
62
|
+
* turno del usuario no se puede caer por culpa del historial.
|
|
63
|
+
*/
|
|
64
|
+
export declare function guardarTurno(cwd: string, punto: Checkpoint): {
|
|
65
|
+
guardado: boolean;
|
|
66
|
+
sinCopia: string[];
|
|
67
|
+
};
|
|
68
|
+
/** Los turnos guardados, del mas viejo al mas nuevo. Solo las fichas. */
|
|
69
|
+
export declare function turnosGuardados(cwd: string): TurnoGuardado[];
|
|
70
|
+
/** Recupera un turno con sus bytes, listo para que UndoManager lo deshaga. */
|
|
71
|
+
export declare function leerTurno(cwd: string, id: string): Checkpoint | null;
|
|
72
|
+
/** Un turno ya deshecho no tiene por que quedarse ocupando sitio. */
|
|
73
|
+
export declare function olvidarTurno(cwd: string, id: string): void;
|
|
74
|
+
/** Tira los turnos mas viejos que sobren del tope. */
|
|
75
|
+
export declare function podar(cwd: string, tope?: number): void;
|
|
76
|
+
/** Lo que ocupa el historial, para poder decirlo sin que nadie lo mida a mano. */
|
|
77
|
+
export declare function bytesOcupados(cwd: string): number;
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
/**
|
|
4
|
+
* EL HISTORIAL DE CAMBIOS, EN DISCO
|
|
5
|
+
*
|
|
6
|
+
* DE DONDE SALE
|
|
7
|
+
*
|
|
8
|
+
* `/undo` estaba entero en memoria: `private checkpoints: Checkpoint[] = []`.
|
|
9
|
+
* O sea que la red de seguridad de un agente que edita y borra archivos solo
|
|
10
|
+
* duraba lo que durase la ventana del terminal. Cierras, o se cierra sola, y ya
|
|
11
|
+
* no hay vuelta atras de nada de lo que hizo.
|
|
12
|
+
*
|
|
13
|
+
* Y es al reves de como se usa: el momento en que uno quiere deshacer no es
|
|
14
|
+
* treinta segundos despues -ahi todavia estas mirando-, es al dia siguiente,
|
|
15
|
+
* cuando abres el proyecto y ves que falta algo.
|
|
16
|
+
*
|
|
17
|
+
* DONDE SE GUARDA
|
|
18
|
+
*
|
|
19
|
+
* En `<proyecto>/.chocolatito/cambios/`, al lado de la memoria del proyecto y
|
|
20
|
+
* no en el home: lo que se guarda son archivos DE ESE proyecto, y si el
|
|
21
|
+
* proyecto se borra o se mueve, su historial se va con el. Mezclarlos todos en
|
|
22
|
+
* el home dejaria copias de codigo de clientes distintos en el mismo cajon.
|
|
23
|
+
*
|
|
24
|
+
* QUE SE GUARDA, Y QUE NO
|
|
25
|
+
*
|
|
26
|
+
* Un turno es una carpeta: `meta.json` con la lista de rutas y un archivo por
|
|
27
|
+
* cada copia, numerado. Los bytes se guardan CRUDOS, sin codificar: por aqui
|
|
28
|
+
* pasan imagenes y videos, y un mp4 convertido a texto vuelve corrupto -ya pasó
|
|
29
|
+
* una vez con /undo y utf-8-.
|
|
30
|
+
*
|
|
31
|
+
* Con dos topes, porque esto crece en el disco de otro:
|
|
32
|
+
*
|
|
33
|
+
* - TAMANO POR ARCHIVO. Copiar un video de 500 MB en cada turno que lo toque
|
|
34
|
+
* llena el disco sin que nadie lo pida. Por encima del tope consta como
|
|
35
|
+
* "sin copia", que es exactamente lo que es, y se avisa en el momento.
|
|
36
|
+
* - CUANTOS TURNOS SE GUARDAN. Los mas viejos se tiran enteros. Veinte es lo
|
|
37
|
+
* que cabe en la cabeza de alguien que intenta acordarse de que hizo ayer.
|
|
38
|
+
*/
|
|
39
|
+
/** Por encima de esto no se copia: se anota que no hay copia y se dice. */
|
|
40
|
+
export const TOPE_POR_ARCHIVO = 25 * 1024 * 1024;
|
|
41
|
+
/** Turnos que se conservan. Mas atras, nadie se acuerda de que paso. */
|
|
42
|
+
export const TURNOS_GUARDADOS = 20;
|
|
43
|
+
export const CARPETA = path.join(".chocolatito", "cambios");
|
|
44
|
+
/**
|
|
45
|
+
* Que esto no acabe en el repositorio de nadie.
|
|
46
|
+
*
|
|
47
|
+
* Son copias de archivos del proyecto: commitearlas seria subir el trabajo dos
|
|
48
|
+
* veces, y en un repositorio compartido, ensenarle a todo el equipo lo que
|
|
49
|
+
* habia antes de cada cambio.
|
|
50
|
+
*
|
|
51
|
+
* Se pone un .gitignore DENTRO de la carpeta, con un asterisco, en vez de tocar
|
|
52
|
+
* el .gitignore del usuario: editar un archivo suyo para instalar nuestra basura
|
|
53
|
+
* es justo lo que no se hace. Y va solo en cambios/, no en .chocolatito/ entero:
|
|
54
|
+
* la memoria del proyecto de al lado a lo mejor si la quiere commitear.
|
|
55
|
+
*/
|
|
56
|
+
function asegurarQueGitLoIgnora(cwd) {
|
|
57
|
+
const marca = path.join(carpetaDeCambios(cwd), ".gitignore");
|
|
58
|
+
try {
|
|
59
|
+
if (!fs.existsSync(marca))
|
|
60
|
+
fs.writeFileSync(marca, "*" + String.fromCharCode(10), "utf-8");
|
|
61
|
+
}
|
|
62
|
+
catch { }
|
|
63
|
+
}
|
|
64
|
+
export function carpetaDeCambios(cwd) {
|
|
65
|
+
return path.join(cwd, CARPETA);
|
|
66
|
+
}
|
|
67
|
+
function carpetaDelTurno(cwd, id) {
|
|
68
|
+
return path.join(carpetaDeCambios(cwd), id);
|
|
69
|
+
}
|
|
70
|
+
/** Un nombre de archivo que no puede salirse de su carpeta ni chocar. */
|
|
71
|
+
function nombreDeCopia(indice, ruta) {
|
|
72
|
+
const ext = path.extname(ruta).replace(/[^\w.]/g, "").slice(0, 10);
|
|
73
|
+
return `${String(indice).padStart(3, "0")}${ext}`;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Guarda un turno. Devuelve lo que NO se pudo copiar, para poder decirlo.
|
|
77
|
+
*
|
|
78
|
+
* No lanza nunca: si el disco esta lleno o la carpeta es de solo lectura, el
|
|
79
|
+
* turno del usuario no se puede caer por culpa del historial.
|
|
80
|
+
*/
|
|
81
|
+
export function guardarTurno(cwd, punto) {
|
|
82
|
+
const sinCopia = [];
|
|
83
|
+
try {
|
|
84
|
+
const dir = carpetaDelTurno(cwd, punto.id);
|
|
85
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
86
|
+
asegurarQueGitLoIgnora(cwd);
|
|
87
|
+
const meta = {
|
|
88
|
+
id: punto.id,
|
|
89
|
+
cuando: new Date().toISOString(),
|
|
90
|
+
descripcion: punto.description,
|
|
91
|
+
archivos: [],
|
|
92
|
+
};
|
|
93
|
+
let indice = 0;
|
|
94
|
+
for (const foto of punto.snapshots) {
|
|
95
|
+
indice++;
|
|
96
|
+
if (foto.kind === "absent") {
|
|
97
|
+
meta.archivos.push({ ruta: foto.filePath, tipo: "absent" });
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (foto.kind === "unrestorable") {
|
|
101
|
+
meta.archivos.push({ ruta: foto.filePath, tipo: "unrestorable", motivo: foto.reason });
|
|
102
|
+
sinCopia.push(`${path.basename(foto.filePath)}: ${foto.reason}`);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (foto.previousContent.length > TOPE_POR_ARCHIVO) {
|
|
106
|
+
const motivo = `ocupa ${Math.round(foto.previousContent.length / 1024 / 1024)} MB y el historial no guarda archivos tan grandes`;
|
|
107
|
+
meta.archivos.push({ ruta: foto.filePath, tipo: "unrestorable", motivo });
|
|
108
|
+
sinCopia.push(`${path.basename(foto.filePath)}: ${motivo}`);
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
const copia = nombreDeCopia(indice, foto.filePath);
|
|
112
|
+
fs.writeFileSync(path.join(dir, copia), foto.previousContent);
|
|
113
|
+
meta.archivos.push({
|
|
114
|
+
ruta: foto.filePath,
|
|
115
|
+
tipo: "file",
|
|
116
|
+
copia,
|
|
117
|
+
bytes: foto.previousContent.length,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
fs.writeFileSync(path.join(dir, "meta.json"), JSON.stringify(meta, null, 2), "utf-8");
|
|
121
|
+
podar(cwd);
|
|
122
|
+
return { guardado: true, sinCopia };
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
return { guardado: false, sinCopia };
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/** Los turnos guardados, del mas viejo al mas nuevo. Solo las fichas. */
|
|
129
|
+
export function turnosGuardados(cwd) {
|
|
130
|
+
const dir = carpetaDeCambios(cwd);
|
|
131
|
+
const turnos = [];
|
|
132
|
+
try {
|
|
133
|
+
for (const entrada of fs.readdirSync(dir)) {
|
|
134
|
+
try {
|
|
135
|
+
const meta = JSON.parse(fs.readFileSync(path.join(dir, entrada, "meta.json"), "utf-8"));
|
|
136
|
+
if (meta?.id)
|
|
137
|
+
turnos.push(meta);
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
// Una carpeta a medias -se corto la luz al guardar- no puede tumbar la
|
|
141
|
+
// lista entera: se salta y ya.
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
return [];
|
|
147
|
+
}
|
|
148
|
+
return turnos.sort((a, b) => (a.cuando || "").localeCompare(b.cuando || ""));
|
|
149
|
+
}
|
|
150
|
+
/** Recupera un turno con sus bytes, listo para que UndoManager lo deshaga. */
|
|
151
|
+
export function leerTurno(cwd, id) {
|
|
152
|
+
const dir = carpetaDelTurno(cwd, id);
|
|
153
|
+
let meta;
|
|
154
|
+
try {
|
|
155
|
+
meta = JSON.parse(fs.readFileSync(path.join(dir, "meta.json"), "utf-8"));
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
const snapshots = [];
|
|
161
|
+
for (const a of meta.archivos || []) {
|
|
162
|
+
if (a.tipo === "absent") {
|
|
163
|
+
snapshots.push({ filePath: a.ruta, kind: "absent" });
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (a.tipo === "unrestorable" || !a.copia) {
|
|
167
|
+
snapshots.push({ filePath: a.ruta, kind: "unrestorable", reason: a.motivo || "sin copia" });
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
try {
|
|
171
|
+
snapshots.push({ filePath: a.ruta, kind: "file", previousContent: fs.readFileSync(path.join(dir, a.copia)) });
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
// La copia existia al guardarla y ahora no se puede leer: se dice, no se
|
|
175
|
+
// inventa un archivo vacio encima del que hay.
|
|
176
|
+
snapshots.push({
|
|
177
|
+
filePath: a.ruta,
|
|
178
|
+
kind: "unrestorable",
|
|
179
|
+
reason: "la copia del historial no se pudo leer",
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return { id: meta.id, timestamp: meta.cuando, description: meta.descripcion, snapshots };
|
|
184
|
+
}
|
|
185
|
+
/** Un turno ya deshecho no tiene por que quedarse ocupando sitio. */
|
|
186
|
+
export function olvidarTurno(cwd, id) {
|
|
187
|
+
try {
|
|
188
|
+
fs.rmSync(carpetaDelTurno(cwd, id), { recursive: true, force: true });
|
|
189
|
+
}
|
|
190
|
+
catch { }
|
|
191
|
+
}
|
|
192
|
+
/** Tira los turnos mas viejos que sobren del tope. */
|
|
193
|
+
export function podar(cwd, tope = TURNOS_GUARDADOS) {
|
|
194
|
+
const turnos = turnosGuardados(cwd);
|
|
195
|
+
if (turnos.length <= tope)
|
|
196
|
+
return;
|
|
197
|
+
for (const viejo of turnos.slice(0, turnos.length - tope))
|
|
198
|
+
olvidarTurno(cwd, viejo.id);
|
|
199
|
+
}
|
|
200
|
+
/** Lo que ocupa el historial, para poder decirlo sin que nadie lo mida a mano. */
|
|
201
|
+
export function bytesOcupados(cwd) {
|
|
202
|
+
let total = 0;
|
|
203
|
+
const dir = carpetaDeCambios(cwd);
|
|
204
|
+
try {
|
|
205
|
+
for (const turno of fs.readdirSync(dir)) {
|
|
206
|
+
const sub = path.join(dir, turno);
|
|
207
|
+
try {
|
|
208
|
+
for (const archivo of fs.readdirSync(sub)) {
|
|
209
|
+
try {
|
|
210
|
+
total += fs.statSync(path.join(sub, archivo)).size;
|
|
211
|
+
}
|
|
212
|
+
catch { }
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
catch { }
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
catch { }
|
|
219
|
+
return total;
|
|
220
|
+
}
|
|
@@ -30,6 +30,14 @@ export interface SalidaDelAgente {
|
|
|
30
30
|
/** Escritura sin salto, para separar bloques. Equivale a process.stdout.write. */
|
|
31
31
|
crudo(texto: string): void;
|
|
32
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* La copia al navegador, sin que este modulo dependa del servidor web.
|
|
35
|
+
*
|
|
36
|
+
* Se registra desde fuera (index.ts, al arrancar --servir). Asi una sesion
|
|
37
|
+
* normal de terminal no carga nada del servidor ni paga por tenerlo.
|
|
38
|
+
*/
|
|
39
|
+
type Copia = (texto: string) => void;
|
|
40
|
+
export declare function copiarSalidaAlWeb(fn: Copia | null): void;
|
|
33
41
|
/** Lo que usa el bucle. No sabe ni le importa quien esta al otro lado. */
|
|
34
42
|
export declare const salida: SalidaDelAgente;
|
|
35
43
|
/**
|
|
@@ -41,3 +49,4 @@ export declare const salida: SalidaDelAgente;
|
|
|
41
49
|
export declare function dirigirSalidaDelAgente(destino: SalidaDelAgente | null): void;
|
|
42
50
|
/** Quita los codigos de color, para quien pinte el texto a su manera. */
|
|
43
51
|
export declare function sinColores(texto: string): string;
|
|
52
|
+
export {};
|
|
@@ -13,11 +13,30 @@ import { escribirEncimaDelPie } from "../ui/pieFijo.js";
|
|
|
13
13
|
const A_LA_TERMINAL = {
|
|
14
14
|
linea: (texto) => {
|
|
15
15
|
escribirEncimaDelPie(`${texto}\n`);
|
|
16
|
+
// Y copia para el navegador, si hay sesion web abierta. Sin esto, lo que
|
|
17
|
+
// contesta el agente no sale del terminal: en el movil la sesion parece
|
|
18
|
+
// viva y muda. Ver servidor/captura.ts.
|
|
19
|
+
copiarAlWeb(`${texto}\n`);
|
|
16
20
|
},
|
|
17
21
|
crudo: (texto) => {
|
|
18
22
|
escribirEncimaDelPie(texto);
|
|
23
|
+
copiarAlWeb(texto);
|
|
19
24
|
},
|
|
20
25
|
};
|
|
26
|
+
let copia = null;
|
|
27
|
+
export function copiarSalidaAlWeb(fn) {
|
|
28
|
+
copia = fn;
|
|
29
|
+
}
|
|
30
|
+
function copiarAlWeb(texto) {
|
|
31
|
+
if (!copia)
|
|
32
|
+
return;
|
|
33
|
+
try {
|
|
34
|
+
copia(texto);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
// El navegador no puede tumbar la salida del terminal.
|
|
38
|
+
}
|
|
39
|
+
}
|
|
21
40
|
let actual = A_LA_TERMINAL;
|
|
22
41
|
/** Lo que usa el bucle. No sabe ni le importa quien esta al otro lado. */
|
|
23
42
|
export const salida = {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type TurnoGuardado } from "./historialDeCambios.js";
|
|
1
2
|
/**
|
|
2
3
|
* Estado de una ruta justo antes de que el turno la tocara.
|
|
3
4
|
*
|
|
@@ -40,6 +41,15 @@ export interface UndoResult {
|
|
|
40
41
|
export declare class UndoManager {
|
|
41
42
|
private checkpoints;
|
|
42
43
|
private currentTurnSnapshots;
|
|
44
|
+
/**
|
|
45
|
+
* La carpeta del proyecto, para saber DONDE vive el historial en disco.
|
|
46
|
+
*
|
|
47
|
+
* Se fija desde fuera (index.ts) y se actualiza con /cd: el historial es de
|
|
48
|
+
* cada proyecto, asi que al cambiar de carpeta se cambia de historial. Si
|
|
49
|
+
* nadie la fija, esto funciona como siempre y solo en memoria.
|
|
50
|
+
*/
|
|
51
|
+
private carpeta;
|
|
52
|
+
fijarCarpeta(cwd: string | null): void;
|
|
43
53
|
/**
|
|
44
54
|
* Fotografia una ruta antes de modificarla o borrarla.
|
|
45
55
|
*
|
|
@@ -52,6 +62,17 @@ export declare class UndoManager {
|
|
|
52
62
|
* Finalizes the current turn and saves a rollback checkpoint
|
|
53
63
|
*/
|
|
54
64
|
commitTurn(description: string): void;
|
|
65
|
+
/** Los turnos que se pueden deshacer, del mas viejo al mas nuevo. */
|
|
66
|
+
turnos(): TurnoGuardado[];
|
|
67
|
+
/**
|
|
68
|
+
* Vuelve a como estaba ANTES de un turno concreto.
|
|
69
|
+
*
|
|
70
|
+
* Deshace ese turno y todos los de despues, del mas nuevo al mas viejo. El
|
|
71
|
+
* orden no es un detalle: si un archivo se toco en tres turnos seguidos, la
|
|
72
|
+
* copia buena es la del turno MAS VIEJO, y solo se llega a ella deshaciendo
|
|
73
|
+
* los de encima primero. Al reves quedaria el archivo como estaba a mitad.
|
|
74
|
+
*/
|
|
75
|
+
volverAntesDe(id: string): UndoResult;
|
|
55
76
|
/**
|
|
56
77
|
* Deshace el ultimo turno de modificaciones.
|
|
57
78
|
*
|
|
@@ -1,8 +1,20 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { guardarTurno, leerTurno, olvidarTurno, turnosGuardados, } from "./historialDeCambios.js";
|
|
3
4
|
export class UndoManager {
|
|
4
5
|
checkpoints = [];
|
|
5
6
|
currentTurnSnapshots = new Map();
|
|
7
|
+
/**
|
|
8
|
+
* La carpeta del proyecto, para saber DONDE vive el historial en disco.
|
|
9
|
+
*
|
|
10
|
+
* Se fija desde fuera (index.ts) y se actualiza con /cd: el historial es de
|
|
11
|
+
* cada proyecto, asi que al cambiar de carpeta se cambia de historial. Si
|
|
12
|
+
* nadie la fija, esto funciona como siempre y solo en memoria.
|
|
13
|
+
*/
|
|
14
|
+
carpeta = null;
|
|
15
|
+
fijarCarpeta(cwd) {
|
|
16
|
+
this.carpeta = cwd;
|
|
17
|
+
}
|
|
6
18
|
/**
|
|
7
19
|
* Fotografia una ruta antes de modificarla o borrarla.
|
|
8
20
|
*
|
|
@@ -71,18 +83,73 @@ export class UndoManager {
|
|
|
71
83
|
this.currentTurnSnapshots.forEach((state, filePath) => {
|
|
72
84
|
snapshots.push({ filePath, ...state });
|
|
73
85
|
});
|
|
74
|
-
|
|
86
|
+
const punto = {
|
|
75
87
|
id: `chk_${Date.now()}`,
|
|
76
88
|
timestamp: new Date().toLocaleTimeString(),
|
|
77
89
|
description,
|
|
78
90
|
snapshots,
|
|
79
|
-
}
|
|
91
|
+
};
|
|
92
|
+
this.checkpoints.push(punto);
|
|
93
|
+
// Y a disco, para que siga estando manana. Si falla -disco lleno, carpeta
|
|
94
|
+
// de solo lectura- no se dice nada aqui: el turno del usuario no se puede
|
|
95
|
+
// caer por culpa del historial, y en memoria sigue estando.
|
|
96
|
+
if (this.carpeta)
|
|
97
|
+
guardarTurno(this.carpeta, punto);
|
|
80
98
|
this.currentTurnSnapshots.clear();
|
|
81
99
|
// Keep maximum 10 recent checkpoints in memory
|
|
82
100
|
if (this.checkpoints.length > 10) {
|
|
83
101
|
this.checkpoints.shift();
|
|
84
102
|
}
|
|
85
103
|
}
|
|
104
|
+
/** Los turnos que se pueden deshacer, del mas viejo al mas nuevo. */
|
|
105
|
+
turnos() {
|
|
106
|
+
return this.carpeta ? turnosGuardados(this.carpeta) : [];
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Vuelve a como estaba ANTES de un turno concreto.
|
|
110
|
+
*
|
|
111
|
+
* Deshace ese turno y todos los de despues, del mas nuevo al mas viejo. El
|
|
112
|
+
* orden no es un detalle: si un archivo se toco en tres turnos seguidos, la
|
|
113
|
+
* copia buena es la del turno MAS VIEJO, y solo se llega a ella deshaciendo
|
|
114
|
+
* los de encima primero. Al reves quedaria el archivo como estaba a mitad.
|
|
115
|
+
*/
|
|
116
|
+
volverAntesDe(id) {
|
|
117
|
+
if (!this.carpeta) {
|
|
118
|
+
return { success: false, message: "No hay historial en disco para este proyecto.", restoredFiles: [], failures: [] };
|
|
119
|
+
}
|
|
120
|
+
const guardados = turnosGuardados(this.carpeta);
|
|
121
|
+
const desde = guardados.findIndex((t) => t.id === id);
|
|
122
|
+
if (desde < 0) {
|
|
123
|
+
return { success: false, message: `No encuentro el turno ${id}.`, restoredFiles: [], failures: [] };
|
|
124
|
+
}
|
|
125
|
+
const restoredFiles = [];
|
|
126
|
+
const failures = [];
|
|
127
|
+
let deshechos = 0;
|
|
128
|
+
for (let i = guardados.length - 1; i >= desde; i--) {
|
|
129
|
+
const punto = leerTurno(this.carpeta, guardados[i].id);
|
|
130
|
+
if (!punto) {
|
|
131
|
+
failures.push(`(Sin copia) el turno ${guardados[i].id} no se pudo leer`);
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
this.checkpoints.push(punto);
|
|
135
|
+
const res = this.undoLast();
|
|
136
|
+
restoredFiles.push(...res.restoredFiles);
|
|
137
|
+
failures.push(...res.failures);
|
|
138
|
+
if (res.success)
|
|
139
|
+
deshechos++;
|
|
140
|
+
}
|
|
141
|
+
const cuando = new Date(guardados[desde].cuando).toLocaleString();
|
|
142
|
+
return {
|
|
143
|
+
success: failures.length === 0 && restoredFiles.length > 0,
|
|
144
|
+
message: failures.length > 0
|
|
145
|
+
? `⚠ Vuelta a medias a antes de "${guardados[desde].descripcion}" (${cuando}): ${failures.length} ruta(s) sin restaurar`
|
|
146
|
+
: restoredFiles.length === 0
|
|
147
|
+
? `Nada que deshacer: los archivos ya estaban como antes de "${guardados[desde].descripcion}".`
|
|
148
|
+
: `✔ Vuelto a antes de "${guardados[desde].descripcion}" (${cuando}): ${deshechos} turno(s) deshechos`,
|
|
149
|
+
restoredFiles,
|
|
150
|
+
failures,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
86
153
|
/**
|
|
87
154
|
* Deshace el ultimo turno de modificaciones.
|
|
88
155
|
*
|
|
@@ -91,15 +158,24 @@ export class UndoManager {
|
|
|
91
158
|
* peor mensaje posible: el usuario da por recuperado lo que ya no existe.
|
|
92
159
|
*/
|
|
93
160
|
undoLast() {
|
|
94
|
-
|
|
161
|
+
// Si en memoria no hay nada, se mira el disco: la sesion de ayer tambien
|
|
162
|
+
// cuenta. Es justo el caso que importa, porque uno no deshace treinta
|
|
163
|
+
// segundos despues -ahi todavia esta mirando- sino al dia siguiente.
|
|
164
|
+
let checkpoint = this.checkpoints.pop();
|
|
165
|
+
if (!checkpoint && this.carpeta) {
|
|
166
|
+
const guardados = turnosGuardados(this.carpeta);
|
|
167
|
+
const ultimo = guardados[guardados.length - 1];
|
|
168
|
+
if (ultimo)
|
|
169
|
+
checkpoint = leerTurno(this.carpeta, ultimo.id) ?? undefined;
|
|
170
|
+
}
|
|
171
|
+
if (!checkpoint) {
|
|
95
172
|
return {
|
|
96
173
|
success: false,
|
|
97
|
-
message: "No hay acciones previas para deshacer
|
|
174
|
+
message: "No hay acciones previas para deshacer.",
|
|
98
175
|
restoredFiles: [],
|
|
99
176
|
failures: [],
|
|
100
177
|
};
|
|
101
178
|
}
|
|
102
|
-
const checkpoint = this.checkpoints.pop();
|
|
103
179
|
const restoredFiles = [];
|
|
104
180
|
const failures = [];
|
|
105
181
|
for (const snap of checkpoint.snapshots) {
|
|
@@ -128,6 +204,10 @@ export class UndoManager {
|
|
|
128
204
|
failures.push(`(Error restaurando) ${nombre}: ${err?.message}`);
|
|
129
205
|
}
|
|
130
206
|
}
|
|
207
|
+
// Deshecho es deshecho: la copia ya no pinta nada y ocupa sitio en el
|
|
208
|
+
// proyecto de otro.
|
|
209
|
+
if (this.carpeta)
|
|
210
|
+
olvidarTurno(this.carpeta, checkpoint.id);
|
|
131
211
|
const cabecera = `"${checkpoint.description}" (${checkpoint.timestamp})`;
|
|
132
212
|
if (failures.length > 0) {
|
|
133
213
|
return {
|