tab-agent 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +62 -0
  2. package/bin/tab-agent.js +40 -0
  3. package/cli/detect-extension.js +131 -0
  4. package/cli/setup.js +133 -0
  5. package/cli/start.js +19 -0
  6. package/cli/status.js +70 -0
  7. package/extension/content-script.js +510 -0
  8. package/extension/icons/icon128.png +0 -0
  9. package/extension/icons/icon16.png +0 -0
  10. package/extension/icons/icon48.png +0 -0
  11. package/extension/manifest.json +40 -0
  12. package/extension/popup/popup.html +142 -0
  13. package/extension/popup/popup.js +104 -0
  14. package/extension/service-worker.js +471 -0
  15. package/extension/snapshot.js +194 -0
  16. package/package.json +25 -0
  17. package/relay/install-native-host.sh +57 -0
  18. package/relay/native-host-wrapper.cmd +3 -0
  19. package/relay/native-host-wrapper.sh +29 -0
  20. package/relay/native-host.js +128 -0
  21. package/relay/node_modules/.package-lock.json +29 -0
  22. package/relay/node_modules/ws/LICENSE +20 -0
  23. package/relay/node_modules/ws/README.md +548 -0
  24. package/relay/node_modules/ws/browser.js +8 -0
  25. package/relay/node_modules/ws/index.js +13 -0
  26. package/relay/node_modules/ws/lib/buffer-util.js +131 -0
  27. package/relay/node_modules/ws/lib/constants.js +19 -0
  28. package/relay/node_modules/ws/lib/event-target.js +292 -0
  29. package/relay/node_modules/ws/lib/extension.js +203 -0
  30. package/relay/node_modules/ws/lib/limiter.js +55 -0
  31. package/relay/node_modules/ws/lib/permessage-deflate.js +528 -0
  32. package/relay/node_modules/ws/lib/receiver.js +706 -0
  33. package/relay/node_modules/ws/lib/sender.js +602 -0
  34. package/relay/node_modules/ws/lib/stream.js +161 -0
  35. package/relay/node_modules/ws/lib/subprotocol.js +62 -0
  36. package/relay/node_modules/ws/lib/validation.js +152 -0
  37. package/relay/node_modules/ws/lib/websocket-server.js +554 -0
  38. package/relay/node_modules/ws/lib/websocket.js +1393 -0
  39. package/relay/node_modules/ws/package.json +69 -0
  40. package/relay/node_modules/ws/wrapper.mjs +8 -0
  41. package/relay/package-lock.json +36 -0
  42. package/relay/package.json +12 -0
  43. package/relay/server.js +114 -0
  44. package/skills/claude-code/tab-agent.md +53 -0
  45. package/skills/codex/tab-agent.md +40 -0
package/README.md ADDED
@@ -0,0 +1,62 @@
1
+ # Tab Agent
2
+
3
+ Secure tab-level browser control for Claude Code and Codex — only the tabs you explicitly activate, not your entire browser.
4
+
5
+ ## Install
6
+
7
+ ### 1. Load Extension
8
+ ```bash
9
+ git clone https://github.com/DrHB/tab-agent
10
+ ```
11
+
12
+ 1. Open `chrome://extensions`
13
+ 2. Enable **Developer mode**
14
+ 3. Click **Load unpacked** → select `extension/` folder
15
+
16
+ ### 2. Setup
17
+ ```bash
18
+ npx tab-agent setup
19
+ ```
20
+
21
+ That's it! The setup auto-detects your extension and configures everything.
22
+
23
+ ## Use
24
+
25
+ 1. Click Tab Agent icon on any tab (turns green = active)
26
+ 2. Ask Claude/Codex: "Use tab-agent to search Google for 'hello world'"
27
+
28
+ ## Commands
29
+
30
+ | Command | Description |
31
+ |---------|-------------|
32
+ | `tabs` | List activated tabs |
33
+ | `snapshot` | Get AI-readable page with refs [e1], [e2]... |
34
+ | `screenshot` | Capture viewport (or `fullPage: true` for full page) |
35
+ | `click` | Click element by ref |
36
+ | `fill` | Fill form field |
37
+ | `type` | Type text (with optional `submit: true`) |
38
+ | `press` | Press key (Enter, Escape, Tab, Arrow*) |
39
+ | `scroll` | Scroll page |
40
+ | `scrollintoview` | Scroll element into view |
41
+ | `navigate` | Go to URL |
42
+ | `wait` | Wait for text or selector |
43
+ | `evaluate` | Run JavaScript in page context |
44
+ | `batchfill` | Fill multiple fields at once |
45
+ | `dialog` | Handle alert/confirm/prompt |
46
+
47
+ ## Manual Commands
48
+
49
+ ```bash
50
+ npx tab-agent status # Check configuration
51
+ npx tab-agent start # Start relay manually
52
+ ```
53
+
54
+ ## Architecture
55
+
56
+ ```
57
+ Claude/Codex → WebSocket:9876 → Relay → Native Messaging → Extension → DOM
58
+ ```
59
+
60
+ ## License
61
+
62
+ MIT
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env node
2
+ const command = process.argv[2];
3
+ const pkg = require('../package.json');
4
+
5
+ function showHelp() {
6
+ console.log(`
7
+ tab-agent - Browser control for Claude/Codex
8
+
9
+ Commands:
10
+ setup Auto-detect extension, register native host, install skills
11
+ start Start the relay server
12
+ status Check configuration status
13
+
14
+ Usage:
15
+ npx tab-agent setup
16
+ npx tab-agent start
17
+ `);
18
+ }
19
+
20
+ switch (command) {
21
+ case 'setup':
22
+ require('../cli/setup.js');
23
+ break;
24
+ case 'start':
25
+ require('../cli/start.js');
26
+ break;
27
+ case 'status':
28
+ require('../cli/status.js');
29
+ break;
30
+ case '-v':
31
+ case '--version':
32
+ console.log(pkg.version);
33
+ break;
34
+ case undefined:
35
+ showHelp();
36
+ break;
37
+ default:
38
+ showHelp();
39
+ process.exit(1);
40
+ }
@@ -0,0 +1,131 @@
1
+ // cli/detect-extension.js
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const os = require('os');
5
+
6
+ // Support multiple browsers: Chrome, Brave, Edge, Chromium
7
+ function getAllBrowserExtensionPaths() {
8
+ const platform = os.platform();
9
+ const home = os.homedir();
10
+ const paths = [];
11
+
12
+ if (platform === 'darwin') {
13
+ const base = path.join(home, 'Library/Application Support');
14
+ paths.push(
15
+ path.join(base, 'Google/Chrome'),
16
+ path.join(base, 'Google/Chrome Canary'),
17
+ path.join(base, 'Chromium'),
18
+ path.join(base, 'BraveSoftware/Brave-Browser'),
19
+ path.join(base, 'Microsoft Edge'),
20
+ );
21
+ } else if (platform === 'linux') {
22
+ paths.push(
23
+ path.join(home, '.config/google-chrome'),
24
+ path.join(home, '.config/chromium'),
25
+ path.join(home, '.config/BraveSoftware/Brave-Browser'),
26
+ path.join(home, '.config/microsoft-edge'),
27
+ );
28
+ } else if (platform === 'win32') {
29
+ const localAppData = process.env.LOCALAPPDATA;
30
+ paths.push(
31
+ path.join(localAppData, 'Google/Chrome/User Data'),
32
+ path.join(localAppData, 'Chromium/User Data'),
33
+ path.join(localAppData, 'BraveSoftware/Brave-Browser/User Data'),
34
+ path.join(localAppData, 'Microsoft/Edge/User Data'),
35
+ );
36
+ }
37
+
38
+ // Expand to include Default and Profile N directories
39
+ const expandedPaths = [];
40
+ for (const browserPath of paths) {
41
+ if (!fs.existsSync(browserPath)) continue;
42
+
43
+ const profiles = ['Default', ...fs.readdirSync(browserPath).filter(f => f.startsWith('Profile '))];
44
+ for (const profile of profiles) {
45
+ const extPath = path.join(browserPath, profile, 'Extensions');
46
+ if (fs.existsSync(extPath)) {
47
+ expandedPaths.push(extPath);
48
+ }
49
+ }
50
+ }
51
+
52
+ return expandedPaths;
53
+ }
54
+
55
+ function findTabAgentExtension() {
56
+ const extensionPaths = getAllBrowserExtensionPaths();
57
+
58
+ for (const extPath of extensionPaths) {
59
+ try {
60
+ const extIds = fs.readdirSync(extPath);
61
+
62
+ for (const extId of extIds) {
63
+ const extDir = path.join(extPath, extId);
64
+ if (!fs.statSync(extDir).isDirectory()) continue;
65
+
66
+ const versions = fs.readdirSync(extDir).filter(v => !v.startsWith('.'));
67
+
68
+ for (const version of versions) {
69
+ const manifestPath = path.join(extDir, version, 'manifest.json');
70
+ if (fs.existsSync(manifestPath)) {
71
+ try {
72
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
73
+ if (manifest.name === 'Tab Agent') {
74
+ return { extId, browser: extPath };
75
+ }
76
+ } catch (e) {}
77
+ }
78
+ }
79
+ }
80
+ } catch (e) {}
81
+ }
82
+ return null;
83
+ }
84
+
85
+ function checkExistingManifest() {
86
+ const platform = os.platform();
87
+ const home = os.homedir();
88
+ let manifestPath;
89
+
90
+ if (platform === 'darwin') {
91
+ manifestPath = path.join(home, 'Library/Application Support/Google/Chrome/NativeMessagingHosts/com.tabagent.relay.json');
92
+ } else if (platform === 'linux') {
93
+ manifestPath = path.join(home, '.config/google-chrome/NativeMessagingHosts/com.tabagent.relay.json');
94
+ } else if (platform === 'win32') {
95
+ manifestPath = path.join(home, 'AppData/Local/Google/Chrome/User Data/NativeMessagingHosts/com.tabagent.relay.json');
96
+ }
97
+
98
+ if (manifestPath && fs.existsSync(manifestPath)) {
99
+ try {
100
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
101
+ const origin = manifest.allowed_origins?.[0];
102
+ if (origin) {
103
+ const match = origin.match(/chrome-extension:\/\/([^/]+)/);
104
+ if (match) return match[1];
105
+ }
106
+ } catch (e) {}
107
+ }
108
+ return null;
109
+ }
110
+
111
+ async function promptForExtensionId() {
112
+ const readline = require('readline');
113
+ const rl = readline.createInterface({
114
+ input: process.stdin,
115
+ output: process.stdout
116
+ });
117
+
118
+ return new Promise((resolve) => {
119
+ rl.question('Enter extension ID from chrome://extensions: ', (answer) => {
120
+ rl.close();
121
+ resolve(answer.trim());
122
+ });
123
+ });
124
+ }
125
+
126
+ module.exports = {
127
+ findTabAgentExtension,
128
+ checkExistingManifest,
129
+ promptForExtensionId,
130
+ getAllBrowserExtensionPaths
131
+ };
package/cli/setup.js ADDED
@@ -0,0 +1,133 @@
1
+ // cli/setup.js
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const os = require('os');
5
+ const { findTabAgentExtension, checkExistingManifest, promptForExtensionId } = require('./detect-extension');
6
+
7
+ async function setup() {
8
+ console.log('Tab Agent Setup\n');
9
+
10
+ // 1. Detect extension ID
11
+ console.log('Detecting extension...');
12
+ let extensionId = null;
13
+
14
+ const found = findTabAgentExtension();
15
+ if (found) {
16
+ extensionId = found.extId;
17
+ console.log(`✓ Found extension: ${extensionId}`);
18
+ } else {
19
+ extensionId = checkExistingManifest();
20
+ if (extensionId) {
21
+ console.log(`✓ Found existing config: ${extensionId}`);
22
+ } else {
23
+ console.log('✗ Could not auto-detect extension');
24
+ console.log(' Make sure Tab Agent is loaded in chrome://extensions\n');
25
+ extensionId = await promptForExtensionId();
26
+ }
27
+ }
28
+
29
+ if (!extensionId || extensionId.length !== 32) {
30
+ console.error('Invalid extension ID');
31
+ process.exit(1);
32
+ }
33
+
34
+ // 2. Install native messaging host
35
+ console.log('\nInstalling native messaging host...');
36
+ installNativeHost(extensionId);
37
+ console.log('✓ Native messaging host installed');
38
+
39
+ // 3. Install skills
40
+ console.log('\nInstalling skills...');
41
+ installSkills();
42
+
43
+ console.log('\n✓ Setup complete!\n');
44
+ console.log('Usage:');
45
+ console.log(' 1. Click Tab Agent icon on any tab (turns green)');
46
+ console.log(' 2. Ask Claude/Codex: "Use tab-agent to search Google"');
47
+ console.log('\nThe relay server starts automatically when needed.');
48
+ }
49
+
50
+ function installNativeHost(extensionId) {
51
+ const platform = os.platform();
52
+ const home = os.homedir();
53
+ const packageDir = path.dirname(__dirname);
54
+
55
+ let manifestDir;
56
+ let wrapperName;
57
+
58
+ if (platform === 'darwin') {
59
+ manifestDir = path.join(home, 'Library/Application Support/Google/Chrome/NativeMessagingHosts');
60
+ wrapperName = 'native-host-wrapper.sh';
61
+ } else if (platform === 'linux') {
62
+ manifestDir = path.join(home, '.config/google-chrome/NativeMessagingHosts');
63
+ wrapperName = 'native-host-wrapper.sh';
64
+ } else if (platform === 'win32') {
65
+ manifestDir = path.join(home, 'AppData/Local/Google/Chrome/User Data/NativeMessagingHosts');
66
+ wrapperName = 'native-host-wrapper.cmd';
67
+ }
68
+
69
+ fs.mkdirSync(manifestDir, { recursive: true });
70
+
71
+ const wrapperPath = path.join(packageDir, 'relay', wrapperName);
72
+ const manifest = {
73
+ name: 'com.tabagent.relay',
74
+ description: 'Tab Agent Native Messaging Host',
75
+ path: wrapperPath,
76
+ type: 'stdio',
77
+ allowed_origins: [`chrome-extension://${extensionId}/`]
78
+ };
79
+
80
+ const manifestPath = path.join(manifestDir, 'com.tabagent.relay.json');
81
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
82
+
83
+ // Make wrapper executable (Unix only)
84
+ if (platform !== 'win32') {
85
+ fs.chmodSync(wrapperPath, '755');
86
+ }
87
+
88
+ // Windows: also set registry key
89
+ if (platform === 'win32') {
90
+ const { execSync } = require('child_process');
91
+ const regPath = 'HKCU\\Software\\Google\\Chrome\\NativeMessagingHosts\\com.tabagent.relay';
92
+ execSync(`reg add "${regPath}" /ve /t REG_SZ /d "${manifestPath}" /f`);
93
+ }
94
+ }
95
+
96
+ function installSkills() {
97
+ const home = os.homedir();
98
+ const packageDir = path.dirname(__dirname);
99
+ const skillSource = path.join(packageDir, 'skills');
100
+
101
+ // Claude Code - always install
102
+ const claudeSkillDir = path.join(home, '.claude', 'skills');
103
+ const claudeSkillPath = path.join(claudeSkillDir, 'tab-agent.md');
104
+ fs.mkdirSync(claudeSkillDir, { recursive: true });
105
+
106
+ if (fs.existsSync(claudeSkillPath)) {
107
+ console.log(` Updating existing skill at ${claudeSkillPath}`);
108
+ }
109
+ fs.copyFileSync(
110
+ path.join(skillSource, 'claude-code', 'tab-agent.md'),
111
+ claudeSkillPath
112
+ );
113
+ console.log('✓ Installed Claude Code skill');
114
+
115
+ // Codex (only if .codex exists)
116
+ const codexDir = path.join(home, '.codex');
117
+ if (fs.existsSync(codexDir)) {
118
+ const codexSkillDir = path.join(codexDir, 'skills');
119
+ const codexSkillPath = path.join(codexSkillDir, 'tab-agent.md');
120
+ fs.mkdirSync(codexSkillDir, { recursive: true });
121
+
122
+ if (fs.existsSync(codexSkillPath)) {
123
+ console.log(` Updating existing skill at ${codexSkillPath}`);
124
+ }
125
+ fs.copyFileSync(
126
+ path.join(skillSource, 'codex', 'tab-agent.md'),
127
+ codexSkillPath
128
+ );
129
+ console.log('✓ Installed Codex skill');
130
+ }
131
+ }
132
+
133
+ setup().catch(console.error);
package/cli/start.js ADDED
@@ -0,0 +1,19 @@
1
+ // cli/start.js
2
+ const path = require('path');
3
+ const { spawn } = require('child_process');
4
+
5
+ const serverPath = path.join(__dirname, '..', 'relay', 'server.js');
6
+ const server = spawn('node', [serverPath], {
7
+ stdio: 'inherit',
8
+ detached: false
9
+ });
10
+
11
+ server.on('error', (err) => {
12
+ console.error('Failed to start server:', err.message);
13
+ process.exit(1);
14
+ });
15
+
16
+ process.on('SIGINT', () => {
17
+ server.kill();
18
+ process.exit(0);
19
+ });
package/cli/status.js ADDED
@@ -0,0 +1,70 @@
1
+ // cli/status.js
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const os = require('os');
5
+ const http = require('http');
6
+
7
+ function checkRelayServer() {
8
+ return new Promise((resolve) => {
9
+ const req = http.get('http://localhost:9876/health', (res) => {
10
+ let data = '';
11
+ res.on('data', chunk => data += chunk);
12
+ res.on('end', () => {
13
+ try {
14
+ const json = JSON.parse(data);
15
+ resolve({ running: true, clients: json.clients });
16
+ } catch {
17
+ resolve({ running: false });
18
+ }
19
+ });
20
+ });
21
+ req.on('error', () => resolve({ running: false }));
22
+ req.setTimeout(2000, () => {
23
+ req.destroy();
24
+ resolve({ running: false });
25
+ });
26
+ });
27
+ }
28
+
29
+ async function status() {
30
+ console.log('Tab Agent Status\n');
31
+
32
+ // Check native host
33
+ const home = os.homedir();
34
+ const platform = os.platform();
35
+ let manifestPath;
36
+
37
+ if (platform === 'darwin') {
38
+ manifestPath = path.join(home, 'Library/Application Support/Google/Chrome/NativeMessagingHosts/com.tabagent.relay.json');
39
+ } else if (platform === 'linux') {
40
+ manifestPath = path.join(home, '.config/google-chrome/NativeMessagingHosts/com.tabagent.relay.json');
41
+ } else if (platform === 'win32') {
42
+ manifestPath = path.join(home, 'AppData/Local/Google/Chrome/User Data/NativeMessagingHosts/com.tabagent.relay.json');
43
+ }
44
+
45
+ if (fs.existsSync(manifestPath)) {
46
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
47
+ console.log('Native Host: Installed');
48
+ console.log(` Extension: ${manifest.allowed_origins[0]}`);
49
+ } else {
50
+ console.log('Native Host: Not installed (run: npx tab-agent setup)');
51
+ }
52
+
53
+ // Check skills
54
+ const claudeSkill = path.join(home, '.claude', 'skills', 'tab-agent.md');
55
+ const codexSkill = path.join(home, '.codex', 'skills', 'tab-agent.md');
56
+
57
+ console.log(`\nClaude Skill: ${fs.existsSync(claudeSkill) ? 'Installed' : 'Not installed'} ${claudeSkill}`);
58
+ console.log(`Codex Skill: ${fs.existsSync(codexSkill) ? 'Installed' : 'Not installed (optional)'} ${codexSkill}`);
59
+
60
+ // Check relay server
61
+ console.log('\nRelay Server:');
62
+ const relayStatus = await checkRelayServer();
63
+ if (relayStatus.running) {
64
+ console.log(` Status: Running (${relayStatus.clients} client${relayStatus.clients !== 1 ? 's' : ''})`);
65
+ } else {
66
+ console.log(' Status: Not running (starts automatically when needed)');
67
+ }
68
+ }
69
+
70
+ status().catch(console.error);