chocolatito-code 1.6.7 → 1.6.8

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 (55) hide show
  1. package/README.md +437 -341
  2. package/dist/agent/context.d.ts +6 -0
  3. package/dist/agent/context.js +69 -2
  4. package/dist/agent/loop.js +8 -0
  5. package/dist/agent/toolGate.js +13 -7
  6. package/dist/agent/verifier.js +2 -1
  7. package/dist/config/permissions.d.ts +17 -0
  8. package/dist/config/permissions.js +47 -7
  9. package/dist/config/plataforma.d.ts +62 -0
  10. package/dist/config/plataforma.js +109 -0
  11. package/dist/config/updater.js +4 -1
  12. package/dist/hooks/manager.js +2 -1
  13. package/dist/index.js +99 -6
  14. package/dist/memory/manager.d.ts +15 -5
  15. package/dist/memory/manager.js +146 -29
  16. package/dist/servidor/captura.d.ts +36 -0
  17. package/dist/servidor/captura.js +74 -0
  18. package/dist/servidor/pagina.d.ts +27 -0
  19. package/dist/servidor/pagina.js +268 -0
  20. package/dist/servidor/puente.d.ts +34 -0
  21. package/dist/servidor/puente.js +115 -0
  22. package/dist/servidor/servidor.d.ts +61 -0
  23. package/dist/servidor/servidor.js +249 -0
  24. package/dist/sessions/manager.d.ts +8 -0
  25. package/dist/sessions/manager.js +44 -0
  26. package/dist/sessions/resume.d.ts +12 -0
  27. package/dist/sessions/resume.js +10 -0
  28. package/dist/tools/backgroundTask.d.ts +64 -0
  29. package/dist/tools/backgroundTask.js +264 -0
  30. package/dist/tools/browserExtension.d.ts +2 -1
  31. package/dist/tools/browserExtension.js +48 -0
  32. package/dist/tools/computerUse.js +51 -9
  33. package/dist/tools/definitions.js +93 -1
  34. package/dist/tools/gitAudit.d.ts +41 -0
  35. package/dist/tools/gitAudit.js +282 -0
  36. package/dist/tools/runCommand.js +5 -2
  37. package/dist/tools/runner.js +36 -2
  38. package/dist/tools/safety.d.ts +1 -0
  39. package/dist/tools/safety.js +3 -0
  40. package/dist/tools/todoTool.d.ts +2 -0
  41. package/dist/tools/todoTool.js +29 -0
  42. package/dist/tools/toolDefsComputer.js +9 -0
  43. package/dist/tools/win/hostScript.js +7 -2
  44. package/dist/ui/comandos.js +2 -0
  45. package/dist/ui/marco.d.ts +1 -0
  46. package/dist/ui/marco.js +4 -2
  47. package/dist/ui/permissionPrompt.d.ts +9 -0
  48. package/dist/ui/permissionPrompt.js +30 -0
  49. package/dist/ui/pieFijo.d.ts +1 -1
  50. package/dist/ui/pieFijo.js +1 -1
  51. package/dist/ui/renderer.js +8 -0
  52. package/extension/background.js +80 -0
  53. package/extension/content.js +106 -0
  54. package/extension/manifest.json +61 -60
  55. package/package.json +3 -2
@@ -25,6 +25,7 @@ import { globalUndoManager } from "../agent/undoManager.js";
25
25
  import { validateFileSyntax } from "../agent/syntaxValidator.js";
26
26
  import { fileTracker } from "../agent/fileTracker.js";
27
27
  import { mcpManager } from "../mcp/manager.js";
28
+ import { taskManager } from "./backgroundTask.js";
28
29
  import fs from "node:fs";
29
30
  import path from "node:path";
30
31
  /**
@@ -82,10 +83,12 @@ export async function executeToolCall(name, args, cwd = process.cwd(), apiKey, m
82
83
  break;
83
84
  }
84
85
  case "save_memory":
85
- rawResult = memoryManager.saveMemory(args.topic, args.fact);
86
+ memoryManager.setCwd(cwd);
87
+ rawResult = memoryManager.saveMemory(args.topic, args.fact, args.scope);
86
88
  break;
87
89
  case "read_memory":
88
- rawResult = memoryManager.readMemory(args.topic);
90
+ memoryManager.setCwd(cwd);
91
+ rawResult = memoryManager.readMemory(args.topic, args.scope);
89
92
  break;
90
93
  case "spawn_agent": {
91
94
  if (!apiKey) {
@@ -201,6 +204,37 @@ export async function executeToolCall(name, args, cwd = process.cwd(), apiKey, m
201
204
  case "run_command":
202
205
  rawResult = await runCommand(args.command, cwd, args.timeout);
203
206
  break;
207
+ case "start_background_task": {
208
+ const res = await taskManager.startTask(args.command, args.cwd || cwd, args.description);
209
+ rawResult = `Tarea iniciada en segundo plano:\n- ID: ${res.taskId}\n- PID: ${res.pid}\n- Estado: ${res.status}\n- Log: ${res.logPath}`;
210
+ break;
211
+ }
212
+ case "read_task_output": {
213
+ const res = taskManager.readTaskOutput(args.taskId, args.lines, args.offset);
214
+ if (res.status === "not_found") {
215
+ rawResult = res.output;
216
+ }
217
+ else {
218
+ rawResult = `[Tarea ${res.taskId}] Estado: ${res.status} (Exit Code: ${res.exitCode !== null ? res.exitCode : "N/A"})\nTotal de líneas: ${res.totalLines}\n--- Salida ---\n${res.output || "(sin salida aún)"}`;
219
+ }
220
+ break;
221
+ }
222
+ case "list_background_tasks": {
223
+ const tasks = taskManager.listTasks();
224
+ if (tasks.length === 0) {
225
+ rawResult = "No hay tareas en segundo plano registradas.";
226
+ }
227
+ else {
228
+ const lines = tasks.map((t) => `- [${t.taskId}] PID ${t.pid} | ${t.status.toUpperCase()} | Ejecución: ${t.runtimeSeconds}s | Comando: "${t.command}"${t.description ? ` (${t.description})` : ""}${t.exitCode !== null ? ` | Exit code: ${t.exitCode}` : ""}`);
229
+ rawResult = `Tareas en segundo plano (${tasks.length}):\n${lines.join("\n")}`;
230
+ }
231
+ break;
232
+ }
233
+ case "stop_background_task": {
234
+ const res = await taskManager.stopTask(args.taskId, args.force);
235
+ rawResult = res.message;
236
+ break;
237
+ }
204
238
  case "list_dir":
205
239
  rawResult = await listDir(args.dirPath || ".", cwd);
206
240
  break;
@@ -7,6 +7,7 @@
7
7
  * estado. En cambio dos escrituras seguidas SI pueden pisarse, y el orden
8
8
  * importa, asi que esas van una detras de otra.
9
9
  */
10
+ export declare const READ_ONLY_TOOLS: Set<string>;
10
11
  export declare function isReadOnly(toolName: string): boolean;
11
12
  /**
12
13
  * Un lote solo se paraleliza si TODAS sus llamadas son de solo lectura y hay
@@ -18,7 +18,10 @@ const READ_ONLY = new Set([
18
18
  "web_search",
19
19
  "read_memory",
20
20
  "use_skill",
21
+ "read_task_output",
22
+ "list_background_tasks",
21
23
  ]);
24
+ export const READ_ONLY_TOOLS = READ_ONLY;
22
25
  export function isReadOnly(toolName) {
23
26
  return READ_ONLY.has(toolName);
24
27
  }
@@ -5,4 +5,6 @@ export interface TaskItem {
5
5
  }
6
6
  export declare function setSessionTasks(tasks: TaskItem[]): string;
7
7
  export declare function getSessionTasks(): TaskItem[];
8
+ export declare function renderTaskWidget(tasks?: TaskItem[]): string | null;
9
+ export declare function sincronizarTareasConPie(tasks?: TaskItem[]): void;
8
10
  export declare function renderTaskList(tasks: TaskItem[]): string;
@@ -1,12 +1,41 @@
1
1
  import chalk from "chalk";
2
+ import { fijarSeccion, quitarSeccion } from "../ui/pieFijo.js";
2
3
  let sessionTasks = [];
3
4
  export function setSessionTasks(tasks) {
4
5
  sessionTasks = tasks;
6
+ sincronizarTareasConPie(sessionTasks);
5
7
  return renderTaskList(sessionTasks);
6
8
  }
7
9
  export function getSessionTasks() {
8
10
  return sessionTasks;
9
11
  }
12
+ export function renderTaskWidget(tasks) {
13
+ const list = tasks !== undefined ? tasks : sessionTasks;
14
+ if (!list || list.length === 0)
15
+ return null;
16
+ const total = list.length;
17
+ const completed = list.filter((t) => t.status === "completed").length;
18
+ const inProgress = list.find((t) => t.status === "in_progress");
19
+ return (chalk.hex("#D97757")("⎿") +
20
+ " " +
21
+ chalk.green("✔") +
22
+ ` ${completed}/${total} tareas` +
23
+ (inProgress
24
+ ? chalk.gray(" · ") + chalk.hex("#D97757")("En curso: ") + chalk.bold.white(inProgress.text)
25
+ : completed === total
26
+ ? chalk.green(" · Completado")
27
+ : ""));
28
+ }
29
+ export function sincronizarTareasConPie(tasks) {
30
+ const list = tasks !== undefined ? tasks : sessionTasks;
31
+ const widget = renderTaskWidget(list);
32
+ if (widget) {
33
+ fijarSeccion("tareas", [widget]);
34
+ }
35
+ else {
36
+ quitarSeccion("tareas");
37
+ }
38
+ }
10
39
  export function renderTaskList(tasks) {
11
40
  if (tasks.length === 0)
12
41
  return "No hay tareas registradas.";
@@ -111,6 +111,8 @@ export const COMPUTER_USE_TOOL = {
111
111
  "scroll",
112
112
  "type",
113
113
  "key",
114
+ "hotkey",
115
+ "press",
114
116
  "wait",
115
117
  "wait_change",
116
118
  "cursor_position",
@@ -183,11 +185,14 @@ export const CHROME_TOOL = {
183
185
  "status", "tabs", "open", "select_tab", "navigate", "snapshot",
184
186
  "click", "type", "press", "get_text", "eval", "wait_for",
185
187
  "screenshot", "notice", "close_tab", "done",
188
+ "console_logs", "network_errors",
186
189
  ],
187
190
  description: "status: comprueba que la extension esta conectada. tabs: lista las pestanas del usuario. " +
188
191
  "open: abre una pestana nueva SIN quitarle la pantalla al usuario. select_tab: elige una ya abierta. " +
189
192
  "snapshot: LEE la pagina y numera los elementos. click/type: actuan sobre un ref. " +
190
193
  "wait_for: espera un texto o unos ms. screenshot: captura la pestana aunque no este visible y la describe. " +
194
+ "console_logs: obtiene errores y advertencias de consola y excepciones no controladas de la pagina. " +
195
+ "network_errors: obtiene peticiones de red HTTP fallidas (4xx/5xx o errores de conexion). " +
191
196
  "notice: cambia el texto del aviso flotante. close_tab: cierra una pestaña que abriste. done: cierra el trabajo y quita el aviso.",
192
197
  },
193
198
  url: { type: "string", description: "URL para open o navigate." },
@@ -212,6 +217,10 @@ export const CHROME_TOOL = {
212
217
  analyze: { type: "boolean", description: "false para no gastar tokens de vision." },
213
218
  outputPath: { type: "string", description: "Ruta donde guardar la captura." },
214
219
  note: { type: "string", description: "Texto inicial del aviso flotante al abrir la pestana." },
220
+ clear: {
221
+ type: "boolean",
222
+ description: "true para limpiar el buffer tras consultar (en console_logs o network_errors).",
223
+ },
215
224
  },
216
225
  required: ["action"],
217
226
  },
@@ -283,9 +283,14 @@ public static class Choco
283
283
  double scale = 1.0;
284
284
  int longEdge = Math.Max(w, h);
285
285
  if (maxLongEdge > 0 && longEdge > maxLongEdge) scale = (double)maxLongEdge / (double)longEdge;
286
+ ImageFormat fmt = ImageFormat.Png;
287
+ if (path.EndsWith(".jpg", StringComparison.OrdinalIgnoreCase) || path.EndsWith(".jpeg", StringComparison.OrdinalIgnoreCase))
288
+ {
289
+ fmt = ImageFormat.Jpeg;
290
+ }
286
291
  if (scale >= 0.999)
287
292
  {
288
- bmp.Save(path, ImageFormat.Png);
293
+ bmp.Save(path, fmt);
289
294
  return 1.0;
290
295
  }
291
296
  int nw = (int)Math.Round(w * scale), nh = (int)Math.Round(h * scale);
@@ -294,7 +299,7 @@ public static class Choco
294
299
  {
295
300
  g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
296
301
  g.DrawImage(bmp, 0, 0, nw, nh);
297
- dst.Save(path, ImageFormat.Png);
302
+ dst.Save(path, fmt);
298
303
  }
299
304
  return scale;
300
305
  }
@@ -35,6 +35,8 @@ export const SLASH_COMMANDS = [
35
35
  { name: "/goal", description: "Ejecutar meta compleja autónoma con auto-corrección", args: "<meta>" },
36
36
  { name: "/compact", description: "Comprimir contexto y optimizar memoria de la sesión" },
37
37
  { name: "/diff", description: "Ver diferencias de código (Git diff) del proyecto" },
38
+ { name: "/audit", description: "Auditar git diff en busca de secretos, logs residuales y sugerir commit", args: "[review]" },
39
+ { name: "/review", description: "Revisar cambios pendientes de git (alias de /audit)" },
38
40
  { name: "/commit", description: "Crear un commit en Git con mensaje semántico", args: "<mensaje>" },
39
41
  { name: "/cd", description: "Cambiar el directorio de trabajo activo", args: "<ruta>" },
40
42
  { name: "/uso", description: "Ver cuánto llevas consumido de tu cuenta" },
@@ -13,6 +13,7 @@ export interface PromptFrameOptions {
13
13
  cursorIndex?: number;
14
14
  items?: DropdownItem[];
15
15
  selectedIndex?: number;
16
+ taskWidget?: string;
16
17
  }
17
18
  export interface PromptFrame {
18
19
  /** Lineas LOGICAS. Cualquiera puede ocupar mas de una fila fisica. */
package/dist/ui/marco.js CHANGED
@@ -214,7 +214,9 @@ export function buildPromptFrame(opts) {
214
214
  // Dos, no una. Con una, la caja seguia leyendose pegada a lo de arriba: "pusiste
215
215
  // una linea de espacio, que sea bien pues". Con dos se separa de verdad, y es
216
216
  // el mismo aire que hay al arrancar entre el zorro y el aviso.
217
- const lines = ["", "", bar, inputLine, ...dropdownLines, bar, footerLine];
217
+ const widgetLines = opts.taskWidget && opts.taskWidget.trim().length > 0 ? [opts.taskWidget] : [];
218
+ const widgetRows = widgetLines.reduce((n, l) => n + physicalRows(l, cols), 0);
219
+ const lines = ["", "", ...widgetLines, bar, inputLine, ...dropdownLines, bar, footerLine];
218
220
  // 4. Donde queda el cursor, en filas fisicas. Antes se hacia cursorCol =
219
221
  // total % cols: aplicaba el modulo pero nunca sumaba la fila, asi que en
220
222
  // cuanto la entrada se partia el acento se iba a escribir a otra linea.
@@ -222,7 +224,7 @@ export function buildPromptFrame(opts) {
222
224
  const segments = typed.split("\n");
223
225
  // La fila de aire de arriba cuenta: sin sumarla, el cursor se dibuja una fila
224
226
  // por encima de donde se escribe.
225
- let cursorRow = 2 + physicalRows(bar, cols);
227
+ let cursorRow = 2 + widgetRows + physicalRows(bar, cols);
226
228
  for (let i = 0; i < segments.length - 1; i++) {
227
229
  cursorRow += Math.max(1, Math.ceil(((i === 0 ? symbolWidth : 0) + segments[i].length) / cols));
228
230
  }
@@ -26,3 +26,12 @@ export declare function buildPermissionBox(toolName: string, args: Record<string
26
26
  * tarea tal cual y no cuesta nada. Ver ui/ink/prestamo.ts.
27
27
  */
28
28
  export declare function askToolPermission(toolName: string, args: Record<string, any>, cwd?: string): Promise<PermissionPromptResult>;
29
+ /**
30
+ * El mismo contenido del dialogo del terminal, sin color y sin marco, para
31
+ * pintarlo en HTML.
32
+ *
33
+ * Se reutiliza `details()` a proposito: dos sitios decidiendo por separado que
34
+ * hay que enseñar antes de aprobar un borrado acaban discrepando, y el que
35
+ * discrepa siempre es el que menos se mira.
36
+ */
37
+ export declare function detallesEnTextoPlano(toolName: string, args: Record<string, any>, cwd?: string): string[];
@@ -4,6 +4,7 @@ import chalk from "chalk";
4
4
  import { cycleMode, describeMode, getMode, modeAnnouncement } from "./modes.js";
5
5
  import { isOutsideProject, isSvgSource } from "../config/permissions.js";
6
6
  import { conElTerminalPrestado } from "./ink/prestamo.js";
7
+ import { pedirPermisoWeb } from "../servidor/puente.js";
7
8
  /**
8
9
  * Aprobacion automatica: se activa con --yes / -y o con la variable de entorno
9
10
  * CHOCOLATITO_AUTO_APPROVE. Pensada para modo no interactivo y CI.
@@ -362,5 +363,34 @@ function details(toolName, args, cwd, width) {
362
363
  * tarea tal cual y no cuesta nada. Ver ui/ink/prestamo.ts.
363
364
  */
364
365
  export function askToolPermission(toolName, args, cwd = process.cwd()) {
366
+ // --yes primero: si ya se aprobo todo a mano, no hay que molestar a nadie ni
367
+ // abrir en el movil un dialogo que se contestaria solo.
368
+ if (autoApprove)
369
+ return Promise.resolve({ approved: true });
370
+ // Con `--servir` y alguien conectado, la pregunta va al navegador. Devuelve
371
+ // null cuando no hay ningun cliente, y entonces esto sigue como siempre: al
372
+ // terminal, que es donde hay un humano. Lo que NO pasa nunca es que se apruebe
373
+ // algo por no tener a quien preguntar.
374
+ const porWeb = pedirPermisoWeb(toolName, detallesEnTextoPlano(toolName, args, cwd), PERMISSION_OPTIONS);
375
+ if (porWeb)
376
+ return porWeb;
365
377
  return conElTerminalPrestado(() => askToolPermissionDirecto(toolName, args, cwd));
366
378
  }
379
+ /**
380
+ * El mismo contenido del dialogo del terminal, sin color y sin marco, para
381
+ * pintarlo en HTML.
382
+ *
383
+ * Se reutiliza `details()` a proposito: dos sitios decidiendo por separado que
384
+ * hay que enseñar antes de aprobar un borrado acaban discrepando, y el que
385
+ * discrepa siempre es el que menos se mira.
386
+ */
387
+ export function detallesEnTextoPlano(toolName, args, cwd = process.cwd()) {
388
+ const sinColor = (t) => t.replace(/\u001b\[[0-9;]*m/g, "");
389
+ const kind = classify(toolName, args);
390
+ return [
391
+ sinColor(kind.title).trim(),
392
+ ...details(toolName, args, cwd, 72)
393
+ .map((d) => sinColor(d.text).trim())
394
+ .filter((t) => t.length > 0),
395
+ ];
396
+ }
@@ -3,7 +3,7 @@
3
3
  * pintara donde le tocara, dos productores independientes -el razonamiento y el
4
4
  * spinner llegan por caminos distintos- se turnarian el sitio y el pie bailaria.
5
5
  */
6
- declare const ORDEN: readonly ["razonamiento", "spinner", "entrada"];
6
+ declare const ORDEN: readonly ["tareas", "razonamiento", "spinner", "entrada"];
7
7
  export type SeccionDelPie = (typeof ORDEN)[number];
8
8
  /**
9
9
  * Enciende o apaga el pie a mano. Con `null` vuelve a mandar el terminal.
@@ -54,7 +54,7 @@ const MOSTRAR_CURSOR = "\x1b[?25h";
54
54
  * pintara donde le tocara, dos productores independientes -el razonamiento y el
55
55
  * spinner llegan por caminos distintos- se turnarian el sitio y el pie bailaria.
56
56
  */
57
- const ORDEN = ["razonamiento", "spinner", "entrada"];
57
+ const ORDEN = ["tareas", "razonamiento", "spinner", "entrada"];
58
58
  const secciones = new Map();
59
59
  /**
60
60
  * Texto permanente que aun no ha recibido su salto de linea.
@@ -179,6 +179,14 @@ export function formatToolName(name) {
179
179
  return "Move";
180
180
  case "run_command":
181
181
  return "Bash";
182
+ case "start_background_task":
183
+ return "BgTask";
184
+ case "read_task_output":
185
+ return "TaskOutput";
186
+ case "list_background_tasks":
187
+ return "ListTasks";
188
+ case "stop_background_task":
189
+ return "StopTask";
182
190
  case "list_dir":
183
191
  return "ListDir";
184
192
  case "grep_search":
@@ -431,6 +431,68 @@ async function setTabOverlay(tabId, activo) {
431
431
  }
432
432
  }
433
433
 
434
+ // Telemetría de red: errores HTTP (>=400) y fallos de conexión por pestaña
435
+ const MAX_NETWORK_ERRORS = 100;
436
+ /** tabId -> Array<{ method: string, url: string, status?: number, error?: string, timestamp: number }> */
437
+ const networkErrorsByTab = new Map();
438
+
439
+ function registrarErrorRed(tabId, info) {
440
+ if (!tabId || tabId < 0) return;
441
+ let lista = networkErrorsByTab.get(tabId);
442
+ if (!lista) {
443
+ lista = [];
444
+ networkErrorsByTab.set(tabId, lista);
445
+ }
446
+ lista.push({
447
+ method: info.method || "GET",
448
+ url: info.url || "",
449
+ status: info.status !== undefined ? info.status : (info.statusCode !== undefined ? info.statusCode : undefined),
450
+ error: info.error || undefined,
451
+ timestamp: info.timestamp || Date.now(),
452
+ });
453
+ if (lista.length > MAX_NETWORK_ERRORS) {
454
+ lista.shift();
455
+ }
456
+ }
457
+
458
+ try {
459
+ if (typeof chrome !== "undefined" && chrome.webRequest && chrome.webRequest.onCompleted && typeof chrome.webRequest.onCompleted.addListener === "function") {
460
+ chrome.webRequest.onCompleted.addListener(
461
+ (details) => {
462
+ if (details && details.tabId > 0 && details.statusCode >= 400) {
463
+ registrarErrorRed(details.tabId, {
464
+ method: details.method,
465
+ url: details.url,
466
+ status: details.statusCode,
467
+ error: `HTTP ${details.statusCode}`,
468
+ timestamp: Math.round(details.timeStamp || Date.now()),
469
+ });
470
+ }
471
+ },
472
+ { urls: ["<all_urls>"] }
473
+ );
474
+ }
475
+ } catch (_) {}
476
+
477
+ try {
478
+ if (typeof chrome !== "undefined" && chrome.webRequest && chrome.webRequest.onErrorOccurred && typeof chrome.webRequest.onErrorOccurred.addListener === "function") {
479
+ chrome.webRequest.onErrorOccurred.addListener(
480
+ (details) => {
481
+ if (details && details.tabId > 0) {
482
+ registrarErrorRed(details.tabId, {
483
+ method: details.method,
484
+ url: details.url,
485
+ status: undefined,
486
+ error: details.error || "net::ERR_FAILED",
487
+ timestamp: Math.round(details.timeStamp || Date.now()),
488
+ });
489
+ }
490
+ },
491
+ { urls: ["<all_urls>"] }
492
+ );
493
+ }
494
+ } catch (_) {}
495
+
434
496
  // D8: Detección y reporte de descargas disparadas por acciones del agente
435
497
  const descargasRecientes = [];
436
498
  try {
@@ -1012,6 +1074,23 @@ async function handle(msg, st) {
1012
1074
  return res;
1013
1075
  }
1014
1076
 
1077
+ if (cmd === "getConsoleLogs") {
1078
+ const { tabId } = await readTab(msg.tabId, st);
1079
+ const res = await tell(tabId, 0, { cmd: "getConsoleLogs", clear: msg.clear === true });
1080
+ if (!res || !res.ok) throw new Error((res && res.error) || "no se pudieron obtener los logs de consola");
1081
+ return { ok: true, tabId, logs: res.logs || [] };
1082
+ }
1083
+
1084
+ if (cmd === "getNetworkErrors") {
1085
+ const { tabId } = await readTab(msg.tabId, st);
1086
+ const lista = networkErrorsByTab.get(tabId) || [];
1087
+ const errors = [...lista];
1088
+ if (msg.clear === true) {
1089
+ networkErrorsByTab.delete(tabId);
1090
+ }
1091
+ return { ok: true, tabId, errors };
1092
+ }
1093
+
1015
1094
  if (cmd === "eval") {
1016
1095
  // eval ejecuta JavaScript arbitrario en la pagina: puede pulsar, enviar
1017
1096
  // formularios y leerlo todo. Es actuacion, no lectura.
@@ -1138,6 +1217,7 @@ async function handle(msg, st) {
1138
1217
  try {
1139
1218
  chrome.tabs.onRemoved.addListener((tabId) => {
1140
1219
  setTabOverlay(tabId, false);
1220
+ networkErrorsByTab.delete(tabId);
1141
1221
  for (const link of links.values()) {
1142
1222
  if (!link.state) continue;
1143
1223
  link.state.propias.delete(tabId);
@@ -17,6 +17,103 @@
17
17
 
18
18
  const IS_TOP = window.top === window;
19
19
 
20
+ // ======================================================== telemetría de consola y errores de página
21
+ const MAX_CONSOLE_LOGS = 100;
22
+ const consoleLogsBuffer = [];
23
+
24
+ function addConsoleLog(level, message, stack, url) {
25
+ const entry = {
26
+ timestamp: Date.now(),
27
+ level: level === "warn" ? "warn" : "error",
28
+ message: String(message || ""),
29
+ };
30
+ if (stack) entry.stack = String(stack);
31
+ if (url) entry.url = String(url);
32
+
33
+ consoleLogsBuffer.push(entry);
34
+ if (consoleLogsBuffer.length > MAX_CONSOLE_LOGS) {
35
+ consoleLogsBuffer.shift();
36
+ }
37
+ }
38
+
39
+ try {
40
+ window.addEventListener("error", (event) => {
41
+ try {
42
+ const msg = event.message || (event.error && event.error.message) || String(event);
43
+ const stack = (event.error && event.error.stack) || undefined;
44
+ const url = event.filename || undefined;
45
+ addConsoleLog("error", msg, stack, url);
46
+ } catch (_) {}
47
+ });
48
+ } catch (_) {}
49
+
50
+ try {
51
+ window.addEventListener("unhandledrejection", (event) => {
52
+ try {
53
+ const reason = event.reason;
54
+ const msg =
55
+ (reason && (reason.message || (typeof reason === "object" ? JSON.stringify(reason) : String(reason)))) ||
56
+ "Unhandled Promise Rejection";
57
+ const stack = (reason && reason.stack) || undefined;
58
+ addConsoleLog("error", `Unhandled rejection: ${msg}`, stack);
59
+ } catch (_) {}
60
+ });
61
+ } catch (_) {}
62
+
63
+ try {
64
+ const originalConsoleError = console.error;
65
+ console.error = function (...args) {
66
+ try {
67
+ const msg = args
68
+ .map((a) => {
69
+ if (a instanceof Error) return a.message || String(a);
70
+ if (typeof a === "object" && a !== null) {
71
+ try {
72
+ return JSON.stringify(a);
73
+ } catch (_) {
74
+ return String(a);
75
+ }
76
+ }
77
+ return String(a);
78
+ })
79
+ .join(" ");
80
+ const errObj = args.find((a) => a instanceof Error);
81
+ const stack = errObj && errObj.stack ? errObj.stack : undefined;
82
+ addConsoleLog("error", msg, stack);
83
+ } catch (_) {}
84
+ if (typeof originalConsoleError === "function") {
85
+ originalConsoleError.apply(console, args);
86
+ }
87
+ };
88
+ } catch (_) {}
89
+
90
+ try {
91
+ const originalConsoleWarn = console.warn;
92
+ console.warn = function (...args) {
93
+ try {
94
+ const msg = args
95
+ .map((a) => {
96
+ if (a instanceof Error) return a.message || String(a);
97
+ if (typeof a === "object" && a !== null) {
98
+ try {
99
+ return JSON.stringify(a);
100
+ } catch (_) {
101
+ return String(a);
102
+ }
103
+ }
104
+ return String(a);
105
+ })
106
+ .join(" ");
107
+ const errObj = args.find((a) => a instanceof Error);
108
+ const stack = errObj && errObj.stack ? errObj.stack : undefined;
109
+ addConsoleLog("warn", msg, stack);
110
+ } catch (_) {}
111
+ if (typeof originalConsoleWarn === "function") {
112
+ originalConsoleWarn.apply(console, args);
113
+ }
114
+ };
115
+ } catch (_) {}
116
+
20
117
  // ======================================================== overlay visual y borde naranja (#D97757)
21
118
 
22
119
  let overlayHost = null;
@@ -916,6 +1013,15 @@
916
1013
  break;
917
1014
  }
918
1015
 
1016
+ case "getConsoleLogs": {
1017
+ const logs = [...consoleLogsBuffer];
1018
+ if (msg.clear === true) {
1019
+ consoleLogsBuffer.length = 0;
1020
+ }
1021
+ sendResponse({ ok: true, logs });
1022
+ break;
1023
+ }
1024
+
919
1025
  default:
920
1026
  sendResponse({ ok: false, error: "comando desconocido: " + (msg.cmd || msg.tipo) });
921
1027
  }