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.
Files changed (49) hide show
  1. package/COMPUTER-USE.md +37 -0
  2. package/README.md +29 -3
  3. package/dist/agent/historialDeCambios.d.ts +77 -0
  4. package/dist/agent/historialDeCambios.js +220 -0
  5. package/dist/agent/salidaDelAgente.d.ts +9 -0
  6. package/dist/agent/salidaDelAgente.js +19 -0
  7. package/dist/agent/undoManager.d.ts +21 -0
  8. package/dist/agent/undoManager.js +85 -5
  9. package/dist/config/permissions.js +4 -0
  10. package/dist/index.js +216 -27
  11. package/dist/prompts/systemPrompt.js +15 -11
  12. package/dist/servidor/archivos.d.ts +69 -0
  13. package/dist/servidor/archivos.js +156 -0
  14. package/dist/servidor/captura.d.ts +9 -0
  15. package/dist/servidor/captura.js +48 -0
  16. package/dist/servidor/fondo.d.ts +85 -0
  17. package/dist/servidor/fondo.js +134 -0
  18. package/dist/servidor/marca.d.ts +14 -0
  19. package/dist/servidor/marca.js +14 -0
  20. package/dist/servidor/pagina.d.ts +0 -26
  21. package/dist/servidor/pagina.js +337 -10
  22. package/dist/servidor/puente.d.ts +13 -0
  23. package/dist/servidor/puente.js +11 -0
  24. package/dist/servidor/servidor.d.ts +76 -2
  25. package/dist/servidor/servidor.js +245 -13
  26. package/dist/tools/computerUse.d.ts +11 -1
  27. package/dist/tools/computerUse.js +376 -34
  28. package/dist/tools/toolDefsComputer.js +39 -14
  29. package/dist/tools/visionBridge.d.ts +4 -2
  30. package/dist/tools/visionBridge.js +40 -15
  31. package/dist/tools/win/hostScript.js +300 -29
  32. package/dist/ui/comandos.js +1 -0
  33. package/dist/ui/ink/App.js +23 -3
  34. package/dist/ui/ink/Prompt.d.ts +5 -1
  35. package/dist/ui/ink/Prompt.js +92 -4
  36. package/dist/ui/ink/control.d.ts +2 -0
  37. package/dist/ui/ink/control.js +15 -0
  38. package/dist/ui/ink/montarApp.d.ts +0 -19
  39. package/dist/ui/ink/montarApp.js +3 -0
  40. package/dist/ui/permissionPrompt.js +44 -9
  41. package/dist/ui/pieFijo.js +5 -0
  42. package/dist/ui/prompt.js +6 -0
  43. package/dist/ui/renderer.d.ts +1 -0
  44. package/dist/ui/renderer.js +27 -2
  45. package/dist/ui/selector.js +5 -0
  46. package/dist/ui/spinner.d.ts +1 -0
  47. package/dist/ui/spinner.js +6 -0
  48. package/extension/manifest.json +1 -1
  49. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -17,14 +17,15 @@ import { fijarMotor } from "./ui/motorActual.js";
17
17
  import { elegirDeLista } from "./ui/selector.js";
18
18
  import { entrarEnPantallaFija, pantallaFijaPedida, salirDePantallaFija } from "./ui/pantalla.js";
19
19
  import { iniciarServidorWeb } from "./servidor/servidor.js";
20
- import { iniciarCaptura } from "./servidor/captura.js";
20
+ import { anotarCrudoWeb, anotarWeb, iniciarCaptura } from "./servidor/captura.js";
21
+ import { copiarSalidaAlWeb } from "./agent/salidaDelAgente.js";
21
22
  import { entregarEntrada, esperarEntradaWeb, marcarServirActivo } from "./servidor/puente.js";
22
23
  import { montarArbol, desmontarArbol } from "./ui/ink/montarApp.js";
23
24
  import { reafirmarModoCrudo } from "./ui/keyboardGuard.js";
24
25
  import { askInteractivePrompt, SLASH_COMMANDS } from "./ui/prompt.js";
25
26
  import { changeDir } from "./tools/changeDir.js";
26
27
  import { gitDiff, gitCommit } from "./tools/gitTools.js";
27
- import { renderUserPrompt } from "./ui/renderer.js";
28
+ import { renderUserPrompt, formatColorDiff } from "./ui/renderer.js";
28
29
  import { parseFileMentions, buildPromptWithMentions, notaDeMencionesImplicitas } from "./agent/mentions.js";
29
30
  import { SkillManager } from "./skills/manager.js";
30
31
  import { MemoryManager } from "./memory/manager.js";
@@ -33,6 +34,8 @@ import { SessionManager } from "./sessions/manager.js";
33
34
  import { prepararRestauracion, formatearOpcionesDeSesion } from "./sessions/resume.js";
34
35
  import { auditGitDiff, formatAuditReport } from "./tools/gitAudit.js";
35
36
  import { instalarKit, listarKits, quitarKit, crearKit } from "./kits/manager.js";
37
+ import { guardarSesion, lanzarEnFondo, olvidarSesion, pararSesion, sesionViva } from "./servidor/fondo.js";
38
+ import { nombreDeAnfitrion } from "./servidor/servidor.js";
36
39
  import { leerCortes, rutaDelRegistro } from "./agent/cortes.js";
37
40
  import { globalUndoManager } from "./agent/undoManager.js";
38
41
  import { setAutoApprove } from "./ui/permissionPrompt.js";
@@ -220,8 +223,8 @@ async function main() {
220
223
  // errores raros dentro de una dependencia. Ver config/nodeVersion.ts.
221
224
  const nodeViejo = comprobarNode();
222
225
  if (nodeViejo) {
223
- console.error(chalk.red(`
224
- ${nodeViejo}
226
+ console.error(chalk.red(`
227
+ ${nodeViejo}
225
228
  `));
226
229
  process.exit(EXIT_USO);
227
230
  }
@@ -235,13 +238,58 @@ ${nodeViejo}
235
238
  // red local, que es lo que hace falta para entrar desde el movil.
236
239
  const enRed = rawArgs.includes("--servir-red");
237
240
  const quiereServir = enRed || rawArgs.includes("--servir");
241
+ // --fondo suelta la sesion del terminal; --parar y --estado la gobiernan
242
+ // desde fuera, sin tener que buscar la ventana que la lanzo.
243
+ const quiereFondo = rawArgs.includes("--fondo");
244
+ const quiereParar = rawArgs.includes("--parar");
245
+ const quiereEstado = rawArgs.includes("--estado");
246
+ // --tunel autoriza UN nombre de dominio mas, para entrar por cloudflared o
247
+ // ngrok desde fuera de casa. Uno concreto: la defensa contra el DNS rebinding
248
+ // sigue en pie para todos los demas.
249
+ const posTunel = rawArgs.indexOf("--tunel");
250
+ const tunelWeb = posTunel >= 0 ? String(rawArgs[posTunel + 1] || "") : "";
238
251
  const posPuerto = rawArgs.indexOf("--puerto");
239
252
  const puertoWeb = posPuerto >= 0 ? Number(rawArgs[posPuerto + 1]) || 0 : 0;
240
253
  // Ojo: lo que sobra de argv se convierte en la orden a ejecutar. Una bandera
241
254
  // que no se filtre aqui no es una bandera ignorada, es una orden al modelo.
242
- const banderas = new Set(["--yes", "-y", "--servir", "--servir-red", "--puerto"]);
243
- const args = rawArgs.filter((a, i) => !banderas.has(a) && !(posPuerto >= 0 && i === posPuerto + 1 && puertoWeb > 0));
255
+ const banderas = new Set([
256
+ "--yes",
257
+ "-y",
258
+ "--servir",
259
+ "--servir-red",
260
+ "--puerto",
261
+ "--fondo",
262
+ "--parar",
263
+ "--estado",
264
+ "--tunel",
265
+ ]);
266
+ const args = rawArgs.filter((a, i) => !banderas.has(a) &&
267
+ !(posPuerto >= 0 && i === posPuerto + 1 && puertoWeb > 0) &&
268
+ !(posTunel >= 0 && i === posTunel + 1 && !!tunelWeb));
244
269
  const singlePrompt = args.join(" ").trim();
270
+ // ESTAS DOS NO ARRANCAN NADA, y por eso van aqui arriba: montar el agente
271
+ // entero -API key, skills, MCP- para luego solo decir un PID seria hacer
272
+ // esperar al usuario por nada, y ademas pediria la clave a quien solo quiere
273
+ // apagar la sesion de ayer.
274
+ if (quiereParar) {
275
+ const res = await pararSesion();
276
+ console.log(res.ok ? chalk.green(`\n ✔ ${res.mensaje}\n`) : chalk.yellow(`\n ${res.mensaje}\n`));
277
+ process.exit(res.ok ? 0 : 1);
278
+ }
279
+ if (quiereEstado) {
280
+ const viva = sesionViva();
281
+ if (!viva) {
282
+ console.log(chalk.gray("\n No hay ninguna sesion de fondo abierta.\n"));
283
+ process.exit(1);
284
+ }
285
+ console.log(chalk.bold.hex("#e8833a")("\n 🦊 Sesion de fondo abierta"));
286
+ console.log(` ${chalk.bold("Enlace:")} ${chalk.cyan(viva.url)}`);
287
+ console.log(` ${chalk.bold("Carpeta:")} ${chalk.white(viva.carpeta)}`);
288
+ console.log(` ${chalk.bold("Desde:")} ${chalk.white(new Date(viva.desde).toLocaleString())}`);
289
+ console.log(chalk.gray(` pid ${viva.pid} · ${viva.enRed ? "visible en tu red" : "solo esta maquina"}`));
290
+ console.log(chalk.gray("\n Para cerrarla: chocolatito --parar\n"));
291
+ process.exit(0);
292
+ }
245
293
  // Los errores de uso se resuelven ANTES de la licencia y de la API key: no
246
294
  // dependen de estar suscrito, y en CI hay que poder distinguir "lo has
247
295
  // invocado mal" de "el agente no pudo".
@@ -283,6 +331,9 @@ ${nodeViejo}
283
331
  let isPlanMode = false;
284
332
  let systemPrompt = buildSystemPrompt(context, currentModel, skillManager, memoryManager, isPlanMode);
285
333
  const agent = new AgentLoop(apiKey, getEngineBaseUrl(), currentModel, systemPrompt);
334
+ // El historial de cambios es de CADA proyecto y vive en su carpeta, asi que
335
+ // hay que decirle cual es. Con /cd se vuelve a fijar mas abajo.
336
+ globalUndoManager.fijarCarpeta(agent.currentCwd);
286
337
  // Shift+Tab (en el prompt, durante la generacion o dentro del propio dialogo)
287
338
  // cambia el modo de permisos del agente al vuelo. El modo plan no entra en el
288
339
  // ciclo: se sigue activando con /plan, para no dejarlo a un golpe de tecla.
@@ -408,16 +459,87 @@ ${nodeViejo}
408
459
  // reemplazo los cuatro canales de console para mandarlos a la transcripcion, y
409
460
  // la captura tiene que envolver ESA version. Al reves, pantalla envolveria a la
410
461
  // captura y al navegador no llegaria nada. Ver servidor/captura.ts.
462
+ // SOLTARSE DEL TERMINAL
463
+ //
464
+ // Se relanza el mismo programa con los mismos argumentos, sin --fondo, y este
465
+ // proceso se va. El hijo no hereda ni la entrada ni la salida: la ventana se
466
+ // puede cerrar y la sesion sigue.
467
+ //
468
+ // El enlace lo imprime el PADRE, pero el token lo genera el HIJO. Asi que en
469
+ // vez de inventar un canal para pasarlo, se espera a que el hijo escriba su
470
+ // archivo de sesion. Eso ademas confirma que arranco de verdad: si el hijo
471
+ // revienta -puerto ocupado, licencia caducada-, el archivo no aparece y se
472
+ // dice que fallo, en lugar de imprimir un enlace a nada.
473
+ if (quiereServir && quiereFondo && !process.env["CHOCOLATITO_EN_FONDO"]) {
474
+ const yaAbierta = sesionViva();
475
+ if (yaAbierta) {
476
+ console.log(chalk.yellow("\n Ya hay una sesion de fondo abierta."));
477
+ console.log(` ${chalk.cyan(yaAbierta.url)}\n`);
478
+ console.log(chalk.gray(" Para cerrarla: chocolatito --parar\n"));
479
+ process.exit(1);
480
+ }
481
+ olvidarSesion();
482
+ const pid = lanzarEnFondo(rawArgs.filter((a) => a !== "--fondo"));
483
+ let viva = null;
484
+ for (let intento = 0; intento < 100 && !viva; intento++) {
485
+ await new Promise((r) => setTimeout(r, 100));
486
+ viva = sesionViva();
487
+ }
488
+ if (!viva) {
489
+ console.log(chalk.red("\n La sesion de fondo no llego a arrancar."));
490
+ console.log(chalk.gray(` Prueba sin --fondo para ver el error (pid ${pid}).\n`));
491
+ process.exit(1);
492
+ }
493
+ console.log(chalk.hex("#e8833a")("\n 🦊 Sesion abierta en segundo plano\n"));
494
+ console.log(` ${chalk.bold("Enlace:")} ${chalk.cyan(viva.url)}`);
495
+ console.log(chalk.gray("\n Ya puedes cerrar esta ventana: la sesion sigue."));
496
+ console.log(chalk.gray(" chocolatito --estado · chocolatito --parar\n"));
497
+ process.exit(0);
498
+ }
411
499
  if (quiereServir) {
412
500
  try {
413
- iniciarCaptura();
414
- const web = await iniciarServidorWeb({ puerto: puertoWeb || undefined, enRed });
501
+ const web = await iniciarServidorWeb({
502
+ puerto: puertoWeb || undefined,
503
+ enRed,
504
+ anfitrionAutorizado: tunelWeb || undefined,
505
+ // La carpeta se pregunta en cada peticion, no se copia aqui: con /cd el
506
+ // agente se cambia a mitad de sesion y lo que suba el movil tiene que
507
+ // caer donde se esta trabajando AHORA.
508
+ carpeta: () => agent.currentCwd,
509
+ });
415
510
  marcarServirActivo(true);
511
+ // Solo el que corre suelto deja su ficha: una sesion normal muere con su
512
+ // terminal y anotarla solo serviria para que --estado mintiera despues.
513
+ if (process.env["CHOCOLATITO_EN_FONDO"]) {
514
+ guardarSesion({
515
+ pid: process.pid,
516
+ puerto: web.puerto,
517
+ token: web.token,
518
+ url: web.urlsDeRed[0] || web.url,
519
+ carpeta: agent.currentCwd,
520
+ desde: new Date().toISOString(),
521
+ enRed,
522
+ });
523
+ const limpiar = () => olvidarSesion();
524
+ process.once("exit", limpiar);
525
+ process.once("SIGINT", () => {
526
+ limpiar();
527
+ process.exit(0);
528
+ });
529
+ process.once("SIGTERM", () => {
530
+ limpiar();
531
+ process.exit(0);
532
+ });
533
+ }
416
534
  console.log(chalk.hex("#e8833a")("\n 🦊 Sesión abierta en el navegador\n"));
417
535
  console.log(` ${chalk.bold("Aquí:")} ${chalk.cyan(web.url)}`);
418
536
  for (const dir of web.urlsDeRed) {
419
537
  console.log(` ${chalk.bold("Del móvil:")} ${chalk.cyan(dir)}`);
420
538
  }
539
+ if (tunelWeb) {
540
+ console.log(chalk.yellow(`\n ⚠ Autorizado el tunel ${nombreDeAnfitrion(tunelWeb)}`) +
541
+ chalk.gray("\n Ese enlace sale a internet: quien lo tenga entero entra. Cierrala al acabar."));
542
+ }
421
543
  if (!enRed) {
422
544
  console.log(chalk.gray("\n Solo desde esta máquina. Para entrar desde el móvil: --servir-red"));
423
545
  }
@@ -427,6 +549,28 @@ ${nodeViejo}
427
549
  "\n puede usar esta sesión. No lo pegues en ningún sitio compartido."));
428
550
  }
429
551
  console.log(chalk.gray("\n Lo que escribas aquí ya no se lee: manda el navegador y este terminal es el monitor.\n"));
552
+ // LA CAPTURA EMPIEZA AQUI, Y NO ANTES, POR DOS MOTIVOS
553
+ //
554
+ // 1. EL TOKEN. Lo que se acaba de imprimir incluye "Aqui: http://...?t=..."
555
+ // con el token entero. Eso es para el que esta delante del PC; copiarlo
556
+ // a la pagina lo deja escrito en la pantalla del movil, a la vista de
557
+ // quien pase por al lado y en cualquier captura que se haga.
558
+ // 2. NO SIRVE DE NADA ALLI. "Para entrar desde el movil: --servir-red" se
559
+ // lo estas leyendo YA desde el movil. Son instrucciones para llegar a
560
+ // un sitio en el que ya estas.
561
+ //
562
+ // Asi que al navegador se le da una bienvenida suya, corta: el zorro y la
563
+ // carpeta, que es lo unico que hace falta saber al abrir.
564
+ iniciarCaptura();
565
+ // Y lo que contesta el agente, que NO va por console: sale por el pie fijo
566
+ // a process.stdout, asi que hay que copiarlo aparte o el movil se queda mudo.
567
+ copiarSalidaAlWeb((texto) => anotarCrudoWeb(texto));
568
+ // La cabecera de la pagina ya lleva el zorro oficial y el nombre, asi que
569
+ // repetirlo aqui con un emoji era decir dos veces lo mismo, y con dos
570
+ // dibujos distintos del mismo zorro. Queda la carpeta, que es lo unico que
571
+ // hace falta saber al abrir: en que proyecto estas.
572
+ anotarWeb(chalk.gray(` ${agent.currentCwd}`));
573
+ anotarWeb("");
430
574
  // Una orden en la linea de comandos con --servir es la PRIMERA del turno,
431
575
  // no una tarea suelta: entra por la misma puerta que lo que llegue del movil.
432
576
  if (singlePrompt)
@@ -504,6 +648,48 @@ ${nodeViejo}
504
648
  }
505
649
  continue;
506
650
  }
651
+ if (trimmed === "/rewind" || trimmed.startsWith("/rewind ")) {
652
+ const pedido = trimmed.slice(7).trim();
653
+ const turnos = globalUndoManager.turnos();
654
+ if (turnos.length === 0) {
655
+ console.log(chalk.gray("\n No hay nada guardado que deshacer en este proyecto.\n"));
656
+ continue;
657
+ }
658
+ if (!pedido) {
659
+ console.log(chalk.bold.hex("#D97757")("\n⏪ VOLVER ATRÁS"));
660
+ console.log(chalk.gray(" Lo que Chocolatito ha tocado en esta carpeta, de lo más nuevo a lo más viejo:\n"));
661
+ // Del mas nuevo al mas viejo: lo que quieres deshacer casi siempre es lo
662
+ // ultimo, y leerlo del reves obliga a contar renglones.
663
+ const alReves = [...turnos].reverse();
664
+ alReves.forEach((t, i) => {
665
+ const cuando = new Date(t.cuando).toLocaleString();
666
+ const conCopia = t.archivos.filter((a) => a.tipo === "file").length;
667
+ const creados = t.archivos.filter((a) => a.tipo === "absent").length;
668
+ const sinCopia = t.archivos.filter((a) => a.tipo === "unrestorable").length;
669
+ const piezas = [
670
+ conCopia ? `${conCopia} restaurable(s)` : "",
671
+ creados ? `${creados} creado(s)` : "",
672
+ sinCopia ? chalk.yellow(`${sinCopia} SIN copia`) : "",
673
+ ].filter(Boolean);
674
+ console.log(` ${chalk.bold.white(String(i + 1).padStart(2))}. ${chalk.white(t.descripcion.slice(0, 60))}`);
675
+ console.log(` ${chalk.gray(cuando)} ${chalk.gray(piezas.join(" · "))}`);
676
+ });
677
+ console.log(chalk.gray("\n /rewind <n> vuelve a como estaba ANTES de ese turno (y deshace los de después).\n"));
678
+ continue;
679
+ }
680
+ const indice = Number(pedido);
681
+ if (!Number.isInteger(indice) || indice < 1 || indice > turnos.length) {
682
+ console.log(chalk.yellow(`\n Elige un número del 1 al ${turnos.length}. /rewind a secas los lista.\n`));
683
+ continue;
684
+ }
685
+ const elegido = [...turnos].reverse()[indice - 1];
686
+ const res = globalUndoManager.volverAntesDe(elegido.id);
687
+ console.log(res.success ? chalk.green(`\n${res.message}`) : chalk.yellow(`\n${res.message}`));
688
+ res.restoredFiles.forEach((r) => console.log(` ${chalk.gray("•")} ${chalk.white(r)}`));
689
+ res.failures.forEach((r) => console.log(` ${chalk.red("✗")} ${chalk.yellow(r)}`));
690
+ console.log("");
691
+ continue;
692
+ }
507
693
  if (trimmed === "/undo") {
508
694
  const undoRes = globalUndoManager.undoLast();
509
695
  // Lo que no se pudo deshacer se imprime siempre, tambien cuando algo si se
@@ -522,15 +708,15 @@ ${nodeViejo}
522
708
  .filter((m) => m.role === "user" || m.role === "assistant")
523
709
  .map((m) => `### 👤 ${m.role === "user" ? "Usuario" : "Chocolatito"}\n\n${typeof m.content === "string" ? m.content : JSON.stringify(m.content, null, 2)}\n`)
524
710
  .join("\n---\n\n");
525
- const mdContent = `# 🦊 Bitácora de Sesión — Chocolatito Code
526
- **Fecha:** ${new Date().toLocaleString()}
527
- **Directorio:** \`${agent.currentCwd}\`
528
- **Motor:** ${currentModel.name}
529
- **Inversión Total:** ${agent.tracker.calculateCost(undefined, undefined, undefined, currentModel.underlyingModel).formattedCost}
530
-
531
- ---
532
-
533
- ${sessionData}
711
+ const mdContent = `# 🦊 Bitácora de Sesión — Chocolatito Code
712
+ **Fecha:** ${new Date().toLocaleString()}
713
+ **Directorio:** \`${agent.currentCwd}\`
714
+ **Motor:** ${currentModel.name}
715
+ **Inversión Total:** ${agent.tracker.calculateCost(undefined, undefined, undefined, currentModel.underlyingModel).formattedCost}
716
+
717
+ ---
718
+
719
+ ${sessionData}
534
720
  `;
535
721
  fs.writeFileSync(exportPath, mdContent, "utf-8");
536
722
  console.log(chalk.green(`\n✔ Bitácora exportada exitosamente en: ${chalk.bold.white(exportPath)}\n`));
@@ -746,12 +932,12 @@ ${sessionData}
746
932
  }
747
933
  console.log(chalk.bold.hex("#A855F7")("\n🎯 INICIANDO MODO META AUTÓNOMA (GOAL MODE):"));
748
934
  console.log(chalk.gray(` Objetivo: "${goalText}"\n`));
749
- const autonomousPrompt = `[MODO META AUTÓNOMA ACTIVADO]
750
- Tu objetivo principal es: "${goalText}"
751
- Directivas de ejecución:
752
- 1. Formula un plan de ejecución mental paso a paso.
753
- 2. Ejecuta cada paso utilizando las herramientas correspondientes (creación, edición, ejecución, verificación).
754
- 3. Si algo falla o hay errores, autocorregir inmediatamente y volver a probar.
935
+ const autonomousPrompt = `[MODO META AUTÓNOMA ACTIVADO]
936
+ Tu objetivo principal es: "${goalText}"
937
+ Directivas de ejecución:
938
+ 1. Formula un plan de ejecución mental paso a paso.
939
+ 2. Ejecuta cada paso utilizando las herramientas correspondientes (creación, edición, ejecución, verificación).
940
+ 3. Si algo falla o hay errores, autocorregir inmediatamente y volver a probar.
755
941
  4. No te detengas hasta verificar que el objetivo esté 100% completado con éxito.`;
756
942
  renderUserPrompt(`/goal ${goalText}`);
757
943
  await agent.runTask(autonomousPrompt);
@@ -774,7 +960,7 @@ Directivas de ejecución:
774
960
  if (trimmed === "/diff") {
775
961
  const diffOut = await gitDiff(false, agent.currentCwd);
776
962
  console.log(chalk.bold.hex("#D97757")("\n📝 DIFERENCIAS DE GIT (DIFF):"));
777
- console.log(diffOut ? chalk.gray(diffOut) : chalk.green("✔ Sin cambios pendientes.") + "\n");
963
+ console.log(diffOut ? `${formatColorDiff(diffOut)}\n` : chalk.green("✔ Sin cambios pendientes.") + "\n");
778
964
  continue;
779
965
  }
780
966
  if (trimmed === "/audit" || trimmed === "/review" || trimmed.startsWith("/audit ") || trimmed.startsWith("/review ")) {
@@ -799,6 +985,9 @@ Directivas de ejecución:
799
985
  if (res.success) {
800
986
  agent.currentCwd = res.newCwd;
801
987
  memoryManager.setCwd(agent.currentCwd);
988
+ // El historial de cambios es de cada proyecto: al cambiar de carpeta se
989
+ // cambia de historial, o /rewind ensenaria los turnos de otro sitio.
990
+ globalUndoManager.fijarCarpeta(agent.currentCwd);
802
991
  const refreshedContext = await getProjectContext(agent.currentCwd);
803
992
  systemPrompt = buildSystemPrompt(refreshedContext, currentModel, skillManager, memoryManager, isPlanMode);
804
993
  agent.clearHistory(systemPrompt);
@@ -861,12 +1050,12 @@ Directivas de ejecución:
861
1050
  const refreshedContext = await getProjectContext(agent.currentCwd);
862
1051
  const updatedPrompt = buildSystemPrompt(refreshedContext, currentModel, skillManager, memoryManager, isPlanMode);
863
1052
  agent.setEspectroModel(currentModel, updatedPrompt);
864
- console.log(chalk.green(`
1053
+ console.log(chalk.green(`
865
1054
  ✔ Nivel actualizado a `) +
866
1055
  chalk.bold.hex("#A855F7")(currentModel.name) +
867
1056
  chalk.gray(` (${currentModel.effortLevel})`) +
868
- `
869
- ${chalk.italic.gray(currentModel.description)}
1057
+ `
1058
+ ${chalk.italic.gray(currentModel.description)}
870
1059
  `);
871
1060
  // EL TECLADO SE QUEDABA TRABADO AL CAMBIAR DE MOTOR
872
1061
  //
@@ -172,17 +172,21 @@ Puedes delegar tareas aisladas con 'spawn_agent':
172
172
  NUNCA pidas al usuario su contraseña ni la escribas. Si hace falta una cuenta, es
173
173
  él quien inicia sesión a mano.
174
174
 
175
- APLICACIONES DE ESCRITORIO: usa 'computer_use'.
176
- * Flujo: computer_use(list_windows) → computer_use(ui_snapshot, window:"...")
177
- → computer_use(ui_click, ref:N).
178
- * Pasa SIEMPRE 'window'. Sin ella actúas sobre lo que esté delante, que es
179
- exactamente como se acaba escribiendo en la ventana equivocada.
180
- * El modo por defecto es 'background': no robes el foco salvo que sea imprescindible.
181
-
182
- CUANDO NO HAY ÁRBOL DE ACCESIBILIDAD (lienzos, vídeo, juegos, imágenes):
183
- * 'screenshot' con 'window' captura esa ventana aunque esté tapada, y te la describe.
184
- * 'find_element' localiza un control por texto y te da sus coordenadas reales.
185
- * Solo entonces tiene sentido un click por coordenadas.
175
+ APLICACIONES DE ESCRITORIO: usa 'computer_use' (nivel Astra / OpenAI Operator):
176
+ * Flujo preferente: computer_use(list_windows) → computer_use(ui_snapshot, window:"...")
177
+ → computer_use(ui_click, ref:N).
178
+ * Pasa SIEMPRE 'window'. Sin ella actúas sobre lo que esté delante, que es
179
+ exactamente como se acaba escribiendo en la ventana equivocada.
180
+ * El modo por defecto es 'background': no robes el foco salvo que sea imprescindible.
181
+ * FLUJOS RÁPIDOS (ACTION CHAINING): Para secuencias interactivas comunes
182
+ (ej. clic en campo -> escribir texto -> pulsar Enter -> esperar resultado),
183
+ usa action: 'sequence' con 'steps'. Ejecuta todo en milisegundos en un solo turno sin latencia.
184
+ * CUANDO NO HAY ÁRBOL DE ACCESIBILIDAD (lienzos, juegos, vídeo, Flutter, interfaces gráficas complejas):
185
+ - Usa 'screenshot' con 'grid: true' para activar la cuadrícula de coordenadas milimétricas y situar controles con exactitud.
186
+ - Usa 'screenshot' con 'region: [x1,y1,x2,y2]' para inspeccionar zonas pequeñas o detalles finos en resolución nativa 1:1.
187
+ - Usa 'wait_visual_change' para esperar animaciones, cargas o cambios visuales cuando la app no emite eventos de accesibilidad.
188
+ - Usa 'find_element' para localizar un control por texto en el árbol o visualmente mediante visión multimodal.
189
+ * COORDENADAS: Soporta píxeles físicos o coordenadas normalizadas (0..1000 con normalized: true). Si 'ui_click' no tiene patrón nativo, se auto-recupera clicando el centro del elemento.
186
190
 
187
191
  PROHIBIDO: 'taskkill' o matar procesos de Chrome del usuario.
188
192
  8. LO QUE PRODUCES ES DEL USUARIO, NO TUYO:
@@ -0,0 +1,69 @@
1
+ /**
2
+ * ARCHIVOS QUE ENTRAN Y ARCHIVOS QUE SALEN
3
+ *
4
+ * DE DONDE SALE
5
+ *
6
+ * La pagina de `--servir` solo sabia texto. Ni un `<input type=file>`, ni una
7
+ * sola `<img>`. Y eso deja el ciclo de trabajo cojo por los dos extremos:
8
+ *
9
+ * - Lo que tienes en el movil -la foto, el video que te mando el cliente, la
10
+ * captura del error- no hay forma de metarlo. Habia que pasarlo antes al PC.
11
+ * - Lo que el agente produce se contesta con la RUTA. En un movil, "lo tienes
12
+ * en C:\Users\...\salida\pieza-01.png" no sirve para nada: no puedes mirarlo,
13
+ * asi que no puedes opinar, asi que no puedes trabajar.
14
+ *
15
+ * LAS DOS REGLAS
16
+ *
17
+ * 1. LO QUE ENTRA CAE EN UN SOLO SITIO: `entradas/` de la carpeta de trabajo.
18
+ * No se deja elegir destino. Un nombre de archivo que llega por la red no
19
+ * es un nombre: es lo que alguien escribio al otro lado, y "..\..\.ssh\id_rsa"
20
+ * tambien es un nombre. Se le quita todo lo que parezca una ruta y se queda
21
+ * el nombre pelado.
22
+ *
23
+ * 2. LO QUE SALE NO PUEDE SALIRSE DE LA CARPETA DE TRABAJO. Se resuelve la
24
+ * ruta de verdad -con enlaces simbolicos ya seguidos- y se comprueba que
25
+ * sigue estando dentro. Sin eso, `?ruta=../../.chocolatitorc` serviria la
26
+ * clave de API por HTTP a quien tenga el enlace.
27
+ *
28
+ * Las dos comprobaciones viven aqui y no en el servidor a proposito: son la
29
+ * clase de cosa que se copia mal cuando se reparte entre tres sitios.
30
+ */
31
+ /** Lo mas grande que se acepta de una vez. Un video de movil cabe de sobra. */
32
+ export declare const LIMITE_DE_SUBIDA: number;
33
+ /** Donde caen los archivos que llegan del navegador. */
34
+ export declare const CARPETA_DE_ENTRADAS = "entradas";
35
+ export declare function tipoDelArchivo(ruta: string): string;
36
+ export declare function esImagen(ruta: string): boolean;
37
+ /**
38
+ * Deja un nombre de archivo en algo que no pueda salirse de su carpeta.
39
+ *
40
+ * No vale con quitar "..": en Windows tambien separa la barra invertida, y
41
+ * "C:algo" es una ruta relativa al disco C. Se parte por los dos separadores y
42
+ * se toma el ultimo trozo, que es lo unico que de verdad es un nombre.
43
+ */
44
+ export declare function nombreSeguro(propuesto: string): string;
45
+ /** Si ya existe, se numera en vez de pisar lo que hubiera. */
46
+ export declare function rutaLibre(carpeta: string, nombre: string): string;
47
+ export interface Guardado {
48
+ ok: boolean;
49
+ ruta?: string;
50
+ error?: string;
51
+ }
52
+ export declare function guardarSubida(cwd: string, nombrePropuesto: string, datos: Buffer): Guardado;
53
+ /**
54
+ * Comprueba que una ruta pedida por el navegador se puede servir.
55
+ *
56
+ * Devuelve la ruta real o null. Null es un 404 siempre, tambien cuando el
57
+ * archivo existe pero esta fuera: decir "existe pero no te lo doy" ya es contar
58
+ * algo del disco de alguien a quien no le hemos preguntado.
59
+ */
60
+ export declare function rutaServible(cwd: string, pedida: string): string | null;
61
+ /**
62
+ * La linea que se le mete a la conversacion cuando llegan archivos.
63
+ *
64
+ * Lleva las rutas COMPLETAS y no los nombres: el modelo tiene que poder
65
+ * abrirlos sin adivinar donde cayeron, y "la foto que acabo de subir" no es una
66
+ * ruta. Y se dice que vienen del navegador, porque cambia lo que hay que hacer
67
+ * con ellos: no son archivos del proyecto, son material que acaba de llegar.
68
+ */
69
+ export declare function avisoDeSubida(rutas: string[]): string;
@@ -0,0 +1,156 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ /**
4
+ * ARCHIVOS QUE ENTRAN Y ARCHIVOS QUE SALEN
5
+ *
6
+ * DE DONDE SALE
7
+ *
8
+ * La pagina de `--servir` solo sabia texto. Ni un `<input type=file>`, ni una
9
+ * sola `<img>`. Y eso deja el ciclo de trabajo cojo por los dos extremos:
10
+ *
11
+ * - Lo que tienes en el movil -la foto, el video que te mando el cliente, la
12
+ * captura del error- no hay forma de metarlo. Habia que pasarlo antes al PC.
13
+ * - Lo que el agente produce se contesta con la RUTA. En un movil, "lo tienes
14
+ * en C:\Users\...\salida\pieza-01.png" no sirve para nada: no puedes mirarlo,
15
+ * asi que no puedes opinar, asi que no puedes trabajar.
16
+ *
17
+ * LAS DOS REGLAS
18
+ *
19
+ * 1. LO QUE ENTRA CAE EN UN SOLO SITIO: `entradas/` de la carpeta de trabajo.
20
+ * No se deja elegir destino. Un nombre de archivo que llega por la red no
21
+ * es un nombre: es lo que alguien escribio al otro lado, y "..\..\.ssh\id_rsa"
22
+ * tambien es un nombre. Se le quita todo lo que parezca una ruta y se queda
23
+ * el nombre pelado.
24
+ *
25
+ * 2. LO QUE SALE NO PUEDE SALIRSE DE LA CARPETA DE TRABAJO. Se resuelve la
26
+ * ruta de verdad -con enlaces simbolicos ya seguidos- y se comprueba que
27
+ * sigue estando dentro. Sin eso, `?ruta=../../.chocolatitorc` serviria la
28
+ * clave de API por HTTP a quien tenga el enlace.
29
+ *
30
+ * Las dos comprobaciones viven aqui y no en el servidor a proposito: son la
31
+ * clase de cosa que se copia mal cuando se reparte entre tres sitios.
32
+ */
33
+ /** Lo mas grande que se acepta de una vez. Un video de movil cabe de sobra. */
34
+ export const LIMITE_DE_SUBIDA = 64 * 1024 * 1024;
35
+ /** Donde caen los archivos que llegan del navegador. */
36
+ export const CARPETA_DE_ENTRADAS = "entradas";
37
+ const TIPOS = {
38
+ ".png": "image/png",
39
+ ".jpg": "image/jpeg",
40
+ ".jpeg": "image/jpeg",
41
+ ".webp": "image/webp",
42
+ ".gif": "image/gif",
43
+ ".svg": "image/svg+xml",
44
+ ".bmp": "image/bmp",
45
+ ".avif": "image/avif",
46
+ ".pdf": "application/pdf",
47
+ ".mp4": "video/mp4",
48
+ ".webm": "video/webm",
49
+ ".mov": "video/quicktime",
50
+ ".mp3": "audio/mpeg",
51
+ ".wav": "audio/wav",
52
+ ".ogg": "audio/ogg",
53
+ ".txt": "text/plain; charset=utf-8",
54
+ ".md": "text/plain; charset=utf-8",
55
+ ".json": "application/json",
56
+ ".csv": "text/csv; charset=utf-8",
57
+ };
58
+ export function tipoDelArchivo(ruta) {
59
+ return TIPOS[path.extname(ruta).toLowerCase()] || "application/octet-stream";
60
+ }
61
+ export function esImagen(ruta) {
62
+ return tipoDelArchivo(ruta).startsWith("image/");
63
+ }
64
+ /**
65
+ * Deja un nombre de archivo en algo que no pueda salirse de su carpeta.
66
+ *
67
+ * No vale con quitar "..": en Windows tambien separa la barra invertida, y
68
+ * "C:algo" es una ruta relativa al disco C. Se parte por los dos separadores y
69
+ * se toma el ultimo trozo, que es lo unico que de verdad es un nombre.
70
+ */
71
+ export function nombreSeguro(propuesto) {
72
+ const ultimo = String(propuesto || "")
73
+ .split(/[\\/]/)
74
+ .pop()
75
+ .replace(/^\.+/, "") // nada de ".." ni de archivos ocultos por accidente
76
+ .replace(/[\x00-\x1f<>:"|?*]/g, "") // lo que Windows no admite, y los de control
77
+ .trim();
78
+ const limpio = ultimo.slice(0, 120);
79
+ return limpio || `archivo-${Date.now()}`;
80
+ }
81
+ /** Si ya existe, se numera en vez de pisar lo que hubiera. */
82
+ export function rutaLibre(carpeta, nombre) {
83
+ const ext = path.extname(nombre);
84
+ const base = path.basename(nombre, ext);
85
+ let intento = path.join(carpeta, nombre);
86
+ let n = 2;
87
+ while (fs.existsSync(intento)) {
88
+ intento = path.join(carpeta, `${base}-${n}${ext}`);
89
+ n++;
90
+ if (n > 9999)
91
+ return path.join(carpeta, `${base}-${Date.now()}${ext}`);
92
+ }
93
+ return intento;
94
+ }
95
+ export function guardarSubida(cwd, nombrePropuesto, datos) {
96
+ if (datos.length === 0)
97
+ return { ok: false, error: "El archivo llego vacio." };
98
+ if (datos.length > LIMITE_DE_SUBIDA) {
99
+ return { ok: false, error: `El archivo pasa de ${Math.round(LIMITE_DE_SUBIDA / 1024 / 1024)} MB.` };
100
+ }
101
+ try {
102
+ const carpeta = path.join(cwd, CARPETA_DE_ENTRADAS);
103
+ fs.mkdirSync(carpeta, { recursive: true });
104
+ const destino = rutaLibre(carpeta, nombreSeguro(nombrePropuesto));
105
+ fs.writeFileSync(destino, datos);
106
+ return { ok: true, ruta: destino };
107
+ }
108
+ catch (err) {
109
+ return { ok: false, error: err?.message || String(err) };
110
+ }
111
+ }
112
+ /**
113
+ * Comprueba que una ruta pedida por el navegador se puede servir.
114
+ *
115
+ * Devuelve la ruta real o null. Null es un 404 siempre, tambien cuando el
116
+ * archivo existe pero esta fuera: decir "existe pero no te lo doy" ya es contar
117
+ * algo del disco de alguien a quien no le hemos preguntado.
118
+ */
119
+ export function rutaServible(cwd, pedida) {
120
+ if (!pedida)
121
+ return null;
122
+ try {
123
+ const absoluta = path.isAbsolute(pedida) ? pedida : path.resolve(cwd, pedida);
124
+ // realpath ANTES de comparar: si no, un enlace simbolico dentro de la
125
+ // carpeta apuntando a C:\Users\... pasaria la comprobacion y serviria
126
+ // cualquier cosa del disco.
127
+ const real = fs.realpathSync(absoluta);
128
+ const raiz = fs.realpathSync(cwd);
129
+ const dentro = real === raiz || real.startsWith(raiz + path.sep);
130
+ if (!dentro)
131
+ return null;
132
+ if (!fs.statSync(real).isFile())
133
+ return null;
134
+ return real;
135
+ }
136
+ catch {
137
+ return null;
138
+ }
139
+ }
140
+ /**
141
+ * La linea que se le mete a la conversacion cuando llegan archivos.
142
+ *
143
+ * Lleva las rutas COMPLETAS y no los nombres: el modelo tiene que poder
144
+ * abrirlos sin adivinar donde cayeron, y "la foto que acabo de subir" no es una
145
+ * ruta. Y se dice que vienen del navegador, porque cambia lo que hay que hacer
146
+ * con ellos: no son archivos del proyecto, son material que acaba de llegar.
147
+ */
148
+ export function avisoDeSubida(rutas) {
149
+ if (rutas.length === 0)
150
+ return "";
151
+ if (rutas.length === 1)
152
+ return `[El usuario subio un archivo desde el navegador: ${rutas[0]}]`;
153
+ return (`[El usuario subio ${rutas.length} archivos desde el navegador:\n` +
154
+ rutas.map((r) => `- ${r}`).join("\n") +
155
+ "]");
156
+ }
@@ -28,6 +28,15 @@ export interface LineaWeb {
28
28
  }
29
29
  /** Mete una linea propia (avisos del servidor, eco de lo que llega del movil). */
30
30
  export declare function anotarWeb(texto: string): void;
31
+ export declare function anotarCrudoWeb(texto: string): void;
32
+ /**
33
+ * Suelta lo que quede sin salto de linea.
34
+ *
35
+ * Hace falta porque la ultima linea de una respuesta muchas veces no lo trae, y
36
+ * sin esto se quedaria esperando un \n que no va a llegar: el final de cada
37
+ * respuesta desapareceria del movil.
38
+ */
39
+ export declare function cerrarLineaWeb(): void;
31
40
  export declare function iniciarCaptura(): void;
32
41
  export declare function detenerCaptura(): void;
33
42
  export declare function historialWeb(desde?: number): LineaWeb[];