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.
- package/.mcp.json.ejemplo +19 -0
- package/COMPUTER-USE.md +484 -0
- package/LICENSE +21 -0
- package/README.md +186 -0
- package/dist/agent/context.d.ts +9 -0
- package/dist/agent/context.js +123 -0
- package/dist/agent/fileTracker.d.ts +16 -0
- package/dist/agent/fileTracker.js +53 -0
- package/dist/agent/loop.d.ts +25 -0
- package/dist/agent/loop.js +524 -0
- package/dist/agent/mentions.d.ts +11 -0
- package/dist/agent/mentions.js +43 -0
- package/dist/agent/retry.d.ts +17 -0
- package/dist/agent/retry.js +57 -0
- package/dist/agent/subagent.d.ts +2 -0
- package/dist/agent/subagent.js +116 -0
- package/dist/agent/syntaxValidator.d.ts +5 -0
- package/dist/agent/syntaxValidator.js +71 -0
- package/dist/agent/tracker.d.ts +19 -0
- package/dist/agent/tracker.js +38 -0
- package/dist/agent/undoManager.d.ts +37 -0
- package/dist/agent/undoManager.js +95 -0
- package/dist/config/constants.d.ts +77 -0
- package/dist/config/constants.js +146 -0
- package/dist/config/engine.d.ts +49 -0
- package/dist/config/engine.js +86 -0
- package/dist/config/env.d.ts +13 -0
- package/dist/config/env.js +103 -0
- package/dist/config/license.d.ts +39 -0
- package/dist/config/license.js +306 -0
- package/dist/config/permissions.d.ts +38 -0
- package/dist/config/permissions.js +239 -0
- package/dist/config/quota.d.ts +58 -0
- package/dist/config/quota.js +156 -0
- package/dist/hooks/manager.d.ts +60 -0
- package/dist/hooks/manager.js +193 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +470 -0
- package/dist/mcp/client.d.ts +55 -0
- package/dist/mcp/client.js +282 -0
- package/dist/mcp/manager.d.ts +26 -0
- package/dist/mcp/manager.js +167 -0
- package/dist/memory/manager.d.ts +9 -0
- package/dist/memory/manager.js +59 -0
- package/dist/prompts/systemPrompt.d.ts +5 -0
- package/dist/prompts/systemPrompt.js +177 -0
- package/dist/sessions/manager.d.ts +21 -0
- package/dist/sessions/manager.js +54 -0
- package/dist/skills/manager.d.ts +14 -0
- package/dist/skills/manager.js +86 -0
- package/dist/tools/browserCdp.d.ts +26 -0
- package/dist/tools/browserCdp.js +610 -0
- package/dist/tools/browserExtension.d.ts +58 -0
- package/dist/tools/browserExtension.js +479 -0
- package/dist/tools/browserLive.d.ts +12 -0
- package/dist/tools/browserLive.js +265 -0
- package/dist/tools/changeDir.d.ts +5 -0
- package/dist/tools/changeDir.js +63 -0
- package/dist/tools/computerUse.d.ts +58 -0
- package/dist/tools/computerUse.js +567 -0
- package/dist/tools/createImage.d.ts +1 -0
- package/dist/tools/createImage.js +34 -0
- package/dist/tools/definitions.d.ts +8 -0
- package/dist/tools/definitions.js +401 -0
- package/dist/tools/deleteFile.d.ts +1 -0
- package/dist/tools/deleteFile.js +17 -0
- package/dist/tools/editFile.d.ts +5 -0
- package/dist/tools/editFile.js +88 -0
- package/dist/tools/findFiles.d.ts +1 -0
- package/dist/tools/findFiles.js +33 -0
- package/dist/tools/getSystemInfo.d.ts +1 -0
- package/dist/tools/getSystemInfo.js +23 -0
- package/dist/tools/gitTools.d.ts +3 -0
- package/dist/tools/gitTools.js +47 -0
- package/dist/tools/grepSearch.d.ts +1 -0
- package/dist/tools/grepSearch.js +37 -0
- package/dist/tools/listDir.d.ts +1 -0
- package/dist/tools/listDir.js +33 -0
- package/dist/tools/moveCopyFile.d.ts +1 -0
- package/dist/tools/moveCopyFile.js +21 -0
- package/dist/tools/runCommand.d.ts +1 -0
- package/dist/tools/runCommand.js +61 -0
- package/dist/tools/runner.d.ts +5 -0
- package/dist/tools/runner.js +177 -0
- package/dist/tools/safety.d.ts +17 -0
- package/dist/tools/safety.js +33 -0
- package/dist/tools/todoTool.d.ts +8 -0
- package/dist/tools/todoTool.js +27 -0
- package/dist/tools/toolDefsComputer.d.ts +13 -0
- package/dist/tools/toolDefsComputer.js +205 -0
- package/dist/tools/truncator.d.ts +11 -0
- package/dist/tools/truncator.js +24 -0
- package/dist/tools/viewFile.d.ts +1 -0
- package/dist/tools/viewFile.js +45 -0
- package/dist/tools/visionBridge.d.ts +37 -0
- package/dist/tools/visionBridge.js +107 -0
- package/dist/tools/webFetch.d.ts +1 -0
- package/dist/tools/webFetch.js +25 -0
- package/dist/tools/webSearch.d.ts +1 -0
- package/dist/tools/webSearch.js +35 -0
- package/dist/tools/win/hostClient.d.ts +31 -0
- package/dist/tools/win/hostClient.js +158 -0
- package/dist/tools/win/hostScript.d.ts +1 -0
- package/dist/tools/win/hostScript.js +925 -0
- package/dist/tools/writeFile.d.ts +1 -0
- package/dist/tools/writeFile.js +13 -0
- package/dist/ui/banner.d.ts +10 -0
- package/dist/ui/banner.js +165 -0
- package/dist/ui/diffViewer.d.ts +1 -0
- package/dist/ui/diffViewer.js +60 -0
- package/dist/ui/interrupt.d.ts +17 -0
- package/dist/ui/interrupt.js +156 -0
- package/dist/ui/markdownStream.d.ts +23 -0
- package/dist/ui/markdownStream.js +0 -0
- package/dist/ui/modes.d.ts +28 -0
- package/dist/ui/modes.js +57 -0
- package/dist/ui/permissionPrompt.d.ts +35 -0
- package/dist/ui/permissionPrompt.js +271 -0
- package/dist/ui/prompt.d.ts +7 -0
- package/dist/ui/prompt.js +466 -0
- package/dist/ui/reasoningStream.d.ts +6 -0
- package/dist/ui/reasoningStream.js +38 -0
- package/dist/ui/renderer.d.ts +10 -0
- package/dist/ui/renderer.js +142 -0
- package/dist/ui/spinner.d.ts +24 -0
- package/dist/ui/spinner.js +111 -0
- package/dist/ui/theme.d.ts +36 -0
- package/dist/ui/theme.js +37 -0
- package/dist/ui/typeAhead.d.ts +31 -0
- package/dist/ui/typeAhead.js +87 -0
- package/extension/README.md +74 -0
- package/extension/background.js +575 -0
- package/extension/content.js +446 -0
- package/extension/manifest.json +36 -0
- package/extension/popup.html +22 -0
- package/extension/popup.js +12 -0
- package/package.json +66 -0
- package/skills/complex-refactor/SKILL.md +13 -0
- package/skills/deep-debugger/SKILL.md +14 -0
- package/skills/software-architect/SKILL.md +20 -0
|
@@ -0,0 +1,575 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Service worker de Chocolatito Code.
|
|
3
|
+
*
|
|
4
|
+
* Hace de puente entre la CLI (que escucha en un WebSocket local) y las
|
|
5
|
+
* pestanas del usuario. Todo lo que ejecuta va por la API de extensiones, asi
|
|
6
|
+
* que NO necesita foco, ni raton, ni que Chrome este delante: el usuario puede
|
|
7
|
+
* estar jugando a pantalla completa mientras el agente trabaja.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// Varios puertos a proposito: el puente solo lo puede tener un proceso por
|
|
11
|
+
// puerto, asi que con un unico puerto una segunda sesion de Chocolatito se
|
|
12
|
+
// quedaba sin extension. Conectando a un rango, cada sesion tiene el suyo y
|
|
13
|
+
// todas pueden trabajar.
|
|
14
|
+
const WS_PORTS = [8787, 8788, 8789, 8790];
|
|
15
|
+
const GROUP_TITLE = "Chocolatito Code";
|
|
16
|
+
|
|
17
|
+
/** puerto -> { socket, delay, timer } */
|
|
18
|
+
const links = new Map();
|
|
19
|
+
|
|
20
|
+
/** ref -> { tabId, frameId }, para resolver despues los clics. */
|
|
21
|
+
const refIndex = new Map();
|
|
22
|
+
let workingTabId = null;
|
|
23
|
+
let groupId = null;
|
|
24
|
+
|
|
25
|
+
// ============================================================ conexion
|
|
26
|
+
|
|
27
|
+
function connect(port) {
|
|
28
|
+
let ws;
|
|
29
|
+
try {
|
|
30
|
+
ws = new WebSocket("ws://127.0.0.1:" + port);
|
|
31
|
+
} catch (_) {
|
|
32
|
+
scheduleReconnect(port);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const link = links.get(port) || { delay: 1000 };
|
|
37
|
+
link.socket = ws;
|
|
38
|
+
links.set(port, link);
|
|
39
|
+
|
|
40
|
+
ws.onopen = () => {
|
|
41
|
+
link.delay = 1000;
|
|
42
|
+
sendTo(ws, { type: "hello", agent: "chocolatito-extension", version: "1.1.0", port });
|
|
43
|
+
refreshBadge();
|
|
44
|
+
// El service worker de MV3 se duerme al quedar inactivo. Un ping periodico
|
|
45
|
+
// mantiene viva la conexion y con ella el indice de referencias.
|
|
46
|
+
clearInterval(link.timer);
|
|
47
|
+
link.timer = setInterval(() => sendTo(ws, { type: "keepalive" }), 20000);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
ws.onmessage = async (ev) => {
|
|
51
|
+
let msg;
|
|
52
|
+
try {
|
|
53
|
+
msg = JSON.parse(ev.data);
|
|
54
|
+
} catch (_) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (!msg || typeof msg.id !== "number") return;
|
|
58
|
+
try {
|
|
59
|
+
const data = await handle(msg);
|
|
60
|
+
sendTo(ws, { type: "result", id: msg.id, ok: true, data });
|
|
61
|
+
} catch (err) {
|
|
62
|
+
sendTo(ws, { type: "result", id: msg.id, ok: false, error: String((err && err.message) || err) });
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
ws.onclose = () => {
|
|
67
|
+
clearInterval(link.timer);
|
|
68
|
+
if (link.socket === ws) link.socket = null;
|
|
69
|
+
refreshBadge();
|
|
70
|
+
scheduleReconnect(port);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
ws.onerror = () => {
|
|
74
|
+
try { ws.close(); } catch (_) {}
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function scheduleReconnect(port) {
|
|
79
|
+
const link = links.get(port) || { delay: 1000 };
|
|
80
|
+
links.set(port, link);
|
|
81
|
+
setTimeout(() => connect(port), link.delay);
|
|
82
|
+
// Tope bajo a proposito: el servidor es local y reintentar cuesta nada.
|
|
83
|
+
link.delay = Math.min((link.delay || 1000) * 1.5, 3000);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function sendTo(ws, obj) {
|
|
87
|
+
if (ws && ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify(obj));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function activeCount() {
|
|
91
|
+
let n = 0;
|
|
92
|
+
for (const link of links.values()) {
|
|
93
|
+
if (link.socket && link.socket.readyState === WebSocket.OPEN) n++;
|
|
94
|
+
}
|
|
95
|
+
return n;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function refreshBadge() {
|
|
99
|
+
const n = activeCount();
|
|
100
|
+
setBadge(n > 0 ? String(n) : "");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function setBadge(text) {
|
|
104
|
+
try {
|
|
105
|
+
chrome.action.setBadgeText({ text: text || "" });
|
|
106
|
+
chrome.action.setBadgeBackgroundColor({ color: "#D97757" });
|
|
107
|
+
} catch (_) {}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ============================================================ utilidades
|
|
111
|
+
|
|
112
|
+
/** Manda un mensaje al content script, inyectandolo si aun no estaba. */
|
|
113
|
+
async function tell(tabId, frameId, msg) {
|
|
114
|
+
const opts = frameId === undefined || frameId === null ? {} : { frameId };
|
|
115
|
+
try {
|
|
116
|
+
return await chrome.tabs.sendMessage(tabId, msg, opts);
|
|
117
|
+
} catch (_) {
|
|
118
|
+
// Pagina cargada antes de instalar la extension: se inyecta al vuelo.
|
|
119
|
+
await chrome.scripting.executeScript({
|
|
120
|
+
target: frameId ? { tabId, frameIds: [frameId] } : { tabId, allFrames: true },
|
|
121
|
+
files: ["content.js"],
|
|
122
|
+
});
|
|
123
|
+
return await chrome.tabs.sendMessage(tabId, msg, opts);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function framesOf(tabId) {
|
|
128
|
+
try {
|
|
129
|
+
const frames = await chrome.webNavigation.getAllFrames({ tabId });
|
|
130
|
+
return (frames || []).map((f) => f.frameId);
|
|
131
|
+
} catch (_) {
|
|
132
|
+
return [0];
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function resolveTab(tabId) {
|
|
137
|
+
if (tabId) return tabId;
|
|
138
|
+
if (workingTabId) {
|
|
139
|
+
try {
|
|
140
|
+
await chrome.tabs.get(workingTabId);
|
|
141
|
+
return workingTabId;
|
|
142
|
+
} catch (_) {
|
|
143
|
+
workingTabId = null;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const [active] = await chrome.tabs.query({ active: true, lastFocusedWindow: true });
|
|
147
|
+
if (!active) throw new Error("no hay ninguna pestana disponible");
|
|
148
|
+
return active.id;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Mete la pestana en un grupo visible llamado "Chocolatito Code", para que el
|
|
153
|
+
* usuario vea de un vistazo donde esta trabajando el agente.
|
|
154
|
+
*/
|
|
155
|
+
async function groupTab(tabId) {
|
|
156
|
+
try {
|
|
157
|
+
if (groupId !== null) {
|
|
158
|
+
try {
|
|
159
|
+
await chrome.tabGroups.get(groupId);
|
|
160
|
+
} catch (_) {
|
|
161
|
+
groupId = null;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
const newGroupId = await chrome.tabs.group(
|
|
165
|
+
groupId === null ? { tabIds: [tabId] } : { tabIds: [tabId], groupId }
|
|
166
|
+
);
|
|
167
|
+
groupId = newGroupId;
|
|
168
|
+
await chrome.tabGroups.update(groupId, {
|
|
169
|
+
title: GROUP_TITLE,
|
|
170
|
+
color: "orange",
|
|
171
|
+
collapsed: false,
|
|
172
|
+
});
|
|
173
|
+
return groupId;
|
|
174
|
+
} catch (err) {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function banner(tabId, text) {
|
|
180
|
+
try {
|
|
181
|
+
await tell(tabId, 0, { cmd: "banner", text });
|
|
182
|
+
} catch (_) {}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
/** Activa la pestana, ejecuta la accion por CDP y lo deja todo como estaba. */
|
|
188
|
+
async function withTrustedTab(tabId, fn) {
|
|
189
|
+
const target = { tabId };
|
|
190
|
+
let previaId = null;
|
|
191
|
+
let attached = false;
|
|
192
|
+
try {
|
|
193
|
+
const tab = await chrome.tabs.get(tabId);
|
|
194
|
+
const [activa] = await chrome.tabs.query({ active: true, windowId: tab.windowId });
|
|
195
|
+
if (activa && activa.id !== tabId) previaId = activa.id;
|
|
196
|
+
if (!tab.active) await chrome.tabs.update(tabId, { active: true });
|
|
197
|
+
await sleep(250);
|
|
198
|
+
} catch (_) {}
|
|
199
|
+
|
|
200
|
+
try {
|
|
201
|
+
await chrome.debugger.attach(target, "1.3");
|
|
202
|
+
attached = true;
|
|
203
|
+
return await fn(target);
|
|
204
|
+
} finally {
|
|
205
|
+
if (attached) {
|
|
206
|
+
try { await chrome.debugger.detach(target); } catch (_) {}
|
|
207
|
+
}
|
|
208
|
+
if (previaId !== null) {
|
|
209
|
+
try { await chrome.tabs.update(previaId, { active: true }); } catch (_) {}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Clic con eventos de raton reales.
|
|
216
|
+
*
|
|
217
|
+
* Los eventos sinteticos del DOM valen para la mayoria de sitios, pero algunas
|
|
218
|
+
* aplicaciones comprueban isTrusted o dependen del gestor de foco del navegador.
|
|
219
|
+
* En Flow, el boton de enviar recibia el clic sintetico y no pasaba nada.
|
|
220
|
+
*/
|
|
221
|
+
async function clickTrusted(tabId, frameId, ref) {
|
|
222
|
+
const pos = await tell(tabId, frameId, { cmd: "rect", ref });
|
|
223
|
+
if (!pos || !pos.ok) throw new Error((pos && pos.error) || "no se pudo situar el elemento");
|
|
224
|
+
|
|
225
|
+
await withTrustedTab(tabId, async (target) => {
|
|
226
|
+
const base = { x: pos.x, y: pos.y, button: "left", clickCount: 1 };
|
|
227
|
+
await cdp(target, "Input.dispatchMouseEvent", { type: "mouseMoved", x: pos.x, y: pos.y });
|
|
228
|
+
await sleep(60);
|
|
229
|
+
await cdp(target, "Input.dispatchMouseEvent", { ...base, type: "mousePressed", buttons: 1 });
|
|
230
|
+
await sleep(60);
|
|
231
|
+
await cdp(target, "Input.dispatchMouseEvent", { ...base, type: "mouseReleased", buttons: 0 });
|
|
232
|
+
await sleep(300);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
return { label: pos.label };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ==================================================== escritura de confianza
|
|
239
|
+
|
|
240
|
+
function cdp(target, method, params) {
|
|
241
|
+
return chrome.debugger.sendCommand(target, method, params || {});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Escribe con eventos de entrada REALES por el protocolo DevTools.
|
|
246
|
+
*
|
|
247
|
+
* Por que hace falta: en un editor rico (Flow usa uno) asignar el valor desde el
|
|
248
|
+
* content script deja el texto en el DOM pero el framework no se entera. El campo
|
|
249
|
+
* se ve relleno, el boton de enviar sigue apagado, y la app responde "se debe
|
|
250
|
+
* proporcionar una instruccion". Es exactamente lo que pasaba.
|
|
251
|
+
*
|
|
252
|
+
* Input.insertText y Input.dispatchKeyEvent los procesa el renderizador igual que
|
|
253
|
+
* si los hubiera tecleado una persona, y funcionan con la pestana en segundo
|
|
254
|
+
* plano: no hace falta foco ni traer la ventana al frente.
|
|
255
|
+
*/
|
|
256
|
+
async function typeTrusted(tabId, frameId, ref, text, submit, replace) {
|
|
257
|
+
const pos = await tell(tabId, frameId, { cmd: "rect", ref });
|
|
258
|
+
if (!pos || !pos.ok) throw new Error((pos && pos.error) || "no se pudo situar el campo");
|
|
259
|
+
|
|
260
|
+
const target = { tabId };
|
|
261
|
+
let attached = false;
|
|
262
|
+
|
|
263
|
+
// El renderizador de una pestana que no esta activa descarta los eventos de
|
|
264
|
+
// texto: por eso escribir en segundo plano dejaba el campo vacio. Se activa la
|
|
265
|
+
// pestana solo lo que dura la escritura y despues se devuelve la que estaba.
|
|
266
|
+
//
|
|
267
|
+
// Importante: esto NO toca el foco del sistema operativo. Solo cambia que
|
|
268
|
+
// pestana se ve dentro de Chrome, y se deja como estaba. Si el usuario esta en
|
|
269
|
+
// un juego a pantalla completa o en otra aplicacion, no nota nada.
|
|
270
|
+
let previaId = null;
|
|
271
|
+
try {
|
|
272
|
+
const tab = await chrome.tabs.get(tabId);
|
|
273
|
+
const [activa] = await chrome.tabs.query({ active: true, windowId: tab.windowId });
|
|
274
|
+
if (activa && activa.id !== tabId) previaId = activa.id;
|
|
275
|
+
if (!tab.active) await chrome.tabs.update(tabId, { active: true });
|
|
276
|
+
await sleep(250);
|
|
277
|
+
} catch (_) {}
|
|
278
|
+
|
|
279
|
+
try {
|
|
280
|
+
await chrome.debugger.attach(target, "1.3");
|
|
281
|
+
attached = true;
|
|
282
|
+
|
|
283
|
+
// Clic real para que el editor tome el foco de verdad.
|
|
284
|
+
const click = { x: pos.x, y: pos.y, button: "left", clickCount: 1, buttons: 1 };
|
|
285
|
+
await cdp(target, "Input.dispatchMouseEvent", { type: "mouseMoved", x: pos.x, y: pos.y });
|
|
286
|
+
await cdp(target, "Input.dispatchMouseEvent", { ...click, type: "mousePressed" });
|
|
287
|
+
await cdp(target, "Input.dispatchMouseEvent", { ...click, type: "mouseReleased", buttons: 0 });
|
|
288
|
+
await sleep(150);
|
|
289
|
+
|
|
290
|
+
if (replace !== false) {
|
|
291
|
+
// Ctrl+A para sustituir lo que hubiera, sin vaciar el campo antes (vaciarlo
|
|
292
|
+
// hace que el editor se vuelva a montar y se pierda el foco).
|
|
293
|
+
// 'commands' es imprescindible: CDP entrega la tecla, pero es el navegador
|
|
294
|
+
// quien traduce Ctrl+A a la orden de edicion "seleccionar todo". Sin esto
|
|
295
|
+
// la seleccion no ocurre y el texto nuevo se AÑADE al que ya habia, que es
|
|
296
|
+
// justo lo que pasaba ("...en el jardinUn perro jugando...").
|
|
297
|
+
const a = {
|
|
298
|
+
key: "a", code: "KeyA", windowsVirtualKeyCode: 65,
|
|
299
|
+
nativeVirtualKeyCode: 65, modifiers: 2, commands: ["selectAll"],
|
|
300
|
+
};
|
|
301
|
+
await cdp(target, "Input.dispatchKeyEvent", { ...a, type: "rawKeyDown" });
|
|
302
|
+
await cdp(target, "Input.dispatchKeyEvent", { ...a, type: "keyUp" });
|
|
303
|
+
await sleep(120);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
await cdp(target, "Input.insertText", { text });
|
|
307
|
+
await sleep(250);
|
|
308
|
+
|
|
309
|
+
if (submit) {
|
|
310
|
+
const enter = {
|
|
311
|
+
key: "Enter", code: "Enter", windowsVirtualKeyCode: 13,
|
|
312
|
+
nativeVirtualKeyCode: 13, text: "\r", unmodifiedText: "\r",
|
|
313
|
+
};
|
|
314
|
+
await cdp(target, "Input.dispatchKeyEvent", { ...enter, type: "keyDown" });
|
|
315
|
+
await cdp(target, "Input.dispatchKeyEvent", { ...enter, type: "char" });
|
|
316
|
+
await cdp(target, "Input.dispatchKeyEvent", { ...enter, type: "keyUp" });
|
|
317
|
+
await sleep(400);
|
|
318
|
+
}
|
|
319
|
+
} finally {
|
|
320
|
+
if (attached) {
|
|
321
|
+
try { await chrome.debugger.detach(target); } catch (_) {}
|
|
322
|
+
}
|
|
323
|
+
// Se devuelve la pestana que el usuario tenia delante.
|
|
324
|
+
if (previaId !== null) {
|
|
325
|
+
try { await chrome.tabs.update(previaId, { active: true }); } catch (_) {}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const after = await tell(tabId, frameId, { cmd: "value", ref });
|
|
330
|
+
return { label: pos.label, value: after && after.ok ? after.value : "" };
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// ============================================================ comandos
|
|
334
|
+
|
|
335
|
+
async function handle(msg) {
|
|
336
|
+
const cmd = msg.cmd;
|
|
337
|
+
|
|
338
|
+
if (cmd === "ping") {
|
|
339
|
+
return { pong: true, version: "1.0.0" };
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (cmd === "tabs") {
|
|
343
|
+
const tabs = await chrome.tabs.query({});
|
|
344
|
+
return {
|
|
345
|
+
working: workingTabId,
|
|
346
|
+
tabs: tabs.map((t) => ({
|
|
347
|
+
id: t.id,
|
|
348
|
+
title: t.title || "",
|
|
349
|
+
url: t.url || "",
|
|
350
|
+
active: !!t.active,
|
|
351
|
+
grouped: t.groupId !== undefined && t.groupId === groupId,
|
|
352
|
+
})),
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (cmd === "open") {
|
|
357
|
+
// active:false es la clave: la pestana se crea SIN robar el foco ni cambiar
|
|
358
|
+
// lo que el usuario esta viendo.
|
|
359
|
+
const tab = await chrome.tabs.create({ url: msg.url, active: false });
|
|
360
|
+
workingTabId = tab.id;
|
|
361
|
+
await waitForLoad(tab.id, msg.timeout || 30000);
|
|
362
|
+
await groupTab(tab.id);
|
|
363
|
+
await banner(tab.id, msg.note || "pestana abierta");
|
|
364
|
+
const info = await chrome.tabs.get(tab.id);
|
|
365
|
+
return { tabId: tab.id, title: info.title || "", url: info.url || "" };
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (cmd === "select") {
|
|
369
|
+
const tabs = await chrome.tabs.query({});
|
|
370
|
+
const needle = String(msg.match || "").toLowerCase();
|
|
371
|
+
const hit = tabs.find(
|
|
372
|
+
(t) => (t.url || "").toLowerCase().includes(needle) || (t.title || "").toLowerCase().includes(needle)
|
|
373
|
+
);
|
|
374
|
+
if (!hit) throw new Error('ninguna pestana contiene "' + msg.match + '"');
|
|
375
|
+
workingTabId = hit.id;
|
|
376
|
+
await groupTab(hit.id);
|
|
377
|
+
await banner(hit.id, "pestana seleccionada");
|
|
378
|
+
return { tabId: hit.id, title: hit.title || "", url: hit.url || "" };
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (cmd === "navigate") {
|
|
382
|
+
const tabId = await resolveTab(msg.tabId);
|
|
383
|
+
await chrome.tabs.update(tabId, { url: msg.url });
|
|
384
|
+
await waitForLoad(tabId, msg.timeout || 30000);
|
|
385
|
+
workingTabId = tabId;
|
|
386
|
+
await groupTab(tabId);
|
|
387
|
+
const info = await chrome.tabs.get(tabId);
|
|
388
|
+
await banner(tabId, "navegando a " + (info.url || "").slice(0, 60));
|
|
389
|
+
return { tabId, title: info.title || "", url: info.url || "" };
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (cmd === "snapshot") {
|
|
393
|
+
const tabId = await resolveTab(msg.tabId);
|
|
394
|
+
workingTabId = tabId;
|
|
395
|
+
await banner(tabId, "leyendo la pagina");
|
|
396
|
+
refIndex.clear();
|
|
397
|
+
|
|
398
|
+
const frames = await framesOf(tabId);
|
|
399
|
+
const rows = [];
|
|
400
|
+
let counter = 0;
|
|
401
|
+
for (const frameId of frames) {
|
|
402
|
+
let res;
|
|
403
|
+
try {
|
|
404
|
+
res = await tell(tabId, frameId, { cmd: "snapshot", startIndex: counter, filter: msg.filter || "" });
|
|
405
|
+
} catch (_) {
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
if (!res || !res.ok) continue;
|
|
409
|
+
for (const item of res.items) {
|
|
410
|
+
refIndex.set(item.ref, { tabId, frameId });
|
|
411
|
+
rows.push(item);
|
|
412
|
+
}
|
|
413
|
+
counter += res.items.length;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const info = await chrome.tabs.get(tabId);
|
|
417
|
+
const max = msg.max || 60;
|
|
418
|
+
return {
|
|
419
|
+
tabId,
|
|
420
|
+
title: info.title || "",
|
|
421
|
+
url: info.url || "",
|
|
422
|
+
truncated: rows.length > max,
|
|
423
|
+
elements: rows.slice(0, max),
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (cmd === "click" || cmd === "type" || cmd === "press") {
|
|
428
|
+
const loc = refIndex.get(msg.ref);
|
|
429
|
+
if (msg.ref !== undefined && msg.ref !== null && !loc) {
|
|
430
|
+
throw new Error("el ref " + msg.ref + " no existe; vuelve a hacer snapshot");
|
|
431
|
+
}
|
|
432
|
+
const tabId = loc ? loc.tabId : await resolveTab(msg.tabId);
|
|
433
|
+
const frameId = loc ? loc.frameId : 0;
|
|
434
|
+
workingTabId = tabId;
|
|
435
|
+
|
|
436
|
+
if (cmd === "click") await banner(tabId, "pulsando un elemento");
|
|
437
|
+
if (cmd === "type") await banner(tabId, "escribiendo");
|
|
438
|
+
|
|
439
|
+
let res;
|
|
440
|
+
if (cmd === "type") {
|
|
441
|
+
// Primero por CDP, que es lo unico que registran los editores ricos.
|
|
442
|
+
try {
|
|
443
|
+
const out = await typeTrusted(tabId, frameId, msg.ref, msg.text, msg.submit, msg.replace);
|
|
444
|
+
res = { ok: true, value: out.value, label: out.label, via: "cdp" };
|
|
445
|
+
} catch (err) {
|
|
446
|
+
// Si el depurador no esta disponible (otra herramienta adjunta, permiso
|
|
447
|
+
// denegado), se cae al metodo del DOM en vez de quedarse sin escribir.
|
|
448
|
+
const fallback = await tell(tabId, frameId, {
|
|
449
|
+
cmd, ref: msg.ref, text: msg.text, submit: msg.submit, replace: msg.replace,
|
|
450
|
+
});
|
|
451
|
+
if (!fallback || !fallback.ok) throw new Error((fallback && fallback.error) || String(err));
|
|
452
|
+
res = { ...fallback, via: "dom", warning: String((err && err.message) || err) };
|
|
453
|
+
}
|
|
454
|
+
} else if (cmd === "click" && msg.ref !== undefined && msg.ref !== null) {
|
|
455
|
+
try {
|
|
456
|
+
const out = await clickTrusted(tabId, frameId, msg.ref);
|
|
457
|
+
res = { ok: true, label: out.label, via: "cdp" };
|
|
458
|
+
} catch (err) {
|
|
459
|
+
const fallback = await tell(tabId, frameId, { cmd, ref: msg.ref });
|
|
460
|
+
if (!fallback || !fallback.ok) throw new Error((fallback && fallback.error) || String(err));
|
|
461
|
+
res = { ...fallback, via: "dom", warning: String((err && err.message) || err) };
|
|
462
|
+
}
|
|
463
|
+
} else {
|
|
464
|
+
res = await tell(tabId, frameId, {
|
|
465
|
+
cmd,
|
|
466
|
+
ref: msg.ref,
|
|
467
|
+
text: msg.text,
|
|
468
|
+
keys: msg.keys,
|
|
469
|
+
submit: msg.submit,
|
|
470
|
+
replace: msg.replace,
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
if (!res || !res.ok) throw new Error((res && res.error) || "el content script no respondio");
|
|
474
|
+
|
|
475
|
+
// Se deja respirar a la pagina para que reaccione antes del siguiente paso.
|
|
476
|
+
await sleep(msg.settle || 600);
|
|
477
|
+
const info = await chrome.tabs.get(tabId);
|
|
478
|
+
return { tabId, title: info.title || "", url: info.url || "", label: res.label, value: res.value, via: res.via, warning: res.warning };
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
if (cmd === "getText") {
|
|
482
|
+
const tabId = await resolveTab(msg.tabId);
|
|
483
|
+
const res = await tell(tabId, 0, { cmd: "getText" });
|
|
484
|
+
if (!res || !res.ok) throw new Error("no se pudo leer el texto");
|
|
485
|
+
return res;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if (cmd === "eval") {
|
|
489
|
+
const tabId = await resolveTab(msg.tabId);
|
|
490
|
+
const res = await tell(tabId, 0, { cmd: "eval", script: msg.script });
|
|
491
|
+
if (!res || !res.ok) throw new Error("no se pudo evaluar");
|
|
492
|
+
return res;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
if (cmd === "waitFor") {
|
|
496
|
+
const tabId = await resolveTab(msg.tabId);
|
|
497
|
+
const deadline = Date.now() + (msg.timeout || 20000);
|
|
498
|
+
if (msg.text) {
|
|
499
|
+
while (Date.now() < deadline) {
|
|
500
|
+
const res = await tell(tabId, 0, { cmd: "getText" });
|
|
501
|
+
if (res && res.ok && res.text.includes(msg.text)) return { found: true };
|
|
502
|
+
await sleep(700);
|
|
503
|
+
}
|
|
504
|
+
return { found: false };
|
|
505
|
+
}
|
|
506
|
+
await sleep(msg.ms || 2000);
|
|
507
|
+
return { waited: msg.ms || 2000 };
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
if (cmd === "screenshot") {
|
|
511
|
+
// Page.captureScreenshot por el protocolo DevTools funciona sobre pestanas
|
|
512
|
+
// en segundo plano; chrome.tabs.captureVisibleTab exigiria que la pestana
|
|
513
|
+
// estuviera activa y la ventana enfocada.
|
|
514
|
+
const tabId = await resolveTab(msg.tabId);
|
|
515
|
+
const target = { tabId };
|
|
516
|
+
await chrome.debugger.attach(target, "1.3");
|
|
517
|
+
try {
|
|
518
|
+
const res = await chrome.debugger.sendCommand(target, "Page.captureScreenshot", {
|
|
519
|
+
format: "png",
|
|
520
|
+
captureBeyondViewport: false,
|
|
521
|
+
});
|
|
522
|
+
return { data: res.data };
|
|
523
|
+
} finally {
|
|
524
|
+
try { await chrome.debugger.detach(target); } catch (_) {}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
if (cmd === "banner") {
|
|
529
|
+
const tabId = await resolveTab(msg.tabId);
|
|
530
|
+
await banner(tabId, msg.text);
|
|
531
|
+
return { ok: true };
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
if (cmd === "close_tab") {
|
|
535
|
+
// Cerrar lo que se abre es parte de dejar el navegador como se encontro.
|
|
536
|
+
const ids = Array.isArray(msg.tabIds) ? msg.tabIds : [await resolveTab(msg.tabId)];
|
|
537
|
+
await chrome.tabs.remove(ids);
|
|
538
|
+
if (ids.includes(workingTabId)) workingTabId = null;
|
|
539
|
+
return { closed: ids.length };
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
if (cmd === "ungroup") {
|
|
543
|
+
if (workingTabId) {
|
|
544
|
+
try { await chrome.tabs.ungroup(workingTabId); } catch (_) {}
|
|
545
|
+
await banner(workingTabId, null);
|
|
546
|
+
}
|
|
547
|
+
groupId = null;
|
|
548
|
+
workingTabId = null;
|
|
549
|
+
return { ok: true };
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
throw new Error("comando desconocido: " + cmd);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function sleep(ms) {
|
|
556
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function waitForLoad(tabId, timeout) {
|
|
560
|
+
return new Promise((resolve) => {
|
|
561
|
+
const deadline = Date.now() + timeout;
|
|
562
|
+
const check = async () => {
|
|
563
|
+
try {
|
|
564
|
+
const t = await chrome.tabs.get(tabId);
|
|
565
|
+
if (t.status === "complete" || Date.now() > deadline) return resolve();
|
|
566
|
+
} catch (_) {
|
|
567
|
+
return resolve();
|
|
568
|
+
}
|
|
569
|
+
setTimeout(check, 250);
|
|
570
|
+
};
|
|
571
|
+
check();
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
for (const p of WS_PORTS) connect(p);
|