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.
- package/COMPUTER-USE.md +37 -0
- package/README.md +9 -2
- package/dist/config/permissions.js +4 -0
- package/dist/index.js +129 -24
- 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/fondo.d.ts +85 -0
- package/dist/servidor/fondo.js +134 -0
- package/dist/servidor/pagina.js +266 -2
- package/dist/servidor/puente.d.ts +13 -0
- package/dist/servidor/puente.js +11 -0
- package/dist/servidor/servidor.d.ts +6 -0
- package/dist/servidor/servidor.js +129 -6
- 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/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
|
@@ -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
|
-
|
|
95
|
-
|
|
98
|
+
const rutasConocidas = ["/", "/subir", "/archivo"];
|
|
99
|
+
if (!rutasConocidas.includes(ruta)) {
|
|
100
|
+
res.writeHead(404).end();
|
|
96
101
|
return;
|
|
97
102
|
}
|
|
98
|
-
|
|
99
|
-
|
|
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":
|
|
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>;
|