chocolatito-code 1.0.0

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 (140) hide show
  1. package/.mcp.json.ejemplo +19 -0
  2. package/COMPUTER-USE.md +484 -0
  3. package/LICENSE +21 -0
  4. package/README.md +186 -0
  5. package/dist/agent/context.d.ts +9 -0
  6. package/dist/agent/context.js +123 -0
  7. package/dist/agent/fileTracker.d.ts +16 -0
  8. package/dist/agent/fileTracker.js +53 -0
  9. package/dist/agent/loop.d.ts +25 -0
  10. package/dist/agent/loop.js +524 -0
  11. package/dist/agent/mentions.d.ts +11 -0
  12. package/dist/agent/mentions.js +43 -0
  13. package/dist/agent/retry.d.ts +17 -0
  14. package/dist/agent/retry.js +57 -0
  15. package/dist/agent/subagent.d.ts +2 -0
  16. package/dist/agent/subagent.js +116 -0
  17. package/dist/agent/syntaxValidator.d.ts +5 -0
  18. package/dist/agent/syntaxValidator.js +71 -0
  19. package/dist/agent/tracker.d.ts +19 -0
  20. package/dist/agent/tracker.js +38 -0
  21. package/dist/agent/undoManager.d.ts +37 -0
  22. package/dist/agent/undoManager.js +95 -0
  23. package/dist/config/constants.d.ts +77 -0
  24. package/dist/config/constants.js +146 -0
  25. package/dist/config/engine.d.ts +49 -0
  26. package/dist/config/engine.js +86 -0
  27. package/dist/config/env.d.ts +13 -0
  28. package/dist/config/env.js +103 -0
  29. package/dist/config/license.d.ts +39 -0
  30. package/dist/config/license.js +306 -0
  31. package/dist/config/permissions.d.ts +38 -0
  32. package/dist/config/permissions.js +239 -0
  33. package/dist/config/quota.d.ts +58 -0
  34. package/dist/config/quota.js +156 -0
  35. package/dist/hooks/manager.d.ts +60 -0
  36. package/dist/hooks/manager.js +193 -0
  37. package/dist/index.d.ts +2 -0
  38. package/dist/index.js +470 -0
  39. package/dist/mcp/client.d.ts +55 -0
  40. package/dist/mcp/client.js +282 -0
  41. package/dist/mcp/manager.d.ts +26 -0
  42. package/dist/mcp/manager.js +167 -0
  43. package/dist/memory/manager.d.ts +9 -0
  44. package/dist/memory/manager.js +59 -0
  45. package/dist/prompts/systemPrompt.d.ts +5 -0
  46. package/dist/prompts/systemPrompt.js +177 -0
  47. package/dist/sessions/manager.d.ts +21 -0
  48. package/dist/sessions/manager.js +54 -0
  49. package/dist/skills/manager.d.ts +14 -0
  50. package/dist/skills/manager.js +86 -0
  51. package/dist/tools/browserCdp.d.ts +26 -0
  52. package/dist/tools/browserCdp.js +610 -0
  53. package/dist/tools/browserExtension.d.ts +58 -0
  54. package/dist/tools/browserExtension.js +479 -0
  55. package/dist/tools/browserLive.d.ts +12 -0
  56. package/dist/tools/browserLive.js +265 -0
  57. package/dist/tools/changeDir.d.ts +5 -0
  58. package/dist/tools/changeDir.js +63 -0
  59. package/dist/tools/computerUse.d.ts +58 -0
  60. package/dist/tools/computerUse.js +567 -0
  61. package/dist/tools/createImage.d.ts +1 -0
  62. package/dist/tools/createImage.js +34 -0
  63. package/dist/tools/definitions.d.ts +8 -0
  64. package/dist/tools/definitions.js +401 -0
  65. package/dist/tools/deleteFile.d.ts +1 -0
  66. package/dist/tools/deleteFile.js +17 -0
  67. package/dist/tools/editFile.d.ts +5 -0
  68. package/dist/tools/editFile.js +88 -0
  69. package/dist/tools/findFiles.d.ts +1 -0
  70. package/dist/tools/findFiles.js +33 -0
  71. package/dist/tools/getSystemInfo.d.ts +1 -0
  72. package/dist/tools/getSystemInfo.js +23 -0
  73. package/dist/tools/gitTools.d.ts +3 -0
  74. package/dist/tools/gitTools.js +47 -0
  75. package/dist/tools/grepSearch.d.ts +1 -0
  76. package/dist/tools/grepSearch.js +37 -0
  77. package/dist/tools/listDir.d.ts +1 -0
  78. package/dist/tools/listDir.js +33 -0
  79. package/dist/tools/moveCopyFile.d.ts +1 -0
  80. package/dist/tools/moveCopyFile.js +21 -0
  81. package/dist/tools/runCommand.d.ts +1 -0
  82. package/dist/tools/runCommand.js +61 -0
  83. package/dist/tools/runner.d.ts +5 -0
  84. package/dist/tools/runner.js +177 -0
  85. package/dist/tools/safety.d.ts +17 -0
  86. package/dist/tools/safety.js +33 -0
  87. package/dist/tools/todoTool.d.ts +8 -0
  88. package/dist/tools/todoTool.js +27 -0
  89. package/dist/tools/toolDefsComputer.d.ts +13 -0
  90. package/dist/tools/toolDefsComputer.js +205 -0
  91. package/dist/tools/truncator.d.ts +11 -0
  92. package/dist/tools/truncator.js +24 -0
  93. package/dist/tools/viewFile.d.ts +1 -0
  94. package/dist/tools/viewFile.js +45 -0
  95. package/dist/tools/visionBridge.d.ts +37 -0
  96. package/dist/tools/visionBridge.js +107 -0
  97. package/dist/tools/webFetch.d.ts +1 -0
  98. package/dist/tools/webFetch.js +25 -0
  99. package/dist/tools/webSearch.d.ts +1 -0
  100. package/dist/tools/webSearch.js +35 -0
  101. package/dist/tools/win/hostClient.d.ts +31 -0
  102. package/dist/tools/win/hostClient.js +158 -0
  103. package/dist/tools/win/hostScript.d.ts +1 -0
  104. package/dist/tools/win/hostScript.js +925 -0
  105. package/dist/tools/writeFile.d.ts +1 -0
  106. package/dist/tools/writeFile.js +13 -0
  107. package/dist/ui/banner.d.ts +10 -0
  108. package/dist/ui/banner.js +165 -0
  109. package/dist/ui/diffViewer.d.ts +1 -0
  110. package/dist/ui/diffViewer.js +60 -0
  111. package/dist/ui/interrupt.d.ts +17 -0
  112. package/dist/ui/interrupt.js +156 -0
  113. package/dist/ui/markdownStream.d.ts +23 -0
  114. package/dist/ui/markdownStream.js +0 -0
  115. package/dist/ui/modes.d.ts +28 -0
  116. package/dist/ui/modes.js +57 -0
  117. package/dist/ui/permissionPrompt.d.ts +35 -0
  118. package/dist/ui/permissionPrompt.js +271 -0
  119. package/dist/ui/prompt.d.ts +7 -0
  120. package/dist/ui/prompt.js +466 -0
  121. package/dist/ui/reasoningStream.d.ts +6 -0
  122. package/dist/ui/reasoningStream.js +38 -0
  123. package/dist/ui/renderer.d.ts +10 -0
  124. package/dist/ui/renderer.js +142 -0
  125. package/dist/ui/spinner.d.ts +24 -0
  126. package/dist/ui/spinner.js +111 -0
  127. package/dist/ui/theme.d.ts +36 -0
  128. package/dist/ui/theme.js +37 -0
  129. package/dist/ui/typeAhead.d.ts +31 -0
  130. package/dist/ui/typeAhead.js +87 -0
  131. package/extension/README.md +74 -0
  132. package/extension/background.js +575 -0
  133. package/extension/content.js +446 -0
  134. package/extension/manifest.json +36 -0
  135. package/extension/popup.html +22 -0
  136. package/extension/popup.js +12 -0
  137. package/package.json +66 -0
  138. package/skills/complex-refactor/SKILL.md +13 -0
  139. package/skills/deep-debugger/SKILL.md +14 -0
  140. package/skills/software-architect/SKILL.md +20 -0
@@ -0,0 +1,479 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import os from "node:os";
4
+ import { WebSocketServer, WebSocket } from "ws";
5
+ import { describeScreen } from "./visionBridge.js";
6
+ /**
7
+ * PUENTE CON LA EXTENSION DE CHROME
8
+ *
9
+ * Es la unica forma de trabajar de verdad en segundo plano dentro del Chrome del
10
+ * usuario, con sus sesiones ya iniciadas.
11
+ *
12
+ * Por que no valen las otras vias:
13
+ * - CDP por puerto de depuracion: desde Chrome 136 Google lo bloquea en el
14
+ * perfil por defecto, asi que solo sirve con un perfil vacio y sin sesiones.
15
+ * - Automatizacion del sistema (UI Automation + raton y teclado reales): para
16
+ * escribir hay que traer la ventana al primer plano, lo que interrumpe al
17
+ * usuario. Si esta jugando, directamente le saca del juego.
18
+ *
19
+ * La extension corre dentro de Chrome: usa la sesion real, ejecuta en pestanas
20
+ * que no estan visibles y no toca el foco ni el raton. El usuario puede seguir
21
+ * jugando mientras el agente trabaja.
22
+ */
23
+ // La extension se conecta a todo este rango, asi que varias sesiones de
24
+ // Chocolatito pueden convivir: cada una se queda con el primer puerto libre.
25
+ const PORT_RANGE = [8787, 8788, 8789, 8790];
26
+ const FORCED_PORT = process.env.CHOCOLATITO_EXT_PORT ? Number(process.env.CHOCOLATITO_EXT_PORT) : null;
27
+ const PORTS = FORCED_PORT ? [FORCED_PORT] : PORT_RANGE;
28
+ const CONNECT_HINT = "La extension de Chrome no esta conectada. Cargala una vez en chrome://extensions " +
29
+ '(activa "Modo de desarrollador" y pulsa "Cargar descomprimida" apuntando a la carpeta ' +
30
+ "'extension' del proyecto). Si ya la cargaste, abre Chrome y espera unos segundos.";
31
+ class ExtensionBridge {
32
+ wss = null;
33
+ client = null;
34
+ nextId = 1;
35
+ pending = new Map();
36
+ starting = null;
37
+ portTaken = false;
38
+ /** Origen de la extension con la que se emparejo esta sesion. */
39
+ pairedOrigin = null;
40
+ /** Ultima extension distinta que intento conectarse; se le avisa al usuario. */
41
+ rejectedOrigin = null;
42
+ port = 0;
43
+ start() {
44
+ if (this.starting)
45
+ return this.starting;
46
+ this.starting = (async () => {
47
+ for (const port of PORTS) {
48
+ const ok = await this.listenOn(port);
49
+ if (ok) {
50
+ this.port = port;
51
+ this.portTaken = false;
52
+ return;
53
+ }
54
+ }
55
+ // Todos ocupados: hay tantas sesiones como puertos.
56
+ this.portTaken = true;
57
+ })();
58
+ return this.starting;
59
+ }
60
+ listenOn(port) {
61
+ return new Promise((resolve) => {
62
+ let server;
63
+ try {
64
+ // Solo en la interfaz local: nada de exponer esto a la red.
65
+ server = new WebSocketServer({ host: "127.0.0.1", port });
66
+ }
67
+ catch {
68
+ resolve(false);
69
+ return;
70
+ }
71
+ server.on("listening", () => {
72
+ this.wss = server;
73
+ resolve(true);
74
+ });
75
+ server.on("error", () => {
76
+ // Puerto ocupado por otra sesion: se prueba el siguiente.
77
+ try {
78
+ server.close();
79
+ }
80
+ catch { }
81
+ resolve(false);
82
+ });
83
+ server.on("connection", (ws, req) => {
84
+ // Una pagina web cualquiera tambien puede abrir un WebSocket a
85
+ // localhost. Solo se acepta a la extension, que se identifica con un
86
+ // origen chrome-extension://
87
+ const origin = String(req.headers.origin || "");
88
+ if (!origin.startsWith("chrome-extension://")) {
89
+ ws.close(1008, "origen no permitido");
90
+ return;
91
+ }
92
+ // El puente conduce el Chrome del usuario con sus sesiones iniciadas, y
93
+ // CUALQUIER extension instalada puede abrir este socket. La primera que
94
+ // se empareja se queda con el puente durante toda la sesion: asi otra
95
+ // extension no puede colarse despues y recibir ordenes pensadas para
96
+ // esta (que incluyen leer el contenido de sus pestanas).
97
+ if (this.pairedOrigin && origin !== this.pairedOrigin) {
98
+ this.rejectedOrigin = origin;
99
+ ws.close(1008, "el puente ya esta emparejado con otra extension");
100
+ return;
101
+ }
102
+ this.pairedOrigin = origin;
103
+ this.client = ws;
104
+ ws.on("message", (raw) => {
105
+ let msg;
106
+ try {
107
+ msg = JSON.parse(raw.toString());
108
+ }
109
+ catch {
110
+ return;
111
+ }
112
+ if (msg.type !== "result" || typeof msg.id !== "number")
113
+ return;
114
+ const waiter = this.pending.get(msg.id);
115
+ if (!waiter)
116
+ return;
117
+ clearTimeout(waiter.timer);
118
+ this.pending.delete(msg.id);
119
+ waiter.resolve(msg);
120
+ });
121
+ ws.on("close", () => {
122
+ if (this.client === ws)
123
+ this.client = null;
124
+ });
125
+ ws.on("error", () => {
126
+ if (this.client === ws)
127
+ this.client = null;
128
+ });
129
+ });
130
+ });
131
+ }
132
+ get activePort() {
133
+ return this.port;
134
+ }
135
+ async connected() {
136
+ try {
137
+ await this.start();
138
+ }
139
+ catch {
140
+ return false;
141
+ }
142
+ return !!(this.client && this.client.readyState === WebSocket.OPEN);
143
+ }
144
+ everConnected = false;
145
+ /**
146
+ * Espera a que la extension aparezca.
147
+ *
148
+ * El service worker de MV3 se duerme cuando Chrome lleva un rato sin usarlo, y
149
+ * tarda unos segundos en despertar y reconectar. Si se pregunta con prisa, se
150
+ * concluye que "no esta instalada" cuando en realidad solo estaba dormida. Por
151
+ * eso la primera espera es generosa y las siguientes, cortas.
152
+ */
153
+ async waitForClient(ms) {
154
+ const budget = ms ?? (this.everConnected ? 4000 : 12_000);
155
+ return this.waitUpTo(budget);
156
+ }
157
+ async waitUpTo(ms) {
158
+ const deadline = Date.now() + ms;
159
+ while (Date.now() < deadline) {
160
+ if (await this.connected()) {
161
+ this.everConnected = true;
162
+ return true;
163
+ }
164
+ await new Promise((r) => setTimeout(r, 250));
165
+ }
166
+ const last = await this.connected();
167
+ if (last)
168
+ this.everConnected = true;
169
+ return last;
170
+ }
171
+ get heldByAnotherProcess() {
172
+ return this.portTaken;
173
+ }
174
+ get pairing() {
175
+ return { origin: this.pairedOrigin, rejected: this.rejectedOrigin };
176
+ }
177
+ async send(cmd, args = {}, timeoutMs = 45_000) {
178
+ if (!(await this.waitForClient())) {
179
+ if (this.portTaken) {
180
+ return {
181
+ ok: false,
182
+ error: `Otra instancia de Chocolatito Code ya tiene el puente con la extension (puertos ${PORTS.join(", ")}). ` +
183
+ `La extension solo puede hablar con una a la vez: usa esa sesion, cierrala, ` +
184
+ `o arranca esta con otro puerto (CHOCOLATITO_EXT_PORT).`,
185
+ };
186
+ }
187
+ return { ok: false, error: CONNECT_HINT };
188
+ }
189
+ const id = this.nextId++;
190
+ const payload = JSON.stringify({ id, cmd, ...args });
191
+ return new Promise((resolve) => {
192
+ const timer = setTimeout(() => {
193
+ this.pending.delete(id);
194
+ resolve({ ok: false, error: `La accion "${cmd}" supero el limite de ${timeoutMs}ms.` });
195
+ }, timeoutMs);
196
+ this.pending.set(id, { resolve, timer });
197
+ try {
198
+ this.client.send(payload);
199
+ }
200
+ catch (err) {
201
+ clearTimeout(timer);
202
+ this.pending.delete(id);
203
+ resolve({ ok: false, error: `No se pudo hablar con la extension: ${err?.message || err}` });
204
+ }
205
+ });
206
+ }
207
+ shutdown() {
208
+ try {
209
+ this.client?.close();
210
+ this.wss?.close();
211
+ }
212
+ catch { }
213
+ }
214
+ }
215
+ export const extensionBridge = new ExtensionBridge();
216
+ process.on("exit", () => extensionBridge.shutdown());
217
+ /**
218
+ * Ejecuta una accion sobre un ref y, si el ref se ha quedado obsoleto porque la
219
+ * pagina se re-renderizo, rehace el snapshot y lo intenta una vez mas.
220
+ *
221
+ * Las aplicaciones vivas (Flow re-renderiza su composer cada pocos segundos)
222
+ * invalidan los refs constantemente. Devolver "ref no encontrado" y quedarse
223
+ * ahi obliga al modelo a repetir el snapshot a mano en cada paso; hacerlo aqui
224
+ * es lo que convierte la herramienta en algo usable.
225
+ */
226
+ async function withRefRetry(cmd, args, snapshotFilter) {
227
+ let res = await extensionBridge.send(cmd, args);
228
+ const obsoleto = !res.ok && /ref .*no (existe|encontrado)|vuelve a hacer snapshot/i.test(String(res.error || ""));
229
+ if (!obsoleto)
230
+ return res;
231
+ const fresh = await extensionBridge.send("snapshot", { filter: snapshotFilter || "", max: 120 });
232
+ if (!fresh.ok)
233
+ return res;
234
+ return await extensionBridge.send(cmd, args);
235
+ }
236
+ function fail(action, msg) {
237
+ return `Error en chrome (${action}): ${msg}`;
238
+ }
239
+ export async function chromeExtension(params, cwd = process.cwd(), apiKey) {
240
+ const { action } = params;
241
+ try {
242
+ switch (action) {
243
+ case "status": {
244
+ const on = await extensionBridge.waitForClient();
245
+ if (!on) {
246
+ if (extensionBridge.heldByAnotherProcess) {
247
+ return (`El puente esta ocupado por otra instancia de Chocolatito Code (puertos ${PORT_RANGE.join(", ")}).\n` +
248
+ `La extension solo habla con una sesion a la vez. Cierra la otra o cambia CHOCOLATITO_EXT_PORT.`);
249
+ }
250
+ return `Extension NO conectada.\n${CONNECT_HINT}`;
251
+ }
252
+ const res = await extensionBridge.send("ping");
253
+ if (!res.ok)
254
+ return fail(action, res.error);
255
+ const { origin, rejected } = extensionBridge.pairing;
256
+ const aviso = rejected
257
+ ? `\nAviso de seguridad: otra extension (${rejected}) intento conectarse al puente y fue rechazada. ` +
258
+ `Si no la reconoces, revisala en chrome://extensions.`
259
+ : "";
260
+ return (`Extension conectada (v${res.data.version}), emparejada con ${origin}. ` +
261
+ `Puedo trabajar en tus pestanas en segundo plano, sin robarte el foco.${aviso}`);
262
+ }
263
+ case "tabs": {
264
+ const res = await extensionBridge.send("tabs");
265
+ if (!res.ok)
266
+ return fail(action, res.error);
267
+ const rows = res.data.tabs
268
+ .map((t) => {
269
+ const mark = t.id === res.data.working ? "*" : " ";
270
+ return `${mark} [${t.id}] "${t.title}" -> ${String(t.url).slice(0, 110)}`;
271
+ })
272
+ .join("\n");
273
+ return `PESTANAS DE TU CHROME (* = donde estoy trabajando):\n${rows}\n\nUsa select_tab con "match", u "open" para una nueva.`;
274
+ }
275
+ case "open": {
276
+ if (!params.url)
277
+ return fail(action, 'falta "url".');
278
+ const res = await extensionBridge.send("open", { url: params.url, note: params.note }, 60_000);
279
+ if (!res.ok)
280
+ return fail(action, res.error);
281
+ return (`Pestana abierta EN SEGUNDO PLANO (no te cambio de pantalla): "${res.data.title}"\n` +
282
+ `URL: ${res.data.url} · tabId ${res.data.tabId}\n` +
283
+ `Esta agrupada en el grupo naranja "Chocolatito Code" y muestra un aviso en la pagina.`);
284
+ }
285
+ case "select_tab": {
286
+ const res = await extensionBridge.send("select", { match: params.match || params.url || "" });
287
+ if (!res.ok)
288
+ return fail(action, res.error);
289
+ return `Trabajando ahora en "${res.data.title}" (tabId ${res.data.tabId}). Agrupada y marcada con el aviso.`;
290
+ }
291
+ case "navigate": {
292
+ if (!params.url)
293
+ return fail(action, 'falta "url".');
294
+ const res = await extensionBridge.send("navigate", { url: params.url, tabId: params.tabId }, 60_000);
295
+ if (!res.ok)
296
+ return fail(action, res.error);
297
+ return `Navegado a ${res.data.url} — "${res.data.title}" (tabId ${res.data.tabId}).`;
298
+ }
299
+ case "snapshot": {
300
+ const res = await extensionBridge.send("snapshot", {
301
+ tabId: params.tabId,
302
+ filter: params.filter || "",
303
+ max: params.max ?? 60,
304
+ });
305
+ if (!res.ok)
306
+ return fail(action, res.error);
307
+ const els = res.data.elements || [];
308
+ if (els.length === 0) {
309
+ return (`Pagina "${res.data.title}": sin elementos accionables` +
310
+ `${params.filter ? ` que contengan "${params.filter}"` : ""}. ` +
311
+ `Prueba wait_for, quita el filtro, o mira la pagina con screenshot.`);
312
+ }
313
+ const rows = els
314
+ .map((e) => {
315
+ const val = e.value ? ` = "${e.value}"` : "";
316
+ const dis = e.disabled ? " [deshabilitado]" : "";
317
+ const off = e.inView ? "" : " [fuera de vista]";
318
+ return ` [${e.ref}] ${e.role} "${e.name}"${val}${dis}${off}`;
319
+ })
320
+ .join("\n");
321
+ return (`PAGINA: "${res.data.title}"\nURL: ${res.data.url} · tabId ${res.data.tabId}\n` +
322
+ `${els.length} elementos${res.data.truncated ? " (truncado; usa filter)" : ""}\n\n${rows}\n\n` +
323
+ `Actua con click / type usando el numero entre corchetes.`);
324
+ }
325
+ case "click": {
326
+ if (params.ref === undefined)
327
+ return fail(action, 'indica el "ref" del snapshot.');
328
+ const res = await withRefRetry("click", { ref: params.ref }, params.filter);
329
+ if (!res.ok)
330
+ return fail(action, res.error);
331
+ return `Pulsado [${params.ref}] "${res.data.label || ""}". Pagina ahora: "${res.data.title}" (${res.data.url})`;
332
+ }
333
+ case "type": {
334
+ if (params.ref === undefined)
335
+ return fail(action, 'indica el "ref" del snapshot.');
336
+ if (params.text === undefined)
337
+ return fail(action, 'falta "text".');
338
+ // Se intenta dos veces. Un editor rico recien montado (Lexical, el que
339
+ // usa Flow) descarta la entrada durante su hidratacion: el campo queda
340
+ // vacio y la aplicacion responde "se debe proporcionar una instruccion".
341
+ // Verificar y reintentar una vez es lo que convierte un fallo
342
+ // intermitente en una accion fiable.
343
+ let res = null;
344
+ let got = "";
345
+ const want = params.text;
346
+ const quiere = want.slice(0, Math.min(30, want.length));
347
+ for (let intento = 1; intento <= 2; intento++) {
348
+ res = await withRefRetry("type", {
349
+ ref: params.ref,
350
+ text: params.text,
351
+ submit: params.submit === true,
352
+ replace: params.replace !== false,
353
+ }, params.filter);
354
+ if (!res.ok)
355
+ return fail(action, res.error);
356
+ got = String(res.data.value || "");
357
+ if (got.includes(quiere))
358
+ break;
359
+ if (intento === 1) {
360
+ // Se le da tiempo al editor a terminar de montarse y se reintenta.
361
+ await new Promise((r) => setTimeout(r, 1200));
362
+ }
363
+ }
364
+ // Escribir sin verificar es como se acaba pulsando "enviar" sobre un
365
+ // campo vacio.
366
+ const landed = got.includes(quiere);
367
+ const corto = want.length > 60 ? want.slice(0, 57) + "..." : want;
368
+ const via = res.data.via === "cdp"
369
+ ? " (teclado real por CDP)"
370
+ : res.data.via === "dom"
371
+ ? " (por DOM: el depurador no estaba disponible)"
372
+ : "";
373
+ if (!landed) {
374
+ // Se dice POR QUE via se intento y que fallo: sin eso, depurar esto es
375
+ // adivinar. Fue exactamente lo que costo encontrar el problema.
376
+ const detalle = res.data.warning ? ` Motivo: ${res.data.warning}` : "";
377
+ return (`Aviso: se escribio en [${params.ref}]${via} pero el campo contiene "${got.slice(0, 60)}".${detalle} ` +
378
+ `Vuelve a hacer snapshot y comprueba antes de enviar.`);
379
+ }
380
+ // Que el texto este en el campo NO garantiza que la aplicacion lo haya
381
+ // registrado: en un editor rico el boton de enviar puede seguir apagado.
382
+ // Se mira el estado real del boton antes de dar el paso por bueno.
383
+ const check = await extensionBridge.send("snapshot", { max: 80 });
384
+ let botones = "";
385
+ if (check.ok) {
386
+ const enviar = (check.data.elements || []).filter((e) => /crear|enviar|generar|submit|send/i.test(e.name || ""));
387
+ const apagados = enviar.filter((e) => e.disabled);
388
+ if (enviar.length > 0 && apagados.length === enviar.length) {
389
+ return (`Escrito en [${params.ref}]${via}: "${corto}"\n` +
390
+ `PERO el boton de enviar sigue deshabilitado (${apagados
391
+ .map((e) => `"${e.name}"`)
392
+ .join(", ")}). La aplicacion no ha registrado el texto. ` +
393
+ `Haz snapshot y revisa si falta seleccionar algo (modo, modelo) antes de enviar.`);
394
+ }
395
+ if (enviar.length > 0) {
396
+ botones = ` El boton de enviar esta activo: ${enviar.map((e) => `[${e.ref}] "${e.name}"`).join(", ")}.`;
397
+ }
398
+ }
399
+ return (`Escrito en [${params.ref}]${via} y verificado en el campo: "${corto}"` +
400
+ `${params.submit ? " (enviado con Enter)" : ""}.${botones}`);
401
+ }
402
+ case "press": {
403
+ const res = await extensionBridge.send("press", { ref: params.ref, keys: params.keys || "Enter" });
404
+ if (!res.ok)
405
+ return fail(action, res.error);
406
+ return `Tecla "${params.keys || "Enter"}" enviada a la pagina.`;
407
+ }
408
+ case "get_text": {
409
+ const res = await extensionBridge.send("getText", { tabId: params.tabId });
410
+ if (!res.ok)
411
+ return fail(action, res.error);
412
+ return `TEXTO DE "${res.data.title}" (${res.data.url}):\n\n${res.data.text}`;
413
+ }
414
+ case "eval": {
415
+ if (!params.script)
416
+ return fail(action, 'falta "script".');
417
+ const res = await extensionBridge.send("eval", { tabId: params.tabId, script: params.script });
418
+ if (!res.ok)
419
+ return fail(action, res.error);
420
+ return `Resultado:\n${res.data.result}`;
421
+ }
422
+ case "wait_for": {
423
+ const res = await extensionBridge.send("waitFor", { tabId: params.tabId, text: params.text, ms: params.ms, timeout: params.timeout || 20_000 }, (params.timeout || 20_000) + 5_000);
424
+ if (!res.ok)
425
+ return fail(action, res.error);
426
+ if (params.text) {
427
+ return res.data.found
428
+ ? `Aparecio el texto "${params.text}".`
429
+ : `No aparecio "${params.text}" en el tiempo dado. Haz snapshot para ver como esta la pagina.`;
430
+ }
431
+ return `Espera de ${res.data.waited}ms completada.`;
432
+ }
433
+ case "screenshot": {
434
+ const res = await extensionBridge.send("screenshot", { tabId: params.tabId }, 60_000);
435
+ if (!res.ok)
436
+ return fail(action, res.error);
437
+ const dir = path.join(os.homedir(), ".chocolatito", "cache");
438
+ if (!fs.existsSync(dir))
439
+ fs.mkdirSync(dir, { recursive: true });
440
+ const out = params.outputPath
441
+ ? path.isAbsolute(params.outputPath)
442
+ ? params.outputPath
443
+ : path.resolve(cwd, params.outputPath)
444
+ : path.join(dir, "chrome_tab.png");
445
+ const outDir = path.dirname(out);
446
+ if (!fs.existsSync(outDir))
447
+ fs.mkdirSync(outDir, { recursive: true });
448
+ fs.writeFileSync(out, Buffer.from(res.data.data, "base64"));
449
+ const kb = (fs.statSync(out).size / 1024).toFixed(1);
450
+ const meta = `Captura de la pestana (en segundo plano) guardada en ${out} (${kb} KB).`;
451
+ if (params.analyze === false || !apiKey)
452
+ return meta;
453
+ const desc = await describeScreen(apiKey, out, params.question);
454
+ if (!desc.ok)
455
+ return `${meta}\nNo se pudo analizar: ${desc.error}`;
456
+ return `${meta}\n\nLO QUE SE VE:\n${desc.text}`;
457
+ }
458
+ case "close_tab": {
459
+ const res = await extensionBridge.send("close_tab", { tabId: params.tabId });
460
+ return res.ok ? `Cerrada${res.data.closed > 1 ? "s" : ""} ${res.data.closed} pestaña(s).` : fail(action, res.error);
461
+ }
462
+ case "notice": {
463
+ const res = await extensionBridge.send("banner", { tabId: params.tabId, text: params.text || "" });
464
+ return res.ok ? `Aviso actualizado en la pagina: "${params.text}"` : fail(action, res.error);
465
+ }
466
+ case "done": {
467
+ const res = await extensionBridge.send("ungroup", {});
468
+ return res.ok
469
+ ? "Trabajo cerrado: pestana desagrupada y aviso retirado."
470
+ : fail(action, res.error);
471
+ }
472
+ default:
473
+ return fail(String(action), "accion no reconocida.");
474
+ }
475
+ }
476
+ catch (err) {
477
+ return fail(String(action), err?.message || String(err));
478
+ }
479
+ }
@@ -0,0 +1,12 @@
1
+ export interface BrowserLiveParams {
2
+ action: "launch" | "navigate" | "click" | "type" | "screenshot" | "evaluate" | "get_console_logs" | "get_content" | "get_interactive_elements" | "open_system" | "close";
3
+ url?: string;
4
+ selector?: string;
5
+ text?: string;
6
+ script?: string;
7
+ outputPath?: string;
8
+ headless?: boolean;
9
+ profileDirectory?: string;
10
+ userDataDir?: string;
11
+ }
12
+ export declare function browserLive(params: BrowserLiveParams, cwd?: string): Promise<string>;