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,610 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import os from "node:os";
4
+ import http from "node:http";
5
+ import { spawn } from "node:child_process";
6
+ // puppeteer-core, NO puppeteer: el paquete completo descarga ~700 MB de Chromium
7
+ // al instalarse, y aqui no se usa nunca. Este modulo solo hace connect() contra
8
+ // un Chrome ya en marcha, y cuando hace falta lanzarlo arranca el chrome.exe que
9
+ // el usuario ya tiene instalado. Bajar un navegador entero para no abrirlo
10
+ // convertia "npm install -g chocolatito-code" en una espera de varios minutos.
11
+ import puppeteer from "puppeteer-core";
12
+ import { describeScreen } from "./visionBridge.js";
13
+ /**
14
+ * CONTROL DE NAVEGADOR EN SEGUNDO PLANO
15
+ *
16
+ * Por que existe esto y no basta con computer_use:
17
+ *
18
+ * Una pagina web no expone su contenido al arbol de accesibilidad de Windows de
19
+ * forma util, asi que manejar Chrome a golpe de clics de raton obliga a adivinar
20
+ * pixeles, a tener la ventana delante y a pelearse con el usuario por el foco.
21
+ *
22
+ * El protocolo DevTools (CDP) resuelve las tres cosas a la vez:
23
+ * - Input.dispatchMouseEvent entrega el clic directamente a la pestana, sin
24
+ * mover el raton real y sin que la ventana tenga que estar visible.
25
+ * - El DOM da el texto exacto de cada boton, asi que se hace clic por nombre y
26
+ * no por coordenada inventada.
27
+ * - Se conecta al Chrome del usuario, con sus cookies y su sesion de Google.
28
+ *
29
+ * Es el mismo enfoque de snapshot con referencias que usa Playwright MCP, y la
30
+ * razon por la que ese tipo de agente no falla eligiendo el boton equivocado.
31
+ */
32
+ let browser = null;
33
+ let page = null;
34
+ let launchedByUs = false;
35
+ const consoleLogs = [];
36
+ const netErrors = [];
37
+ /** ref -> frame donde vive el elemento, para poder resolverlo despues. */
38
+ let refFrames = new Map();
39
+ const DEFAULT_PORT = Number(process.env.CHOCOLATITO_CDP_PORT || 9222);
40
+ const AGENT_PROFILE = path.join(os.homedir(), ".chocolatito", "chrome-profile");
41
+ // ---------------------------------------------------------------- conexion
42
+ function httpJson(url, timeoutMs = 1500) {
43
+ return new Promise((resolve, reject) => {
44
+ const req = http.get(url, { timeout: timeoutMs }, (res) => {
45
+ let data = "";
46
+ res.on("data", (c) => (data += c));
47
+ res.on("end", () => {
48
+ try {
49
+ resolve(JSON.parse(data));
50
+ }
51
+ catch (e) {
52
+ reject(e);
53
+ }
54
+ });
55
+ });
56
+ req.on("timeout", () => {
57
+ req.destroy();
58
+ reject(new Error("timeout"));
59
+ });
60
+ req.on("error", reject);
61
+ });
62
+ }
63
+ function chromePath() {
64
+ const candidates = [
65
+ path.join(process.env["ProgramFiles"] || "C:/Program Files", "Google/Chrome/Application/chrome.exe"),
66
+ path.join(process.env["ProgramFiles(x86)"] || "C:/Program Files (x86)", "Google/Chrome/Application/chrome.exe"),
67
+ path.join(process.env["LOCALAPPDATA"] || "", "Google/Chrome/Application/chrome.exe"),
68
+ ];
69
+ for (const c of candidates) {
70
+ if (c && fs.existsSync(c))
71
+ return c;
72
+ }
73
+ return null;
74
+ }
75
+ function systemProfileDir() {
76
+ return path.join(process.env["LOCALAPPDATA"] || os.homedir(), "Google", "Chrome", "User Data");
77
+ }
78
+ async function connectTo(port) {
79
+ try {
80
+ const version = await httpJson(`http://127.0.0.1:${port}/json/version`);
81
+ if (!version?.webSocketDebuggerUrl)
82
+ return null;
83
+ return await puppeteer.connect({
84
+ browserWSEndpoint: version.webSocketDebuggerUrl,
85
+ defaultViewport: null,
86
+ });
87
+ }
88
+ catch {
89
+ return null;
90
+ }
91
+ }
92
+ async function ensureBrowser(params) {
93
+ if (browser && browser.connected)
94
+ return { ok: true };
95
+ const port = params.port || DEFAULT_PORT;
96
+ // 1. Si ya hay un Chrome escuchando el puerto de depuracion, nos enganchamos.
97
+ const existing = await connectTo(port);
98
+ if (existing) {
99
+ browser = existing;
100
+ launchedByUs = false;
101
+ await pickPage();
102
+ return { ok: true, note: `enganchado a Chrome existente en el puerto ${port}` };
103
+ }
104
+ // 2. Si no, lo lanzamos nosotros con el puerto abierto.
105
+ const exe = chromePath();
106
+ if (!exe)
107
+ return { ok: false, error: "No se encontro chrome.exe. Instala Google Chrome o indica otro puerto con un Chrome ya abierto." };
108
+ const useSystem = params.useSystemProfile === true;
109
+ const userDataDir = useSystem ? systemProfileDir() : AGENT_PROFILE;
110
+ if (useSystem) {
111
+ // Chrome solo abre el puerto de depuracion en el proceso que posee el
112
+ // user-data-dir. Si el usuario ya tiene Chrome abierto con ese perfil, el
113
+ // nuevo proceso delega en el viejo y muere sin puerto: hay que avisar en vez
114
+ // de dejar al agente esperando para siempre.
115
+ const running = await isChromeRunning();
116
+ if (running) {
117
+ return {
118
+ ok: false,
119
+ error: "Para usar tu perfil real de Chrome, ese Chrome tiene que haber arrancado con el puerto de depuracion. " +
120
+ "Ahora mismo hay un Chrome abierto sin el. Cierra Chrome por completo y repite la accion, " +
121
+ "o usa el perfil del agente (useSystemProfile: false) e inicia sesion en Google una sola vez ahi.",
122
+ };
123
+ }
124
+ }
125
+ if (!fs.existsSync(userDataDir))
126
+ fs.mkdirSync(userDataDir, { recursive: true });
127
+ const args = [
128
+ `--remote-debugging-port=${port}`,
129
+ `--user-data-dir=${userDataDir}`,
130
+ "--no-first-run",
131
+ "--no-default-browser-check",
132
+ "--disable-features=Translate",
133
+ // Sin esto Chrome congela temporizadores y renderizado cuando la ventana no
134
+ // esta delante, que es justo como la vamos a usar.
135
+ "--disable-background-timer-throttling",
136
+ "--disable-backgrounding-occluded-windows",
137
+ "--disable-renderer-backgrounding",
138
+ "--window-size=1440,900",
139
+ ];
140
+ if (params.profileDirectory)
141
+ args.push(`--profile-directory=${params.profileDirectory}`);
142
+ const child = spawn(exe, args, { detached: true, stdio: "ignore" });
143
+ child.unref();
144
+ // Esperamos a que el puerto responda.
145
+ for (let i = 0; i < 40; i++) {
146
+ await new Promise((r) => setTimeout(r, 250));
147
+ const b = await connectTo(port);
148
+ if (b) {
149
+ browser = b;
150
+ launchedByUs = true;
151
+ await pickPage();
152
+ return {
153
+ ok: true,
154
+ note: useSystem
155
+ ? `Chrome lanzado con tu perfil del sistema y depuracion en el puerto ${port}`
156
+ : `Chrome lanzado con el perfil del agente (${userDataDir}) en el puerto ${port}`,
157
+ };
158
+ }
159
+ }
160
+ return { ok: false, error: `Chrome se lanzo pero el puerto ${port} no respondio en 10s.` };
161
+ }
162
+ async function isChromeRunning() {
163
+ return new Promise((resolve) => {
164
+ const p = spawn("tasklist.exe", ["/FI", "IMAGENAME eq chrome.exe", "/NH"], { windowsHide: true });
165
+ let out = "";
166
+ p.stdout.on("data", (d) => (out += d.toString()));
167
+ p.on("close", () => resolve(out.toLowerCase().includes("chrome.exe")));
168
+ p.on("error", () => resolve(false));
169
+ });
170
+ }
171
+ async function pickPage() {
172
+ if (!browser)
173
+ return;
174
+ const pages = await browser.pages();
175
+ const visible = pages.filter((p) => !p.url().startsWith("devtools://"));
176
+ page = visible.length > 0 ? visible[visible.length - 1] : await browser.newPage();
177
+ attachListeners(page);
178
+ }
179
+ function attachListeners(p) {
180
+ if (p.__chocoWired)
181
+ return;
182
+ p.__chocoWired = true;
183
+ p.on("console", (m) => consoleLogs.push({ type: m.type(), text: m.text().slice(0, 500), time: new Date().toLocaleTimeString() }));
184
+ p.on("pageerror", (e) => consoleLogs.push({ type: "error", text: `[pageerror] ${e?.message || e}`, time: new Date().toLocaleTimeString() }));
185
+ p.on("requestfailed", (r) => {
186
+ netErrors.push(`[${r.method()}] ${r.url().slice(0, 160)} -> ${r.failure()?.errorText || "FAILED"}`);
187
+ if (netErrors.length > 40)
188
+ netErrors.shift();
189
+ });
190
+ }
191
+ // ---------------------------------------------------------------- snapshot
192
+ /**
193
+ * Recorre el DOM (incluidos los iframes accesibles) y devuelve los elementos
194
+ * accionables con su nombre visible. Cada uno queda marcado con data-choco-ref,
195
+ * asi que despues se puede pulsar por numero y no por coordenada.
196
+ */
197
+ const SNAPSHOT_FN = function (startIndex, filterText) {
198
+ const out = [];
199
+ let idx = startIndex;
200
+ const filter = (filterText || "").toLowerCase();
201
+ const nameOf = (el) => {
202
+ const aria = el.getAttribute("aria-label");
203
+ if (aria && aria.trim())
204
+ return aria.trim();
205
+ const labelledBy = el.getAttribute("aria-labelledby");
206
+ if (labelledBy) {
207
+ const l = document.getElementById(labelledBy);
208
+ if (l && l.textContent && l.textContent.trim())
209
+ return l.textContent.trim();
210
+ }
211
+ const anyEl = el;
212
+ if (anyEl.placeholder && anyEl.placeholder.trim())
213
+ return anyEl.placeholder.trim();
214
+ const title = el.getAttribute("title");
215
+ if (title && title.trim())
216
+ return title.trim();
217
+ if (el.tagName === "IMG") {
218
+ const alt = el.getAttribute("alt");
219
+ if (alt && alt.trim())
220
+ return alt.trim();
221
+ }
222
+ const txt = el.innerText || el.textContent || "";
223
+ return txt.replace(/\s+/g, " ").trim();
224
+ };
225
+ const roleOf = (el) => {
226
+ const r = el.getAttribute("role");
227
+ if (r)
228
+ return r;
229
+ const tag = el.tagName.toLowerCase();
230
+ if (tag === "a")
231
+ return "link";
232
+ if (tag === "button")
233
+ return "button";
234
+ if (tag === "textarea")
235
+ return "textbox";
236
+ if (tag === "select")
237
+ return "combobox";
238
+ if (tag === "input") {
239
+ const t = el.type || "text";
240
+ if (t === "checkbox")
241
+ return "checkbox";
242
+ if (t === "radio")
243
+ return "radio";
244
+ if (t === "submit" || t === "button")
245
+ return "button";
246
+ return "textbox";
247
+ }
248
+ if (el.isContentEditable)
249
+ return "textbox";
250
+ return tag;
251
+ };
252
+ const sel = 'a[href], button, input, textarea, select, summary, [role="button"], [role="link"], ' +
253
+ '[role="textbox"], [role="tab"], [role="menuitem"], [role="option"], [role="checkbox"], ' +
254
+ "[role=\"radio\"], [role=\"combobox\"], [contenteditable=\"true\"], [onclick], [tabindex]";
255
+ // Las apps modernas (Google entre ellas) esconden media interfaz dentro de
256
+ // shadow roots, que un querySelectorAll normal no atraviesa. Sin esto el
257
+ // snapshot sale medio vacio y el agente vuelve a quedarse a ciegas.
258
+ const roots = [document];
259
+ const nodes = [];
260
+ for (let i = 0; i < roots.length && i < 400; i++) {
261
+ const root = roots[i];
262
+ const all = root.querySelectorAll("*");
263
+ for (let j = 0; j < all.length; j++) {
264
+ const sr = all[j].shadowRoot;
265
+ if (sr)
266
+ roots.push(sr);
267
+ }
268
+ const matches = root.querySelectorAll(sel);
269
+ for (let j = 0; j < matches.length; j++)
270
+ nodes.push(matches[j]);
271
+ }
272
+ const seen = new Set();
273
+ for (const el of nodes) {
274
+ if (seen.has(el))
275
+ continue;
276
+ seen.add(el);
277
+ const rect = el.getBoundingClientRect();
278
+ if (rect.width < 2 || rect.height < 2)
279
+ continue;
280
+ const style = window.getComputedStyle(el);
281
+ if (style.visibility === "hidden" || style.display === "none" || Number(style.opacity) === 0)
282
+ continue;
283
+ // Fuera del viewport por arriba o por la izquierda: no es accionable ahora.
284
+ if (rect.bottom < 0 || rect.right < 0)
285
+ continue;
286
+ if (rect.top > window.innerHeight * 3)
287
+ continue;
288
+ const name = nameOf(el).slice(0, 120);
289
+ const role = roleOf(el);
290
+ const value = el.value ? String(el.value).slice(0, 80) : "";
291
+ if (!name && !value && role !== "textbox")
292
+ continue;
293
+ if (filter) {
294
+ const hay = (name + " " + value + " " + role).toLowerCase();
295
+ if (hay.indexOf(filter) < 0)
296
+ continue;
297
+ }
298
+ idx++;
299
+ el.setAttribute("data-choco-ref", String(idx));
300
+ const disabled = el.disabled === true || el.getAttribute("aria-disabled") === "true";
301
+ const inView = rect.top >= 0 && rect.top < window.innerHeight;
302
+ out.push({
303
+ ref: idx,
304
+ role,
305
+ name,
306
+ value,
307
+ x: Math.round(rect.left + rect.width / 2),
308
+ y: Math.round(rect.top + rect.height / 2),
309
+ disabled,
310
+ inView,
311
+ });
312
+ }
313
+ return out;
314
+ };
315
+ async function buildSnapshot(max, filter) {
316
+ refFrames = new Map();
317
+ const rows = [];
318
+ let counter = 0;
319
+ const frames = page.frames();
320
+ for (const frame of frames) {
321
+ if (rows.length >= max)
322
+ break;
323
+ try {
324
+ const items = (await frame.evaluate(SNAPSHOT_FN, counter, filter));
325
+ for (const it of items) {
326
+ refFrames.set(it.ref, frame);
327
+ rows.push(it);
328
+ }
329
+ counter += items.length;
330
+ }
331
+ catch {
332
+ // Un iframe de otro origen no se puede inspeccionar: se ignora sin romper.
333
+ }
334
+ }
335
+ const truncated = rows.length > max;
336
+ return {
337
+ title: await page.title(),
338
+ url: page.url(),
339
+ rows: rows.slice(0, max),
340
+ truncated,
341
+ };
342
+ }
343
+ async function handleFor(params) {
344
+ if (params.ref !== undefined) {
345
+ const frame = refFrames.get(params.ref);
346
+ if (!frame)
347
+ return { handle: null, error: `El ref ${params.ref} no existe. Vuelve a ejecutar snapshot.` };
348
+ // La busqueda tiene que atravesar shadow roots igual que el snapshot, o los
349
+ // elementos que si aparecen en la lista despues resultarian inalcanzables.
350
+ const jsHandle = await frame.evaluateHandle((ref) => {
351
+ const roots = [document];
352
+ for (let i = 0; i < roots.length && i < 400; i++) {
353
+ const found = roots[i].querySelector(`[data-choco-ref="${ref}"]`);
354
+ if (found)
355
+ return found;
356
+ const all = roots[i].querySelectorAll("*");
357
+ for (let j = 0; j < all.length; j++) {
358
+ const sr = all[j].shadowRoot;
359
+ if (sr)
360
+ roots.push(sr);
361
+ }
362
+ }
363
+ return null;
364
+ }, params.ref);
365
+ const el = jsHandle.asElement();
366
+ if (!el)
367
+ return { handle: null, error: `El ref ${params.ref} ya no esta en la pagina. Vuelve a ejecutar snapshot.` };
368
+ return { handle: el };
369
+ }
370
+ if (params.selector) {
371
+ const h = await page.$(params.selector);
372
+ if (!h)
373
+ return { handle: null, error: `No hay ningun elemento que cumpla "${params.selector}".` };
374
+ return { handle: h };
375
+ }
376
+ return { handle: null, error: 'indica "ref" (de snapshot) o "selector".' };
377
+ }
378
+ // ---------------------------------------------------------------- tool
379
+ export async function browserCdp(params, cwd = process.cwd(), apiKey) {
380
+ const { action } = params;
381
+ try {
382
+ if (action === "close") {
383
+ if (browser) {
384
+ // Solo cerramos el navegador si lo abrimos nosotros: si es el Chrome del
385
+ // usuario, nos limitamos a soltar la conexion.
386
+ if (launchedByUs)
387
+ await browser.close();
388
+ else
389
+ await browser.disconnect();
390
+ }
391
+ browser = null;
392
+ page = null;
393
+ return launchedByUs ? "Navegador del agente cerrado." : "Desconectado del Chrome del usuario (sigue abierto).";
394
+ }
395
+ const conn = await ensureBrowser(params);
396
+ if (!conn.ok)
397
+ return `Error de navegador: ${conn.error}`;
398
+ if (!page)
399
+ await pickPage();
400
+ if (!page)
401
+ return "Error de navegador: no hay ninguna pestana disponible.";
402
+ const note = conn.note ? `(${conn.note})\n` : "";
403
+ switch (action) {
404
+ case "attach":
405
+ return `${note}Conectado. Pestana activa: "${await page.title()}" -> ${page.url()}`;
406
+ case "tabs": {
407
+ const pages = await browser.pages();
408
+ const rows = await Promise.all(pages.map(async (p, i) => {
409
+ const mark = p === page ? "*" : " ";
410
+ let t = "";
411
+ try {
412
+ t = await p.title();
413
+ }
414
+ catch { }
415
+ return `${mark} [${i}] "${t}" -> ${p.url().slice(0, 120)}`;
416
+ }));
417
+ return `PESTANAS ABIERTAS (* = activa):\n${rows.join("\n")}\n\nCambia con select_tab y "match".`;
418
+ }
419
+ case "select_tab": {
420
+ const needle = (params.match || params.url || "").toLowerCase();
421
+ if (!needle)
422
+ return 'Error: indica un texto de busqueda en "match".';
423
+ const pages = await browser.pages();
424
+ for (const p of pages) {
425
+ let t = "";
426
+ try {
427
+ t = await p.title();
428
+ }
429
+ catch { }
430
+ if (p.url().toLowerCase().includes(needle) || t.toLowerCase().includes(needle)) {
431
+ page = p;
432
+ attachListeners(p);
433
+ return `Pestana activa ahora: "${t}" -> ${p.url()}`;
434
+ }
435
+ }
436
+ return `No hay ninguna pestana que contenga "${params.match}". Usa tabs para ver la lista.`;
437
+ }
438
+ case "new_tab": {
439
+ const p = await browser.newPage();
440
+ page = p;
441
+ attachListeners(p);
442
+ if (params.url)
443
+ await p.goto(params.url, { waitUntil: "domcontentloaded", timeout: params.timeout || 30_000 });
444
+ return `${note}Pestana nueva${params.url ? ` en ${params.url}` : ""}: "${await p.title()}"`;
445
+ }
446
+ case "navigate": {
447
+ if (!params.url)
448
+ return 'Error: falta "url".';
449
+ await page.goto(params.url, { waitUntil: "domcontentloaded", timeout: params.timeout || 30_000 });
450
+ return `${note}Navegado a ${page.url()} — "${await page.title()}"`;
451
+ }
452
+ case "snapshot": {
453
+ const snap = await buildSnapshot(params.max ?? 60, params.filter || "");
454
+ if (snap.rows.length === 0) {
455
+ return `Pagina "${snap.title}" (${snap.url}): no se detectaron elementos accionables${params.filter ? ` que contengan "${params.filter}"` : ""}. Prueba wait_for o quita el filtro.`;
456
+ }
457
+ const rows = snap.rows
458
+ .map((e) => {
459
+ const val = e.value ? ` = "${e.value}"` : "";
460
+ const dis = e.disabled ? " [deshabilitado]" : "";
461
+ const off = e.inView ? "" : " [hay que hacer scroll]";
462
+ return ` [${e.ref}] ${e.role} "${e.name}"${val}${dis}${off}`;
463
+ })
464
+ .join("\n");
465
+ return (`PAGINA: "${snap.title}"\nURL: ${snap.url}\n${snap.rows.length} elementos accionables` +
466
+ `${snap.truncated ? " (truncado; usa filter)" : ""}\n\n${rows}\n\n` +
467
+ `Actua con click / type usando el numero entre corchetes.`);
468
+ }
469
+ case "click": {
470
+ const { handle, error } = await handleFor(params);
471
+ if (!handle)
472
+ return `Error: ${error}`;
473
+ await handle.scrollIntoView().catch(() => { });
474
+ const label = await handle.evaluate((el) => (el.innerText || el.value || el.getAttribute("aria-label") || "").trim().slice(0, 60));
475
+ // handle.click() manda el evento por CDP: no mueve el raton del usuario
476
+ // ni necesita que la ventana este delante.
477
+ await handle.click();
478
+ await page.waitForNetworkIdle({ idleTime: 400, timeout: 4000 }).catch(() => { });
479
+ return `Clic en "${label || params.selector || `ref ${params.ref}`}". Pagina ahora: "${await page.title()}" (${page.url()})`;
480
+ }
481
+ case "type": {
482
+ const { handle, error } = await handleFor(params);
483
+ if (!handle)
484
+ return `Error: ${error}`;
485
+ if (params.text === undefined)
486
+ return 'Error: falta "text".';
487
+ await handle.scrollIntoView().catch(() => { });
488
+ // Triple clic para seleccionar lo que ya hubiera escrito y sustituirlo.
489
+ await handle.click({ count: 3 }).catch(() => { });
490
+ await handle.evaluate((el) => {
491
+ if ("value" in el)
492
+ el.value = "";
493
+ else if (el.isContentEditable)
494
+ el.textContent = "";
495
+ });
496
+ await handle.type(params.text, { delay: 12 });
497
+ if (params.submit) {
498
+ await page.keyboard.press("Enter");
499
+ await page.waitForNetworkIdle({ idleTime: 500, timeout: 6000 }).catch(() => { });
500
+ }
501
+ return `Escrito "${params.text.length > 60 ? params.text.slice(0, 57) + "..." : params.text}"${params.submit ? " y enviado con Enter" : ""}.`;
502
+ }
503
+ case "press": {
504
+ const key = params.keys || params.text;
505
+ if (!key)
506
+ return 'Error: falta "keys" (ej. "Enter", "Escape", "Control+A").';
507
+ const parts = key.split("+");
508
+ const main = parts.pop();
509
+ for (const m of parts)
510
+ await page.keyboard.down(m);
511
+ await page.keyboard.press(main);
512
+ for (const m of parts.reverse())
513
+ await page.keyboard.up(m);
514
+ return `Tecla "${key}" enviada a la pagina.`;
515
+ }
516
+ case "select_option": {
517
+ const { handle, error } = await handleFor(params);
518
+ if (!handle)
519
+ return `Error: ${error}`;
520
+ if (params.value === undefined)
521
+ return 'Error: falta "value".';
522
+ const sel = await handle.evaluate((el, v) => {
523
+ if (el.tagName !== "SELECT")
524
+ return false;
525
+ el.value = v;
526
+ el.dispatchEvent(new Event("change", { bubbles: true }));
527
+ return true;
528
+ }, params.value);
529
+ return sel ? `Opcion "${params.value}" seleccionada.` : "El elemento no es un <select>.";
530
+ }
531
+ case "scroll": {
532
+ const amount = (params.amount ?? 3) * 300 * (params.direction === "up" ? -1 : 1);
533
+ await page.evaluate((d) => window.scrollBy(0, d), amount);
534
+ await new Promise((r) => setTimeout(r, 250));
535
+ return `Scroll ${params.direction || "down"} aplicado en la pagina.`;
536
+ }
537
+ case "wait_for": {
538
+ const timeout = params.timeout || 15_000;
539
+ if (params.selector) {
540
+ await page.waitForSelector(params.selector, { timeout });
541
+ return `Apareció "${params.selector}".`;
542
+ }
543
+ if (params.text) {
544
+ const needle = params.text;
545
+ await page.waitForFunction((t) => document.body.innerText.includes(t), { timeout }, needle);
546
+ return `Aparecio el texto "${needle}" en la pagina.`;
547
+ }
548
+ const ms = params.ms || 2000;
549
+ await new Promise((r) => setTimeout(r, ms));
550
+ return `Espera de ${ms}ms completada.`;
551
+ }
552
+ case "get_text": {
553
+ const txt = await page.evaluate(() => document.body.innerText);
554
+ const clipped = txt.replace(/\n{3,}/g, "\n\n").slice(0, 6000);
555
+ return `TEXTO DE "${await page.title()}" (${page.url()}):\n\n${clipped}`;
556
+ }
557
+ case "eval": {
558
+ if (!params.script)
559
+ return 'Error: falta "script".';
560
+ const r = await page.evaluate((code) => {
561
+ try {
562
+ // eslint-disable-next-line no-eval
563
+ const out = eval(code);
564
+ return typeof out === "object" ? JSON.stringify(out) : String(out);
565
+ }
566
+ catch (e) {
567
+ return `Error JS: ${e?.message || e}`;
568
+ }
569
+ }, params.script);
570
+ return `Resultado:\n${r}`;
571
+ }
572
+ case "screenshot": {
573
+ const dir = path.join(os.homedir(), ".chocolatito", "cache");
574
+ if (!fs.existsSync(dir))
575
+ fs.mkdirSync(dir, { recursive: true });
576
+ const out = params.outputPath
577
+ ? path.isAbsolute(params.outputPath)
578
+ ? params.outputPath
579
+ : path.resolve(cwd, params.outputPath)
580
+ : path.join(dir, "browser_latest.png");
581
+ const outDir = path.dirname(out);
582
+ if (!fs.existsSync(outDir))
583
+ fs.mkdirSync(outDir, { recursive: true });
584
+ await page.screenshot({ path: out });
585
+ const kb = (fs.statSync(out).size / 1024).toFixed(1);
586
+ const meta = `Captura de "${await page.title()}" guardada en ${out} (${kb} KB).`;
587
+ if (params.analyze === false || !apiKey)
588
+ return meta;
589
+ const desc = await describeScreen(apiKey, out, params.question);
590
+ if (!desc.ok)
591
+ return `${meta}\nNo se pudo analizar: ${desc.error}`;
592
+ return `${meta}\n\nLO QUE SE VE:\n${desc.text}`;
593
+ }
594
+ case "console": {
595
+ const logs = consoleLogs.slice(-25);
596
+ const errs = netErrors.slice(-10);
597
+ if (logs.length === 0 && errs.length === 0)
598
+ return "Consola limpia: sin logs ni errores de red.";
599
+ const l = logs.map((x) => `[${x.time}] [${x.type.toUpperCase()}] ${x.text}`).join("\n");
600
+ const e = errs.length ? `\n\nERRORES DE RED:\n${errs.join("\n")}` : "";
601
+ return `CONSOLA DEL NAVEGADOR:\n${l}${e}`;
602
+ }
603
+ default:
604
+ return `Accion de navegador no reconocida: ${action}`;
605
+ }
606
+ }
607
+ catch (err) {
608
+ return `Error de navegador (${action}): ${err?.message || String(err)}`;
609
+ }
610
+ }
@@ -0,0 +1,58 @@
1
+ declare class ExtensionBridge {
2
+ private wss;
3
+ private client;
4
+ private nextId;
5
+ private pending;
6
+ private starting;
7
+ private portTaken;
8
+ /** Origen de la extension con la que se emparejo esta sesion. */
9
+ private pairedOrigin;
10
+ /** Ultima extension distinta que intento conectarse; se le avisa al usuario. */
11
+ private rejectedOrigin;
12
+ private port;
13
+ private start;
14
+ private listenOn;
15
+ get activePort(): number;
16
+ connected(): Promise<boolean>;
17
+ private everConnected;
18
+ /**
19
+ * Espera a que la extension aparezca.
20
+ *
21
+ * El service worker de MV3 se duerme cuando Chrome lleva un rato sin usarlo, y
22
+ * tarda unos segundos en despertar y reconectar. Si se pregunta con prisa, se
23
+ * concluye que "no esta instalada" cuando en realidad solo estaba dormida. Por
24
+ * eso la primera espera es generosa y las siguientes, cortas.
25
+ */
26
+ waitForClient(ms?: number): Promise<boolean>;
27
+ private waitUpTo;
28
+ get heldByAnotherProcess(): boolean;
29
+ get pairing(): {
30
+ origin: string | null;
31
+ rejected: string | null;
32
+ };
33
+ send(cmd: string, args?: Record<string, any>, timeoutMs?: number): Promise<any>;
34
+ shutdown(): void;
35
+ }
36
+ export declare const extensionBridge: ExtensionBridge;
37
+ export interface ChromeParams {
38
+ action: "status" | "tabs" | "open" | "select_tab" | "navigate" | "snapshot" | "click" | "type" | "press" | "get_text" | "eval" | "wait_for" | "screenshot" | "notice" | "close_tab" | "done";
39
+ url?: string;
40
+ tabId?: number;
41
+ ref?: number;
42
+ text?: string;
43
+ keys?: string;
44
+ match?: string;
45
+ filter?: string;
46
+ max?: number;
47
+ script?: string;
48
+ ms?: number;
49
+ timeout?: number;
50
+ submit?: boolean;
51
+ replace?: boolean;
52
+ question?: string;
53
+ analyze?: boolean;
54
+ outputPath?: string;
55
+ note?: string;
56
+ }
57
+ export declare function chromeExtension(params: ChromeParams, cwd?: string, apiKey?: string): Promise<string>;
58
+ export {};