@opentabs-dev/browser-extension 0.0.67 → 0.0.68

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 (38) hide show
  1. package/manifest.json +1 -1
  2. package/package.json +1 -1
  3. package/dist/bg-log-state.d.ts +0 -10
  4. package/dist/bg-log-state.d.ts.map +0 -1
  5. package/dist/bg-log-state.js +0 -11
  6. package/dist/bg-log-state.js.map +0 -1
  7. package/dist/browser-commands.d.ts +0 -36
  8. package/dist/browser-commands.d.ts.map +0 -1
  9. package/dist/browser-commands.js +0 -1931
  10. package/dist/browser-commands.js.map +0 -1
  11. package/dist/resource-prompt-dispatch.d.ts +0 -14
  12. package/dist/resource-prompt-dispatch.d.ts.map +0 -1
  13. package/dist/resource-prompt-dispatch.js +0 -225
  14. package/dist/resource-prompt-dispatch.js.map +0 -1
  15. package/dist/side-panel/components/ChromeIcon.d.ts +0 -5
  16. package/dist/side-panel/components/ChromeIcon.d.ts.map +0 -1
  17. package/dist/side-panel/components/ChromeIcon.js +0 -4
  18. package/dist/side-panel/components/ChromeIcon.js.map +0 -1
  19. package/dist/side-panel/components/Header.d.ts +0 -5
  20. package/dist/side-panel/components/Header.d.ts.map +0 -1
  21. package/dist/side-panel/components/Header.js +0 -6
  22. package/dist/side-panel/components/Header.js.map +0 -1
  23. package/dist/side-panel/components/VersionMismatchBanner.d.ts +0 -3
  24. package/dist/side-panel/components/VersionMismatchBanner.d.ts.map +0 -1
  25. package/dist/side-panel/components/VersionMismatchBanner.js +0 -5
  26. package/dist/side-panel/components/VersionMismatchBanner.js.map +0 -1
  27. package/dist/side-panel/lib/utils.d.ts +0 -3
  28. package/dist/side-panel/lib/utils.d.ts.map +0 -1
  29. package/dist/side-panel/lib/utils.js +0 -4
  30. package/dist/side-panel/lib/utils.js.map +0 -1
  31. package/dist/side-panel-state.d.ts +0 -11
  32. package/dist/side-panel-state.d.ts.map +0 -1
  33. package/dist/side-panel-state.js +0 -38
  34. package/dist/side-panel-state.js.map +0 -1
  35. package/dist/types.d.ts +0 -82
  36. package/dist/types.d.ts.map +0 -1
  37. package/dist/types.js +0 -2
  38. package/dist/types.js.map +0 -1
@@ -1,1931 +0,0 @@
1
- import { bgLogCollector } from './bg-log-state.js';
2
- import { IS_READY_TIMEOUT_MS, SCRIPT_TIMEOUT_MS, WS_CONNECTED_KEY } from './constants.js';
3
- import { sendToServer } from './messaging.js';
4
- import { isCapturing, startCapture, stopCapture, getRequests, getConsoleLogs, clearConsoleLogs, getActiveCapturesSummary, } from './network-capture.js';
5
- import { getAllPluginMeta, getPluginMeta } from './plugin-storage.js';
6
- import { sanitizeErrorMessage } from './sanitize-error.js';
7
- import { findAllMatchingTabs } from './tab-matching.js';
8
- import { getLastKnownStates } from './tab-state.js';
9
- import { isBlockedUrlScheme } from '@opentabs-dev/shared';
10
- /** MIME types that represent text content and should be decoded from base64 */
11
- const TEXT_MIME_PREFIXES = ['text/'];
12
- const TEXT_MIME_EXACT = new Set([
13
- 'application/javascript',
14
- 'application/json',
15
- 'application/xml',
16
- 'application/xhtml+xml',
17
- 'application/x-javascript',
18
- 'application/ecmascript',
19
- ]);
20
- const isTextMimeType = (mimeType) => {
21
- if (TEXT_MIME_PREFIXES.some(prefix => mimeType.startsWith(prefix)))
22
- return true;
23
- return TEXT_MIME_EXACT.has(mimeType);
24
- };
25
- /**
26
- * Find the frameId that owns a resource URL by walking the CDP resource tree.
27
- * Returns the frame ID or null if the resource is not found in any frame.
28
- */
29
- const findFrameForResource = (tree, targetUrl) => {
30
- for (const r of tree.resources) {
31
- if (r.url === targetUrl) {
32
- return { frameId: tree.frame.id, mimeType: r.mimeType };
33
- }
34
- }
35
- if (tree.childFrames) {
36
- for (const child of tree.childFrames) {
37
- const found = findFrameForResource(child, targetUrl);
38
- if (found)
39
- return found;
40
- }
41
- }
42
- return null;
43
- };
44
- /**
45
- * Manages Chrome debugger attach/detach lifecycle for commands that need CDP access.
46
- * Reuses an existing debugger session (from network capture) if one is active,
47
- * otherwise temporarily attaches and detaches in the finally block.
48
- */
49
- const withDebugger = async (tabId, fn) => {
50
- const alreadyAttached = isCapturing(tabId);
51
- if (!alreadyAttached) {
52
- try {
53
- await chrome.debugger.attach({ tabId }, '1.3');
54
- }
55
- catch (err) {
56
- const msg = err instanceof Error ? err.message : String(err);
57
- throw new Error(msg.includes('Another debugger')
58
- ? 'Failed to attach debugger — another debugger (e.g., DevTools) is already attached. ' +
59
- 'Close DevTools or enable network capture first (browser_enable_network_capture) ' +
60
- 'so this tool can reuse the existing debugger session.'
61
- : `Failed to attach debugger: ${sanitizeErrorMessage(msg)}`);
62
- }
63
- }
64
- try {
65
- return await fn();
66
- }
67
- finally {
68
- if (!alreadyAttached) {
69
- await chrome.debugger.detach({ tabId }).catch(() => { });
70
- }
71
- }
72
- };
73
- export const handleBrowserListTabs = async (id) => {
74
- try {
75
- const tabs = await chrome.tabs.query({});
76
- const result = tabs.map(tab => ({
77
- id: tab.id,
78
- title: tab.title ?? '',
79
- url: tab.url ?? '',
80
- active: tab.active,
81
- windowId: tab.windowId,
82
- }));
83
- sendToServer({ jsonrpc: '2.0', result, id });
84
- }
85
- catch (err) {
86
- sendToServer({
87
- jsonrpc: '2.0',
88
- error: { code: -32603, message: sanitizeErrorMessage(err instanceof Error ? err.message : String(err)) },
89
- id,
90
- });
91
- }
92
- };
93
- export const handleBrowserOpenTab = async (params, id) => {
94
- try {
95
- const url = params.url;
96
- if (typeof url !== 'string') {
97
- sendToServer({ jsonrpc: '2.0', error: { code: -32602, message: 'Missing or invalid url parameter' }, id });
98
- return;
99
- }
100
- if (isBlockedUrlScheme(url)) {
101
- sendToServer({
102
- jsonrpc: '2.0',
103
- error: {
104
- code: -32602,
105
- message: 'URL scheme not allowed (javascript:, data:, file:, chrome:, blob: are blocked)',
106
- },
107
- id,
108
- });
109
- return;
110
- }
111
- const tab = await chrome.tabs.create({ url });
112
- sendToServer({
113
- jsonrpc: '2.0',
114
- result: { id: tab.id, title: tab.title ?? '', url: tab.url ?? url, windowId: tab.windowId },
115
- id,
116
- });
117
- }
118
- catch (err) {
119
- sendToServer({
120
- jsonrpc: '2.0',
121
- error: { code: -32603, message: sanitizeErrorMessage(err instanceof Error ? err.message : String(err)) },
122
- id,
123
- });
124
- }
125
- };
126
- export const handleBrowserCloseTab = async (params, id) => {
127
- try {
128
- const tabId = params.tabId;
129
- if (typeof tabId !== 'number') {
130
- sendToServer({ jsonrpc: '2.0', error: { code: -32602, message: 'Missing or invalid tabId parameter' }, id });
131
- return;
132
- }
133
- await chrome.tabs.remove(tabId);
134
- sendToServer({ jsonrpc: '2.0', result: { ok: true }, id });
135
- }
136
- catch (err) {
137
- sendToServer({
138
- jsonrpc: '2.0',
139
- error: { code: -32603, message: sanitizeErrorMessage(err instanceof Error ? err.message : String(err)) },
140
- id,
141
- });
142
- }
143
- };
144
- export const handleBrowserNavigateTab = async (params, id) => {
145
- try {
146
- const tabId = params.tabId;
147
- const url = params.url;
148
- if (typeof tabId !== 'number') {
149
- sendToServer({ jsonrpc: '2.0', error: { code: -32602, message: 'Missing or invalid tabId parameter' }, id });
150
- return;
151
- }
152
- if (typeof url !== 'string') {
153
- sendToServer({ jsonrpc: '2.0', error: { code: -32602, message: 'Missing or invalid url parameter' }, id });
154
- return;
155
- }
156
- if (isBlockedUrlScheme(url)) {
157
- sendToServer({
158
- jsonrpc: '2.0',
159
- error: {
160
- code: -32602,
161
- message: 'URL scheme not allowed (javascript:, data:, file:, chrome:, blob: are blocked)',
162
- },
163
- id,
164
- });
165
- return;
166
- }
167
- const tab = await chrome.tabs.update(tabId, { url });
168
- sendToServer({
169
- jsonrpc: '2.0',
170
- result: { id: tab?.id ?? tabId, title: tab?.title ?? '', url: tab?.url ?? url },
171
- id,
172
- });
173
- }
174
- catch (err) {
175
- sendToServer({
176
- jsonrpc: '2.0',
177
- error: { code: -32603, message: sanitizeErrorMessage(err instanceof Error ? err.message : String(err)) },
178
- id,
179
- });
180
- }
181
- };
182
- export const handleBrowserFocusTab = async (params, id) => {
183
- try {
184
- const tabId = params.tabId;
185
- if (typeof tabId !== 'number') {
186
- sendToServer({ jsonrpc: '2.0', error: { code: -32602, message: 'Missing or invalid tabId parameter' }, id });
187
- return;
188
- }
189
- const tab = await chrome.tabs.update(tabId, { active: true });
190
- if (!tab) {
191
- sendToServer({ jsonrpc: '2.0', error: { code: -32602, message: `Tab ${tabId} not found` }, id });
192
- return;
193
- }
194
- await chrome.windows.update(tab.windowId, { focused: true });
195
- sendToServer({
196
- jsonrpc: '2.0',
197
- result: { id: tab.id, title: tab.title ?? '', url: tab.url ?? '', active: true },
198
- id,
199
- });
200
- }
201
- catch (err) {
202
- sendToServer({
203
- jsonrpc: '2.0',
204
- error: { code: -32603, message: sanitizeErrorMessage(err instanceof Error ? err.message : String(err)) },
205
- id,
206
- });
207
- }
208
- };
209
- export const handleBrowserGetTabInfo = async (params, id) => {
210
- try {
211
- const tabId = params.tabId;
212
- if (typeof tabId !== 'number') {
213
- sendToServer({ jsonrpc: '2.0', error: { code: -32602, message: 'Missing or invalid tabId parameter' }, id });
214
- return;
215
- }
216
- const tab = await chrome.tabs.get(tabId);
217
- sendToServer({
218
- jsonrpc: '2.0',
219
- result: {
220
- id: tab.id,
221
- title: tab.title ?? '',
222
- url: tab.url ?? '',
223
- status: tab.status ?? '',
224
- active: tab.active,
225
- windowId: tab.windowId,
226
- favIconUrl: tab.favIconUrl ?? '',
227
- incognito: tab.incognito,
228
- },
229
- id,
230
- });
231
- }
232
- catch (err) {
233
- sendToServer({
234
- jsonrpc: '2.0',
235
- error: { code: -32603, message: sanitizeErrorMessage(err instanceof Error ? err.message : String(err)) },
236
- id,
237
- });
238
- }
239
- };
240
- export const handleBrowserScreenshotTab = async (params, id) => {
241
- try {
242
- const tabId = params.tabId;
243
- if (typeof tabId !== 'number') {
244
- sendToServer({ jsonrpc: '2.0', error: { code: -32602, message: 'Missing or invalid tabId parameter' }, id });
245
- return;
246
- }
247
- const tab = await chrome.tabs.update(tabId, { active: true });
248
- if (!tab) {
249
- sendToServer({ jsonrpc: '2.0', error: { code: -32602, message: `Tab ${tabId} not found` }, id });
250
- return;
251
- }
252
- await chrome.windows.update(tab.windowId, { focused: true });
253
- // Small delay for the tab to render after focus
254
- await new Promise(resolve => setTimeout(resolve, 100));
255
- const dataUrl = await chrome.tabs.captureVisibleTab(tab.windowId, { format: 'png' });
256
- const base64 = dataUrl.replace(/^data:image\/png;base64,/, '');
257
- sendToServer({ jsonrpc: '2.0', result: { image: base64 }, id });
258
- }
259
- catch (err) {
260
- sendToServer({
261
- jsonrpc: '2.0',
262
- error: { code: -32603, message: sanitizeErrorMessage(err instanceof Error ? err.message : String(err)) },
263
- id,
264
- });
265
- }
266
- };
267
- export const handleBrowserGetTabContent = async (params, id) => {
268
- try {
269
- const tabId = params.tabId;
270
- if (typeof tabId !== 'number') {
271
- sendToServer({ jsonrpc: '2.0', error: { code: -32602, message: 'Missing or invalid tabId parameter' }, id });
272
- return;
273
- }
274
- const selector = typeof params.selector === 'string' ? params.selector : 'body';
275
- const maxLength = typeof params.maxLength === 'number' ? params.maxLength : 50000;
276
- const results = await chrome.scripting.executeScript({
277
- target: { tabId },
278
- world: 'MAIN',
279
- func: (sel, max) => {
280
- const el = document.querySelector(sel);
281
- if (!el)
282
- return { error: `Element not found: ${sel}` };
283
- return {
284
- title: document.title,
285
- url: document.URL,
286
- content: (el.innerText || '').trim().slice(0, max),
287
- };
288
- },
289
- args: [selector, maxLength],
290
- });
291
- const result = results[0]?.result;
292
- if (!result) {
293
- sendToServer({ jsonrpc: '2.0', error: { code: -32603, message: 'No result from script execution' }, id });
294
- return;
295
- }
296
- if (result.error) {
297
- sendToServer({ jsonrpc: '2.0', error: { code: -32602, message: result.error }, id });
298
- return;
299
- }
300
- sendToServer({ jsonrpc: '2.0', result: { title: result.title, url: result.url, content: result.content }, id });
301
- }
302
- catch (err) {
303
- sendToServer({
304
- jsonrpc: '2.0',
305
- error: { code: -32603, message: sanitizeErrorMessage(err instanceof Error ? err.message : String(err)) },
306
- id,
307
- });
308
- }
309
- };
310
- export const handleBrowserGetPageHtml = async (params, id) => {
311
- try {
312
- const tabId = params.tabId;
313
- if (typeof tabId !== 'number') {
314
- sendToServer({ jsonrpc: '2.0', error: { code: -32602, message: 'Missing or invalid tabId parameter' }, id });
315
- return;
316
- }
317
- const selector = typeof params.selector === 'string' ? params.selector : 'html';
318
- const maxLength = typeof params.maxLength === 'number' ? params.maxLength : 200000;
319
- const results = await chrome.scripting.executeScript({
320
- target: { tabId },
321
- world: 'MAIN',
322
- func: (sel, max) => {
323
- const el = document.querySelector(sel);
324
- if (!el)
325
- return { error: `Element not found: ${sel}` };
326
- const html = el.outerHTML;
327
- return {
328
- title: document.title,
329
- url: document.URL,
330
- html: html.length > max ? html.slice(0, max) + '... (truncated)' : html,
331
- };
332
- },
333
- args: [selector, maxLength],
334
- });
335
- const result = results[0]?.result;
336
- if (!result) {
337
- sendToServer({ jsonrpc: '2.0', error: { code: -32603, message: 'No result from script execution' }, id });
338
- return;
339
- }
340
- if (result.error) {
341
- sendToServer({ jsonrpc: '2.0', error: { code: -32602, message: result.error }, id });
342
- return;
343
- }
344
- sendToServer({ jsonrpc: '2.0', result: { title: result.title, url: result.url, html: result.html }, id });
345
- }
346
- catch (err) {
347
- sendToServer({
348
- jsonrpc: '2.0',
349
- error: { code: -32603, message: sanitizeErrorMessage(err instanceof Error ? err.message : String(err)) },
350
- id,
351
- });
352
- }
353
- };
354
- export const handleBrowserGetStorage = async (params, id) => {
355
- try {
356
- const tabId = params.tabId;
357
- if (typeof tabId !== 'number') {
358
- sendToServer({ jsonrpc: '2.0', error: { code: -32602, message: 'Missing or invalid tabId parameter' }, id });
359
- return;
360
- }
361
- const storageType = typeof params.storageType === 'string' ? params.storageType : 'local';
362
- if (storageType !== 'local' && storageType !== 'session') {
363
- sendToServer({
364
- jsonrpc: '2.0',
365
- error: { code: -32602, message: "storageType must be 'local' or 'session'" },
366
- id,
367
- });
368
- return;
369
- }
370
- const key = typeof params.key === 'string' ? params.key : undefined;
371
- const MAX_VALUE_LENGTH = 10000;
372
- const MAX_RESPONSE_LENGTH = 500_000;
373
- const results = await chrome.scripting.executeScript({
374
- target: { tabId },
375
- world: 'MAIN',
376
- func: (type, k, maxVal, maxResp) => {
377
- const storage = type === 'session' ? window.sessionStorage : window.localStorage;
378
- if (k !== null) {
379
- const value = storage.getItem(k);
380
- return {
381
- mode: 'single',
382
- key: k,
383
- value: value !== null && value.length > maxVal ? value.slice(0, maxVal) + '... (truncated)' : value,
384
- };
385
- }
386
- const entries = [];
387
- let totalLength = 0;
388
- const keys = Object.keys(storage);
389
- for (const entryKey of keys) {
390
- const raw = storage.getItem(entryKey);
391
- if (raw === null)
392
- continue;
393
- const value = raw.length > maxVal ? raw.slice(0, maxVal) + '... (truncated)' : raw;
394
- const entryLength = entryKey.length + value.length;
395
- if (totalLength + entryLength > maxResp)
396
- break;
397
- entries.push({ key: entryKey, value });
398
- totalLength += entryLength;
399
- }
400
- return { mode: 'all', entries, count: keys.length };
401
- },
402
- args: [storageType, key ?? null, MAX_VALUE_LENGTH, MAX_RESPONSE_LENGTH],
403
- });
404
- const result = results[0]?.result;
405
- if (!result) {
406
- sendToServer({ jsonrpc: '2.0', error: { code: -32603, message: 'No result from script execution' }, id });
407
- return;
408
- }
409
- if (result.mode === 'single') {
410
- sendToServer({ jsonrpc: '2.0', result: { key: result.key, value: result.value }, id });
411
- }
412
- else {
413
- sendToServer({ jsonrpc: '2.0', result: { entries: result.entries, count: result.count }, id });
414
- }
415
- }
416
- catch (err) {
417
- sendToServer({
418
- jsonrpc: '2.0',
419
- error: { code: -32603, message: sanitizeErrorMessage(err instanceof Error ? err.message : String(err)) },
420
- id,
421
- });
422
- }
423
- };
424
- export const handleBrowserClickElement = async (params, id) => {
425
- try {
426
- const tabId = params.tabId;
427
- if (typeof tabId !== 'number') {
428
- sendToServer({ jsonrpc: '2.0', error: { code: -32602, message: 'Missing or invalid tabId parameter' }, id });
429
- return;
430
- }
431
- const selector = params.selector;
432
- if (typeof selector !== 'string' || selector.length === 0) {
433
- sendToServer({ jsonrpc: '2.0', error: { code: -32602, message: 'Missing or invalid selector parameter' }, id });
434
- return;
435
- }
436
- const results = await chrome.scripting.executeScript({
437
- target: { tabId },
438
- world: 'MAIN',
439
- func: (sel) => {
440
- const el = document.querySelector(sel);
441
- if (!el)
442
- return { error: `Element not found: ${sel}` };
443
- el.click();
444
- return {
445
- clicked: true,
446
- tagName: el.tagName.toLowerCase(),
447
- text: (el.textContent || '').trim().slice(0, 200),
448
- };
449
- },
450
- args: [selector],
451
- });
452
- const result = results[0]?.result;
453
- if (!result) {
454
- sendToServer({ jsonrpc: '2.0', error: { code: -32603, message: 'No result from script execution' }, id });
455
- return;
456
- }
457
- if (result.error) {
458
- sendToServer({ jsonrpc: '2.0', error: { code: -32602, message: result.error }, id });
459
- return;
460
- }
461
- sendToServer({
462
- jsonrpc: '2.0',
463
- result: { clicked: result.clicked, tagName: result.tagName, text: result.text },
464
- id,
465
- });
466
- }
467
- catch (err) {
468
- sendToServer({
469
- jsonrpc: '2.0',
470
- error: { code: -32603, message: sanitizeErrorMessage(err instanceof Error ? err.message : String(err)) },
471
- id,
472
- });
473
- }
474
- };
475
- export const handleBrowserTypeText = async (params, id) => {
476
- try {
477
- const tabId = params.tabId;
478
- if (typeof tabId !== 'number') {
479
- sendToServer({ jsonrpc: '2.0', error: { code: -32602, message: 'Missing or invalid tabId parameter' }, id });
480
- return;
481
- }
482
- const selector = params.selector;
483
- if (typeof selector !== 'string' || selector.length === 0) {
484
- sendToServer({ jsonrpc: '2.0', error: { code: -32602, message: 'Missing or invalid selector parameter' }, id });
485
- return;
486
- }
487
- const text = params.text;
488
- if (typeof text !== 'string') {
489
- sendToServer({ jsonrpc: '2.0', error: { code: -32602, message: 'Missing or invalid text parameter' }, id });
490
- return;
491
- }
492
- const clear = typeof params.clear === 'boolean' ? params.clear : true;
493
- const results = await chrome.scripting.executeScript({
494
- target: { tabId },
495
- world: 'MAIN',
496
- func: (sel, txt, clr) => {
497
- const el = document.querySelector(sel);
498
- if (!el)
499
- return { error: `Element not found: ${sel}` };
500
- const tag = el.tagName.toLowerCase();
501
- const isEditable = tag === 'input' || tag === 'textarea' || el.isContentEditable;
502
- if (!isEditable)
503
- return { error: `Element is not a text input (found <${tag}>)` };
504
- if (tag === 'input' || tag === 'textarea') {
505
- const input = el;
506
- input.focus();
507
- input.value = clr ? txt : input.value + txt;
508
- input.dispatchEvent(new Event('input', { bubbles: true }));
509
- input.dispatchEvent(new Event('change', { bubbles: true }));
510
- return { typed: true, tagName: tag, value: input.value };
511
- }
512
- // contentEditable element — insert via Selection/Range API
513
- const htmlEl = el;
514
- htmlEl.focus();
515
- if (clr)
516
- htmlEl.textContent = '';
517
- const selection = window.getSelection();
518
- if (selection) {
519
- if (selection.rangeCount === 0) {
520
- const range = document.createRange();
521
- range.selectNodeContents(htmlEl);
522
- range.collapse(false);
523
- selection.removeAllRanges();
524
- selection.addRange(range);
525
- }
526
- const range = selection.getRangeAt(0);
527
- range.deleteContents();
528
- range.insertNode(document.createTextNode(txt));
529
- range.collapse(false);
530
- selection.removeAllRanges();
531
- selection.addRange(range);
532
- }
533
- htmlEl.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'insertText', data: txt }));
534
- return { typed: true, tagName: tag, value: htmlEl.textContent || '' };
535
- },
536
- args: [selector, text, clear],
537
- });
538
- const result = results[0]?.result;
539
- if (!result) {
540
- sendToServer({ jsonrpc: '2.0', error: { code: -32603, message: 'No result from script execution' }, id });
541
- return;
542
- }
543
- if (result.error) {
544
- sendToServer({ jsonrpc: '2.0', error: { code: -32602, message: result.error }, id });
545
- return;
546
- }
547
- sendToServer({ jsonrpc: '2.0', result: { typed: result.typed, tagName: result.tagName, value: result.value }, id });
548
- }
549
- catch (err) {
550
- sendToServer({
551
- jsonrpc: '2.0',
552
- error: { code: -32603, message: sanitizeErrorMessage(err instanceof Error ? err.message : String(err)) },
553
- id,
554
- });
555
- }
556
- };
557
- export const handleBrowserSelectOption = async (params, id) => {
558
- try {
559
- const tabId = params.tabId;
560
- if (typeof tabId !== 'number') {
561
- sendToServer({ jsonrpc: '2.0', error: { code: -32602, message: 'Missing or invalid tabId parameter' }, id });
562
- return;
563
- }
564
- const selector = params.selector;
565
- if (typeof selector !== 'string' || selector.length === 0) {
566
- sendToServer({ jsonrpc: '2.0', error: { code: -32602, message: 'Missing or invalid selector parameter' }, id });
567
- return;
568
- }
569
- const value = typeof params.value === 'string' ? params.value : undefined;
570
- const label = typeof params.label === 'string' ? params.label : undefined;
571
- if (value === undefined && label === undefined) {
572
- sendToServer({
573
- jsonrpc: '2.0',
574
- error: { code: -32602, message: 'At least one of value or label must be provided' },
575
- id,
576
- });
577
- return;
578
- }
579
- const results = await chrome.scripting.executeScript({
580
- target: { tabId },
581
- world: 'MAIN',
582
- func: (sel, val, lbl) => {
583
- const el = document.querySelector(sel);
584
- if (!el)
585
- return { error: `Element not found: ${sel}` };
586
- if (el.tagName.toLowerCase() !== 'select')
587
- return { error: `Element is not a <select>: ${sel}` };
588
- const select = el;
589
- const options = Array.from(select.options);
590
- let matchedIndex = -1;
591
- for (let i = 0; i < options.length; i++) {
592
- const opt = options[i];
593
- if (!opt)
594
- continue;
595
- const isMatch = val !== null ? opt.value === val : (opt.textContent || '').trim() === lbl;
596
- if (isMatch) {
597
- matchedIndex = i;
598
- break;
599
- }
600
- }
601
- if (matchedIndex === -1) {
602
- const criterion = val !== null ? `value="${val}"` : `label="${String(lbl)}"`;
603
- return { error: `Option not found: ${criterion}` };
604
- }
605
- select.selectedIndex = matchedIndex;
606
- select.dispatchEvent(new Event('change', { bubbles: true }));
607
- const selectedOpt = options[matchedIndex];
608
- return {
609
- selected: true,
610
- value: selectedOpt ? selectedOpt.value : '',
611
- label: selectedOpt ? (selectedOpt.textContent || '').trim() : '',
612
- };
613
- },
614
- args: [selector, value ?? null, label ?? null],
615
- });
616
- const result = results[0]?.result;
617
- if (!result) {
618
- sendToServer({ jsonrpc: '2.0', error: { code: -32603, message: 'No result from script execution' }, id });
619
- return;
620
- }
621
- if (result.error) {
622
- sendToServer({ jsonrpc: '2.0', error: { code: -32602, message: result.error }, id });
623
- return;
624
- }
625
- sendToServer({
626
- jsonrpc: '2.0',
627
- result: { selected: result.selected, value: result.value, label: result.label },
628
- id,
629
- });
630
- }
631
- catch (err) {
632
- sendToServer({
633
- jsonrpc: '2.0',
634
- error: { code: -32603, message: sanitizeErrorMessage(err instanceof Error ? err.message : String(err)) },
635
- id,
636
- });
637
- }
638
- };
639
- export const handleBrowserWaitForElement = async (params, id) => {
640
- try {
641
- const tabId = params.tabId;
642
- if (typeof tabId !== 'number') {
643
- sendToServer({ jsonrpc: '2.0', error: { code: -32602, message: 'Missing or invalid tabId parameter' }, id });
644
- return;
645
- }
646
- const selector = params.selector;
647
- if (typeof selector !== 'string' || selector.length === 0) {
648
- sendToServer({ jsonrpc: '2.0', error: { code: -32602, message: 'Missing or invalid selector parameter' }, id });
649
- return;
650
- }
651
- const timeout = typeof params.timeout === 'number' ? params.timeout : 10000;
652
- const visible = typeof params.visible === 'boolean' ? params.visible : false;
653
- const results = await chrome.scripting.executeScript({
654
- target: { tabId },
655
- world: 'MAIN',
656
- func: (sel, tmo, vis) => new Promise(resolve => {
657
- let elapsed = 0;
658
- const poll = setInterval(() => {
659
- const el = document.querySelector(sel);
660
- if (el) {
661
- const htmlEl = el;
662
- const isVisible = !vis || htmlEl.offsetParent !== null || getComputedStyle(htmlEl).display !== 'none';
663
- if (isVisible) {
664
- clearInterval(poll);
665
- resolve({
666
- found: true,
667
- tagName: el.tagName.toLowerCase(),
668
- text: (el.textContent || '').trim().slice(0, 200),
669
- });
670
- return;
671
- }
672
- }
673
- elapsed += 100;
674
- if (elapsed >= tmo) {
675
- clearInterval(poll);
676
- resolve({ error: `Timeout waiting for element: ${sel} (${tmo}ms)` });
677
- }
678
- }, 100);
679
- }),
680
- args: [selector, timeout, visible],
681
- });
682
- const result = results[0]?.result;
683
- if (!result) {
684
- sendToServer({ jsonrpc: '2.0', error: { code: -32603, message: 'No result from script execution' }, id });
685
- return;
686
- }
687
- if (result.error) {
688
- sendToServer({ jsonrpc: '2.0', error: { code: -32602, message: result.error }, id });
689
- return;
690
- }
691
- sendToServer({
692
- jsonrpc: '2.0',
693
- result: { found: result.found, tagName: result.tagName, text: result.text },
694
- id,
695
- });
696
- }
697
- catch (err) {
698
- sendToServer({
699
- jsonrpc: '2.0',
700
- error: { code: -32603, message: sanitizeErrorMessage(err instanceof Error ? err.message : String(err)) },
701
- id,
702
- });
703
- }
704
- };
705
- export const handleBrowserQueryElements = async (params, id) => {
706
- try {
707
- const tabId = params.tabId;
708
- if (typeof tabId !== 'number') {
709
- sendToServer({ jsonrpc: '2.0', error: { code: -32602, message: 'Missing or invalid tabId parameter' }, id });
710
- return;
711
- }
712
- const selector = params.selector;
713
- if (typeof selector !== 'string' || selector.length === 0) {
714
- sendToServer({ jsonrpc: '2.0', error: { code: -32602, message: 'Missing or invalid selector parameter' }, id });
715
- return;
716
- }
717
- const limit = typeof params.limit === 'number' ? params.limit : 100;
718
- const attributes = Array.isArray(params.attributes)
719
- ? params.attributes.filter((a) => typeof a === 'string')
720
- : ['id', 'class', 'href', 'src', 'type', 'name', 'value', 'placeholder'];
721
- const results = await chrome.scripting.executeScript({
722
- target: { tabId },
723
- world: 'MAIN',
724
- func: (sel, lim, attrs) => {
725
- const all = document.querySelectorAll(sel);
726
- const elements = Array.from(all)
727
- .slice(0, lim)
728
- .map(el => ({
729
- tagName: el.tagName.toLowerCase(),
730
- text: (el.textContent || '').trim().slice(0, 200),
731
- attributes: Object.fromEntries(attrs.filter(a => el.hasAttribute(a)).map(a => [a, el.getAttribute(a)])),
732
- }));
733
- return { count: all.length, elements };
734
- },
735
- args: [selector, limit, attributes],
736
- });
737
- const result = results[0]?.result;
738
- if (!result) {
739
- sendToServer({ jsonrpc: '2.0', error: { code: -32603, message: 'No result from script execution' }, id });
740
- return;
741
- }
742
- sendToServer({ jsonrpc: '2.0', result: { count: result.count, elements: result.elements }, id });
743
- }
744
- catch (err) {
745
- sendToServer({
746
- jsonrpc: '2.0',
747
- error: { code: -32603, message: sanitizeErrorMessage(err instanceof Error ? err.message : String(err)) },
748
- id,
749
- });
750
- }
751
- };
752
- export const handleBrowserGetCookies = async (params, id) => {
753
- try {
754
- const url = params.url;
755
- if (typeof url !== 'string') {
756
- sendToServer({ jsonrpc: '2.0', error: { code: -32602, message: 'Missing or invalid url parameter' }, id });
757
- return;
758
- }
759
- if (isBlockedUrlScheme(url)) {
760
- sendToServer({
761
- jsonrpc: '2.0',
762
- error: {
763
- code: -32602,
764
- message: 'URL scheme not allowed (javascript:, data:, file:, chrome:, blob: are blocked)',
765
- },
766
- id,
767
- });
768
- return;
769
- }
770
- const filter = { url };
771
- const name = params.name;
772
- if (typeof name === 'string') {
773
- filter.name = name;
774
- }
775
- const cookies = await chrome.cookies.getAll(filter);
776
- sendToServer({
777
- jsonrpc: '2.0',
778
- result: {
779
- cookies: cookies.map(c => ({
780
- name: c.name,
781
- value: c.value,
782
- domain: c.domain,
783
- path: c.path,
784
- secure: c.secure,
785
- httpOnly: c.httpOnly,
786
- sameSite: c.sameSite,
787
- expirationDate: c.expirationDate,
788
- })),
789
- },
790
- id,
791
- });
792
- }
793
- catch (err) {
794
- sendToServer({
795
- jsonrpc: '2.0',
796
- error: { code: -32603, message: sanitizeErrorMessage(err instanceof Error ? err.message : String(err)) },
797
- id,
798
- });
799
- }
800
- };
801
- export const handleBrowserSetCookie = async (params, id) => {
802
- try {
803
- const url = params.url;
804
- if (typeof url !== 'string') {
805
- sendToServer({ jsonrpc: '2.0', error: { code: -32602, message: 'Missing or invalid url parameter' }, id });
806
- return;
807
- }
808
- if (isBlockedUrlScheme(url)) {
809
- sendToServer({
810
- jsonrpc: '2.0',
811
- error: {
812
- code: -32602,
813
- message: 'URL scheme not allowed (javascript:, data:, file:, chrome:, blob: are blocked)',
814
- },
815
- id,
816
- });
817
- return;
818
- }
819
- const name = params.name;
820
- if (typeof name !== 'string' || name.length === 0) {
821
- sendToServer({ jsonrpc: '2.0', error: { code: -32602, message: 'Missing or invalid name parameter' }, id });
822
- return;
823
- }
824
- const value = params.value;
825
- if (typeof value !== 'string') {
826
- sendToServer({ jsonrpc: '2.0', error: { code: -32602, message: 'Missing or invalid value parameter' }, id });
827
- return;
828
- }
829
- const details = { url, name, value };
830
- if (typeof params.domain === 'string')
831
- details.domain = params.domain;
832
- if (typeof params.path === 'string')
833
- details.path = params.path;
834
- if (typeof params.secure === 'boolean')
835
- details.secure = params.secure;
836
- if (typeof params.httpOnly === 'boolean')
837
- details.httpOnly = params.httpOnly;
838
- if (typeof params.expirationDate === 'number')
839
- details.expirationDate = params.expirationDate;
840
- const cookie = await chrome.cookies.set(details);
841
- if (!cookie) {
842
- sendToServer({ jsonrpc: '2.0', error: { code: -32603, message: 'Failed to set cookie' }, id });
843
- return;
844
- }
845
- sendToServer({
846
- jsonrpc: '2.0',
847
- result: {
848
- name: cookie.name,
849
- value: cookie.value,
850
- domain: cookie.domain,
851
- path: cookie.path,
852
- secure: cookie.secure,
853
- httpOnly: cookie.httpOnly,
854
- sameSite: cookie.sameSite,
855
- expirationDate: cookie.expirationDate,
856
- },
857
- id,
858
- });
859
- }
860
- catch (err) {
861
- sendToServer({
862
- jsonrpc: '2.0',
863
- error: { code: -32603, message: sanitizeErrorMessage(err instanceof Error ? err.message : String(err)) },
864
- id,
865
- });
866
- }
867
- };
868
- export const handleBrowserDeleteCookies = async (params, id) => {
869
- try {
870
- const url = params.url;
871
- if (typeof url !== 'string') {
872
- sendToServer({ jsonrpc: '2.0', error: { code: -32602, message: 'Missing or invalid url parameter' }, id });
873
- return;
874
- }
875
- if (isBlockedUrlScheme(url)) {
876
- sendToServer({
877
- jsonrpc: '2.0',
878
- error: {
879
- code: -32602,
880
- message: 'URL scheme not allowed (javascript:, data:, file:, chrome:, blob: are blocked)',
881
- },
882
- id,
883
- });
884
- return;
885
- }
886
- const name = params.name;
887
- if (typeof name !== 'string' || name.length === 0) {
888
- sendToServer({ jsonrpc: '2.0', error: { code: -32602, message: 'Missing or invalid name parameter' }, id });
889
- return;
890
- }
891
- await chrome.cookies.remove({ url, name });
892
- sendToServer({ jsonrpc: '2.0', result: { deleted: true, name, url }, id });
893
- }
894
- catch (err) {
895
- sendToServer({
896
- jsonrpc: '2.0',
897
- error: { code: -32603, message: sanitizeErrorMessage(err instanceof Error ? err.message : String(err)) },
898
- id,
899
- });
900
- }
901
- };
902
- export const handleBrowserExecuteScript = async (params, id) => {
903
- try {
904
- const tabId = params.tabId;
905
- if (typeof tabId !== 'number') {
906
- sendToServer({ jsonrpc: '2.0', error: { code: -32602, message: 'Missing or invalid tabId parameter' }, id });
907
- return;
908
- }
909
- const execFile = params.execFile;
910
- if (typeof execFile !== 'string' || execFile.length === 0) {
911
- sendToServer({ jsonrpc: '2.0', error: { code: -32602, message: 'Missing or invalid execFile parameter' }, id });
912
- return;
913
- }
914
- if (!/^__exec-[a-f0-9-]+\.js$/.test(execFile)) {
915
- sendToServer({ jsonrpc: '2.0', error: { code: -32602, message: 'Invalid execFile format' }, id });
916
- return;
917
- }
918
- let timeoutId;
919
- // Step 1: Inject the exec file into the tab's MAIN world (bypasses page CSP)
920
- const injectPromise = (async () => {
921
- await chrome.scripting.executeScript({
922
- target: { tabId },
923
- world: 'MAIN',
924
- files: [`adapters/${execFile}`],
925
- });
926
- // Step 2: Read the result. For sync code, __lastExecResult is set
927
- // immediately by the wrapper. For async code (Promises), the wrapper
928
- // sets __lastExecAsync=true and resolves __lastExecResult when the
929
- // Promise settles. Poll until the result is available.
930
- const maxAsyncWait = 10_000;
931
- const pollInterval = 50;
932
- let elapsed = 0;
933
- while (elapsed <= maxAsyncWait) {
934
- const results = await chrome.scripting.executeScript({
935
- target: { tabId },
936
- world: 'MAIN',
937
- func: () => {
938
- const ot = globalThis.__openTabs;
939
- if (!ot)
940
- return { pending: false, result: { error: '__openTabs not found' } };
941
- const result = ot.__lastExecResult;
942
- const isAsync = ot.__lastExecAsync === true;
943
- // Result available (sync or async resolved) — read and clean up
944
- if (result && ('value' in result || 'error' in result)) {
945
- const captured = { ...result };
946
- // undefined is dropped by structured cloning — normalize to null
947
- if (captured.value === undefined)
948
- captured.value = null;
949
- // Serialize non-primitive values
950
- if (captured.value !== null && typeof captured.value === 'object') {
951
- try {
952
- const json = JSON.stringify(captured.value);
953
- captured.value = json.length > 50_000 ? json.slice(0, 50_000) + '... (truncated)' : JSON.parse(json);
954
- }
955
- catch {
956
- captured.value = String(captured.value);
957
- }
958
- }
959
- // Clean up globals
960
- delete ot.__lastExecResult;
961
- delete ot.__lastExecAsync;
962
- return { pending: false, result: captured };
963
- }
964
- // Async code hasn't resolved yet — keep polling
965
- if (isAsync)
966
- return { pending: true };
967
- // Sync code produced no __lastExecResult (should not happen)
968
- return { pending: false, result: { error: 'No result captured' } };
969
- },
970
- });
971
- const first = results[0];
972
- const data = first?.result;
973
- if (data && !data.pending) {
974
- return { value: data.result };
975
- }
976
- // Still pending — wait and retry
977
- await new Promise(resolve => setTimeout(resolve, pollInterval));
978
- elapsed += pollInterval;
979
- }
980
- // Async timed out — clean up and report error
981
- await chrome.scripting
982
- .executeScript({
983
- target: { tabId },
984
- world: 'MAIN',
985
- func: () => {
986
- const ot = globalThis.__openTabs;
987
- if (ot) {
988
- delete ot.__lastExecResult;
989
- delete ot.__lastExecAsync;
990
- }
991
- },
992
- })
993
- .catch(() => { });
994
- return { value: { error: `Async code did not resolve within ${maxAsyncWait}ms` } };
995
- })();
996
- const timeoutPromise = new Promise((_resolve, reject) => {
997
- timeoutId = setTimeout(() => {
998
- reject(new Error(`Script execution timed out after ${SCRIPT_TIMEOUT_MS}ms`));
999
- }, SCRIPT_TIMEOUT_MS);
1000
- });
1001
- let result;
1002
- try {
1003
- result = await Promise.race([injectPromise, timeoutPromise]);
1004
- }
1005
- finally {
1006
- clearTimeout(timeoutId);
1007
- }
1008
- sendToServer({ jsonrpc: '2.0', result, id });
1009
- }
1010
- catch (err) {
1011
- sendToServer({
1012
- jsonrpc: '2.0',
1013
- error: { code: -32603, message: sanitizeErrorMessage(err instanceof Error ? err.message : String(err)) },
1014
- id,
1015
- });
1016
- }
1017
- };
1018
- export const handleBrowserEnableNetworkCapture = async (params, id) => {
1019
- try {
1020
- const tabId = params.tabId;
1021
- if (typeof tabId !== 'number') {
1022
- sendToServer({ jsonrpc: '2.0', error: { code: -32602, message: 'Missing or invalid tabId parameter' }, id });
1023
- return;
1024
- }
1025
- const maxRequests = typeof params.maxRequests === 'number' ? params.maxRequests : 100;
1026
- const urlFilter = typeof params.urlFilter === 'string' ? params.urlFilter : undefined;
1027
- const maxConsoleLogs = typeof params.maxConsoleLogs === 'number' ? params.maxConsoleLogs : 500;
1028
- await startCapture(tabId, maxRequests, urlFilter, maxConsoleLogs);
1029
- sendToServer({ jsonrpc: '2.0', result: { enabled: true, tabId }, id });
1030
- }
1031
- catch (err) {
1032
- sendToServer({
1033
- jsonrpc: '2.0',
1034
- error: { code: -32603, message: sanitizeErrorMessage(err instanceof Error ? err.message : String(err)) },
1035
- id,
1036
- });
1037
- }
1038
- };
1039
- export const handleBrowserGetNetworkRequests = (params, id) => {
1040
- try {
1041
- const tabId = params.tabId;
1042
- if (typeof tabId !== 'number') {
1043
- sendToServer({ jsonrpc: '2.0', error: { code: -32602, message: 'Missing or invalid tabId parameter' }, id });
1044
- return;
1045
- }
1046
- const clear = typeof params.clear === 'boolean' ? params.clear : false;
1047
- const requests = getRequests(tabId, clear);
1048
- sendToServer({ jsonrpc: '2.0', result: { requests }, id });
1049
- }
1050
- catch (err) {
1051
- sendToServer({
1052
- jsonrpc: '2.0',
1053
- error: { code: -32603, message: sanitizeErrorMessage(err instanceof Error ? err.message : String(err)) },
1054
- id,
1055
- });
1056
- }
1057
- };
1058
- export const handleBrowserDisableNetworkCapture = (params, id) => {
1059
- try {
1060
- const tabId = params.tabId;
1061
- if (typeof tabId !== 'number') {
1062
- sendToServer({ jsonrpc: '2.0', error: { code: -32602, message: 'Missing or invalid tabId parameter' }, id });
1063
- return;
1064
- }
1065
- stopCapture(tabId);
1066
- sendToServer({ jsonrpc: '2.0', result: { disabled: true, tabId }, id });
1067
- }
1068
- catch (err) {
1069
- sendToServer({
1070
- jsonrpc: '2.0',
1071
- error: { code: -32603, message: sanitizeErrorMessage(err instanceof Error ? err.message : String(err)) },
1072
- id,
1073
- });
1074
- }
1075
- };
1076
- export const handleBrowserGetConsoleLogs = (params, id) => {
1077
- try {
1078
- const tabId = params.tabId;
1079
- if (typeof tabId !== 'number') {
1080
- sendToServer({ jsonrpc: '2.0', error: { code: -32602, message: 'Missing or invalid tabId parameter' }, id });
1081
- return;
1082
- }
1083
- const clear = typeof params.clear === 'boolean' ? params.clear : false;
1084
- const level = typeof params.level === 'string' ? params.level : undefined;
1085
- const logs = getConsoleLogs(tabId, clear, level);
1086
- sendToServer({ jsonrpc: '2.0', result: { logs }, id });
1087
- }
1088
- catch (err) {
1089
- sendToServer({
1090
- jsonrpc: '2.0',
1091
- error: { code: -32603, message: sanitizeErrorMessage(err instanceof Error ? err.message : String(err)) },
1092
- id,
1093
- });
1094
- }
1095
- };
1096
- export const handleBrowserClearConsoleLogs = (params, id) => {
1097
- try {
1098
- const tabId = params.tabId;
1099
- if (typeof tabId !== 'number') {
1100
- sendToServer({ jsonrpc: '2.0', error: { code: -32602, message: 'Missing or invalid tabId parameter' }, id });
1101
- return;
1102
- }
1103
- clearConsoleLogs(tabId);
1104
- sendToServer({ jsonrpc: '2.0', result: { cleared: true }, id });
1105
- }
1106
- catch (err) {
1107
- sendToServer({
1108
- jsonrpc: '2.0',
1109
- error: { code: -32603, message: sanitizeErrorMessage(err instanceof Error ? err.message : String(err)) },
1110
- id,
1111
- });
1112
- }
1113
- };
1114
- export const handleBrowserPressKey = async (params, id) => {
1115
- try {
1116
- const tabId = params.tabId;
1117
- if (typeof tabId !== 'number') {
1118
- sendToServer({ jsonrpc: '2.0', error: { code: -32602, message: 'Missing or invalid tabId parameter' }, id });
1119
- return;
1120
- }
1121
- const key = params.key;
1122
- if (typeof key !== 'string' || key.length === 0) {
1123
- sendToServer({ jsonrpc: '2.0', error: { code: -32602, message: 'Missing or invalid key parameter' }, id });
1124
- return;
1125
- }
1126
- const selector = typeof params.selector === 'string' && params.selector.length > 0 ? params.selector : null;
1127
- const modifiers = typeof params.modifiers === 'object' && params.modifiers !== null
1128
- ? params.modifiers
1129
- : {};
1130
- const shiftKey = modifiers.shift === true;
1131
- const ctrlKey = modifiers.ctrl === true;
1132
- const altKey = modifiers.alt === true;
1133
- const metaKey = modifiers.meta === true;
1134
- const results = await chrome.scripting.executeScript({
1135
- target: { tabId },
1136
- world: 'MAIN',
1137
- func: (k, sel, shift, ctrl, alt, meta) => {
1138
- // Resolve target element
1139
- let target = null;
1140
- if (sel) {
1141
- target = document.querySelector(sel);
1142
- if (!target)
1143
- return { error: `Element not found: ${sel}` };
1144
- target.focus();
1145
- }
1146
- else {
1147
- target = document.activeElement ?? document.body;
1148
- }
1149
- // Derive code from key
1150
- const deriveCode = (k) => {
1151
- if (k.length === 1) {
1152
- const upper = k.toUpperCase();
1153
- if (upper >= 'A' && upper <= 'Z')
1154
- return `Key${upper}`;
1155
- if (k >= '0' && k <= '9')
1156
- return `Digit${k}`;
1157
- if (k === ' ')
1158
- return 'Space';
1159
- return k;
1160
- }
1161
- return k;
1162
- };
1163
- // Map key to legacy keyCode
1164
- const KEY_CODES = {
1165
- Enter: 13,
1166
- Escape: 27,
1167
- Tab: 9,
1168
- Backspace: 8,
1169
- Delete: 46,
1170
- ArrowUp: 38,
1171
- ArrowDown: 40,
1172
- ArrowLeft: 37,
1173
- ArrowRight: 39,
1174
- Home: 36,
1175
- End: 35,
1176
- PageUp: 33,
1177
- PageDown: 34,
1178
- ' ': 32,
1179
- };
1180
- const getKeyCode = (k) => {
1181
- if (KEY_CODES[k] !== undefined)
1182
- return KEY_CODES[k];
1183
- if (k.length === 1)
1184
- return k.toUpperCase().charCodeAt(0);
1185
- return 0;
1186
- };
1187
- const code = deriveCode(k);
1188
- const keyCode = getKeyCode(k);
1189
- const isPrintable = k.length === 1;
1190
- const eventInit = {
1191
- key: k,
1192
- code,
1193
- keyCode,
1194
- which: keyCode,
1195
- bubbles: true,
1196
- cancelable: true,
1197
- shiftKey: shift,
1198
- ctrlKey: ctrl,
1199
- metaKey: meta,
1200
- altKey: alt,
1201
- };
1202
- // Dispatch keyboard event sequence
1203
- target.dispatchEvent(new KeyboardEvent('keydown', eventInit));
1204
- if (isPrintable) {
1205
- target.dispatchEvent(new KeyboardEvent('keypress', eventInit));
1206
- }
1207
- target.dispatchEvent(new KeyboardEvent('keyup', eventInit));
1208
- // For printable characters, insert the character and dispatch InputEvent on editable elements
1209
- if (isPrintable) {
1210
- const tag = target.tagName.toLowerCase();
1211
- const isEditable = tag === 'input' || tag === 'textarea' || target.isContentEditable;
1212
- if (isEditable) {
1213
- if (tag === 'input' || tag === 'textarea') {
1214
- const input = target;
1215
- const start = input.selectionStart ?? input.value.length;
1216
- const end = input.selectionEnd ?? start;
1217
- input.value = input.value.slice(0, start) + k + input.value.slice(end);
1218
- input.selectionStart = input.selectionEnd = start + 1;
1219
- }
1220
- else {
1221
- // contentEditable — insert via Selection/Range API
1222
- const selection = window.getSelection();
1223
- if (selection && selection.rangeCount > 0) {
1224
- const range = selection.getRangeAt(0);
1225
- range.deleteContents();
1226
- range.insertNode(document.createTextNode(k));
1227
- range.collapse(false);
1228
- selection.removeAllRanges();
1229
- selection.addRange(range);
1230
- }
1231
- }
1232
- target.dispatchEvent(new InputEvent('input', {
1233
- bubbles: true,
1234
- cancelable: true,
1235
- inputType: 'insertText',
1236
- data: k,
1237
- }));
1238
- }
1239
- }
1240
- return {
1241
- pressed: true,
1242
- key: k,
1243
- target: {
1244
- tagName: target.tagName.toLowerCase(),
1245
- id: target.id || undefined,
1246
- },
1247
- };
1248
- },
1249
- args: [key, selector, shiftKey, ctrlKey, altKey, metaKey],
1250
- });
1251
- const result = results[0]?.result;
1252
- if (!result) {
1253
- sendToServer({ jsonrpc: '2.0', error: { code: -32603, message: 'No result from script execution' }, id });
1254
- return;
1255
- }
1256
- if (result.error) {
1257
- sendToServer({ jsonrpc: '2.0', error: { code: -32602, message: result.error }, id });
1258
- return;
1259
- }
1260
- sendToServer({
1261
- jsonrpc: '2.0',
1262
- result: { pressed: result.pressed, key: result.key, target: result.target },
1263
- id,
1264
- });
1265
- }
1266
- catch (err) {
1267
- sendToServer({
1268
- jsonrpc: '2.0',
1269
- error: { code: -32603, message: sanitizeErrorMessage(err instanceof Error ? err.message : String(err)) },
1270
- id,
1271
- });
1272
- }
1273
- };
1274
- export const handleBrowserScroll = async (params, id) => {
1275
- try {
1276
- const tabId = params.tabId;
1277
- if (typeof tabId !== 'number') {
1278
- sendToServer({ jsonrpc: '2.0', error: { code: -32602, message: 'Missing or invalid tabId parameter' }, id });
1279
- return;
1280
- }
1281
- const selector = typeof params.selector === 'string' && params.selector.length > 0 ? params.selector : null;
1282
- const direction = typeof params.direction === 'string' ? params.direction : null;
1283
- const distance = typeof params.distance === 'number' ? params.distance : null;
1284
- const position = typeof params.position === 'object' && params.position !== null
1285
- ? params.position
1286
- : null;
1287
- const container = typeof params.container === 'string' && params.container.length > 0 ? params.container : null;
1288
- const results = await chrome.scripting.executeScript({
1289
- target: { tabId },
1290
- world: 'MAIN',
1291
- func: (sel, dir, dist, pos, ctr) => {
1292
- // Resolve scroll target (container or page)
1293
- let scrollEl = null;
1294
- if (ctr) {
1295
- scrollEl = document.querySelector(ctr);
1296
- if (!scrollEl)
1297
- return { error: `Container not found: ${ctr}` };
1298
- }
1299
- // Helper to get scroll metrics from the scroll target
1300
- const getMetrics = () => {
1301
- if (scrollEl) {
1302
- return {
1303
- scrollPosition: { x: scrollEl.scrollLeft, y: scrollEl.scrollTop },
1304
- scrollSize: { width: scrollEl.scrollWidth, height: scrollEl.scrollHeight },
1305
- viewportSize: { width: scrollEl.clientWidth, height: scrollEl.clientHeight },
1306
- };
1307
- }
1308
- return {
1309
- scrollPosition: { x: window.scrollX, y: window.scrollY },
1310
- scrollSize: {
1311
- width: document.documentElement.scrollWidth,
1312
- height: document.documentElement.scrollHeight,
1313
- },
1314
- viewportSize: { width: window.innerWidth, height: window.innerHeight },
1315
- };
1316
- };
1317
- // Mode 1: scroll element into view
1318
- if (sel) {
1319
- const el = document.querySelector(sel);
1320
- if (!el)
1321
- return { error: `Element not found: ${sel}` };
1322
- el.scrollIntoView({ behavior: 'instant', block: 'center' });
1323
- const text = (el.textContent || '').trim().slice(0, 200);
1324
- return {
1325
- scrolledTo: { tagName: el.tagName.toLowerCase(), text },
1326
- ...getMetrics(),
1327
- };
1328
- }
1329
- // Mode 2: relative scroll by direction
1330
- if (dir) {
1331
- const metrics = getMetrics();
1332
- const defaultVertical = metrics.viewportSize.height;
1333
- const defaultHorizontal = metrics.viewportSize.width;
1334
- let dx = 0;
1335
- let dy = 0;
1336
- if (dir === 'down')
1337
- dy = dist ?? defaultVertical;
1338
- else if (dir === 'up')
1339
- dy = -(dist ?? defaultVertical);
1340
- else if (dir === 'right')
1341
- dx = dist ?? defaultHorizontal;
1342
- else if (dir === 'left')
1343
- dx = -(dist ?? defaultHorizontal);
1344
- if (scrollEl) {
1345
- scrollEl.scrollBy({ left: dx, top: dy, behavior: 'instant' });
1346
- }
1347
- else {
1348
- window.scrollBy({ left: dx, top: dy, behavior: 'instant' });
1349
- }
1350
- return getMetrics();
1351
- }
1352
- // Mode 3: absolute scroll to position
1353
- if (pos) {
1354
- const opts = { behavior: 'instant' };
1355
- if (pos.x !== undefined)
1356
- opts.left = pos.x;
1357
- if (pos.y !== undefined)
1358
- opts.top = pos.y;
1359
- if (scrollEl) {
1360
- scrollEl.scrollTo(opts);
1361
- }
1362
- else {
1363
- window.scrollTo(opts);
1364
- }
1365
- return getMetrics();
1366
- }
1367
- // No scroll target specified — return current position
1368
- return getMetrics();
1369
- },
1370
- args: [
1371
- selector,
1372
- direction,
1373
- distance,
1374
- position
1375
- ? {
1376
- x: position.x,
1377
- y: position.y,
1378
- }
1379
- : null,
1380
- container,
1381
- ],
1382
- });
1383
- const result = results[0]?.result;
1384
- if (!result) {
1385
- sendToServer({ jsonrpc: '2.0', error: { code: -32603, message: 'No result from script execution' }, id });
1386
- return;
1387
- }
1388
- if (result.error) {
1389
- sendToServer({ jsonrpc: '2.0', error: { code: -32602, message: result.error }, id });
1390
- return;
1391
- }
1392
- sendToServer({ jsonrpc: '2.0', result, id });
1393
- }
1394
- catch (err) {
1395
- sendToServer({
1396
- jsonrpc: '2.0',
1397
- error: { code: -32603, message: sanitizeErrorMessage(err instanceof Error ? err.message : String(err)) },
1398
- id,
1399
- });
1400
- }
1401
- };
1402
- export const handleBrowserHoverElement = async (params, id) => {
1403
- try {
1404
- const tabId = params.tabId;
1405
- if (typeof tabId !== 'number') {
1406
- sendToServer({ jsonrpc: '2.0', error: { code: -32602, message: 'Missing or invalid tabId parameter' }, id });
1407
- return;
1408
- }
1409
- const selector = params.selector;
1410
- if (typeof selector !== 'string' || selector.length === 0) {
1411
- sendToServer({ jsonrpc: '2.0', error: { code: -32602, message: 'Missing or invalid selector parameter' }, id });
1412
- return;
1413
- }
1414
- const results = await chrome.scripting.executeScript({
1415
- target: { tabId },
1416
- world: 'MAIN',
1417
- func: (sel) => {
1418
- const el = document.querySelector(sel);
1419
- if (!el)
1420
- return { error: `Element not found: ${sel}` };
1421
- const rect = el.getBoundingClientRect();
1422
- const clientX = rect.left + rect.width / 2;
1423
- const clientY = rect.top + rect.height / 2;
1424
- const pointerOpts = {
1425
- clientX,
1426
- clientY,
1427
- pointerId: 1,
1428
- pointerType: 'mouse',
1429
- };
1430
- const mouseOpts = { clientX, clientY };
1431
- // Full hover event sequence matching real browser behavior
1432
- el.dispatchEvent(new PointerEvent('pointerenter', { bubbles: false, ...pointerOpts }));
1433
- el.dispatchEvent(new PointerEvent('pointerover', { bubbles: true, ...pointerOpts }));
1434
- el.dispatchEvent(new MouseEvent('mouseenter', { bubbles: false, ...mouseOpts }));
1435
- el.dispatchEvent(new MouseEvent('mouseover', { bubbles: true, ...mouseOpts }));
1436
- el.dispatchEvent(new PointerEvent('pointermove', { bubbles: true, ...pointerOpts }));
1437
- el.dispatchEvent(new MouseEvent('mousemove', { bubbles: true, ...mouseOpts }));
1438
- return {
1439
- hovered: true,
1440
- tagName: el.tagName.toLowerCase(),
1441
- text: (el.textContent || '').trim().slice(0, 200),
1442
- bounds: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
1443
- };
1444
- },
1445
- args: [selector],
1446
- });
1447
- const result = results[0]?.result;
1448
- if (!result) {
1449
- sendToServer({ jsonrpc: '2.0', error: { code: -32603, message: 'No result from script execution' }, id });
1450
- return;
1451
- }
1452
- if (result.error) {
1453
- sendToServer({ jsonrpc: '2.0', error: { code: -32602, message: result.error }, id });
1454
- return;
1455
- }
1456
- sendToServer({
1457
- jsonrpc: '2.0',
1458
- result: { hovered: result.hovered, tagName: result.tagName, text: result.text, bounds: result.bounds },
1459
- id,
1460
- });
1461
- }
1462
- catch (err) {
1463
- sendToServer({
1464
- jsonrpc: '2.0',
1465
- error: { code: -32603, message: sanitizeErrorMessage(err instanceof Error ? err.message : String(err)) },
1466
- id,
1467
- });
1468
- }
1469
- };
1470
- export const handleBrowserHandleDialog = async (params, id) => {
1471
- try {
1472
- const tabId = params.tabId;
1473
- if (typeof tabId !== 'number') {
1474
- sendToServer({ jsonrpc: '2.0', error: { code: -32602, message: 'Missing or invalid tabId parameter' }, id });
1475
- return;
1476
- }
1477
- const action = params.action;
1478
- if (action !== 'accept' && action !== 'dismiss') {
1479
- sendToServer({
1480
- jsonrpc: '2.0',
1481
- error: { code: -32602, message: "action must be 'accept' or 'dismiss'" },
1482
- id,
1483
- });
1484
- return;
1485
- }
1486
- const accept = action === 'accept';
1487
- const promptText = typeof params.promptText === 'string' ? params.promptText : undefined;
1488
- await withDebugger(tabId, async () => {
1489
- await chrome.debugger.sendCommand({ tabId }, 'Page.enable');
1490
- try {
1491
- await chrome.debugger.sendCommand({ tabId }, 'Page.handleJavaScriptDialog', {
1492
- accept,
1493
- ...(promptText !== undefined ? { promptText } : {}),
1494
- });
1495
- sendToServer({ jsonrpc: '2.0', result: { handled: true, action }, id });
1496
- }
1497
- catch (err) {
1498
- const msg = err instanceof Error ? err.message : String(err);
1499
- const isNoDialog = msg.includes('No dialog is showing') || msg.includes('no dialog');
1500
- sendToServer({
1501
- jsonrpc: '2.0',
1502
- error: {
1503
- code: -32603,
1504
- message: isNoDialog ? 'No JavaScript dialog is currently open on this tab' : sanitizeErrorMessage(msg),
1505
- },
1506
- id,
1507
- });
1508
- }
1509
- });
1510
- }
1511
- catch (err) {
1512
- sendToServer({
1513
- jsonrpc: '2.0',
1514
- error: { code: -32603, message: sanitizeErrorMessage(err instanceof Error ? err.message : String(err)) },
1515
- id,
1516
- });
1517
- }
1518
- };
1519
- export const handleBrowserListResources = async (params, id) => {
1520
- try {
1521
- const tabId = params.tabId;
1522
- if (typeof tabId !== 'number') {
1523
- sendToServer({ jsonrpc: '2.0', error: { code: -32602, message: 'Missing or invalid tabId parameter' }, id });
1524
- return;
1525
- }
1526
- const typeFilter = typeof params.type === 'string' ? params.type : undefined;
1527
- await withDebugger(tabId, async () => {
1528
- await chrome.debugger.sendCommand({ tabId }, 'Page.enable');
1529
- const treeResult = (await chrome.debugger.sendCommand({ tabId }, 'Page.getResourceTree'));
1530
- const frames = [];
1531
- const resources = [];
1532
- const walk = (node) => {
1533
- frames.push({ url: node.frame.url, securityOrigin: node.frame.securityOrigin });
1534
- for (const r of node.resources) {
1535
- if (typeFilter && r.type !== typeFilter)
1536
- continue;
1537
- resources.push({
1538
- url: r.url,
1539
- type: r.type,
1540
- mimeType: r.mimeType,
1541
- contentLength: r.contentLength ?? -1,
1542
- });
1543
- }
1544
- if (node.childFrames) {
1545
- for (const child of node.childFrames)
1546
- walk(child);
1547
- }
1548
- };
1549
- walk(treeResult.frameTree);
1550
- resources.sort((a, b) => a.type.localeCompare(b.type) || a.url.localeCompare(b.url));
1551
- sendToServer({ jsonrpc: '2.0', result: { frames, resources }, id });
1552
- });
1553
- }
1554
- catch (err) {
1555
- sendToServer({
1556
- jsonrpc: '2.0',
1557
- error: { code: -32603, message: sanitizeErrorMessage(err instanceof Error ? err.message : String(err)) },
1558
- id,
1559
- });
1560
- }
1561
- };
1562
- export const handleBrowserGetResourceContent = async (params, id) => {
1563
- try {
1564
- const tabId = params.tabId;
1565
- if (typeof tabId !== 'number') {
1566
- sendToServer({ jsonrpc: '2.0', error: { code: -32602, message: 'Missing or invalid tabId parameter' }, id });
1567
- return;
1568
- }
1569
- const url = params.url;
1570
- if (typeof url !== 'string' || url.length === 0) {
1571
- sendToServer({ jsonrpc: '2.0', error: { code: -32602, message: 'Missing or invalid url parameter' }, id });
1572
- return;
1573
- }
1574
- const maxLength = typeof params.maxLength === 'number' ? params.maxLength : 500_000;
1575
- await withDebugger(tabId, async () => {
1576
- await chrome.debugger.sendCommand({ tabId }, 'Page.enable');
1577
- // Get the resource tree to find which frame owns the requested resource
1578
- const treeResult = (await chrome.debugger.sendCommand({ tabId }, 'Page.getResourceTree'));
1579
- const match = findFrameForResource(treeResult.frameTree, url);
1580
- if (!match) {
1581
- sendToServer({
1582
- jsonrpc: '2.0',
1583
- error: {
1584
- code: -32602,
1585
- message: `Resource not found in page: ${url}. Use browser_list_resources to find valid resource URLs.`,
1586
- },
1587
- id,
1588
- });
1589
- return;
1590
- }
1591
- const contentResult = (await chrome.debugger.sendCommand({ tabId }, 'Page.getResourceContent', {
1592
- frameId: match.frameId,
1593
- url,
1594
- }));
1595
- let content = contentResult.content;
1596
- let base64Encoded = contentResult.base64Encoded;
1597
- // Decode base64 text resources to UTF-8 strings
1598
- if (base64Encoded && isTextMimeType(match.mimeType)) {
1599
- try {
1600
- content = new TextDecoder().decode(Uint8Array.from(atob(content), c => c.charCodeAt(0)));
1601
- base64Encoded = false;
1602
- }
1603
- catch {
1604
- // Decoding failed — return base64 as-is
1605
- }
1606
- }
1607
- // Truncate text content that exceeds maxLength
1608
- let truncated = false;
1609
- if (!base64Encoded && content.length > maxLength) {
1610
- content = content.slice(0, maxLength) + '... (truncated)';
1611
- truncated = true;
1612
- }
1613
- sendToServer({
1614
- jsonrpc: '2.0',
1615
- result: { url, content, base64Encoded, mimeType: match.mimeType, truncated },
1616
- id,
1617
- });
1618
- });
1619
- }
1620
- catch (err) {
1621
- sendToServer({
1622
- jsonrpc: '2.0',
1623
- error: { code: -32603, message: sanitizeErrorMessage(err instanceof Error ? err.message : String(err)) },
1624
- id,
1625
- });
1626
- }
1627
- };
1628
- export const handleExtensionGetState = async (id) => {
1629
- try {
1630
- // Connection state from chrome.storage.session
1631
- const sessionData = await chrome.storage.session
1632
- .get(WS_CONNECTED_KEY)
1633
- .catch(() => ({}));
1634
- const wsConnected = typeof sessionData[WS_CONNECTED_KEY] === 'boolean' ? sessionData[WS_CONNECTED_KEY] : false;
1635
- // MCP server URL from chrome.storage.local
1636
- const localData = await chrome.storage.local
1637
- .get('mcpServerUrl')
1638
- .catch(() => ({}));
1639
- const mcpServerUrl = typeof localData.mcpServerUrl === 'string' ? localData.mcpServerUrl : 'ws://localhost:9515/ws';
1640
- // Plugin metadata with tab states
1641
- const pluginIndex = await getAllPluginMeta();
1642
- const lastKnownStates = getLastKnownStates();
1643
- const plugins = Object.values(pluginIndex).map(meta => ({
1644
- name: meta.name,
1645
- version: meta.version,
1646
- displayName: meta.displayName,
1647
- urlPatterns: meta.urlPatterns,
1648
- toolCount: meta.tools.length,
1649
- tabState: lastKnownStates.get(meta.name) ?? 'closed',
1650
- }));
1651
- // Active network captures
1652
- const networkCaptures = getActiveCapturesSummary();
1653
- // Offscreen document existence
1654
- let offscreenExists = false;
1655
- try {
1656
- const contexts = await chrome.runtime.getContexts({
1657
- contextTypes: ['OFFSCREEN_DOCUMENT'],
1658
- });
1659
- offscreenExists = contexts.length > 0;
1660
- }
1661
- catch {
1662
- // chrome.runtime.getContexts may not be available in all Chrome versions
1663
- }
1664
- sendToServer({
1665
- jsonrpc: '2.0',
1666
- result: {
1667
- connection: { wsConnected, mcpServerUrl },
1668
- plugins,
1669
- networkCaptures,
1670
- offscreen: { exists: offscreenExists },
1671
- },
1672
- id,
1673
- });
1674
- }
1675
- catch (err) {
1676
- sendToServer({
1677
- jsonrpc: '2.0',
1678
- error: { code: -32603, message: sanitizeErrorMessage(err instanceof Error ? err.message : String(err)) },
1679
- id,
1680
- });
1681
- }
1682
- };
1683
- export const handleExtensionGetLogs = async (params, id) => {
1684
- try {
1685
- const filterOptions = {};
1686
- if (typeof params.level === 'string' && params.level !== 'all') {
1687
- filterOptions.level = params.level;
1688
- }
1689
- if (typeof params.source === 'string' && params.source !== 'all') {
1690
- filterOptions.source = params.source;
1691
- }
1692
- if (typeof params.limit === 'number') {
1693
- filterOptions.limit = params.limit;
1694
- }
1695
- if (typeof params.since === 'number') {
1696
- filterOptions.since = params.since;
1697
- }
1698
- // Get background logs directly from the local collector
1699
- const bgEntries = bgLogCollector.getEntries(filterOptions);
1700
- const bgStats = bgLogCollector.getStats();
1701
- // Get offscreen logs via internal message
1702
- let offscreenEntries = [];
1703
- let offscreenStats = {
1704
- totalCaptured: 0,
1705
- bufferSize: 0,
1706
- oldestTimestamp: null,
1707
- newestTimestamp: null,
1708
- };
1709
- try {
1710
- const raw = await chrome.runtime.sendMessage({
1711
- type: 'bg:getLogs',
1712
- options: Object.keys(filterOptions).length > 0 ? filterOptions : undefined,
1713
- });
1714
- const response = raw;
1715
- if (response && Array.isArray(response.entries)) {
1716
- offscreenEntries = response.entries;
1717
- }
1718
- if (response?.stats) {
1719
- offscreenStats = response.stats;
1720
- }
1721
- }
1722
- catch {
1723
- // Offscreen document may not be running
1724
- }
1725
- // Merge entries by timestamp (newest first — both arrays are already newest-first)
1726
- const merged = [...bgEntries, ...offscreenEntries].sort((a, b) => b.timestamp - a.timestamp);
1727
- // Apply limit to the merged result
1728
- const limit = filterOptions.limit ?? 100;
1729
- const entries = merged.slice(0, limit);
1730
- sendToServer({
1731
- jsonrpc: '2.0',
1732
- result: {
1733
- entries,
1734
- stats: {
1735
- totalBackground: bgStats.totalCaptured,
1736
- totalOffscreen: offscreenStats.totalCaptured,
1737
- bufferSize: bgStats.bufferSize + offscreenStats.bufferSize,
1738
- },
1739
- },
1740
- id,
1741
- });
1742
- }
1743
- catch (err) {
1744
- sendToServer({
1745
- jsonrpc: '2.0',
1746
- error: { code: -32603, message: sanitizeErrorMessage(err instanceof Error ? err.message : String(err)) },
1747
- id,
1748
- });
1749
- }
1750
- };
1751
- export const handleExtensionGetSidePanel = async (id) => {
1752
- try {
1753
- const SIDE_PANEL_TIMEOUT_MS = 3000;
1754
- const sidePanelResult = await Promise.race([
1755
- chrome.runtime.sendMessage({ type: 'sp:getState' }).then((raw) => raw),
1756
- new Promise(resolve => setTimeout(() => resolve(null), SIDE_PANEL_TIMEOUT_MS)),
1757
- ]);
1758
- if (!sidePanelResult || typeof sidePanelResult !== 'object') {
1759
- sendToServer({ jsonrpc: '2.0', result: { open: false }, id });
1760
- return;
1761
- }
1762
- const response = sidePanelResult;
1763
- sendToServer({
1764
- jsonrpc: '2.0',
1765
- result: { open: true, state: response.state, html: response.html },
1766
- id,
1767
- });
1768
- }
1769
- catch {
1770
- // Side panel not open or message failed — return { open: false }
1771
- sendToServer({ jsonrpc: '2.0', result: { open: false }, id });
1772
- }
1773
- };
1774
- export const handleExtensionCheckAdapter = async (params, id) => {
1775
- try {
1776
- const pluginName = params.plugin;
1777
- if (typeof pluginName !== 'string' || pluginName.length === 0) {
1778
- sendToServer({
1779
- jsonrpc: '2.0',
1780
- error: { code: -32602, message: 'Missing or invalid plugin parameter' },
1781
- id,
1782
- });
1783
- return;
1784
- }
1785
- const meta = await getPluginMeta(pluginName);
1786
- if (!meta) {
1787
- sendToServer({
1788
- jsonrpc: '2.0',
1789
- error: { code: -32602, message: `Plugin not found: "${pluginName}"` },
1790
- id,
1791
- });
1792
- return;
1793
- }
1794
- const matchingTabs = await findAllMatchingTabs(meta);
1795
- const tabResults = await Promise.allSettled(matchingTabs.map(async (tab) => {
1796
- const tabId = tab.id;
1797
- if (tabId === undefined)
1798
- return null;
1799
- // Inspect the adapter in the tab's MAIN world
1800
- const inspectResults = await chrome.scripting.executeScript({
1801
- target: { tabId },
1802
- world: 'MAIN',
1803
- func: (pName) => {
1804
- const ot = globalThis.__openTabs;
1805
- const adapter = ot?.adapters?.[pName];
1806
- if (!adapter || typeof adapter !== 'object') {
1807
- return { adapterPresent: false };
1808
- }
1809
- const toolNames = [];
1810
- if (Array.isArray(adapter.tools)) {
1811
- for (const tool of adapter.tools) {
1812
- if (tool && typeof tool === 'object' && typeof tool.name === 'string') {
1813
- toolNames.push(tool.name);
1814
- }
1815
- }
1816
- }
1817
- return {
1818
- adapterPresent: true,
1819
- adapterHash: typeof adapter.__hash === 'string' ? adapter.__hash : null,
1820
- toolCount: toolNames.length,
1821
- toolNames,
1822
- };
1823
- },
1824
- args: [pluginName],
1825
- });
1826
- const inspectResult = inspectResults[0]?.result;
1827
- if (!inspectResult) {
1828
- return {
1829
- tabId,
1830
- tabUrl: tab.url ?? '',
1831
- adapterPresent: false,
1832
- adapterHash: null,
1833
- hashMatch: false,
1834
- isReady: false,
1835
- toolCount: 0,
1836
- toolNames: [],
1837
- };
1838
- }
1839
- if (!inspectResult.adapterPresent) {
1840
- return {
1841
- tabId,
1842
- tabUrl: tab.url ?? '',
1843
- adapterPresent: false,
1844
- adapterHash: null,
1845
- hashMatch: false,
1846
- isReady: false,
1847
- toolCount: 0,
1848
- toolNames: [],
1849
- };
1850
- }
1851
- // Probe isReady() with timeout
1852
- let isReady = false;
1853
- try {
1854
- const readyResults = await Promise.race([
1855
- chrome.scripting.executeScript({
1856
- target: { tabId },
1857
- world: 'MAIN',
1858
- func: async (pName) => {
1859
- const ot = globalThis.__openTabs;
1860
- const adapter = ot?.adapters?.[pName];
1861
- if (!adapter || typeof adapter.isReady !== 'function')
1862
- return false;
1863
- return await adapter.isReady();
1864
- },
1865
- args: [pluginName],
1866
- }),
1867
- new Promise(resolve => setTimeout(() => resolve(null), IS_READY_TIMEOUT_MS)),
1868
- ]);
1869
- if (readyResults !== null) {
1870
- const readyResult = readyResults[0];
1871
- isReady = readyResult?.result === true;
1872
- }
1873
- }
1874
- catch {
1875
- // isReady probe failed — leave as false
1876
- }
1877
- return {
1878
- tabId,
1879
- tabUrl: tab.url ?? '',
1880
- adapterPresent: true,
1881
- adapterHash: inspectResult.adapterHash ?? null,
1882
- hashMatch: meta.adapterHash ? inspectResult.adapterHash === meta.adapterHash : false,
1883
- isReady,
1884
- toolCount: inspectResult.toolCount ?? 0,
1885
- toolNames: inspectResult.toolNames ?? [],
1886
- };
1887
- }));
1888
- const matchingTabResults = [];
1889
- for (const result of tabResults) {
1890
- if (result.status === 'fulfilled' && result.value !== null) {
1891
- matchingTabResults.push(result.value);
1892
- }
1893
- }
1894
- sendToServer({
1895
- jsonrpc: '2.0',
1896
- result: {
1897
- plugin: pluginName,
1898
- expectedHash: meta.adapterHash ?? null,
1899
- matchingTabs: matchingTabResults,
1900
- },
1901
- id,
1902
- });
1903
- }
1904
- catch (err) {
1905
- sendToServer({
1906
- jsonrpc: '2.0',
1907
- error: { code: -32603, message: sanitizeErrorMessage(err instanceof Error ? err.message : String(err)) },
1908
- id,
1909
- });
1910
- }
1911
- };
1912
- export const handleExtensionForceReconnect = async (id) => {
1913
- try {
1914
- // Send the success response FIRST, before the WebSocket is torn down.
1915
- // The response travels over the current WebSocket connection; if we
1916
- // close it first, the response would never reach the MCP server.
1917
- sendToServer({ jsonrpc: '2.0', result: { reconnecting: true }, id });
1918
- // Small delay so the response flushes over the WebSocket before we
1919
- // ask the offscreen document to close and reconnect.
1920
- await new Promise(resolve => setTimeout(resolve, 50));
1921
- await chrome.runtime.sendMessage({
1922
- type: 'bg:forceReconnect',
1923
- });
1924
- }
1925
- catch (err) {
1926
- // The response was already sent above, so this catch is best-effort.
1927
- // If sendToServer itself failed, there's nothing more we can do.
1928
- console.warn('[opentabs] extension.forceReconnect failed:', err);
1929
- }
1930
- };
1931
- //# sourceMappingURL=browser-commands.js.map