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.
Files changed (46) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +201 -26
  3. package/bin/tab-agent.js +23 -8
  4. package/cli/command.js +113 -9
  5. package/cli/detect-extension.js +96 -14
  6. package/cli/launch-chrome.js +150 -0
  7. package/cli/setup.js +99 -23
  8. package/cli/start.js +65 -13
  9. package/cli/status.js +41 -7
  10. package/extension/content-script.js +218 -17
  11. package/extension/manifest.json +4 -3
  12. package/extension/manifest.safari.json +45 -0
  13. package/extension/popup/popup.html +58 -1
  14. package/extension/popup/popup.js +18 -0
  15. package/extension/service-worker.js +106 -13
  16. package/package.json +14 -3
  17. package/relay/install-native-host.sh +14 -7
  18. package/relay/native-host-wrapper.sh +1 -1
  19. package/relay/native-host.js +3 -1
  20. package/relay/server.js +124 -17
  21. package/skills/claude-code/tab-agent/SKILL.md +92 -0
  22. package/skills/codex/tab-agent/SKILL.md +92 -0
  23. package/relay/node_modules/.package-lock.json +0 -29
  24. package/relay/node_modules/ws/LICENSE +0 -20
  25. package/relay/node_modules/ws/README.md +0 -548
  26. package/relay/node_modules/ws/browser.js +0 -8
  27. package/relay/node_modules/ws/index.js +0 -13
  28. package/relay/node_modules/ws/lib/buffer-util.js +0 -131
  29. package/relay/node_modules/ws/lib/constants.js +0 -19
  30. package/relay/node_modules/ws/lib/event-target.js +0 -292
  31. package/relay/node_modules/ws/lib/extension.js +0 -203
  32. package/relay/node_modules/ws/lib/limiter.js +0 -55
  33. package/relay/node_modules/ws/lib/permessage-deflate.js +0 -528
  34. package/relay/node_modules/ws/lib/receiver.js +0 -706
  35. package/relay/node_modules/ws/lib/sender.js +0 -602
  36. package/relay/node_modules/ws/lib/stream.js +0 -161
  37. package/relay/node_modules/ws/lib/subprotocol.js +0 -62
  38. package/relay/node_modules/ws/lib/validation.js +0 -152
  39. package/relay/node_modules/ws/lib/websocket-server.js +0 -554
  40. package/relay/node_modules/ws/lib/websocket.js +0 -1393
  41. package/relay/node_modules/ws/package.json +0 -69
  42. package/relay/node_modules/ws/wrapper.mjs +0 -8
  43. package/relay/package-lock.json +0 -36
  44. package/relay/package.json +0 -12
  45. package/skills/claude-code/tab-agent.md +0 -57
  46. 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
- for (const char of text) {
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
- if (element.tagName === 'INPUT' || element.tagName === 'TEXTAREA') {
291
- element.value += char;
292
- element.dispatchEvent(new Event('input', { bubbles: true }));
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
- element.dispatchEvent(new KeyboardEvent('keyup', { key: char, bubbles: true }));
296
- await new Promise(r => setTimeout(r, 10));
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.value = value;
325
- element.dispatchEvent(new Event('input', { bubbles: true }));
326
- element.dispatchEvent(new Event('change', { bubbles: true }));
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 timeout)
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
  }
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "Tab Agent",
4
- "version": "0.1.0",
5
- "description": "Browser control for Claude Code and Codex via WebSocket",
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 toggle",
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 | v0.1.0</div>
195
+ <div class="footer" id="footer">ws://localhost:9876</div>
139
196
 
140
197
  <script src="popup.js"></script>
141
198
  </body>
@@ -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 (click to deactivate)' : 'Tab Agent - Click to activate'
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' && state.activatedTabs.has(tabId)) {
352
- ensureContentScript(tabId);
353
- updateBadge(tabId);
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.4",
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
- "relay/",
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
- "extension/"
28
+ "CHANGELOG.md"
18
29
  ],
19
30
  "dependencies": {
20
31
  "ws": "^8.16.0"