chocolatito-code 1.6.13 → 1.6.14

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.
@@ -1,11 +1,14 @@
1
1
  import http from "node:http";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
2
4
  import os from "node:os";
3
5
  import crypto from "node:crypto";
4
6
  import chalk from "chalk";
5
7
  import { WebSocketServer, WebSocket } from "ws";
6
8
  import { paginaHtml } from "./pagina.js";
9
+ import { LIMITE_DE_SUBIDA, avisoDeSubida, guardarSubida, rutaServible, tipoDelArchivo, } from "./archivos.js";
7
10
  import { alEscribirWeb, anotarWeb, historialWeb } from "./captura.js";
8
- import { cancelarPermisosAbiertos, entregarEntrada, fijarEmisorDePermiso, responderPermiso, } from "./puente.js";
11
+ import { cancelarPermisosAbiertos, entregarEntrada, fijarAvisoDeTurno, fijarEmisorDePermiso, responderPermiso, } from "./puente.js";
9
12
  const PUERTO_POR_DEFECTO = 4700;
10
13
  /**
11
14
  * Identifica ESTA sesion, y existe por un caso concreto.
@@ -89,16 +92,30 @@ export async function iniciarServidorWeb(opciones = {}) {
89
92
  const autorizada = (req) => anfitrionAceptable(req.headers.host) &&
90
93
  origenAceptable(req.headers.origin, req.headers.host) &&
91
94
  mismoToken(tokenDeLaPeticion(req.url), token);
95
+ const carpetaDeTrabajo = opciones.carpeta || (() => process.cwd());
92
96
  const servidor = http.createServer((req, res) => {
93
97
  const ruta = (req.url || "/").split("?")[0];
94
- if (req.method !== "GET") {
95
- res.writeHead(405).end();
98
+ const rutasConocidas = ["/", "/subir", "/archivo"];
99
+ if (!rutasConocidas.includes(ruta)) {
100
+ res.writeHead(404).end();
96
101
  return;
97
102
  }
98
- if (ruta !== "/") {
99
- res.writeHead(404).end();
103
+ const metodoValido = ruta === "/subir"
104
+ ? req.method === "POST"
105
+ : // HEAD solo en /archivo: la pagina pregunta "existe esto?" antes de
106
+ // ofrecer el enlace de guardar, y pedir el archivo entero para
107
+ // averiguarlo seria mandar un video de 40 MB para no ensenarlo.
108
+ req.method === "GET" || (ruta === "/archivo" && req.method === "HEAD");
109
+ if (!metodoValido) {
110
+ res.writeHead(405).end();
100
111
  return;
101
112
  }
113
+ // LA PUERTA ES UNA SOLA PARA LAS TRES RUTAS.
114
+ //
115
+ // Antes habia una ruta y por eso la comprobacion vivia pegada a ella. Con
116
+ // tres, lo que no puede pasar es que cada una traiga su copia: la cuarta se
117
+ // escribe un martes con prisa y se olvida, y detras de esta puerta hay
118
+ // ejecucion de comandos en la maquina de alguien.
102
119
  if (!autorizada(req)) {
103
120
  // Se dice en el terminal: si a alguien le tocan la puerta, tiene derecho a
104
121
  // enterarse en el momento y no al revisar registros que nadie revisa.
@@ -106,16 +123,105 @@ export async function iniciarServidorWeb(opciones = {}) {
106
123
  res.writeHead(401, { "content-type": "text/plain; charset=utf-8" }).end("No.");
107
124
  return;
108
125
  }
126
+ if (ruta === "/subir") {
127
+ recibirSubida(req, res);
128
+ return;
129
+ }
130
+ if (ruta === "/archivo") {
131
+ servirArchivo(req, res);
132
+ return;
133
+ }
109
134
  res.writeHead(200, {
110
135
  "content-type": "text/html; charset=utf-8",
111
136
  "cache-control": "no-store",
112
137
  // La pagina no tiene que salir de aqui ni meterse en ningun marco ajeno.
113
138
  "x-frame-options": "DENY",
114
139
  "referrer-policy": "no-referrer",
115
- "content-security-policy": "default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline'; connect-src 'self' ws: wss:",
140
+ "content-security-policy":
141
+ // img/media 'self': las miniaturas y los videos salen de /archivo, que es
142
+ // esta misma casa. blob: para la vista previa de lo que aun no se ha
143
+ // subido. Nada de terceros: la pagina sigue sin pedirle nada a nadie.
144
+ "default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline'; " +
145
+ "img-src 'self' blob: data:; media-src 'self' blob:; connect-src 'self' ws: wss:",
116
146
  });
117
147
  res.end(paginaHtml());
118
148
  });
149
+ /** Un archivo que llega del navegador: cuerpo crudo, nombre en la consulta. */
150
+ function recibirSubida(req, res) {
151
+ const consulta = new URL(req.url || "/", "http://x").searchParams;
152
+ const nombre = consulta.get("nombre") || "archivo";
153
+ // Se corta por tamano MIENTRAS llega, no despues: esperar a tener 500 MB en
154
+ // memoria para entonces decir que no es justo lo que no hay que hacer.
155
+ const trozos = [];
156
+ let total = 0;
157
+ let cortado = false;
158
+ req.on("data", (trozo) => {
159
+ if (cortado)
160
+ return;
161
+ total += trozo.length;
162
+ if (total > LIMITE_DE_SUBIDA) {
163
+ cortado = true;
164
+ res.writeHead(413, { "content-type": "application/json" }).end(JSON.stringify({ ok: false, error: "El archivo es demasiado grande." }));
165
+ req.destroy();
166
+ return;
167
+ }
168
+ trozos.push(trozo);
169
+ });
170
+ req.on("end", () => {
171
+ if (cortado)
172
+ return;
173
+ const guardado = guardarSubida(carpetaDeTrabajo(), nombre, Buffer.concat(trozos));
174
+ if (!guardado.ok) {
175
+ res.writeHead(400, { "content-type": "application/json" }).end(JSON.stringify(guardado));
176
+ return;
177
+ }
178
+ anotarWeb(chalk.gray(` ⇩ Llego del navegador: ${guardado.ruta}`));
179
+ res.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify(guardado));
180
+ });
181
+ req.on("error", () => {
182
+ if (!cortado)
183
+ res.writeHead(400).end();
184
+ });
185
+ }
186
+ /** Un archivo de la carpeta de trabajo, para verlo en el navegador. */
187
+ function servirArchivo(req, res) {
188
+ const consulta = new URL(req.url || "/", "http://x").searchParams;
189
+ const real = rutaServible(carpetaDeTrabajo(), consulta.get("ruta") || "");
190
+ // Fuera de la carpeta es un 404, no un 403: decir "existe pero no te lo doy"
191
+ // ya cuenta algo del disco de alguien que no ha preguntado.
192
+ if (!real) {
193
+ res.writeHead(404).end();
194
+ return;
195
+ }
196
+ // "descargar" cambia una palabra de la cabecera y con eso el navegador deja
197
+ // de ensenar el archivo y lo guarda. Es la ultima milla del movil: el agente
198
+ // exporta la pieza, la ves, y te la llevas para mandarsela al cliente sin
199
+ // pasar por el ordenador.
200
+ const guardar = consulta.get("descargar") === "1";
201
+ const nombre = path.basename(real).replace(/"/g, "");
202
+ try {
203
+ const tam = fs.statSync(real).size;
204
+ res.writeHead(200, {
205
+ "content-type": tipoDelArchivo(real),
206
+ "content-length": String(tam),
207
+ "cache-control": "no-store",
208
+ // Que el navegador no se ponga a adivinar el tipo: un .txt con HTML
209
+ // dentro no se ejecuta como pagina.
210
+ "x-content-type-options": "nosniff",
211
+ "content-disposition": `${guardar ? "attachment" : "inline"}; filename="${nombre}"`,
212
+ });
213
+ // HEAD es la pregunta sin la respuesta: las cabeceras dicen que existe,
214
+ // que tamano tiene y de que tipo es, y no se mueve un solo byte del disco.
215
+ if (req.method === "HEAD") {
216
+ res.end();
217
+ return;
218
+ }
219
+ res.end(fs.readFileSync(real));
220
+ }
221
+ catch {
222
+ res.writeHead(404).end();
223
+ }
224
+ }
119
225
  servidor.on("upgrade", (req, socket, head) => {
120
226
  const ruta = (req.url || "/").split("?")[0];
121
227
  if (ruta !== "/ws" || !autorizada(req)) {
@@ -180,6 +286,19 @@ export async function iniciarServidorWeb(opciones = {}) {
180
286
  catch {
181
287
  return;
182
288
  }
289
+ if (m?.t === "entrada" && typeof m.texto === "string") {
290
+ // Los archivos que se acaban de subir van DELANTE de lo que escribio el
291
+ // usuario, y con la ruta entera. El aviso se compone aqui y no en la
292
+ // pagina para que la forma de decirlo viva en un solo sitio.
293
+ const subidos = Array.isArray(m.archivos)
294
+ ? m.archivos.filter((r) => typeof r === "string" && r.length > 0)
295
+ : [];
296
+ if (subidos.length > 0) {
297
+ const aviso = avisoDeSubida(subidos);
298
+ m.texto = m.texto.trim() ? `${aviso}
299
+ ${m.texto}` : aviso;
300
+ }
301
+ }
183
302
  if (m?.t === "entrada" && typeof m.texto === "string") {
184
303
  const texto = m.texto.slice(0, 20_000).trim();
185
304
  if (texto)
@@ -204,6 +323,10 @@ export async function iniciarServidorWeb(opciones = {}) {
204
323
  });
205
324
  });
206
325
  fijarEmisorDePermiso((peticion) => aTodos({ t: "permiso", ...peticion }) > 0);
326
+ // El agente se puso a esperar: para el movil, eso es "ya esta".
327
+ fijarAvisoDeTurno(() => {
328
+ aTodos({ t: "turno" });
329
+ });
207
330
  const puerto = await new Promise((resolve, reject) => {
208
331
  servidor.once("error", reject);
209
332
  servidor.listen(puertoPedido, anfitrion, () => {
@@ -23,7 +23,7 @@
23
23
  * "background": se actua sobre una ventana concreta sin traerla al frente y
24
24
  * sin mover el raton del usuario.
25
25
  */
26
- export type ComputerAction = "screenshot" | "ui_snapshot" | "ui_click" | "ui_type" | "ui_focus" | "find_element" | "left_click" | "click" | "double_click" | "triple_click" | "right_click" | "middle_click" | "mouse_move" | "move" | "left_click_drag" | "scroll" | "type" | "key" | "hotkey" | "wait" | "wait_change" | "sleep" | "cursor_position" | "list_windows" | "focus_window" | "get_active_window" | "open_app";
26
+ export type ComputerAction = "screenshot" | "ui_snapshot" | "ui_click" | "ui_type" | "ui_focus" | "find_element" | "left_click" | "click" | "mouse_click" | "double_click" | "triple_click" | "right_click" | "middle_click" | "mouse_move" | "move" | "left_click_drag" | "drag" | "scroll" | "type" | "key" | "hotkey" | "press" | "key_press" | "wait" | "wait_change" | "wait_visual_change" | "sequence" | "scroll_into_view" | "scroll_to" | "read_text" | "get_text" | "sleep" | "cursor_position" | "list_windows" | "focus_window" | "get_active_window" | "open_app";
27
27
  export interface ComputerUseParams {
28
28
  action: ComputerAction;
29
29
  /** Ventana objetivo por titulo o proceso (ej. "chrome", "Flow"). */
@@ -42,6 +42,8 @@ export interface ComputerUseParams {
42
42
  key?: string;
43
43
  query?: string;
44
44
  filter?: string;
45
+ target?: string | number;
46
+ maxAttempts?: number;
45
47
  direction?: "up" | "down" | "left" | "right";
46
48
  amount?: number;
47
49
  repeat?: number;
@@ -54,5 +56,13 @@ export interface ComputerUseParams {
54
56
  question?: string;
55
57
  /** Solo para screenshot: pone false para saltarse la vision y ahorrar tokens. */
56
58
  analyze?: boolean;
59
+ /** Solo para screenshot/find_element: dibuja una cuadrícula de coordenadas sobre la captura. */
60
+ grid?: boolean;
61
+ /** Región de interés [x1, y1, x2, y2] para capturar o esperar cambios en alta resolución. */
62
+ region?: [number, number, number, number];
63
+ /** Sub-acciones a ejecutar en ráfaga para action="sequence". */
64
+ steps?: ComputerUseParams[];
65
+ /** true si las coordenadas están en escala 0..1000 estilo OpenAI Operator / Astra. */
66
+ normalized?: boolean;
57
67
  }
58
68
  export declare function computerUse(params: ComputerUseParams, cwd?: string, apiKey?: string): Promise<string>;