chocolatito-code 1.6.12 → 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.
Files changed (42) hide show
  1. package/COMPUTER-USE.md +37 -0
  2. package/README.md +9 -2
  3. package/dist/agent/cortes.d.ts +41 -0
  4. package/dist/agent/cortes.js +52 -0
  5. package/dist/agent/loop.js +35 -0
  6. package/dist/config/engine.js +7 -0
  7. package/dist/config/permissions.js +4 -0
  8. package/dist/index.js +147 -24
  9. package/dist/prompts/systemPrompt.js +15 -11
  10. package/dist/servidor/archivos.d.ts +69 -0
  11. package/dist/servidor/archivos.js +156 -0
  12. package/dist/servidor/fondo.d.ts +85 -0
  13. package/dist/servidor/fondo.js +134 -0
  14. package/dist/servidor/pagina.js +266 -2
  15. package/dist/servidor/puente.d.ts +13 -0
  16. package/dist/servidor/puente.js +11 -0
  17. package/dist/servidor/servidor.d.ts +6 -0
  18. package/dist/servidor/servidor.js +129 -6
  19. package/dist/tools/computerUse.d.ts +11 -1
  20. package/dist/tools/computerUse.js +376 -34
  21. package/dist/tools/toolDefsComputer.js +39 -14
  22. package/dist/tools/visionBridge.d.ts +4 -2
  23. package/dist/tools/visionBridge.js +40 -15
  24. package/dist/tools/win/hostScript.js +300 -29
  25. package/dist/ui/comandos.js +1 -0
  26. package/dist/ui/ink/App.js +23 -3
  27. package/dist/ui/ink/Prompt.d.ts +5 -1
  28. package/dist/ui/ink/Prompt.js +92 -4
  29. package/dist/ui/ink/control.d.ts +2 -0
  30. package/dist/ui/ink/control.js +15 -0
  31. package/dist/ui/ink/montarApp.d.ts +0 -19
  32. package/dist/ui/ink/montarApp.js +3 -0
  33. package/dist/ui/permissionPrompt.js +44 -9
  34. package/dist/ui/pieFijo.js +5 -0
  35. package/dist/ui/prompt.js +6 -0
  36. package/dist/ui/renderer.d.ts +1 -0
  37. package/dist/ui/renderer.js +27 -2
  38. package/dist/ui/selector.js +5 -0
  39. package/dist/ui/spinner.d.ts +1 -0
  40. package/dist/ui/spinner.js +6 -0
  41. package/extension/manifest.json +1 -1
  42. package/package.json +1 -1
@@ -47,6 +47,31 @@ const PAGINA = [
47
47
  'header b { color:var(--zorro); font-weight:600; }',
48
48
  '#estado { margin-left:auto; font-size:12px; color:var(--suave); display:flex; align-items:center; gap:6px; }',
49
49
  '#punto { width:8px; height:8px; border-radius:50%; background:#d14; transition:background .2s; }',
50
+ '/* Lo que produce el agente, mirable. max-width en porcentaje y no en pixeles:',
51
+ ' en un movil en vertical caben 300px justos y una pieza de 1080 rompe el ancho. */',
52
+ '.mini { margin:6px 0; }',
53
+ '/* El enlace de guardar, pegado bajo la miniatura y con sitio para el dedo. */',
54
+ '.guardar { display:inline-flex; align-items:center; gap:6px; margin-top:4px; padding:6px 12px;',
55
+ ' background:var(--caja); border:1px solid var(--borde); border-radius:999px;',
56
+ ' color:var(--texto); font-size:12px; text-decoration:none; }',
57
+ '.guardar:active { border-color:var(--zorro); }',
58
+ '.mini img, .mini video { max-width:min(100%, 420px); max-height:320px; border-radius:8px;',
59
+ ' border:1px solid var(--borde); background:#000; display:block; }',
60
+ '/* Lo que esta a punto de subir, en fila sobre la caja de escribir. */',
61
+ '#adjuntos { display:flex; gap:6px; overflow-x:auto; padding:6px 12px 0; }',
62
+ '#adjuntos:empty { display:none; }',
63
+ '.chip { display:flex; align-items:center; gap:6px; flex:0 0 auto; max-width:60%;',
64
+ ' background:var(--caja); border:1px solid var(--borde); border-radius:999px; padding:4px 10px; font-size:12px; }',
65
+ '.chip img { width:22px; height:22px; object-fit:cover; border-radius:4px; }',
66
+ '.chip span { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }',
67
+ '.chip b { color:var(--suave); cursor:pointer; font-weight:400; padding:0 2px; }',
68
+ '#clip { background:none; border:1px solid var(--borde); color:var(--suave); border-radius:10px;',
69
+ ' font-size:18px; line-height:1; padding:0 12px; cursor:pointer; }',
70
+ '/* El velo de arrastrar. pointer-events:none evita el parpadeo: si el velo',
71
+ ' recibe los eventos, al aparecer dispara dragleave y se va solo. */',
72
+ '#suelta { position:fixed; inset:0; display:none; place-items:center; z-index:20;',
73
+ ' background:rgba(11,11,13,.88); color:var(--zorro); font-size:16px; pointer-events:none; }',
74
+ '#suelta.visible { display:grid; }',
50
75
  '#punto.ok { background:#2ea043; }',
51
76
  '#salida { flex:1; overflow-y:auto; overflow-x:auto; padding:12px; white-space:pre; -webkit-overflow-scrolling:touch; }',
52
77
  '#salida .fila { min-height:1.5em; }',
@@ -84,10 +109,14 @@ const PAGINA = [
84
109
  '<header><b>Chocolatito</b><span class="d" style="font-size:12px">Code</span>',
85
110
  '<span id="estado"><span id="punto"></span><span id="estadoTexto">conectando</span></span></header>',
86
111
  '<div id="salida"></div>',
112
+ '<div id="adjuntos"></div>',
87
113
  '<form id="envio">',
114
+ '<input id="fichero" type="file" multiple style="display:none">',
115
+ '<button id="clip" type="button" title="Adjuntar archivos">+</button>',
88
116
  '<textarea id="texto" rows="1" placeholder="Escribele lo que quieres que haga" autocapitalize="sentences" autocomplete="off"></textarea>',
89
117
  '<button id="enviar" type="submit">Ir</button>',
90
118
  '</form>',
119
+ '<div id="suelta">Suelta aqui los archivos</div>',
91
120
  '<div id="velo"><div id="permiso">',
92
121
  '<h2 id="permisoTitulo">Pide permiso</h2>',
93
122
  '<p id="permisoDetalle"></p>',
@@ -144,6 +173,102 @@ const PAGINA = [
144
173
  ' }',
145
174
  ' return html || "&nbsp;";',
146
175
  ' }',
176
+ ' // MIRAR, NO SOLO LEER LA RUTA',
177
+ ' //',
178
+ ' // Sin esto, cuando el agente exporta una imagen contesta con la ruta. Delante',
179
+ ' // del PC se abre la carpeta; en un movil no puedes mirarlo, asi que no puedes',
180
+ ' // opinar, asi que no puedes trabajar. La ruta se detecta aqui y el permiso lo',
181
+ ' // da el servidor: /archivo solo sirve lo que este dentro de la carpeta.',
182
+ ' var EXT_VISIBLE = /^(png|jpe?g|webp|gif|bmp|avif|svg|mp4|webm|mov)$/i;',
183
+ ' // Todo lo que en una linea tiene pinta de ruta: con separador y con punto.',
184
+ ' function rutasDelTexto(crudo) {',
185
+ ' var limpio = String(crudo).replace(/\\x1b\\[[0-9;?]*[A-Za-z]/g, "");',
186
+ ' var encontradas = [];',
187
+ ' var trozos = limpio.split(/[\\s"\\u0027`<>|()\\[\\]]+/);',
188
+ ' for (var i = 0; i < trozos.length; i++) {',
189
+ ' var t = trozos[i].replace(/[.,;:]+$/, "");',
190
+ ' if (t.indexOf("/") < 0 && t.indexOf("\\\\") < 0) continue;',
191
+ ' if (t.lastIndexOf(".") < 0) continue;',
192
+ ' if (encontradas.indexOf(t) < 0) encontradas.push(t);',
193
+ ' if (encontradas.length >= 6) break;',
194
+ ' }',
195
+ ' return encontradas;',
196
+ ' }',
197
+ ' function extensionDe(ruta) {',
198
+ ' var punto = String(ruta).lastIndexOf(".");',
199
+ ' return punto < 0 ? "" : ruta.slice(punto + 1);',
200
+ ' }',
201
+ ' function rutasVisibles(crudo) {',
202
+ ' return rutasDelTexto(crudo).filter(function (r) { return EXT_VISIBLE.test(extensionDe(r)); }).slice(0, 4);',
203
+ ' }',
204
+ ' // Las que no se pueden ensenar pero si llevarse: el ZIP, el PDF, el CSV.',
205
+ ' function rutasGuardables(crudo, yaVistas) {',
206
+ ' return rutasDelTexto(crudo)',
207
+ ' .filter(function (r) { return yaVistas.indexOf(r) < 0 && EXT_GUARDABLE.test(extensionDe(r)); })',
208
+ ' .slice(0, 3);',
209
+ ' }',
210
+ ' // GUARDARLO, QUE ES LA ULTIMA MILLA',
211
+ ' //',
212
+ ' // Ver la pieza en el movil no termina el trabajo: el diseno sale del PC para',
213
+ ' // ir a un cliente, y ese cliente esta en el WhatsApp del telefono. Sin esto,',
214
+ ' // el circuito se queda a un paso: recibes por WhatsApp, procesas en el PC,',
215
+ ' // ves el resultado... y tienes que ir al ordenador a por el archivo.',
216
+ ' //',
217
+ ' // Y no solo imagenes: el ZIP del entregable, el PDF, el CSV y los subtitulos',
218
+ ' // no tienen miniatura que ensenar, asi que sin un enlace no existen para el',
219
+ ' // movil. Para esos se pregunta antes con HEAD si el archivo esta de verdad,',
220
+ ' // para no ofrecer un enlace roto sobre cualquier palabra con un punto.',
221
+ ' var EXT_GUARDABLE = /^(pdf|zip|rar|7z|csv|txt|md|srt|vtt|json|docx|xlsx|pptx|psd|ai|svg|mp3|wav|ogg|mp4|webm|mov|png|jpe?g|webp|gif)$/i;',
222
+ ' function urlDe(ruta, descargar) {',
223
+ ' return "/archivo?t=" + encodeURIComponent(token) + "&ruta=" + encodeURIComponent(ruta) +',
224
+ ' (descargar ? "&descargar=1" : "");',
225
+ ' }',
226
+ ' function nombreDe(ruta) {',
227
+ ' var trozos = String(ruta).split(/[\\\\/]/);',
228
+ ' return trozos[trozos.length - 1] || ruta;',
229
+ ' }',
230
+ ' function enlaceDeGuardar(ruta) {',
231
+ ' var a = document.createElement("a");',
232
+ ' a.className = "guardar";',
233
+ ' a.href = urlDe(ruta, true);',
234
+ ' a.setAttribute("download", nombreDe(ruta));',
235
+ ' a.textContent = "\u2913 Guardar " + nombreDe(ruta);',
236
+ ' return a;',
237
+ ' }',
238
+ ' // Un archivo sin miniatura: se ofrece solo si existe de verdad.',
239
+ ' function quizaDescargable(ruta, donde) {',
240
+ ' var punto = String(ruta).lastIndexOf(".");',
241
+ ' if (punto < 0 || !EXT_GUARDABLE.test(ruta.slice(punto + 1))) return;',
242
+ ' fetch(urlDe(ruta, false), { method: "HEAD" }).then(function (r) {',
243
+ ' if (!r.ok) return;',
244
+ ' donde.appendChild(enlaceDeGuardar(ruta));',
245
+ ' }).catch(function () {});',
246
+ ' }',
247
+ ' function rutasGuardables(crudo, yaVistas) {',
248
+ ' var todas = rutasDelTexto(crudo);',
249
+ ' var fuera = [];',
250
+ ' for (var i = 0; i < todas.length; i++) {',
251
+ ' if (yaVistas.indexOf(todas[i]) < 0) fuera.push(todas[i]);',
252
+ ' }',
253
+ ' return fuera.slice(0, 3);',
254
+ ' }',
255
+ ' function miniatura(ruta) {',
256
+ ' var url = "/archivo?t=" + encodeURIComponent(token) + "&ruta=" + encodeURIComponent(ruta);',
257
+ ' var caja = document.createElement("div");',
258
+ ' caja.className = "mini";',
259
+ ' var esVideo = /(mp4|webm|mov)$/i.test(ruta);',
260
+ ' var vista = document.createElement(esVideo ? "video" : "img");',
261
+ ' vista.src = url;',
262
+ ' if (esVideo) { vista.controls = true; vista.preload = "metadata"; }',
263
+ ' // Si no se puede servir -fuera de la carpeta, borrado, o es una ruta que',
264
+ ' // solo lo parecia- no se deja el icono de imagen rota: se quita la caja.',
265
+ ' vista.onerror = function () { try { caja.parentNode.removeChild(caja); } catch (e) {} };',
266
+ ' // El enlace solo aparece cuando la imagen ha cargado: si el archivo no se',
267
+ ' // puede servir, la caja entera se va y no queda un boton que no guarda nada.',
268
+ ' vista.onload = function () { caja.appendChild(enlaceDeGuardar(ruta)); };',
269
+ ' caja.appendChild(vista);',
270
+ ' return caja;',
271
+ ' }',
147
272
  ' function pintar(lineas) {',
148
273
  ' if (!lineas || !lineas.length) return;',
149
274
  ' var pegado = alFinal();',
@@ -153,12 +278,67 @@ const PAGINA = [
153
278
  ' div.className = "fila";',
154
279
  ' div.innerHTML = ansi(lineas[i].texto);',
155
280
  ' frag.appendChild(div);',
281
+ ' var vistas = rutasVisibles(lineas[i].texto);',
282
+ ' for (var v = 0; v < vistas.length; v++) frag.appendChild(miniatura(vistas[v]));',
283
+ ' var otras = rutasGuardables(lineas[i].texto, vistas);',
284
+ ' if (otras.length) {',
285
+ ' var fila = document.createElement("div");',
286
+ ' fila.className = "mini";',
287
+ ' frag.appendChild(fila);',
288
+ ' for (var g = 0; g < otras.length; g++) quizaDescargable(otras[g], fila);',
289
+ ' }',
156
290
  ' if (lineas[i].id > ultimoId) ultimoId = lineas[i].id;',
157
291
  ' }',
158
292
  ' salida.appendChild(frag);',
159
293
  ' while (salida.childNodes.length > 3000) salida.removeChild(salida.firstChild);',
160
294
  ' if (pegado) salida.scrollTop = salida.scrollHeight;',
161
295
  ' }',
296
+ ' // AVISAR AL MOVIL, SIN SER PESADO',
297
+ ' //',
298
+ ' // Sin esto, la sesion web solo sirve si estas mirandola. Dejas una tarea de',
299
+ ' // cinco minutos, te vas, y acabas sacando el telefono cada treinta segundos',
300
+ ' // por si acaso. Que es exactamente lo que hace inutil trabajar desde el movil.',
301
+ ' //',
302
+ ' // TRES DECISIONES:',
303
+ ' //',
304
+ ' // 1. SOLO SI NO ESTAS MIRANDO. Con la pagina delante, una notificacion de',
305
+ ' // algo que ya ves en pantalla es ruido, y el ruido se acaba silenciando',
306
+ ' // entero, tambien el aviso que si importaba.',
307
+ ' // 2. EL PERMISO SE PIDE AL ENVIAR, NO AL CARGAR. Chrome ignora -y Safari',
308
+ ' // rechaza- una peticion de notificaciones que no venga de un gesto del',
309
+ ' // usuario. Pedirlo al abrir la pagina es gastar la unica oportunidad.',
310
+ ' // 3. EL TITULO TAMBIEN AVISA. Si las notificaciones estan denegadas, el',
311
+ ' // punto en la pestana es lo unico que queda, y se ve igual de bien.',
312
+ ' var tituloNormal = document.title;',
313
+ ' var avisoPuesto = false;',
314
+ ' function puedeAvisar() {',
315
+ ' return typeof Notification !== "undefined" && Notification.permission === "granted";',
316
+ ' }',
317
+ ' function pedirPermisoDeAviso() {',
318
+ ' if (typeof Notification === "undefined" || Notification.permission !== "default") return;',
319
+ ' try { Notification.requestPermission(); } catch (e) {}',
320
+ ' }',
321
+ ' function marcarTitulo(si) {',
322
+ ' avisoPuesto = si;',
323
+ ' document.title = si ? "\u25cf " + tituloNormal : tituloNormal;',
324
+ ' }',
325
+ ' function avisar(titulo, cuerpo, urgente) {',
326
+ ' // Mirando la pagina no se avisa de nada: ya lo estas viendo.',
327
+ ' if (!document.hidden) return;',
328
+ ' marcarTitulo(true);',
329
+ ' if (navigator.vibrate) {',
330
+ ' // Dos pulsos para un permiso -que te esta esperando- y uno para el final.',
331
+ ' try { navigator.vibrate(urgente ? [120, 80, 120] : [90]); } catch (e) {}',
332
+ ' }',
333
+ ' if (!puedeAvisar()) return;',
334
+ ' try {',
335
+ ' var n = new Notification(titulo, { body: cuerpo, tag: "chocolatito", renotify: true });',
336
+ ' n.onclick = function () { try { window.focus(); } catch (e) {} try { n.close(); } catch (e) {} };',
337
+ ' } catch (e) {}',
338
+ ' }',
339
+ ' document.addEventListener("visibilitychange", function () {',
340
+ ' if (!document.hidden && avisoPuesto) marcarTitulo(false);',
341
+ ' });',
162
342
  ' function conectado(si, etiqueta) {',
163
343
  ' punto.className = si ? "ok" : "";',
164
344
  ' estadoTexto.textContent = etiqueta;',
@@ -171,6 +351,7 @@ const PAGINA = [
171
351
  ' peticionActual = null;',
172
352
  ' }',
173
353
  ' function abrirPermiso(p) {',
354
+ ' avisar("Chocolatito te pide permiso", (p.detalle && p.detalle[0]) || p.herramienta, true);',
174
355
  ' peticionActual = p;',
175
356
  ' document.getElementById("permisoTitulo").textContent = "Permiso: " + p.herramienta;',
176
357
  ' document.getElementById("permisoDetalle").textContent = (p.detalle || []).join("\\n");',
@@ -220,6 +401,7 @@ const PAGINA = [
220
401
  ' pintar(m.lineas);',
221
402
  ' }',
222
403
  ' else if (m.t === "permiso") abrirPermiso(m);',
404
+ ' else if (m.t === "turno") avisar("Chocolatito ha terminado", "Te toca a ti.", false);',
223
405
  ' else if (m.t === "cierre") { velo.className = ""; peticionActual = null; }',
224
406
  ' else if (m.t === "fin") { terminada = true; conectado(false, "sesion terminada"); try { ws.close(); } catch (e) {} }',
225
407
  ' };',
@@ -233,11 +415,93 @@ const PAGINA = [
233
415
  ' };',
234
416
  ' ws.onerror = function () { try { ws.close(); } catch (e) {} };',
235
417
  ' }',
418
+ ' // LOS ARCHIVOS QUE ENTRAN',
419
+ ' //',
420
+ ' // Tres gestos, porque son tres sitios distintos: el boton y arrastrar (PC), y',
421
+ ' // PEGAR, que es el mas usado de los tres y el que casi nadie pone: una captura',
422
+ ' // de pantalla se pega, no se guarda antes en una carpeta para luego buscarla.',
423
+ ' var adjuntos = [];',
424
+ ' var tiraAdjuntos = document.getElementById("adjuntos");',
425
+ ' var fichero = document.getElementById("fichero");',
426
+ ' var suelta = document.getElementById("suelta");',
427
+ ' function pintarAdjuntos() {',
428
+ ' tiraAdjuntos.innerHTML = "";',
429
+ ' for (var i = 0; i < adjuntos.length; i++) {',
430
+ ' (function (a) {',
431
+ ' var chip = document.createElement("div");',
432
+ ' chip.className = "chip";',
433
+ ' if (a.vista) { var im = document.createElement("img"); im.src = a.vista; chip.appendChild(im); }',
434
+ ' var nom = document.createElement("span");',
435
+ ' nom.textContent = a.subiendo ? a.nombre + "\u2026" : a.nombre;',
436
+ ' chip.appendChild(nom);',
437
+ ' var x = document.createElement("b");',
438
+ ' x.textContent = "\u00d7";',
439
+ ' x.onclick = function () { adjuntos.splice(adjuntos.indexOf(a), 1); pintarAdjuntos(); };',
440
+ ' chip.appendChild(x);',
441
+ ' tiraAdjuntos.appendChild(chip);',
442
+ ' })(adjuntos[i]);',
443
+ ' }',
444
+ ' }',
445
+ ' function adjuntar(archivos) {',
446
+ ' for (var i = 0; i < archivos.length; i++) {',
447
+ ' (function (archivo) {',
448
+ ' var entrada = {',
449
+ ' nombre: archivo.name || "pegado.png",',
450
+ ' subiendo: true,',
451
+ ' ruta: "",',
452
+ ' vista: /^image\\//.test(archivo.type) ? URL.createObjectURL(archivo) : ""',
453
+ ' };',
454
+ ' adjuntos.push(entrada);',
455
+ ' pintarAdjuntos();',
456
+ ' fetch("/subir?t=" + encodeURIComponent(token) + "&nombre=" + encodeURIComponent(entrada.nombre), {',
457
+ ' method: "POST",',
458
+ ' body: archivo',
459
+ ' }).then(function (r) { return r.json(); }).then(function (j) {',
460
+ ' if (j && j.ok) { entrada.ruta = j.ruta; entrada.subiendo = false; }',
461
+ ' else {',
462
+ ' adjuntos.splice(adjuntos.indexOf(entrada), 1);',
463
+ ' pintar([{ id: 0, texto: "\u001b[33m No se pudo subir " + entrada.nombre + ": " + ((j && j.error) || "error") + "\u001b[39m" }]);',
464
+ ' }',
465
+ ' pintarAdjuntos();',
466
+ ' }).catch(function () {',
467
+ ' adjuntos.splice(adjuntos.indexOf(entrada), 1);',
468
+ ' pintarAdjuntos();',
469
+ ' });',
470
+ ' })(archivos[i]);',
471
+ ' }',
472
+ ' }',
473
+ ' document.getElementById("clip").onclick = function () { fichero.click(); };',
474
+ ' fichero.onchange = function () { adjuntar(fichero.files); fichero.value = ""; };',
475
+ ' document.addEventListener("paste", function (ev) {',
476
+ ' var datos = ev.clipboardData;',
477
+ ' if (!datos || !datos.files || !datos.files.length) return;',
478
+ ' ev.preventDefault();',
479
+ ' adjuntar(datos.files);',
480
+ ' });',
481
+ ' var arrastres = 0;',
482
+ ' document.addEventListener("dragenter", function (ev) { ev.preventDefault(); arrastres++; suelta.className = "visible"; });',
483
+ ' document.addEventListener("dragover", function (ev) { ev.preventDefault(); });',
484
+ ' document.addEventListener("dragleave", function () { if (--arrastres <= 0) { arrastres = 0; suelta.className = ""; } });',
485
+ ' document.addEventListener("drop", function (ev) {',
486
+ ' ev.preventDefault();',
487
+ ' arrastres = 0;',
488
+ ' suelta.className = "";',
489
+ ' if (ev.dataTransfer && ev.dataTransfer.files && ev.dataTransfer.files.length) adjuntar(ev.dataTransfer.files);',
490
+ ' });',
236
491
  ' document.getElementById("envio").onsubmit = function (ev) {',
237
492
  ' ev.preventDefault();',
238
493
  ' var valor = texto.value.trim();',
239
- ' if (!valor || !ws || ws.readyState !== 1) return;',
240
- ' ws.send(JSON.stringify({ t: "entrada", texto: valor }));',
494
+ ' var listos = [];',
495
+ ' for (var i = 0; i < adjuntos.length; i++) if (adjuntos[i].ruta) listos.push(adjuntos[i].ruta);',
496
+ ' // Se puede mandar SOLO archivos: subir cinco fotos y no escribir nada es una',
497
+ ' // orden perfectamente clara para el que la recibe.',
498
+ ' if ((!valor && !listos.length) || !ws || ws.readyState !== 1) return;',
499
+ ' // Aqui, y no al cargar: Chrome ignora una peticion de notificaciones que no',
500
+ ' // venga de un gesto del usuario, y pedirla al abrir gasta la unica que hay.',
501
+ ' pedirPermisoDeAviso();',
502
+ ' ws.send(JSON.stringify({ t: "entrada", texto: valor, archivos: listos }));',
503
+ ' adjuntos = [];',
504
+ ' pintarAdjuntos();',
241
505
  ' texto.value = "";',
242
506
  ' texto.style.height = "auto";',
243
507
  ' salida.scrollTop = salida.scrollHeight;',
@@ -3,6 +3,19 @@ export declare function servirActivo(): boolean;
3
3
  export declare function marcarServirActivo(valor: boolean): void;
4
4
  /** Llega algo del navegador. */
5
5
  export declare function entregarEntrada(texto: string): void;
6
+ /**
7
+ * QUIEN AVISA AL MOVIL DE QUE YA PUEDE MIRAR
8
+ *
9
+ * El navegador sabia cuando le preguntaban algo -el permiso llega por su
10
+ * canal- pero no sabia cuando el agente TERMINABA. Y ese es justo el momento
11
+ * que importa cuando no estas delante: dejas una tarea de cinco minutos, te vas,
12
+ * y vuelves a mirar el telefono cada treinta segundos por si acaso.
13
+ *
14
+ * El momento no hay que inventarlo ni medirlo: es exactamente cuando el agente
15
+ * se pone a esperar lo que escribas. Ni un evento nuevo ni un temporizador.
16
+ */
17
+ type AvisoDeTurno = () => void;
18
+ export declare function fijarAvisoDeTurno(fn: AvisoDeTurno | null): void;
6
19
  export declare function esperarEntradaWeb(): Promise<string>;
7
20
  export declare function hayEntradaEnCola(): boolean;
8
21
  export interface PeticionDePermiso {
@@ -48,10 +48,20 @@ export function entregarEntrada(texto) {
48
48
  // vez de perderse. Es lo que ya hace el type-ahead del terminal.
49
49
  cola.push(texto);
50
50
  }
51
+ let avisarDelTurno = null;
52
+ export function fijarAvisoDeTurno(fn) {
53
+ avisarDelTurno = fn;
54
+ }
51
55
  export function esperarEntradaWeb() {
52
56
  const pendiente = cola.shift();
57
+ // Con algo en la cola NO se avisa: el usuario ya habia escrito mientras el
58
+ // agente trabajaba, asi que no ha terminado nada para el, sigue la faena.
53
59
  if (pendiente !== undefined)
54
60
  return Promise.resolve(pendiente);
61
+ try {
62
+ avisarDelTurno?.();
63
+ }
64
+ catch { }
55
65
  return new Promise((resolve) => {
56
66
  esperando = resolve;
57
67
  });
@@ -106,6 +116,7 @@ export function permisosAbiertos() {
106
116
  }
107
117
  /** Solo para pruebas. */
108
118
  export function vaciarPuente() {
119
+ avisarDelTurno = null;
109
120
  cola.length = 0;
110
121
  esperando = null;
111
122
  emisor = null;
@@ -36,6 +36,12 @@ export interface OpcionesServidor {
36
36
  enRed?: boolean;
37
37
  /** Para pruebas: token fijo en vez de uno aleatorio. */
38
38
  token?: string;
39
+ /**
40
+ * La carpeta de trabajo, leida en cada peticion y no copiada al arrancar.
41
+ * Con /cd el agente se cambia de carpeta a mitad de sesion, y lo que se sube
42
+ * tiene que caer donde se esta trabajando AHORA, no donde se empezo.
43
+ */
44
+ carpeta?: () => string;
39
45
  }
40
46
  export interface ServidorWeb {
41
47
  puerto: number;
@@ -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>;