@duheso/zerocli 0.8.1 → 0.8.3
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/chrome-extension/README.md +147 -0
- package/chrome-extension/background.js +912 -0
- package/chrome-extension/content.js +26 -0
- package/chrome-extension/icons/icon128.png +0 -0
- package/chrome-extension/icons/icon16.png +0 -0
- package/chrome-extension/icons/icon48.png +0 -0
- package/chrome-extension/manifest.json +56 -0
- package/chrome-extension/offscreen.html +7 -0
- package/chrome-extension/offscreen.js +28 -0
- package/chrome-extension/popup.css +190 -0
- package/chrome-extension/popup.html +65 -0
- package/chrome-extension/popup.js +76 -0
- package/dist/cli.mjs +81 -100
- package/package.json +3 -1
- package/scripts/setup-chrome-native-host.mjs +228 -0
|
@@ -0,0 +1,912 @@
|
|
|
1
|
+
// ZeroCLI Browser Extension — Background Service Worker
|
|
2
|
+
// Handles native messaging connection with ZeroCLI and routes browser automation tools
|
|
3
|
+
|
|
4
|
+
const NATIVE_HOST_NAME = 'com.duheso.zerocli_browser_extension';
|
|
5
|
+
const VERSION = '1.0.0';
|
|
6
|
+
|
|
7
|
+
let nativePort = null;
|
|
8
|
+
let isConnected = false;
|
|
9
|
+
let pendingRequests = new Map(); // requestId -> { resolve, reject, timeoutId }
|
|
10
|
+
let requestCounter = 0;
|
|
11
|
+
let debuggerAttachedTabs = new Set();
|
|
12
|
+
let consoleLogs = new Map(); // tabId -> [messages]
|
|
13
|
+
let networkRequests = new Map(); // tabId -> [requests]
|
|
14
|
+
let captureStreams = new Map(); // tabId -> MediaRecorder
|
|
15
|
+
let gifFrames = new Map(); // requestId -> [frames]
|
|
16
|
+
|
|
17
|
+
// ─── Native Messaging ──────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
function connectNativeHost() {
|
|
20
|
+
try {
|
|
21
|
+
nativePort = chrome.runtime.connectNative(NATIVE_HOST_NAME);
|
|
22
|
+
isConnected = true;
|
|
23
|
+
updateIcon(true);
|
|
24
|
+
|
|
25
|
+
nativePort.onMessage.addListener((message) => {
|
|
26
|
+
handleNativeMessage(message);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
nativePort.onDisconnect.addListener(() => {
|
|
30
|
+
// Read lastError to mark it as "checked" and suppress DevTools warning
|
|
31
|
+
const err = chrome.runtime.lastError;
|
|
32
|
+
const hostNotFound = err && (
|
|
33
|
+
err.message?.includes('not found') ||
|
|
34
|
+
err.message?.includes('Cannot find native messaging host') ||
|
|
35
|
+
err.message?.includes('Specified native messaging host not found')
|
|
36
|
+
);
|
|
37
|
+
isConnected = false;
|
|
38
|
+
nativePort = null;
|
|
39
|
+
updateIcon(false);
|
|
40
|
+
// Reject all pending requests
|
|
41
|
+
for (const [id, pending] of pendingRequests) {
|
|
42
|
+
clearTimeout(pending.timeoutId);
|
|
43
|
+
pending.reject(new Error(err?.message ?? 'Native host disconnected'));
|
|
44
|
+
}
|
|
45
|
+
pendingRequests.clear();
|
|
46
|
+
if (hostNotFound) {
|
|
47
|
+
// Native host not installed yet — retry slowly (every 30s)
|
|
48
|
+
// User needs to run: zero --chrome (to register the native host)
|
|
49
|
+
console.warn('[ZeroCLI] Native messaging host not found. Run "zero --chrome" once to register it, then reload this extension.');
|
|
50
|
+
setTimeout(connectNativeHost, 30000);
|
|
51
|
+
} else {
|
|
52
|
+
// Normal disconnect — reconnect after short delay
|
|
53
|
+
setTimeout(connectNativeHost, 3000);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Send initial ping
|
|
58
|
+
sendToNative({ type: 'ping' });
|
|
59
|
+
} catch (err) {
|
|
60
|
+
isConnected = false;
|
|
61
|
+
updateIcon(false);
|
|
62
|
+
console.warn('[ZeroCLI] connectNativeHost error:', err?.message);
|
|
63
|
+
setTimeout(connectNativeHost, 30000);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function sendToNative(message) {
|
|
68
|
+
if (nativePort) {
|
|
69
|
+
try {
|
|
70
|
+
nativePort.postMessage(message);
|
|
71
|
+
} catch (err) {
|
|
72
|
+
console.error('[ZeroCLI] Failed to send to native host:', err);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function handleNativeMessage(message) {
|
|
78
|
+
if (!message || !message.type) return;
|
|
79
|
+
|
|
80
|
+
switch (message.type) {
|
|
81
|
+
case 'pong':
|
|
82
|
+
// Connection confirmed
|
|
83
|
+
break;
|
|
84
|
+
|
|
85
|
+
case 'status_response':
|
|
86
|
+
// Native host status
|
|
87
|
+
break;
|
|
88
|
+
|
|
89
|
+
case 'tool_request': {
|
|
90
|
+
const { method, params, id: requestId } = message;
|
|
91
|
+
try {
|
|
92
|
+
const result = await dispatchTool(method, params || {});
|
|
93
|
+
sendToNative({
|
|
94
|
+
type: 'tool_response',
|
|
95
|
+
id: requestId,
|
|
96
|
+
result,
|
|
97
|
+
});
|
|
98
|
+
} catch (err) {
|
|
99
|
+
sendToNative({
|
|
100
|
+
type: 'tool_response',
|
|
101
|
+
id: requestId,
|
|
102
|
+
error: { message: err.message || String(err) },
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
case 'mcp_connected':
|
|
109
|
+
updateIcon(true);
|
|
110
|
+
break;
|
|
111
|
+
|
|
112
|
+
case 'mcp_disconnected':
|
|
113
|
+
updateIcon(false);
|
|
114
|
+
break;
|
|
115
|
+
|
|
116
|
+
default:
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ─── Tool Dispatcher ────────────────────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
async function dispatchTool(method, params) {
|
|
124
|
+
switch (method) {
|
|
125
|
+
case 'tabs_context_mcp':
|
|
126
|
+
return toolTabsContext(params);
|
|
127
|
+
case 'tabs_create_mcp':
|
|
128
|
+
return toolTabsCreate(params);
|
|
129
|
+
case 'navigate':
|
|
130
|
+
return toolNavigate(params);
|
|
131
|
+
case 'computer':
|
|
132
|
+
return toolComputer(params);
|
|
133
|
+
case 'javascript_tool':
|
|
134
|
+
return toolJavascript(params);
|
|
135
|
+
case 'read_page':
|
|
136
|
+
return toolReadPage(params);
|
|
137
|
+
case 'find':
|
|
138
|
+
return toolFind(params);
|
|
139
|
+
case 'form_input':
|
|
140
|
+
return toolFormInput(params);
|
|
141
|
+
case 'get_page_text':
|
|
142
|
+
return toolGetPageText(params);
|
|
143
|
+
case 'read_console_messages':
|
|
144
|
+
return toolReadConsole(params);
|
|
145
|
+
case 'read_network_requests':
|
|
146
|
+
return toolReadNetwork(params);
|
|
147
|
+
case 'gif_creator':
|
|
148
|
+
return toolGifCreator(params);
|
|
149
|
+
case 'resize_window':
|
|
150
|
+
return toolResizeWindow(params);
|
|
151
|
+
case 'upload_image':
|
|
152
|
+
return toolUploadImage(params);
|
|
153
|
+
case 'update_plan':
|
|
154
|
+
return toolUpdatePlan(params);
|
|
155
|
+
case 'shortcuts_list':
|
|
156
|
+
return toolShortcutsList(params);
|
|
157
|
+
case 'shortcuts_execute':
|
|
158
|
+
return toolShortcutsExecute(params);
|
|
159
|
+
default:
|
|
160
|
+
throw new Error(`Unknown tool: ${method}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ─── Tool Implementations ───────────────────────────────────────────────────
|
|
165
|
+
|
|
166
|
+
async function toolTabsContext(_params) {
|
|
167
|
+
const tabs = await chrome.tabs.query({});
|
|
168
|
+
return {
|
|
169
|
+
tabs: tabs.map((tab) => ({
|
|
170
|
+
id: tab.id,
|
|
171
|
+
url: tab.url,
|
|
172
|
+
title: tab.title,
|
|
173
|
+
active: tab.active,
|
|
174
|
+
windowId: tab.windowId,
|
|
175
|
+
index: tab.index,
|
|
176
|
+
status: tab.status,
|
|
177
|
+
favIconUrl: tab.favIconUrl,
|
|
178
|
+
})),
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function toolTabsCreate(params) {
|
|
183
|
+
const { url, active = true } = params;
|
|
184
|
+
const tab = await chrome.tabs.create({ url, active });
|
|
185
|
+
// Wait for tab to load
|
|
186
|
+
await waitForTabLoad(tab.id);
|
|
187
|
+
const updatedTab = await chrome.tabs.get(tab.id);
|
|
188
|
+
return {
|
|
189
|
+
tabId: updatedTab.id,
|
|
190
|
+
url: updatedTab.url,
|
|
191
|
+
title: updatedTab.title,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async function toolNavigate(params) {
|
|
196
|
+
const { tabId, url } = params;
|
|
197
|
+
await chrome.tabs.update(tabId, { url });
|
|
198
|
+
await waitForTabLoad(tabId);
|
|
199
|
+
const tab = await chrome.tabs.get(tabId);
|
|
200
|
+
return {
|
|
201
|
+
tabId: tab.id,
|
|
202
|
+
url: tab.url,
|
|
203
|
+
title: tab.title,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function toolComputer(params) {
|
|
208
|
+
const { action, tabId, coordinate, ref, text, scroll_direction, scroll_distance, duration } = params;
|
|
209
|
+
|
|
210
|
+
switch (action) {
|
|
211
|
+
case 'screenshot': {
|
|
212
|
+
const targetTabId = tabId || (await getActiveTabId());
|
|
213
|
+
const dataUrl = await chrome.tabs.captureVisibleTab(null, {
|
|
214
|
+
format: 'png',
|
|
215
|
+
});
|
|
216
|
+
return {
|
|
217
|
+
image: {
|
|
218
|
+
type: 'base64',
|
|
219
|
+
media_type: 'image/png',
|
|
220
|
+
data: dataUrl.replace(/^data:image\/png;base64,/, ''),
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
case 'left_click':
|
|
226
|
+
case 'right_click':
|
|
227
|
+
case 'double_click':
|
|
228
|
+
case 'middle_click': {
|
|
229
|
+
const targetTabId = tabId || (await getActiveTabId());
|
|
230
|
+
const result = await executeInTab(targetTabId, performClick, [
|
|
231
|
+
action,
|
|
232
|
+
coordinate,
|
|
233
|
+
ref,
|
|
234
|
+
]);
|
|
235
|
+
return result;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
case 'type': {
|
|
239
|
+
const targetTabId = tabId || (await getActiveTabId());
|
|
240
|
+
const result = await executeInTab(targetTabId, typeText, [text]);
|
|
241
|
+
return result;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
case 'key': {
|
|
245
|
+
const targetTabId = tabId || (await getActiveTabId());
|
|
246
|
+
const result = await executeInTab(targetTabId, pressKey, [text]);
|
|
247
|
+
return result;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
case 'scroll': {
|
|
251
|
+
const targetTabId = tabId || (await getActiveTabId());
|
|
252
|
+
const result = await executeInTab(targetTabId, scrollPage, [
|
|
253
|
+
scroll_direction || 'down',
|
|
254
|
+
scroll_distance || 3,
|
|
255
|
+
coordinate,
|
|
256
|
+
]);
|
|
257
|
+
return result;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
case 'wait': {
|
|
261
|
+
await new Promise((resolve) => setTimeout(resolve, (duration || 1) * 1000));
|
|
262
|
+
return { success: true };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
case 'left_click_drag': {
|
|
266
|
+
const targetTabId = tabId || (await getActiveTabId());
|
|
267
|
+
const { startCoordinate, endCoordinate } = params;
|
|
268
|
+
const result = await executeInTab(targetTabId, performDrag, [
|
|
269
|
+
startCoordinate,
|
|
270
|
+
endCoordinate,
|
|
271
|
+
]);
|
|
272
|
+
return result;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
default:
|
|
276
|
+
throw new Error(`Unknown computer action: ${action}`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async function toolJavascript(params) {
|
|
281
|
+
const { tabId, code } = params;
|
|
282
|
+
const targetTabId = tabId || (await getActiveTabId());
|
|
283
|
+
const results = await chrome.scripting.executeScript({
|
|
284
|
+
target: { tabId: targetTabId },
|
|
285
|
+
func: (jsCode) => {
|
|
286
|
+
try {
|
|
287
|
+
// eslint-disable-next-line no-eval
|
|
288
|
+
const result = eval(jsCode);
|
|
289
|
+
return { success: true, result: String(result) };
|
|
290
|
+
} catch (err) {
|
|
291
|
+
return { success: false, error: err.message };
|
|
292
|
+
}
|
|
293
|
+
},
|
|
294
|
+
args: [code],
|
|
295
|
+
});
|
|
296
|
+
return results[0]?.result || { success: false, error: 'No result' };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
async function toolReadPage(params) {
|
|
300
|
+
const { tabId, format = 'simplified' } = params;
|
|
301
|
+
const targetTabId = tabId || (await getActiveTabId());
|
|
302
|
+
const results = await executeInTab(targetTabId, getPageStructure, [format]);
|
|
303
|
+
return results;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
async function toolFind(params) {
|
|
307
|
+
const { tabId, query, selector } = params;
|
|
308
|
+
const targetTabId = tabId || (await getActiveTabId());
|
|
309
|
+
const results = await executeInTab(targetTabId, findElements, [query, selector]);
|
|
310
|
+
return results;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
async function toolFormInput(params) {
|
|
314
|
+
const { tabId, selector, ref, value, type = 'text' } = params;
|
|
315
|
+
const targetTabId = tabId || (await getActiveTabId());
|
|
316
|
+
const result = await executeInTab(targetTabId, fillFormField, [
|
|
317
|
+
selector,
|
|
318
|
+
ref,
|
|
319
|
+
value,
|
|
320
|
+
type,
|
|
321
|
+
]);
|
|
322
|
+
return result;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
async function toolGetPageText(params) {
|
|
326
|
+
const { tabId } = params;
|
|
327
|
+
const targetTabId = tabId || (await getActiveTabId());
|
|
328
|
+
const results = await executeInTab(targetTabId, getVisibleText, []);
|
|
329
|
+
return results;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
async function toolReadConsole(params) {
|
|
333
|
+
const { tabId, limit = 100, pattern } = params;
|
|
334
|
+
const targetTabId = tabId || (await getActiveTabId());
|
|
335
|
+
|
|
336
|
+
await ensureDebuggerAttached(targetTabId);
|
|
337
|
+
|
|
338
|
+
const logs = consoleLogs.get(targetTabId) || [];
|
|
339
|
+
let filtered = logs;
|
|
340
|
+
|
|
341
|
+
if (pattern) {
|
|
342
|
+
const regex = new RegExp(pattern, 'i');
|
|
343
|
+
filtered = logs.filter((log) => regex.test(log.text));
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return {
|
|
347
|
+
messages: filtered.slice(-limit).map((log) => ({
|
|
348
|
+
level: log.level,
|
|
349
|
+
text: log.text,
|
|
350
|
+
timestamp: log.timestamp,
|
|
351
|
+
url: log.url,
|
|
352
|
+
line: log.line,
|
|
353
|
+
})),
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
async function toolReadNetwork(params) {
|
|
358
|
+
const { tabId, limit = 50, urlPattern } = params;
|
|
359
|
+
const targetTabId = tabId || (await getActiveTabId());
|
|
360
|
+
|
|
361
|
+
await ensureDebuggerAttached(targetTabId);
|
|
362
|
+
|
|
363
|
+
const requests = networkRequests.get(targetTabId) || [];
|
|
364
|
+
let filtered = requests;
|
|
365
|
+
|
|
366
|
+
if (urlPattern) {
|
|
367
|
+
const regex = new RegExp(urlPattern, 'i');
|
|
368
|
+
filtered = requests.filter((req) => regex.test(req.url));
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return {
|
|
372
|
+
requests: filtered.slice(-limit).map((req) => ({
|
|
373
|
+
url: req.url,
|
|
374
|
+
method: req.method,
|
|
375
|
+
status: req.status,
|
|
376
|
+
type: req.type,
|
|
377
|
+
timestamp: req.timestamp,
|
|
378
|
+
})),
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
async function toolGifCreator(params) {
|
|
383
|
+
const { tabId, filename = 'recording.gif', duration = 5000 } = params;
|
|
384
|
+
const targetTabId = tabId || (await getActiveTabId());
|
|
385
|
+
|
|
386
|
+
// Capture frames via screenshot loop
|
|
387
|
+
const frames = [];
|
|
388
|
+
const interval = 200; // 5fps
|
|
389
|
+
const frameCount = Math.ceil(duration / interval);
|
|
390
|
+
const startTime = Date.now();
|
|
391
|
+
|
|
392
|
+
for (let i = 0; i < frameCount; i++) {
|
|
393
|
+
await new Promise((resolve) => setTimeout(resolve, interval));
|
|
394
|
+
try {
|
|
395
|
+
const dataUrl = await chrome.tabs.captureVisibleTab(null, { format: 'png' });
|
|
396
|
+
frames.push(dataUrl.replace(/^data:image\/png;base64,/, ''));
|
|
397
|
+
} catch (e) {
|
|
398
|
+
// Tab may have navigated, continue
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
return {
|
|
403
|
+
success: true,
|
|
404
|
+
filename,
|
|
405
|
+
frameCount: frames.length,
|
|
406
|
+
duration: Date.now() - startTime,
|
|
407
|
+
// Return first and last frame as preview
|
|
408
|
+
preview: frames.length > 0 ? {
|
|
409
|
+
type: 'base64',
|
|
410
|
+
media_type: 'image/png',
|
|
411
|
+
data: frames[frames.length - 1],
|
|
412
|
+
} : null,
|
|
413
|
+
message: `Recorded ${frames.length} frames over ${Date.now() - startTime}ms. GIF creation requires local processing; frames captured successfully.`,
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
async function toolResizeWindow(params) {
|
|
418
|
+
const { width, height, windowId } = params;
|
|
419
|
+
const targetWindowId = windowId || chrome.windows.WINDOW_ID_CURRENT;
|
|
420
|
+
await chrome.windows.update(targetWindowId, {
|
|
421
|
+
width: Math.round(width),
|
|
422
|
+
height: Math.round(height),
|
|
423
|
+
});
|
|
424
|
+
return { success: true, width, height };
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
async function toolUploadImage(params) {
|
|
428
|
+
const { tabId, selector, imageData, mediaType = 'image/png' } = params;
|
|
429
|
+
const targetTabId = tabId || (await getActiveTabId());
|
|
430
|
+
|
|
431
|
+
const result = await executeInTab(targetTabId, setFileInputValue, [
|
|
432
|
+
selector,
|
|
433
|
+
imageData,
|
|
434
|
+
mediaType,
|
|
435
|
+
]);
|
|
436
|
+
return result;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
async function toolUpdatePlan(params) {
|
|
440
|
+
const { plan } = params;
|
|
441
|
+
await chrome.storage.session.set({ currentPlan: plan });
|
|
442
|
+
return { success: true };
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
async function toolShortcutsList(_params) {
|
|
446
|
+
return {
|
|
447
|
+
shortcuts: [
|
|
448
|
+
{ key: 'Ctrl+C', description: 'Copy' },
|
|
449
|
+
{ key: 'Ctrl+V', description: 'Paste' },
|
|
450
|
+
{ key: 'Ctrl+A', description: 'Select All' },
|
|
451
|
+
{ key: 'Ctrl+Z', description: 'Undo' },
|
|
452
|
+
{ key: 'Ctrl+Y', description: 'Redo' },
|
|
453
|
+
{ key: 'Ctrl+F', description: 'Find' },
|
|
454
|
+
{ key: 'Ctrl+R', description: 'Reload' },
|
|
455
|
+
{ key: 'F5', description: 'Reload' },
|
|
456
|
+
{ key: 'Tab', description: 'Next element' },
|
|
457
|
+
{ key: 'Enter', description: 'Submit/Confirm' },
|
|
458
|
+
{ key: 'Escape', description: 'Cancel/Close' },
|
|
459
|
+
],
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
async function toolShortcutsExecute(params) {
|
|
464
|
+
const { tabId, key } = params;
|
|
465
|
+
const targetTabId = tabId || (await getActiveTabId());
|
|
466
|
+
const result = await executeInTab(targetTabId, pressKey, [key]);
|
|
467
|
+
return result;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// ─── Helper Functions ────────────────────────────────────────────────────────
|
|
471
|
+
|
|
472
|
+
async function getActiveTabId() {
|
|
473
|
+
const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
|
|
474
|
+
if (!tabs[0]) throw new Error('No active tab');
|
|
475
|
+
return tabs[0].id;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
async function waitForTabLoad(tabId, timeoutMs = 10000) {
|
|
479
|
+
return new Promise((resolve) => {
|
|
480
|
+
const listener = (changedTabId, changeInfo) => {
|
|
481
|
+
if (changedTabId === tabId && changeInfo.status === 'complete') {
|
|
482
|
+
chrome.tabs.onUpdated.removeListener(listener);
|
|
483
|
+
resolve();
|
|
484
|
+
}
|
|
485
|
+
};
|
|
486
|
+
chrome.tabs.onUpdated.addListener(listener);
|
|
487
|
+
setTimeout(() => {
|
|
488
|
+
chrome.tabs.onUpdated.removeListener(listener);
|
|
489
|
+
resolve();
|
|
490
|
+
}, timeoutMs);
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
async function executeInTab(tabId, func, args) {
|
|
495
|
+
const results = await chrome.scripting.executeScript({
|
|
496
|
+
target: { tabId },
|
|
497
|
+
func,
|
|
498
|
+
args,
|
|
499
|
+
});
|
|
500
|
+
return results[0]?.result;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
async function ensureDebuggerAttached(tabId) {
|
|
504
|
+
if (debuggerAttachedTabs.has(tabId)) return;
|
|
505
|
+
|
|
506
|
+
await chrome.debugger.attach({ tabId }, '1.3');
|
|
507
|
+
debuggerAttachedTabs.add(tabId);
|
|
508
|
+
|
|
509
|
+
if (!consoleLogs.has(tabId)) consoleLogs.set(tabId, []);
|
|
510
|
+
if (!networkRequests.has(tabId)) networkRequests.set(tabId, []);
|
|
511
|
+
|
|
512
|
+
await chrome.debugger.sendCommand({ tabId }, 'Console.enable');
|
|
513
|
+
await chrome.debugger.sendCommand({ tabId }, 'Network.enable');
|
|
514
|
+
|
|
515
|
+
chrome.debugger.onEvent.addListener((source, method, params) => {
|
|
516
|
+
if (source.tabId !== tabId) return;
|
|
517
|
+
|
|
518
|
+
if (method === 'Console.messageAdded') {
|
|
519
|
+
const msg = params.message;
|
|
520
|
+
const logs = consoleLogs.get(tabId) || [];
|
|
521
|
+
logs.push({
|
|
522
|
+
level: msg.level,
|
|
523
|
+
text: msg.text,
|
|
524
|
+
timestamp: Date.now(),
|
|
525
|
+
url: msg.url,
|
|
526
|
+
line: msg.line,
|
|
527
|
+
});
|
|
528
|
+
if (logs.length > 1000) logs.shift();
|
|
529
|
+
consoleLogs.set(tabId, logs);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
if (method === 'Network.responseReceived') {
|
|
533
|
+
const requests = networkRequests.get(tabId) || [];
|
|
534
|
+
requests.push({
|
|
535
|
+
url: params.response.url,
|
|
536
|
+
method: params.type,
|
|
537
|
+
status: params.response.status,
|
|
538
|
+
type: params.type,
|
|
539
|
+
timestamp: Date.now(),
|
|
540
|
+
});
|
|
541
|
+
if (requests.length > 500) requests.shift();
|
|
542
|
+
networkRequests.set(tabId, requests);
|
|
543
|
+
}
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
chrome.tabs.onRemoved.addListener((removedTabId) => {
|
|
547
|
+
if (removedTabId === tabId) {
|
|
548
|
+
debuggerAttachedTabs.delete(tabId);
|
|
549
|
+
consoleLogs.delete(tabId);
|
|
550
|
+
networkRequests.delete(tabId);
|
|
551
|
+
}
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function updateIcon(connected) {
|
|
556
|
+
const path = connected ? 'icons/icon48.png' : 'icons/icon48.png';
|
|
557
|
+
chrome.action.setIcon({ path }).catch(() => {});
|
|
558
|
+
chrome.action.setBadgeText({ text: connected ? '' : '!' }).catch(() => {});
|
|
559
|
+
chrome.action.setBadgeBackgroundColor({ color: connected ? '#22c55e' : '#ef4444' }).catch(() => {});
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// ─── Content Script Functions (executed in page context) ──────────────────
|
|
563
|
+
|
|
564
|
+
function performClick(action, coordinate, ref) {
|
|
565
|
+
let element = null;
|
|
566
|
+
|
|
567
|
+
if (ref) {
|
|
568
|
+
element = document.querySelector(`[data-ref="${ref}"]`) ||
|
|
569
|
+
document.evaluate(ref, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
if (!element && coordinate) {
|
|
573
|
+
const [x, y] = coordinate;
|
|
574
|
+
element = document.elementFromPoint(x, y);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
if (!element) {
|
|
578
|
+
return { success: false, error: 'Element not found' };
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
const rect = element.getBoundingClientRect();
|
|
582
|
+
const cx = rect.left + rect.width / 2;
|
|
583
|
+
const cy = rect.top + rect.height / 2;
|
|
584
|
+
|
|
585
|
+
const eventProps = {
|
|
586
|
+
bubbles: true,
|
|
587
|
+
cancelable: true,
|
|
588
|
+
clientX: cx,
|
|
589
|
+
clientY: cy,
|
|
590
|
+
screenX: cx,
|
|
591
|
+
screenY: cy,
|
|
592
|
+
};
|
|
593
|
+
|
|
594
|
+
if (action === 'right_click') {
|
|
595
|
+
element.dispatchEvent(new MouseEvent('contextmenu', { ...eventProps, button: 2 }));
|
|
596
|
+
} else if (action === 'double_click') {
|
|
597
|
+
element.dispatchEvent(new MouseEvent('mousedown', eventProps));
|
|
598
|
+
element.dispatchEvent(new MouseEvent('mouseup', eventProps));
|
|
599
|
+
element.dispatchEvent(new MouseEvent('click', eventProps));
|
|
600
|
+
element.dispatchEvent(new MouseEvent('dblclick', eventProps));
|
|
601
|
+
} else if (action === 'middle_click') {
|
|
602
|
+
element.dispatchEvent(new MouseEvent('mousedown', { ...eventProps, button: 1 }));
|
|
603
|
+
element.dispatchEvent(new MouseEvent('mouseup', { ...eventProps, button: 1 }));
|
|
604
|
+
element.dispatchEvent(new MouseEvent('auxclick', { ...eventProps, button: 1 }));
|
|
605
|
+
} else {
|
|
606
|
+
element.dispatchEvent(new MouseEvent('mousedown', eventProps));
|
|
607
|
+
element.dispatchEvent(new MouseEvent('mouseup', eventProps));
|
|
608
|
+
element.dispatchEvent(new MouseEvent('click', eventProps));
|
|
609
|
+
if (element.focus) element.focus();
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
return {
|
|
613
|
+
success: true,
|
|
614
|
+
element: element.tagName.toLowerCase(),
|
|
615
|
+
text: element.textContent?.trim().slice(0, 100),
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function typeText(text) {
|
|
620
|
+
const element = document.activeElement;
|
|
621
|
+
if (!element) return { success: false, error: 'No focused element' };
|
|
622
|
+
|
|
623
|
+
if (element.tagName === 'INPUT' || element.tagName === 'TEXTAREA') {
|
|
624
|
+
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
|
|
625
|
+
element.tagName === 'INPUT' ? HTMLInputElement.prototype : HTMLTextAreaElement.prototype,
|
|
626
|
+
'value'
|
|
627
|
+
)?.set;
|
|
628
|
+
if (nativeInputValueSetter) {
|
|
629
|
+
nativeInputValueSetter.call(element, element.value + text);
|
|
630
|
+
element.dispatchEvent(new Event('input', { bubbles: true }));
|
|
631
|
+
element.dispatchEvent(new Event('change', { bubbles: true }));
|
|
632
|
+
}
|
|
633
|
+
} else if (element.isContentEditable) {
|
|
634
|
+
document.execCommand('insertText', false, text);
|
|
635
|
+
} else {
|
|
636
|
+
for (const char of text) {
|
|
637
|
+
element.dispatchEvent(new KeyboardEvent('keydown', { key: char, bubbles: true }));
|
|
638
|
+
element.dispatchEvent(new KeyboardEvent('keypress', { key: char, bubbles: true }));
|
|
639
|
+
element.dispatchEvent(new KeyboardEvent('keyup', { key: char, bubbles: true }));
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
return { success: true };
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
function pressKey(keyString) {
|
|
647
|
+
const keyMap = {
|
|
648
|
+
'Return': 'Enter', 'Escape': 'Escape', 'Tab': 'Tab',
|
|
649
|
+
'BackSpace': 'Backspace', 'Delete': 'Delete', 'Home': 'Home',
|
|
650
|
+
'End': 'End', 'Page_Up': 'PageUp', 'Page_Down': 'PageDown',
|
|
651
|
+
'Up': 'ArrowUp', 'Down': 'ArrowDown', 'Left': 'ArrowLeft', 'Right': 'ArrowRight',
|
|
652
|
+
'F5': 'F5', 'F12': 'F12',
|
|
653
|
+
};
|
|
654
|
+
|
|
655
|
+
const parts = keyString.split('+');
|
|
656
|
+
const key = keyMap[parts[parts.length - 1]] || parts[parts.length - 1];
|
|
657
|
+
const ctrlKey = parts.includes('Ctrl') || parts.includes('ctrl');
|
|
658
|
+
const shiftKey = parts.includes('Shift') || parts.includes('shift');
|
|
659
|
+
const altKey = parts.includes('Alt') || parts.includes('alt');
|
|
660
|
+
const metaKey = parts.includes('Meta') || parts.includes('meta');
|
|
661
|
+
|
|
662
|
+
const target = document.activeElement || document.body;
|
|
663
|
+
const eventProps = { key, ctrlKey, shiftKey, altKey, metaKey, bubbles: true, cancelable: true };
|
|
664
|
+
|
|
665
|
+
target.dispatchEvent(new KeyboardEvent('keydown', eventProps));
|
|
666
|
+
target.dispatchEvent(new KeyboardEvent('keypress', eventProps));
|
|
667
|
+
target.dispatchEvent(new KeyboardEvent('keyup', eventProps));
|
|
668
|
+
|
|
669
|
+
return { success: true };
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function scrollPage(direction, distance, coordinate) {
|
|
673
|
+
let target = document.body;
|
|
674
|
+
|
|
675
|
+
if (coordinate) {
|
|
676
|
+
const [x, y] = coordinate;
|
|
677
|
+
const el = document.elementFromPoint(x, y);
|
|
678
|
+
if (el) target = el;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
const scrollAmount = distance * 100;
|
|
682
|
+
|
|
683
|
+
switch (direction) {
|
|
684
|
+
case 'up':
|
|
685
|
+
target.scrollBy({ top: -scrollAmount, behavior: 'smooth' });
|
|
686
|
+
break;
|
|
687
|
+
case 'down':
|
|
688
|
+
target.scrollBy({ top: scrollAmount, behavior: 'smooth' });
|
|
689
|
+
break;
|
|
690
|
+
case 'left':
|
|
691
|
+
target.scrollBy({ left: -scrollAmount, behavior: 'smooth' });
|
|
692
|
+
break;
|
|
693
|
+
case 'right':
|
|
694
|
+
target.scrollBy({ left: scrollAmount, behavior: 'smooth' });
|
|
695
|
+
break;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
return { success: true, scrolled: direction, distance };
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
function performDrag(startCoordinate, endCoordinate) {
|
|
702
|
+
if (!startCoordinate || !endCoordinate) {
|
|
703
|
+
return { success: false, error: 'Start and end coordinates required' };
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
const [sx, sy] = startCoordinate;
|
|
707
|
+
const [ex, ey] = endCoordinate;
|
|
708
|
+
const startEl = document.elementFromPoint(sx, sy);
|
|
709
|
+
const endEl = document.elementFromPoint(ex, ey);
|
|
710
|
+
|
|
711
|
+
if (!startEl) return { success: false, error: 'Start element not found' };
|
|
712
|
+
|
|
713
|
+
startEl.dispatchEvent(new MouseEvent('mousedown', { clientX: sx, clientY: sy, bubbles: true }));
|
|
714
|
+
startEl.dispatchEvent(new MouseEvent('mousemove', { clientX: (sx + ex) / 2, clientY: (sy + ey) / 2, bubbles: true }));
|
|
715
|
+
|
|
716
|
+
const dropTarget = endEl || document.body;
|
|
717
|
+
dropTarget.dispatchEvent(new MouseEvent('mousemove', { clientX: ex, clientY: ey, bubbles: true }));
|
|
718
|
+
dropTarget.dispatchEvent(new MouseEvent('mouseup', { clientX: ex, clientY: ey, bubbles: true }));
|
|
719
|
+
|
|
720
|
+
return { success: true };
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
function getPageStructure(format) {
|
|
724
|
+
if (format === 'html') {
|
|
725
|
+
return { html: document.documentElement.outerHTML };
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// Simplified: extract semantic structure
|
|
729
|
+
const result = [];
|
|
730
|
+
|
|
731
|
+
function processNode(node, depth) {
|
|
732
|
+
if (depth > 8) return;
|
|
733
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
734
|
+
const text = node.textContent?.trim();
|
|
735
|
+
if (text) result.push({ type: 'text', content: text.slice(0, 200), depth });
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
if (node.nodeType !== Node.ELEMENT_NODE) return;
|
|
739
|
+
|
|
740
|
+
const el = node;
|
|
741
|
+
const tag = el.tagName.toLowerCase();
|
|
742
|
+
const skip = ['script', 'style', 'noscript', 'svg', 'path'];
|
|
743
|
+
if (skip.includes(tag)) return;
|
|
744
|
+
|
|
745
|
+
const attrs = {};
|
|
746
|
+
if (el.id) attrs.id = el.id;
|
|
747
|
+
if (el.className) attrs.class = typeof el.className === 'string' ? el.className.split(' ').filter(Boolean).slice(0, 3).join(' ') : '';
|
|
748
|
+
if (el.href) attrs.href = el.href;
|
|
749
|
+
if (el.src) attrs.src = el.src;
|
|
750
|
+
if (el.type) attrs.type = el.type;
|
|
751
|
+
if (el.name) attrs.name = el.name;
|
|
752
|
+
if (el.value) attrs.value = el.value;
|
|
753
|
+
if (el.placeholder) attrs.placeholder = el.placeholder;
|
|
754
|
+
if (el.getAttribute('aria-label')) attrs['aria-label'] = el.getAttribute('aria-label');
|
|
755
|
+
if (el.getAttribute('role')) attrs.role = el.getAttribute('role');
|
|
756
|
+
|
|
757
|
+
result.push({ type: 'element', tag, attrs, depth });
|
|
758
|
+
|
|
759
|
+
for (const child of el.childNodes) {
|
|
760
|
+
processNode(child, depth + 1);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
processNode(document.body || document.documentElement, 0);
|
|
765
|
+
return { structure: result.slice(0, 500), url: location.href, title: document.title };
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
function findElements(query, selector) {
|
|
769
|
+
const results = [];
|
|
770
|
+
|
|
771
|
+
if (selector) {
|
|
772
|
+
const elements = document.querySelectorAll(selector);
|
|
773
|
+
for (const el of Array.from(elements).slice(0, 20)) {
|
|
774
|
+
const rect = el.getBoundingClientRect();
|
|
775
|
+
results.push({
|
|
776
|
+
tag: el.tagName.toLowerCase(),
|
|
777
|
+
text: el.textContent?.trim().slice(0, 100),
|
|
778
|
+
selector,
|
|
779
|
+
visible: rect.width > 0 && rect.height > 0,
|
|
780
|
+
coords: { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 },
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
} else if (query) {
|
|
784
|
+
// Text-based search
|
|
785
|
+
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_ELEMENT);
|
|
786
|
+
const lowerQuery = query.toLowerCase();
|
|
787
|
+
let node;
|
|
788
|
+
while ((node = walker.nextNode()) && results.length < 20) {
|
|
789
|
+
const el = node;
|
|
790
|
+
const text = el.textContent?.trim() || '';
|
|
791
|
+
const ariaLabel = el.getAttribute('aria-label') || '';
|
|
792
|
+
if (text.toLowerCase().includes(lowerQuery) || ariaLabel.toLowerCase().includes(lowerQuery)) {
|
|
793
|
+
const rect = el.getBoundingClientRect();
|
|
794
|
+
if (rect.width > 0 && rect.height > 0) {
|
|
795
|
+
results.push({
|
|
796
|
+
tag: el.tagName.toLowerCase(),
|
|
797
|
+
text: text.slice(0, 100),
|
|
798
|
+
ariaLabel,
|
|
799
|
+
coords: { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 },
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
return { elements: results, count: results.length };
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
function fillFormField(selector, ref, value, type) {
|
|
810
|
+
let element = null;
|
|
811
|
+
|
|
812
|
+
if (selector) {
|
|
813
|
+
element = document.querySelector(selector);
|
|
814
|
+
} else if (ref) {
|
|
815
|
+
element = document.querySelector(`[data-ref="${ref}"]`) ||
|
|
816
|
+
document.getElementById(ref) ||
|
|
817
|
+
document.querySelector(`[name="${ref}"]`) ||
|
|
818
|
+
document.querySelector(`[placeholder="${ref}"]`);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
if (!element) {
|
|
822
|
+
return { success: false, error: `Element not found: ${selector || ref}` };
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
if (element.focus) element.focus();
|
|
826
|
+
|
|
827
|
+
if (element.tagName === 'SELECT') {
|
|
828
|
+
const option = Array.from(element.options).find(
|
|
829
|
+
(o) => o.value === value || o.text === value
|
|
830
|
+
);
|
|
831
|
+
if (option) {
|
|
832
|
+
element.value = option.value;
|
|
833
|
+
element.dispatchEvent(new Event('change', { bubbles: true }));
|
|
834
|
+
return { success: true, selected: option.text };
|
|
835
|
+
}
|
|
836
|
+
return { success: false, error: `Option not found: ${value}` };
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
if (element.type === 'checkbox' || element.type === 'radio') {
|
|
840
|
+
const checked = value === true || value === 'true' || value === '1';
|
|
841
|
+
element.checked = checked;
|
|
842
|
+
element.dispatchEvent(new Event('change', { bubbles: true }));
|
|
843
|
+
return { success: true, checked };
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
const nativeSetter = Object.getOwnPropertyDescriptor(
|
|
847
|
+
element instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype,
|
|
848
|
+
'value'
|
|
849
|
+
)?.set;
|
|
850
|
+
|
|
851
|
+
if (nativeSetter) {
|
|
852
|
+
nativeSetter.call(element, value);
|
|
853
|
+
} else {
|
|
854
|
+
element.value = value;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
element.dispatchEvent(new Event('input', { bubbles: true }));
|
|
858
|
+
element.dispatchEvent(new Event('change', { bubbles: true }));
|
|
859
|
+
|
|
860
|
+
return { success: true, value };
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
function getVisibleText() {
|
|
864
|
+
const walk = (node) => {
|
|
865
|
+
if (node.nodeType === Node.TEXT_NODE) return node.textContent || '';
|
|
866
|
+
if (['SCRIPT', 'STYLE', 'NOSCRIPT'].includes(node.tagName)) return '';
|
|
867
|
+
return Array.from(node.childNodes).map(walk).join(' ');
|
|
868
|
+
};
|
|
869
|
+
|
|
870
|
+
const text = walk(document.body)
|
|
871
|
+
.replace(/\s+/g, ' ')
|
|
872
|
+
.trim()
|
|
873
|
+
.slice(0, 50000);
|
|
874
|
+
|
|
875
|
+
return { text, url: location.href, title: document.title, length: text.length };
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
function setFileInputValue(selector, imageData, mediaType) {
|
|
879
|
+
const input = document.querySelector(selector);
|
|
880
|
+
if (!input || input.type !== 'file') {
|
|
881
|
+
return { success: false, error: 'File input not found' };
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
try {
|
|
885
|
+
const byteString = atob(imageData);
|
|
886
|
+
const ab = new ArrayBuffer(byteString.length);
|
|
887
|
+
const ia = new Uint8Array(ab);
|
|
888
|
+
for (let i = 0; i < byteString.length; i++) ia[i] = byteString.charCodeAt(i);
|
|
889
|
+
const blob = new Blob([ab], { type: mediaType });
|
|
890
|
+
const file = new File([blob], 'upload.png', { type: mediaType });
|
|
891
|
+
const dt = new DataTransfer();
|
|
892
|
+
dt.items.add(file);
|
|
893
|
+
input.files = dt.files;
|
|
894
|
+
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
895
|
+
return { success: true };
|
|
896
|
+
} catch (err) {
|
|
897
|
+
return { success: false, error: err.message };
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
// ─── Init ────────────────────────────────────────────────────────────────────
|
|
902
|
+
|
|
903
|
+
chrome.runtime.onInstalled.addListener(() => {
|
|
904
|
+
updateIcon(false);
|
|
905
|
+
});
|
|
906
|
+
|
|
907
|
+
chrome.runtime.onStartup.addListener(() => {
|
|
908
|
+
connectNativeHost();
|
|
909
|
+
});
|
|
910
|
+
|
|
911
|
+
// Connect when extension loads
|
|
912
|
+
connectNativeHost();
|