tab-agent 0.3.4 → 0.4.1
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/CHANGELOG.md +39 -0
- package/README.md +201 -26
- package/bin/tab-agent.js +23 -8
- package/cli/command.js +113 -9
- package/cli/detect-extension.js +96 -14
- package/cli/launch-chrome.js +150 -0
- package/cli/setup.js +99 -23
- package/cli/start.js +65 -13
- package/cli/status.js +41 -7
- package/extension/content-script.js +218 -17
- package/extension/manifest.json +4 -3
- package/extension/manifest.safari.json +45 -0
- package/extension/popup/popup.html +58 -1
- package/extension/popup/popup.js +18 -0
- package/extension/service-worker.js +106 -13
- package/package.json +14 -3
- package/relay/install-native-host.sh +14 -7
- package/relay/native-host-wrapper.sh +1 -1
- package/relay/native-host.js +3 -1
- package/relay/server.js +124 -17
- package/skills/claude-code/tab-agent/SKILL.md +92 -0
- package/skills/codex/tab-agent/SKILL.md +92 -0
- package/relay/node_modules/.package-lock.json +0 -29
- package/relay/node_modules/ws/LICENSE +0 -20
- package/relay/node_modules/ws/README.md +0 -548
- package/relay/node_modules/ws/browser.js +0 -8
- package/relay/node_modules/ws/index.js +0 -13
- package/relay/node_modules/ws/lib/buffer-util.js +0 -131
- package/relay/node_modules/ws/lib/constants.js +0 -19
- package/relay/node_modules/ws/lib/event-target.js +0 -292
- package/relay/node_modules/ws/lib/extension.js +0 -203
- package/relay/node_modules/ws/lib/limiter.js +0 -55
- package/relay/node_modules/ws/lib/permessage-deflate.js +0 -528
- package/relay/node_modules/ws/lib/receiver.js +0 -706
- package/relay/node_modules/ws/lib/sender.js +0 -602
- package/relay/node_modules/ws/lib/stream.js +0 -161
- package/relay/node_modules/ws/lib/subprotocol.js +0 -62
- package/relay/node_modules/ws/lib/validation.js +0 -152
- package/relay/node_modules/ws/lib/websocket-server.js +0 -554
- package/relay/node_modules/ws/lib/websocket.js +0 -1393
- package/relay/node_modules/ws/package.json +0 -69
- package/relay/node_modules/ws/wrapper.mjs +0 -8
- package/relay/package-lock.json +0 -36
- package/relay/package.json +0 -12
- package/skills/claude-code/tab-agent.md +0 -57
- package/skills/codex/tab-agent.md +0 -38
|
@@ -246,7 +246,7 @@ if (window.__tabAgent_contentScriptLoaded) {
|
|
|
246
246
|
};
|
|
247
247
|
}
|
|
248
248
|
|
|
249
|
-
return { getElementByRef, snapshot };
|
|
249
|
+
return { getElementByRef, snapshot, isVisible, getRole, getName, nextRef, storeRef };
|
|
250
250
|
})();
|
|
251
251
|
|
|
252
252
|
function getElementByRef(ref) {
|
|
@@ -283,17 +283,25 @@ if (window.__tabAgent_contentScriptLoaded) {
|
|
|
283
283
|
|
|
284
284
|
element.focus();
|
|
285
285
|
|
|
286
|
-
|
|
287
|
-
element.dispatchEvent(new KeyboardEvent('keydown', { key: char, bubbles: true }));
|
|
288
|
-
element.dispatchEvent(new KeyboardEvent('keypress', { key: char, bubbles: true }));
|
|
286
|
+
const isContentEditable = element.contentEditable === 'true' || element.isContentEditable;
|
|
289
287
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
288
|
+
if (isContentEditable) {
|
|
289
|
+
// For contentEditable elements (rich text editors like Gemini, Notion, etc.)
|
|
290
|
+
// Use document.execCommand which triggers proper input events
|
|
291
|
+
document.execCommand('insertText', false, text);
|
|
292
|
+
} else {
|
|
293
|
+
for (const char of text) {
|
|
294
|
+
element.dispatchEvent(new KeyboardEvent('keydown', { key: char, bubbles: true }));
|
|
295
|
+
element.dispatchEvent(new KeyboardEvent('keypress', { key: char, bubbles: true }));
|
|
296
|
+
|
|
297
|
+
if (element.tagName === 'INPUT' || element.tagName === 'TEXTAREA') {
|
|
298
|
+
element.value += char;
|
|
299
|
+
element.dispatchEvent(new Event('input', { bubbles: true }));
|
|
300
|
+
}
|
|
294
301
|
|
|
295
|
-
|
|
296
|
-
|
|
302
|
+
element.dispatchEvent(new KeyboardEvent('keyup', { key: char, bubbles: true }));
|
|
303
|
+
await new Promise(r => setTimeout(r, 10));
|
|
304
|
+
}
|
|
297
305
|
}
|
|
298
306
|
|
|
299
307
|
// Handle submit if requested
|
|
@@ -318,12 +326,20 @@ if (window.__tabAgent_contentScriptLoaded) {
|
|
|
318
326
|
}
|
|
319
327
|
|
|
320
328
|
element.focus();
|
|
321
|
-
element.value = '';
|
|
322
|
-
element.dispatchEvent(new Event('input', { bubbles: true }));
|
|
323
329
|
|
|
324
|
-
element.
|
|
325
|
-
|
|
326
|
-
|
|
330
|
+
const isContentEditable = element.contentEditable === 'true' || element.isContentEditable;
|
|
331
|
+
|
|
332
|
+
if (isContentEditable) {
|
|
333
|
+
// Clear and fill contentEditable elements
|
|
334
|
+
element.textContent = '';
|
|
335
|
+
document.execCommand('insertText', false, value);
|
|
336
|
+
} else {
|
|
337
|
+
element.value = '';
|
|
338
|
+
element.dispatchEvent(new Event('input', { bubbles: true }));
|
|
339
|
+
element.value = value;
|
|
340
|
+
element.dispatchEvent(new Event('input', { bubbles: true }));
|
|
341
|
+
element.dispatchEvent(new Event('change', { bubbles: true }));
|
|
342
|
+
}
|
|
327
343
|
|
|
328
344
|
return { ok: true, ref, filled: value };
|
|
329
345
|
}
|
|
@@ -394,9 +410,9 @@ if (window.__tabAgent_contentScriptLoaded) {
|
|
|
394
410
|
return { ok: true, direction, scrollY: window.scrollY };
|
|
395
411
|
}
|
|
396
412
|
|
|
397
|
-
// Wait for condition (text, selector, or
|
|
413
|
+
// Wait for condition (text, selector, url pattern, or visible ref)
|
|
398
414
|
async function executeWait(params) {
|
|
399
|
-
const { text, selector, timeout = 30000 } = params;
|
|
415
|
+
const { text, selector, urlPattern, visibleRef, timeout = 30000 } = params;
|
|
400
416
|
const start = Date.now();
|
|
401
417
|
|
|
402
418
|
while (Date.now() - start < timeout) {
|
|
@@ -406,6 +422,15 @@ if (window.__tabAgent_contentScriptLoaded) {
|
|
|
406
422
|
if (selector && document.querySelector(selector)) {
|
|
407
423
|
return { ok: true, found: 'selector' };
|
|
408
424
|
}
|
|
425
|
+
if (urlPattern && window.location.href.includes(urlPattern)) {
|
|
426
|
+
return { ok: true, found: 'url', url: window.location.href };
|
|
427
|
+
}
|
|
428
|
+
if (visibleRef) {
|
|
429
|
+
const el = getElementByRef(visibleRef);
|
|
430
|
+
if (el && snapshotState.isVisible(el)) {
|
|
431
|
+
return { ok: true, found: 'visible', ref: visibleRef };
|
|
432
|
+
}
|
|
433
|
+
}
|
|
409
434
|
await new Promise(r => setTimeout(r, 100));
|
|
410
435
|
}
|
|
411
436
|
|
|
@@ -440,6 +465,167 @@ if (window.__tabAgent_contentScriptLoaded) {
|
|
|
440
465
|
return { ok: results.every(r => r.ok), results };
|
|
441
466
|
}
|
|
442
467
|
|
|
468
|
+
async function executeDrag(params) {
|
|
469
|
+
const { fromRef, toRef } = params;
|
|
470
|
+
const fromEl = getElementByRef(fromRef);
|
|
471
|
+
const toEl = getElementByRef(toRef);
|
|
472
|
+
|
|
473
|
+
if (!fromEl) return { ok: false, error: `Element ${fromRef} not found` };
|
|
474
|
+
if (!toEl) return { ok: false, error: `Element ${toRef} not found` };
|
|
475
|
+
|
|
476
|
+
const fromRect = fromEl.getBoundingClientRect();
|
|
477
|
+
const toRect = toEl.getBoundingClientRect();
|
|
478
|
+
const fromX = fromRect.left + fromRect.width / 2;
|
|
479
|
+
const fromY = fromRect.top + fromRect.height / 2;
|
|
480
|
+
const toX = toRect.left + toRect.width / 2;
|
|
481
|
+
const toY = toRect.top + toRect.height / 2;
|
|
482
|
+
|
|
483
|
+
fromEl.dispatchEvent(new MouseEvent('mousedown', { clientX: fromX, clientY: fromY, bubbles: true }));
|
|
484
|
+
await new Promise(r => setTimeout(r, 50));
|
|
485
|
+
fromEl.dispatchEvent(new MouseEvent('mousemove', { clientX: fromX + 5, clientY: fromY + 5, bubbles: true }));
|
|
486
|
+
await new Promise(r => setTimeout(r, 50));
|
|
487
|
+
toEl.dispatchEvent(new MouseEvent('mousemove', { clientX: toX, clientY: toY, bubbles: true }));
|
|
488
|
+
await new Promise(r => setTimeout(r, 50));
|
|
489
|
+
toEl.dispatchEvent(new MouseEvent('mouseup', { clientX: toX, clientY: toY, bubbles: true }));
|
|
490
|
+
|
|
491
|
+
// Also fire dragstart/drop for HTML5 drag-and-drop
|
|
492
|
+
fromEl.dispatchEvent(new DragEvent('dragstart', { bubbles: true }));
|
|
493
|
+
toEl.dispatchEvent(new DragEvent('drop', { bubbles: true }));
|
|
494
|
+
fromEl.dispatchEvent(new DragEvent('dragend', { bubbles: true }));
|
|
495
|
+
|
|
496
|
+
return { ok: true, from: fromRef, to: toRef };
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
async function executeGet(params) {
|
|
500
|
+
const { subcommand, ref, attr } = params;
|
|
501
|
+
|
|
502
|
+
if (subcommand === 'url') return { ok: true, result: window.location.href };
|
|
503
|
+
if (subcommand === 'title') return { ok: true, result: document.title };
|
|
504
|
+
|
|
505
|
+
if (!ref) return { ok: false, error: 'No ref provided' };
|
|
506
|
+
const element = getElementByRef(ref);
|
|
507
|
+
if (!element) return { ok: false, error: `Element ${ref} not found` };
|
|
508
|
+
|
|
509
|
+
switch (subcommand) {
|
|
510
|
+
case 'text':
|
|
511
|
+
return { ok: true, result: element.textContent.trim() };
|
|
512
|
+
case 'html':
|
|
513
|
+
return { ok: true, result: element.innerHTML };
|
|
514
|
+
case 'value':
|
|
515
|
+
return { ok: true, result: element.value || '' };
|
|
516
|
+
case 'attr':
|
|
517
|
+
return { ok: true, result: element.getAttribute(attr) };
|
|
518
|
+
default:
|
|
519
|
+
return { ok: false, error: `Unknown get subcommand: ${subcommand}` };
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
async function executeFind(params) {
|
|
524
|
+
const { by, query } = params;
|
|
525
|
+
const results = [];
|
|
526
|
+
|
|
527
|
+
const matches = [];
|
|
528
|
+
if (by === 'text') {
|
|
529
|
+
const lowerQuery = query.toLowerCase();
|
|
530
|
+
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_ELEMENT);
|
|
531
|
+
while (walker.nextNode()) {
|
|
532
|
+
const el = walker.currentNode;
|
|
533
|
+
if (!snapshotState.isVisible(el)) continue;
|
|
534
|
+
// Prefer elements with direct text match (not just inherited from children)
|
|
535
|
+
let directText = '';
|
|
536
|
+
for (const node of el.childNodes) {
|
|
537
|
+
if (node.nodeType === Node.TEXT_NODE) directText += node.textContent;
|
|
538
|
+
}
|
|
539
|
+
const hasDirectMatch = directText.trim().toLowerCase().includes(lowerQuery);
|
|
540
|
+
const isLeafLike = el.children.length <= 2;
|
|
541
|
+
if (hasDirectMatch || (isLeafLike && el.textContent.trim().toLowerCase().includes(lowerQuery))) {
|
|
542
|
+
matches.push(el);
|
|
543
|
+
if (matches.length >= 20) break;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
} else if (by === 'role') {
|
|
547
|
+
const els = document.querySelectorAll(`[role="${query}"]`);
|
|
548
|
+
const tagRoles = { button: 'button', a: 'link', input: 'textbox', textarea: 'textbox', select: 'combobox', img: 'img' };
|
|
549
|
+
const tagMatch = Object.entries(tagRoles).find(([, r]) => r === query);
|
|
550
|
+
if (tagMatch) {
|
|
551
|
+
document.querySelectorAll(tagMatch[0]).forEach(el => { if (snapshotState.isVisible(el)) matches.push(el); });
|
|
552
|
+
}
|
|
553
|
+
els.forEach(el => { if (snapshotState.isVisible(el) && !matches.includes(el)) matches.push(el); });
|
|
554
|
+
} else if (by === 'label') {
|
|
555
|
+
const labels = document.querySelectorAll('label');
|
|
556
|
+
labels.forEach(label => {
|
|
557
|
+
if (label.textContent.trim().toLowerCase().includes(query.toLowerCase())) {
|
|
558
|
+
const input = label.control || (label.htmlFor && document.getElementById(label.htmlFor));
|
|
559
|
+
if (input) matches.push(input);
|
|
560
|
+
}
|
|
561
|
+
});
|
|
562
|
+
// Also search aria-label
|
|
563
|
+
document.querySelectorAll(`[aria-label*="${query}" i]`).forEach(el => {
|
|
564
|
+
if (!matches.includes(el)) matches.push(el);
|
|
565
|
+
});
|
|
566
|
+
} else if (by === 'placeholder') {
|
|
567
|
+
document.querySelectorAll(`[placeholder*="${query}" i]`).forEach(el => matches.push(el));
|
|
568
|
+
} else if (by === 'selector') {
|
|
569
|
+
document.querySelectorAll(query).forEach(el => { if (snapshotState.isVisible(el)) matches.push(el); });
|
|
570
|
+
} else {
|
|
571
|
+
return { ok: false, error: `Unknown find method: ${by}` };
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// Assign refs to found elements
|
|
575
|
+
for (const el of matches.slice(0, 20)) {
|
|
576
|
+
const ref = snapshotState.nextRef();
|
|
577
|
+
snapshotState.storeRef(ref, el);
|
|
578
|
+
const role = snapshotState.getRole(el);
|
|
579
|
+
const name = snapshotState.getName(el);
|
|
580
|
+
results.push({ ref, role, name: name.substring(0, 100) });
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
return { ok: true, results, count: results.length };
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
async function executeCookies(params) {
|
|
587
|
+
const { subcommand } = params;
|
|
588
|
+
|
|
589
|
+
if (subcommand === 'get') {
|
|
590
|
+
return { ok: true, result: document.cookie };
|
|
591
|
+
} else if (subcommand === 'clear') {
|
|
592
|
+
document.cookie.split(';').forEach(c => {
|
|
593
|
+
const name = c.split('=')[0].trim();
|
|
594
|
+
document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`;
|
|
595
|
+
});
|
|
596
|
+
return { ok: true, cleared: true };
|
|
597
|
+
} else {
|
|
598
|
+
return { ok: false, error: `Unknown cookies subcommand: ${subcommand}` };
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
async function executeStorage(params) {
|
|
603
|
+
const { subcommand, storageType = 'local', key, value } = params;
|
|
604
|
+
const store = storageType === 'session' ? sessionStorage : localStorage;
|
|
605
|
+
|
|
606
|
+
switch (subcommand) {
|
|
607
|
+
case 'get':
|
|
608
|
+
if (key) return { ok: true, result: store.getItem(key) };
|
|
609
|
+
const all = {};
|
|
610
|
+
for (let i = 0; i < store.length; i++) {
|
|
611
|
+
const k = store.key(i);
|
|
612
|
+
all[k] = store.getItem(k);
|
|
613
|
+
}
|
|
614
|
+
return { ok: true, result: all };
|
|
615
|
+
case 'set':
|
|
616
|
+
store.setItem(key, value);
|
|
617
|
+
return { ok: true, key, value };
|
|
618
|
+
case 'remove':
|
|
619
|
+
store.removeItem(key);
|
|
620
|
+
return { ok: true, removed: key };
|
|
621
|
+
case 'clear':
|
|
622
|
+
store.clear();
|
|
623
|
+
return { ok: true, cleared: true };
|
|
624
|
+
default:
|
|
625
|
+
return { ok: false, error: `Unknown storage subcommand: ${subcommand}` };
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
443
629
|
async function executeNavigate(params) {
|
|
444
630
|
const { url } = params;
|
|
445
631
|
window.location.href = url;
|
|
@@ -496,6 +682,21 @@ if (window.__tabAgent_contentScriptLoaded) {
|
|
|
496
682
|
case 'navigate':
|
|
497
683
|
result = await executeNavigate(params);
|
|
498
684
|
break;
|
|
685
|
+
case 'drag':
|
|
686
|
+
result = await executeDrag(params);
|
|
687
|
+
break;
|
|
688
|
+
case 'get':
|
|
689
|
+
result = await executeGet(params);
|
|
690
|
+
break;
|
|
691
|
+
case 'find':
|
|
692
|
+
result = await executeFind(params);
|
|
693
|
+
break;
|
|
694
|
+
case 'cookies':
|
|
695
|
+
result = await executeCookies(params);
|
|
696
|
+
break;
|
|
697
|
+
case 'storage':
|
|
698
|
+
result = await executeStorage(params);
|
|
699
|
+
break;
|
|
499
700
|
default:
|
|
500
701
|
result = { ok: false, error: `Unknown action: ${action}` };
|
|
501
702
|
}
|
package/extension/manifest.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"manifest_version": 3,
|
|
3
3
|
"name": "Tab Agent",
|
|
4
|
-
"version": "0.1
|
|
5
|
-
"description": "
|
|
4
|
+
"version": "0.4.1",
|
|
5
|
+
"description": "Secure browser control for Claude, Codex, ChatGPT, and other AI tools",
|
|
6
6
|
"permissions": [
|
|
7
7
|
"activeTab",
|
|
8
8
|
"scripting",
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
"type": "module"
|
|
20
20
|
},
|
|
21
21
|
"action": {
|
|
22
|
-
"default_title": "Tab Agent - Click to
|
|
22
|
+
"default_title": "Tab Agent - Click to manage",
|
|
23
|
+
"default_popup": "popup/popup.html",
|
|
23
24
|
"default_icon": {
|
|
24
25
|
"16": "icons/icon16.png",
|
|
25
26
|
"48": "icons/icon48.png",
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"manifest_version": 3,
|
|
3
|
+
"name": "Tab Agent",
|
|
4
|
+
"version": "0.4.1",
|
|
5
|
+
"description": "Secure browser control for Claude, Codex, ChatGPT, and other AI tools",
|
|
6
|
+
"permissions": [
|
|
7
|
+
"activeTab",
|
|
8
|
+
"scripting",
|
|
9
|
+
"storage",
|
|
10
|
+
"tabs",
|
|
11
|
+
"nativeMessaging"
|
|
12
|
+
],
|
|
13
|
+
"host_permissions": [
|
|
14
|
+
"<all_urls>"
|
|
15
|
+
],
|
|
16
|
+
"background": {
|
|
17
|
+
"service_worker": "service-worker.js",
|
|
18
|
+
"type": "module"
|
|
19
|
+
},
|
|
20
|
+
"action": {
|
|
21
|
+
"default_title": "Tab Agent - Click to manage",
|
|
22
|
+
"default_popup": "popup/popup.html",
|
|
23
|
+
"default_icon": {
|
|
24
|
+
"16": "icons/icon16.png",
|
|
25
|
+
"48": "icons/icon48.png",
|
|
26
|
+
"128": "icons/icon128.png"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"icons": {
|
|
30
|
+
"16": "icons/icon16.png",
|
|
31
|
+
"48": "icons/icon48.png",
|
|
32
|
+
"128": "icons/icon128.png"
|
|
33
|
+
},
|
|
34
|
+
"web_accessible_resources": [
|
|
35
|
+
{
|
|
36
|
+
"resources": ["snapshot.js"],
|
|
37
|
+
"matches": ["<all_urls>"]
|
|
38
|
+
}
|
|
39
|
+
],
|
|
40
|
+
"browser_specific_settings": {
|
|
41
|
+
"safari": {
|
|
42
|
+
"strict_min_version": "17.0"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -115,6 +115,55 @@
|
|
|
115
115
|
color: #666;
|
|
116
116
|
text-align: center;
|
|
117
117
|
}
|
|
118
|
+
|
|
119
|
+
.auto-activate {
|
|
120
|
+
margin-bottom: 12px;
|
|
121
|
+
padding: 10px;
|
|
122
|
+
background: #2a2a3e;
|
|
123
|
+
border-radius: 6px;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.toggle {
|
|
127
|
+
display: flex;
|
|
128
|
+
align-items: center;
|
|
129
|
+
gap: 10px;
|
|
130
|
+
cursor: pointer;
|
|
131
|
+
font-size: 13px;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.toggle input { display: none; }
|
|
135
|
+
|
|
136
|
+
.toggle-slider {
|
|
137
|
+
width: 36px;
|
|
138
|
+
height: 20px;
|
|
139
|
+
background: #444;
|
|
140
|
+
border-radius: 10px;
|
|
141
|
+
position: relative;
|
|
142
|
+
transition: background 0.2s;
|
|
143
|
+
flex-shrink: 0;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.toggle-slider::after {
|
|
147
|
+
content: '';
|
|
148
|
+
position: absolute;
|
|
149
|
+
width: 16px;
|
|
150
|
+
height: 16px;
|
|
151
|
+
background: white;
|
|
152
|
+
border-radius: 50%;
|
|
153
|
+
top: 2px;
|
|
154
|
+
left: 2px;
|
|
155
|
+
transition: transform 0.2s;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.toggle input:checked + .toggle-slider {
|
|
159
|
+
background: #3b82f6;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.toggle input:checked + .toggle-slider::after {
|
|
163
|
+
transform: translateX(16px);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.toggle-label { color: #ccc; }
|
|
118
167
|
</style>
|
|
119
168
|
</head>
|
|
120
169
|
<body>
|
|
@@ -122,6 +171,14 @@
|
|
|
122
171
|
|
|
123
172
|
<div id="status" class="status disconnected">Checking connection...</div>
|
|
124
173
|
|
|
174
|
+
<div class="auto-activate">
|
|
175
|
+
<label class="toggle">
|
|
176
|
+
<input type="checkbox" id="autoActivateToggle">
|
|
177
|
+
<span class="toggle-slider"></span>
|
|
178
|
+
<span class="toggle-label">Auto-activate all tabs</span>
|
|
179
|
+
</label>
|
|
180
|
+
</div>
|
|
181
|
+
|
|
125
182
|
<div class="current-tab">
|
|
126
183
|
<div class="label">Current Tab</div>
|
|
127
184
|
<div id="currentUrl" class="url">Loading...</div>
|
|
@@ -135,7 +192,7 @@
|
|
|
135
192
|
</div>
|
|
136
193
|
</div>
|
|
137
194
|
|
|
138
|
-
<div class="footer">ws://localhost:9876
|
|
195
|
+
<div class="footer" id="footer">ws://localhost:9876</div>
|
|
139
196
|
|
|
140
197
|
<script src="popup.js"></script>
|
|
141
198
|
</body>
|
package/extension/popup/popup.js
CHANGED
|
@@ -8,10 +8,28 @@ async function init() {
|
|
|
8
8
|
currentTabId = tab.id;
|
|
9
9
|
|
|
10
10
|
document.getElementById('currentUrl').textContent = tab.url;
|
|
11
|
+
document.getElementById('footer').textContent =
|
|
12
|
+
`ws://localhost:9876 | v${chrome.runtime.getManifest().version}`;
|
|
11
13
|
|
|
12
14
|
await refreshActivatedTabs();
|
|
13
15
|
updateActivateButton();
|
|
14
16
|
checkConnection();
|
|
17
|
+
|
|
18
|
+
// Load auto-activate state
|
|
19
|
+
const autoResponse = await chrome.runtime.sendMessage({ action: 'getAutoActivate' });
|
|
20
|
+
const toggle = document.getElementById('autoActivateToggle');
|
|
21
|
+
if (autoResponse && autoResponse.ok) {
|
|
22
|
+
toggle.checked = autoResponse.autoActivateAll;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
toggle.addEventListener('change', async () => {
|
|
26
|
+
await chrome.runtime.sendMessage({
|
|
27
|
+
action: 'setAutoActivate',
|
|
28
|
+
enabled: toggle.checked
|
|
29
|
+
});
|
|
30
|
+
await refreshActivatedTabs();
|
|
31
|
+
updateActivateButton();
|
|
32
|
+
});
|
|
15
33
|
}
|
|
16
34
|
|
|
17
35
|
async function refreshActivatedTabs() {
|
|
@@ -2,13 +2,32 @@
|
|
|
2
2
|
// Tab Agent - Service Worker
|
|
3
3
|
// Manages activated tabs and routes commands to content scripts
|
|
4
4
|
|
|
5
|
+
// Browser detection
|
|
6
|
+
const IS_SAFARI = typeof browser !== 'undefined' &&
|
|
7
|
+
navigator.userAgent.includes('Safari') &&
|
|
8
|
+
!navigator.userAgent.includes('Chrome');
|
|
9
|
+
const IS_CHROME = typeof chrome !== 'undefined' && !IS_SAFARI;
|
|
10
|
+
|
|
11
|
+
// Safari uses 'browser' namespace, Chrome uses 'chrome'
|
|
12
|
+
const browserAPI = IS_SAFARI ? browser : chrome;
|
|
13
|
+
|
|
5
14
|
const state = {
|
|
6
15
|
activatedTabs: new Map(), // tabId -> { url, title, activatedAt }
|
|
7
16
|
auditLog: [],
|
|
8
17
|
nativeConnected: false,
|
|
9
18
|
lastNativeError: null,
|
|
19
|
+
autoActivateAll: false,
|
|
10
20
|
};
|
|
11
21
|
|
|
22
|
+
// Load auto-activate setting from storage on startup
|
|
23
|
+
chrome.storage.local.get(['autoActivateAll'], (result) => {
|
|
24
|
+
if (result.autoActivateAll) {
|
|
25
|
+
state.autoActivateAll = true;
|
|
26
|
+
autoActivateExistingTabs();
|
|
27
|
+
updateAutoActivateBadge();
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
12
31
|
// Dialog handling with chrome.debugger
|
|
13
32
|
const pendingDialogs = new Map();
|
|
14
33
|
const attachedDebuggerTabs = new Set();
|
|
@@ -53,15 +72,41 @@ async function handleDialog(tabId, accept, promptText = '') {
|
|
|
53
72
|
|
|
54
73
|
// Update badge for a tab
|
|
55
74
|
function updateBadge(tabId) {
|
|
75
|
+
if (state.autoActivateAll) return;
|
|
56
76
|
const isActive = state.activatedTabs.has(tabId);
|
|
57
77
|
chrome.action.setBadgeText({ tabId, text: isActive ? 'ON' : '' });
|
|
58
78
|
chrome.action.setBadgeBackgroundColor({ tabId, color: isActive ? '#22c55e' : '#666' });
|
|
59
79
|
chrome.action.setTitle({
|
|
60
80
|
tabId,
|
|
61
|
-
title: isActive ? 'Tab Agent - Active
|
|
81
|
+
title: isActive ? 'Tab Agent - Active' : 'Tab Agent - Click to manage'
|
|
62
82
|
});
|
|
63
83
|
}
|
|
64
84
|
|
|
85
|
+
// Update badge to show AUTO mode
|
|
86
|
+
function updateAutoActivateBadge() {
|
|
87
|
+
if (state.autoActivateAll) {
|
|
88
|
+
chrome.action.setBadgeText({ text: 'AUTO' });
|
|
89
|
+
chrome.action.setBadgeBackgroundColor({ color: '#3b82f6' });
|
|
90
|
+
chrome.action.setTitle({ title: 'Tab Agent - Auto-activate ON (click to manage)' });
|
|
91
|
+
} else {
|
|
92
|
+
chrome.action.setBadgeText({ text: '' });
|
|
93
|
+
chrome.action.setTitle({ title: 'Tab Agent - Click to manage' });
|
|
94
|
+
for (const [tabId] of state.activatedTabs) {
|
|
95
|
+
updateBadge(tabId);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Activate all existing tabs
|
|
101
|
+
async function autoActivateExistingTabs() {
|
|
102
|
+
const tabs = await chrome.tabs.query({});
|
|
103
|
+
for (const tab of tabs) {
|
|
104
|
+
if (!state.activatedTabs.has(tab.id) && tab.url && !tab.url.startsWith('chrome://')) {
|
|
105
|
+
await activateTab(tab.id);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
65
110
|
// Log all actions for audit trail
|
|
66
111
|
function audit(action, data, result) {
|
|
67
112
|
const entry = {
|
|
@@ -254,6 +299,26 @@ async function routeCommand(tabId, command) {
|
|
|
254
299
|
};
|
|
255
300
|
}
|
|
256
301
|
|
|
302
|
+
// Handle PDF generation - must be done in service worker via debugger
|
|
303
|
+
if (command.action === 'pdf') {
|
|
304
|
+
try {
|
|
305
|
+
try { await chrome.debugger.detach({ tabId }); } catch {}
|
|
306
|
+
await chrome.debugger.attach({ tabId }, '1.3');
|
|
307
|
+
const pdf = await chrome.debugger.sendCommand({ tabId }, 'Page.printToPDF', {
|
|
308
|
+
printBackground: true,
|
|
309
|
+
preferCSSPageSize: true,
|
|
310
|
+
});
|
|
311
|
+
await chrome.debugger.detach({ tabId });
|
|
312
|
+
audit('pdf', { tabId }, { ok: true });
|
|
313
|
+
return { ok: true, pdf: pdf.data, format: 'pdf', encoding: 'base64' };
|
|
314
|
+
} catch (error) {
|
|
315
|
+
try { await chrome.debugger.detach({ tabId }); } catch {}
|
|
316
|
+
const result = { ok: false, error: error.message };
|
|
317
|
+
audit('pdf', { tabId }, result);
|
|
318
|
+
return result;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
257
322
|
const injectResult = await ensureContentScript(tabId);
|
|
258
323
|
if (!injectResult.ok) {
|
|
259
324
|
const result = { ok: false, error: injectResult.error };
|
|
@@ -302,6 +367,22 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|
|
302
367
|
result = listActivatedTabs();
|
|
303
368
|
break;
|
|
304
369
|
|
|
370
|
+
case 'getAutoActivate':
|
|
371
|
+
result = { ok: true, autoActivateAll: state.autoActivateAll };
|
|
372
|
+
break;
|
|
373
|
+
|
|
374
|
+
case 'setAutoActivate': {
|
|
375
|
+
state.autoActivateAll = !!params.enabled;
|
|
376
|
+
chrome.storage.local.set({ autoActivateAll: state.autoActivateAll });
|
|
377
|
+
updateAutoActivateBadge();
|
|
378
|
+
if (state.autoActivateAll) {
|
|
379
|
+
await autoActivateExistingTabs();
|
|
380
|
+
}
|
|
381
|
+
audit('setAutoActivate', { enabled: state.autoActivateAll }, { ok: true });
|
|
382
|
+
result = { ok: true, autoActivateAll: state.autoActivateAll };
|
|
383
|
+
break;
|
|
384
|
+
}
|
|
385
|
+
|
|
305
386
|
default:
|
|
306
387
|
result = { ok: false, error: `Unknown action: ${action}` };
|
|
307
388
|
}
|
|
@@ -312,15 +393,6 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|
|
312
393
|
return true;
|
|
313
394
|
});
|
|
314
395
|
|
|
315
|
-
// Handle extension icon click - toggle activation
|
|
316
|
-
chrome.action.onClicked.addListener(async (tab) => {
|
|
317
|
-
if (state.activatedTabs.has(tab.id)) {
|
|
318
|
-
deactivateTab(tab.id);
|
|
319
|
-
} else {
|
|
320
|
-
await activateTab(tab.id);
|
|
321
|
-
}
|
|
322
|
-
});
|
|
323
|
-
|
|
324
396
|
// Update badge when switching tabs
|
|
325
397
|
chrome.tabs.onActivated.addListener(({ tabId }) => {
|
|
326
398
|
updateBadge(tabId);
|
|
@@ -348,9 +420,13 @@ chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
|
|
|
348
420
|
info.url = changeInfo.url;
|
|
349
421
|
info.title = tab.title;
|
|
350
422
|
}
|
|
351
|
-
if (changeInfo.status === 'complete'
|
|
352
|
-
|
|
353
|
-
|
|
423
|
+
if (changeInfo.status === 'complete') {
|
|
424
|
+
if (state.autoActivateAll && !state.activatedTabs.has(tabId) && tab.url && !tab.url.startsWith('chrome://')) {
|
|
425
|
+
activateTab(tabId);
|
|
426
|
+
} else if (state.activatedTabs.has(tabId)) {
|
|
427
|
+
ensureContentScript(tabId);
|
|
428
|
+
updateBadge(tabId);
|
|
429
|
+
}
|
|
354
430
|
}
|
|
355
431
|
});
|
|
356
432
|
|
|
@@ -361,6 +437,17 @@ function connectNativeHost() {
|
|
|
361
437
|
console.log('Attempting to connect to native host...');
|
|
362
438
|
|
|
363
439
|
try {
|
|
440
|
+
if (IS_SAFARI) {
|
|
441
|
+
// Safari: native messaging is handled by the containing app
|
|
442
|
+
// The app will inject messages via browser.runtime messaging
|
|
443
|
+
console.log('Safari detected - native messaging handled by container app');
|
|
444
|
+
state.nativeConnected = true;
|
|
445
|
+
state.lastNativeError = null;
|
|
446
|
+
// Safari extension will receive commands via runtime.onMessage from the app
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Chrome: use connectNative
|
|
364
451
|
nativePort = chrome.runtime.connectNative('com.tabagent.relay');
|
|
365
452
|
console.log('connectNative called, port created');
|
|
366
453
|
|
|
@@ -424,6 +511,12 @@ function connectNativeHost() {
|
|
|
424
511
|
case 'wait':
|
|
425
512
|
case 'scrollintoview':
|
|
426
513
|
case 'batchfill':
|
|
514
|
+
case 'drag':
|
|
515
|
+
case 'get':
|
|
516
|
+
case 'find':
|
|
517
|
+
case 'cookies':
|
|
518
|
+
case 'storage':
|
|
519
|
+
case 'pdf':
|
|
427
520
|
result = await routeCommand(tabId, { action, ...params });
|
|
428
521
|
break;
|
|
429
522
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tab-agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Give LLMs full control of your browser - secure, click-to-activate automation for Claude, ChatGPT, Codex, and any AI",
|
|
5
5
|
"bin": {
|
|
6
6
|
"tab-agent": "./bin/tab-agent.js"
|
|
@@ -12,9 +12,20 @@
|
|
|
12
12
|
"files": [
|
|
13
13
|
"bin/",
|
|
14
14
|
"cli/",
|
|
15
|
-
"
|
|
15
|
+
"extension/content-script.js",
|
|
16
|
+
"extension/icons/",
|
|
17
|
+
"extension/manifest.json",
|
|
18
|
+
"extension/manifest.safari.json",
|
|
19
|
+
"extension/popup/",
|
|
20
|
+
"extension/service-worker.js",
|
|
21
|
+
"extension/snapshot.js",
|
|
22
|
+
"relay/install-native-host.sh",
|
|
23
|
+
"relay/native-host-wrapper.cmd",
|
|
24
|
+
"relay/native-host-wrapper.sh",
|
|
25
|
+
"relay/native-host.js",
|
|
26
|
+
"relay/server.js",
|
|
16
27
|
"skills/",
|
|
17
|
-
"
|
|
28
|
+
"CHANGELOG.md"
|
|
18
29
|
],
|
|
19
30
|
"dependencies": {
|
|
20
31
|
"ws": "^8.16.0"
|