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.
- package/README.md +62 -0
- package/bin/tab-agent.js +40 -0
- package/cli/detect-extension.js +131 -0
- package/cli/setup.js +133 -0
- package/cli/start.js +19 -0
- package/cli/status.js +70 -0
- package/extension/content-script.js +510 -0
- package/extension/icons/icon128.png +0 -0
- package/extension/icons/icon16.png +0 -0
- package/extension/icons/icon48.png +0 -0
- package/extension/manifest.json +40 -0
- package/extension/popup/popup.html +142 -0
- package/extension/popup/popup.js +104 -0
- package/extension/service-worker.js +471 -0
- package/extension/snapshot.js +194 -0
- package/package.json +25 -0
- package/relay/install-native-host.sh +57 -0
- package/relay/native-host-wrapper.cmd +3 -0
- package/relay/native-host-wrapper.sh +29 -0
- package/relay/native-host.js +128 -0
- package/relay/node_modules/.package-lock.json +29 -0
- package/relay/node_modules/ws/LICENSE +20 -0
- package/relay/node_modules/ws/README.md +548 -0
- package/relay/node_modules/ws/browser.js +8 -0
- package/relay/node_modules/ws/index.js +13 -0
- package/relay/node_modules/ws/lib/buffer-util.js +131 -0
- package/relay/node_modules/ws/lib/constants.js +19 -0
- package/relay/node_modules/ws/lib/event-target.js +292 -0
- package/relay/node_modules/ws/lib/extension.js +203 -0
- package/relay/node_modules/ws/lib/limiter.js +55 -0
- package/relay/node_modules/ws/lib/permessage-deflate.js +528 -0
- package/relay/node_modules/ws/lib/receiver.js +706 -0
- package/relay/node_modules/ws/lib/sender.js +602 -0
- package/relay/node_modules/ws/lib/stream.js +161 -0
- package/relay/node_modules/ws/lib/subprotocol.js +62 -0
- package/relay/node_modules/ws/lib/validation.js +152 -0
- package/relay/node_modules/ws/lib/websocket-server.js +554 -0
- package/relay/node_modules/ws/lib/websocket.js +1393 -0
- package/relay/node_modules/ws/package.json +69 -0
- package/relay/node_modules/ws/wrapper.mjs +8 -0
- package/relay/package-lock.json +36 -0
- package/relay/package.json +12 -0
- package/relay/server.js +114 -0
- package/skills/claude-code/tab-agent.md +53 -0
- package/skills/codex/tab-agent.md +40 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ws",
|
|
3
|
+
"version": "8.19.0",
|
|
4
|
+
"description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"HyBi",
|
|
7
|
+
"Push",
|
|
8
|
+
"RFC-6455",
|
|
9
|
+
"WebSocket",
|
|
10
|
+
"WebSockets",
|
|
11
|
+
"real-time"
|
|
12
|
+
],
|
|
13
|
+
"homepage": "https://github.com/websockets/ws",
|
|
14
|
+
"bugs": "https://github.com/websockets/ws/issues",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/websockets/ws.git"
|
|
18
|
+
},
|
|
19
|
+
"author": "Einar Otto Stangvik <einaros@gmail.com> (http://2x.io)",
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"main": "index.js",
|
|
22
|
+
"exports": {
|
|
23
|
+
".": {
|
|
24
|
+
"browser": "./browser.js",
|
|
25
|
+
"import": "./wrapper.mjs",
|
|
26
|
+
"require": "./index.js"
|
|
27
|
+
},
|
|
28
|
+
"./package.json": "./package.json"
|
|
29
|
+
},
|
|
30
|
+
"browser": "browser.js",
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=10.0.0"
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"browser.js",
|
|
36
|
+
"index.js",
|
|
37
|
+
"lib/*.js",
|
|
38
|
+
"wrapper.mjs"
|
|
39
|
+
],
|
|
40
|
+
"scripts": {
|
|
41
|
+
"test": "nyc --reporter=lcov --reporter=text mocha --throw-deprecation test/*.test.js",
|
|
42
|
+
"integration": "mocha --throw-deprecation test/*.integration.js",
|
|
43
|
+
"lint": "eslint . && prettier --check --ignore-path .gitignore \"**/*.{json,md,yaml,yml}\""
|
|
44
|
+
},
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"bufferutil": "^4.0.1",
|
|
47
|
+
"utf-8-validate": ">=5.0.2"
|
|
48
|
+
},
|
|
49
|
+
"peerDependenciesMeta": {
|
|
50
|
+
"bufferutil": {
|
|
51
|
+
"optional": true
|
|
52
|
+
},
|
|
53
|
+
"utf-8-validate": {
|
|
54
|
+
"optional": true
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"benchmark": "^2.1.4",
|
|
59
|
+
"bufferutil": "^4.0.1",
|
|
60
|
+
"eslint": "^9.0.0",
|
|
61
|
+
"eslint-config-prettier": "^10.0.1",
|
|
62
|
+
"eslint-plugin-prettier": "^5.0.0",
|
|
63
|
+
"globals": "^16.0.0",
|
|
64
|
+
"mocha": "^8.4.0",
|
|
65
|
+
"nyc": "^15.0.0",
|
|
66
|
+
"prettier": "^3.0.0",
|
|
67
|
+
"utf-8-validate": "^6.0.0"
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import createWebSocketStream from './lib/stream.js';
|
|
2
|
+
import Receiver from './lib/receiver.js';
|
|
3
|
+
import Sender from './lib/sender.js';
|
|
4
|
+
import WebSocket from './lib/websocket.js';
|
|
5
|
+
import WebSocketServer from './lib/websocket-server.js';
|
|
6
|
+
|
|
7
|
+
export { createWebSocketStream, Receiver, Sender, WebSocket, WebSocketServer };
|
|
8
|
+
export default WebSocket;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "tab-agent-relay",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"lockfileVersion": 3,
|
|
5
|
+
"requires": true,
|
|
6
|
+
"packages": {
|
|
7
|
+
"": {
|
|
8
|
+
"name": "tab-agent-relay",
|
|
9
|
+
"version": "0.1.0",
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"ws": "^8.16.0"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"node_modules/ws": {
|
|
15
|
+
"version": "8.19.0",
|
|
16
|
+
"resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
|
|
17
|
+
"integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">=10.0.0"
|
|
21
|
+
},
|
|
22
|
+
"peerDependencies": {
|
|
23
|
+
"bufferutil": "^4.0.1",
|
|
24
|
+
"utf-8-validate": ">=5.0.2"
|
|
25
|
+
},
|
|
26
|
+
"peerDependenciesMeta": {
|
|
27
|
+
"bufferutil": {
|
|
28
|
+
"optional": true
|
|
29
|
+
},
|
|
30
|
+
"utf-8-validate": {
|
|
31
|
+
"optional": true
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
package/relay/server.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// relay/server.js
|
|
2
|
+
const WebSocket = require('ws');
|
|
3
|
+
const http = require('http');
|
|
4
|
+
|
|
5
|
+
const PORT = process.env.PORT || 9876;
|
|
6
|
+
|
|
7
|
+
const httpServer = http.createServer((req, res) => {
|
|
8
|
+
if (req.url === '/health') {
|
|
9
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
10
|
+
res.end(JSON.stringify({ ok: true, clients: wss.clients.size }));
|
|
11
|
+
} else {
|
|
12
|
+
res.writeHead(404);
|
|
13
|
+
res.end('Not found');
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const wss = new WebSocket.Server({ server: httpServer });
|
|
18
|
+
|
|
19
|
+
// Store extension connection
|
|
20
|
+
let extensionConnection = null;
|
|
21
|
+
const pendingRequests = new Map();
|
|
22
|
+
|
|
23
|
+
function safeParse(data, label) {
|
|
24
|
+
try {
|
|
25
|
+
return JSON.parse(data);
|
|
26
|
+
} catch (error) {
|
|
27
|
+
console.warn(`Invalid JSON from ${label}:`, error);
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function failPendingRequests(reason) {
|
|
33
|
+
for (const [id, pending] of pendingRequests.entries()) {
|
|
34
|
+
try {
|
|
35
|
+
pending.ws.send(JSON.stringify({ id: pending.clientId, ok: false, error: reason }));
|
|
36
|
+
} catch (error) {
|
|
37
|
+
console.warn('Failed to notify pending request:', error);
|
|
38
|
+
}
|
|
39
|
+
pendingRequests.delete(id);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
wss.on('connection', (ws, req) => {
|
|
44
|
+
const isExtension = req.headers['x-client-type'] === 'extension';
|
|
45
|
+
|
|
46
|
+
if (isExtension) {
|
|
47
|
+
console.log('Extension connected');
|
|
48
|
+
extensionConnection = ws;
|
|
49
|
+
|
|
50
|
+
ws.on('message', (data) => {
|
|
51
|
+
const message = safeParse(data, 'extension');
|
|
52
|
+
if (!message || typeof message.id === 'undefined') {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const { id, ...response } = message;
|
|
56
|
+
|
|
57
|
+
const pending = pendingRequests.get(id);
|
|
58
|
+
if (pending) {
|
|
59
|
+
pending.ws.send(JSON.stringify({ id: pending.clientId, ...response }));
|
|
60
|
+
pendingRequests.delete(id);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
ws.on('close', () => {
|
|
65
|
+
console.log('Extension disconnected');
|
|
66
|
+
extensionConnection = null;
|
|
67
|
+
failPendingRequests('Extension disconnected');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
} else {
|
|
71
|
+
console.log('Skill client connected');
|
|
72
|
+
|
|
73
|
+
ws.on('message', async (data) => {
|
|
74
|
+
const message = safeParse(data, 'client');
|
|
75
|
+
if (!message || typeof message.id === 'undefined') {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const { id, ...command } = message;
|
|
79
|
+
|
|
80
|
+
console.log(`Command: ${command.action}`, command);
|
|
81
|
+
|
|
82
|
+
if (!extensionConnection) {
|
|
83
|
+
ws.send(JSON.stringify({ id, ok: false, error: 'Extension not connected' }));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const internalId = Date.now() + Math.random();
|
|
88
|
+
pendingRequests.set(internalId, { ws, clientId: id });
|
|
89
|
+
|
|
90
|
+
extensionConnection.send(JSON.stringify({ id: internalId, ...command }));
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
ws.on('close', () => {
|
|
94
|
+
console.log('Skill client disconnected');
|
|
95
|
+
for (const [id, pending] of pendingRequests.entries()) {
|
|
96
|
+
if (pending.ws === ws) {
|
|
97
|
+
pendingRequests.delete(id);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
httpServer.listen(PORT, () => {
|
|
105
|
+
console.log(`Tab Agent Relay running on ws://localhost:${PORT}`);
|
|
106
|
+
console.log(`Health check: http://localhost:${PORT}/health`);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
process.on('SIGINT', () => {
|
|
110
|
+
console.log('\nShutting down...');
|
|
111
|
+
wss.close();
|
|
112
|
+
httpServer.close();
|
|
113
|
+
process.exit(0);
|
|
114
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: tab-agent
|
|
3
|
+
description: Browser control via WebSocket - snapshot, click, type, fill, evaluate, screenshot
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Tab Agent
|
|
7
|
+
|
|
8
|
+
WebSocket `ws://localhost:9876`. User activates tabs via extension icon (green = active).
|
|
9
|
+
|
|
10
|
+
## Before First Command
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
curl -s http://localhost:9876/health || (npx tab-agent start &)
|
|
14
|
+
sleep 2
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Commands
|
|
18
|
+
|
|
19
|
+
```json
|
|
20
|
+
{"id": 1, "action": "tabs"} // list active tabs
|
|
21
|
+
{"id": 2, "action": "snapshot", "tabId": ID} // get page with refs [e1], [e2]...
|
|
22
|
+
{"id": 3, "action": "screenshot", "tabId": ID} // viewport screenshot
|
|
23
|
+
{"id": 4, "action": "screenshot", "tabId": ID, "fullPage": true} // full page screenshot
|
|
24
|
+
{"id": 5, "action": "click", "tabId": ID, "ref": "e1"} // click element
|
|
25
|
+
{"id": 6, "action": "fill", "tabId": ID, "ref": "e1", "value": "text"} // fill input
|
|
26
|
+
{"id": 7, "action": "type", "tabId": ID, "ref": "e1", "text": "hello"} // type text
|
|
27
|
+
{"id": 8, "action": "type", "tabId": ID, "ref": "e1", "text": "query", "submit": true} // type and Enter
|
|
28
|
+
{"id": 9, "action": "press", "tabId": ID, "key": "Enter"} // press key
|
|
29
|
+
{"id": 10, "action": "scroll", "tabId": ID, "direction": "down", "amount": 500}
|
|
30
|
+
{"id": 11, "action": "scrollintoview", "tabId": ID, "ref": "e1"} // scroll element visible
|
|
31
|
+
{"id": 12, "action": "navigate", "tabId": ID, "url": "https://..."}
|
|
32
|
+
{"id": 13, "action": "wait", "tabId": ID, "text": "Loading complete"} // wait for text
|
|
33
|
+
{"id": 14, "action": "wait", "tabId": ID, "selector": ".results", "timeout": 5000} // wait for element
|
|
34
|
+
{"id": 15, "action": "evaluate", "tabId": ID, "script": "document.title"} // run JavaScript
|
|
35
|
+
{"id": 16, "action": "batchfill", "tabId": ID, "fields": [{"ref": "e1", "value": "a"}, {"ref": "e2", "value": "b"}]}
|
|
36
|
+
{"id": 17, "action": "dialog", "tabId": ID, "accept": true} // handle alert/confirm
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Usage
|
|
40
|
+
|
|
41
|
+
1. `tabs` -> get tabId of active tab
|
|
42
|
+
2. `snapshot` -> read page, get element refs [e1], [e2]...
|
|
43
|
+
3. `click`/`fill`/`type` using refs
|
|
44
|
+
4. If snapshot incomplete (complex page) -> `screenshot` and analyze visually
|
|
45
|
+
|
|
46
|
+
## Notes
|
|
47
|
+
|
|
48
|
+
- Screenshot returns `{"screenshot": "data:image/png;base64,..."}` - analyze directly
|
|
49
|
+
- Snapshot refs reset on each call - always snapshot before interacting
|
|
50
|
+
- Keys: Enter, Escape, Tab, Backspace, ArrowUp/Down/Left/Right
|
|
51
|
+
- `type` with `submit: true` presses Enter after typing (for search boxes)
|
|
52
|
+
- `evaluate` runs in page context - can access page variables/functions
|
|
53
|
+
- `dialog` handles alert/confirm/prompt - debugger bar appears when attached
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: tab-agent
|
|
3
|
+
description: Browser control via WebSocket
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Tab Agent
|
|
7
|
+
|
|
8
|
+
`ws://localhost:9876` - User activates tabs via extension (green = active)
|
|
9
|
+
|
|
10
|
+
## Start Relay
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
curl -s http://localhost:9876/health || (npx tab-agent start &)
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Commands
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
tabs -> list active tabs
|
|
20
|
+
snapshot tabId -> page with refs [e1], [e2]...
|
|
21
|
+
screenshot tabId -> viewport screenshot (base64 PNG)
|
|
22
|
+
screenshot tabId fullPage=true -> full page screenshot
|
|
23
|
+
click tabId ref -> click element
|
|
24
|
+
fill tabId ref value -> fill input
|
|
25
|
+
type tabId ref text -> type text
|
|
26
|
+
type tabId ref text submit=true -> type and press Enter
|
|
27
|
+
press tabId key -> Enter/Escape/Tab/Arrow*
|
|
28
|
+
scroll tabId direction amount -> scroll page
|
|
29
|
+
scrollintoview tabId ref -> scroll element visible
|
|
30
|
+
navigate tabId url -> go to URL
|
|
31
|
+
wait tabId text="..." -> wait for text
|
|
32
|
+
wait tabId selector="..." timeout=ms -> wait for element
|
|
33
|
+
evaluate tabId script="..." -> run JavaScript
|
|
34
|
+
batchfill tabId fields=[...] -> fill multiple fields
|
|
35
|
+
dialog tabId accept=true -> handle alert/confirm
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Flow
|
|
39
|
+
|
|
40
|
+
`tabs` -> `snapshot` -> `click`/`fill` -> repeat. Use `screenshot` if snapshot incomplete.
|