domagent 1.0.6

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 (4) hide show
  1. package/README.md +111 -0
  2. package/index.js +227 -0
  3. package/package.json +46 -0
  4. package/server.js +587 -0
package/README.md ADDED
@@ -0,0 +1,111 @@
1
+ # domagent — MCP Server
2
+
3
+ > The MCP (Model Context Protocol) server for DOMAgent. Lets AI agents control your real Chrome or Firefox browser through a local WebSocket bridge.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -g domagent
9
+ ```
10
+
11
+ Or run without installing:
12
+
13
+ ```bash
14
+ npx domagent
15
+ ```
16
+
17
+ ## What it does
18
+
19
+ `domagent` is a local server that:
20
+ - Starts a WebSocket listener on `ws://127.0.0.1:18792/extension`
21
+ - Waits for the browser extension (Chrome or Firefox) to connect
22
+ - Exposes browser actions as MCP tools to any AI agent that supports the Model Context Protocol
23
+
24
+ The browser extension and the MCP server work together — the extension relays commands from the server into the real browser tab.
25
+
26
+ ## Requirements
27
+
28
+ - Node.js 18 or newer
29
+ - The **DOMAgent browser extension** installed in Chrome or Firefox
30
+ → [Chrome install guide](../domagent-extension/chrome/README.md)
31
+ → [Firefox install guide](../domagent-extension/firefox/README.md)
32
+
33
+ ## Configure your AI agent
34
+
35
+ Add `domagent` as an MCP server in your agent's config file.
36
+
37
+ ### Claude Desktop (`claude_desktop_config.json`)
38
+
39
+ ```json
40
+ {
41
+ "mcpServers": {
42
+ "domagent": {
43
+ "command": "npx",
44
+ "args": ["domagent"]
45
+ }
46
+ }
47
+ }
48
+ ```
49
+
50
+ If you installed globally (`npm install -g domagent`):
51
+
52
+ ```json
53
+ {
54
+ "mcpServers": {
55
+ "domagent": {
56
+ "command": "domagent"
57
+ }
58
+ }
59
+ }
60
+ ```
61
+
62
+ ### Other MCP-compatible agents
63
+
64
+ Any agent that supports MCP stdio transport can use:
65
+
66
+ ```
67
+ command: npx domagent
68
+ transport: stdio
69
+ ```
70
+
71
+ ## Available MCP Tools
72
+
73
+ | Tool | Description |
74
+ |------|-------------|
75
+ | `navigate` | Open a URL — reuses the automation tab (no duplicate tabs) |
76
+ | `use_current_tab` | Adopt the user's active tab — no new tab created |
77
+ | `click` | Click an element by CSS selector |
78
+ | `type_text` | Type into an input field by CSS selector |
79
+ | `get_text` | Get the text content of an element |
80
+ | `evaluate_script` | Execute arbitrary JavaScript in the page |
81
+ | `get_screenshot` | Capture a PNG screenshot of the current page |
82
+ | `get_interactive_elements` | List all interactive elements with selectors and bounding boxes |
83
+ | `clear_overlays` | Remove all visual overlay boxes from the page |
84
+
85
+ ## Quick start (3 steps)
86
+
87
+ **Step 1** — Start the MCP server:
88
+ ```bash
89
+ npx domagent
90
+ ```
91
+ You should see:
92
+ ```
93
+ DOMAgent Bridge running on ws://127.0.0.1:18792/extension
94
+ ```
95
+
96
+ **Step 2** — Load the browser extension:
97
+ - Chrome: `chrome://extensions` → Developer mode → Load unpacked → select `domagent-extension/chrome/`
98
+ - Firefox: `about:debugging` → Load Temporary Add-on → select `domagent-extension/firefox/manifest.json`
99
+
100
+ **Step 3** — Add to your AI agent config (see above) and start chatting.
101
+
102
+ ## Configuration
103
+
104
+ By default the server binds to `127.0.0.1:18792`. To change this, edit the host/port/path in the browser extension's Options page (right-click the extension icon → Options).
105
+
106
+ The server accepts one WebSocket connection at a time — the browser extension.
107
+
108
+ ## Source
109
+
110
+ Full source, browser extensions, and documentation:
111
+ https://github.com/vaishnavucv/domagent
package/index.js ADDED
@@ -0,0 +1,227 @@
1
+ #!/usr/bin/env node
2
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import {
5
+ CallToolRequestSchema,
6
+ ListToolsRequestSchema,
7
+ } from "@modelcontextprotocol/sdk/types.js";
8
+ import { BridgeServer } from './server.js';
9
+
10
+ /* ─── Initialize servers ────────────────────────────────────────── */
11
+
12
+ const bridgeServer = new BridgeServer();
13
+ bridgeServer.start().catch((err) => {
14
+ console.error("Failed to start Bridge Server:", err);
15
+ process.exit(1);
16
+ });
17
+
18
+ const server = new Server(
19
+ { name: "domagent-mcp", version: "1.0.0" },
20
+ { capabilities: { tools: {} } },
21
+ );
22
+
23
+ /* ─── Tool definitions ──────────────────────────────────────────── */
24
+
25
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
26
+ tools: [
27
+ {
28
+ name: "navigate",
29
+ description:
30
+ "Navigate to a URL in the browser. Reuses the existing automation tab " +
31
+ "if one already exists (no duplicate tabs). Only creates a new tab the " +
32
+ "very first time. Use this for opening websites or changing pages.",
33
+ inputSchema: {
34
+ type: "object",
35
+ properties: {
36
+ url: {
37
+ type: "string",
38
+ description: "Full URL to navigate to (e.g. https://example.com)",
39
+ },
40
+ },
41
+ required: ["url"],
42
+ },
43
+ },
44
+ {
45
+ name: "use_current_tab",
46
+ description:
47
+ "Adopt the user's currently active browser tab as the automation target. " +
48
+ "Use this when the user asks you to look at, study, read, or interact " +
49
+ "with a page they already have open. No new tab is created.",
50
+ inputSchema: {
51
+ type: "object",
52
+ properties: {},
53
+ },
54
+ },
55
+ {
56
+ name: "click",
57
+ description:
58
+ "Click an element on the page using a CSS selector. " +
59
+ "Shows a yellow pulsing highlight box around the element and an orange click dot.",
60
+ inputSchema: {
61
+ type: "object",
62
+ properties: {
63
+ selector: {
64
+ type: "string",
65
+ description: "CSS selector of the element to click.",
66
+ },
67
+ },
68
+ required: ["selector"],
69
+ },
70
+ },
71
+ {
72
+ name: "type_text",
73
+ description:
74
+ "Type text into an input field identified by a CSS selector. " +
75
+ "Shows a green pulsing highlight box around the element and a blue dot.",
76
+ inputSchema: {
77
+ type: "object",
78
+ properties: {
79
+ selector: {
80
+ type: "string",
81
+ description: "CSS selector of the input field.",
82
+ },
83
+ text: {
84
+ type: "string",
85
+ description: "The text to type into the field.",
86
+ },
87
+ },
88
+ required: ["selector", "text"],
89
+ },
90
+ },
91
+ {
92
+ name: "get_text",
93
+ description: "Get the visible text content of an element.",
94
+ inputSchema: {
95
+ type: "object",
96
+ properties: {
97
+ selector: {
98
+ type: "string",
99
+ description: "CSS selector of the element.",
100
+ },
101
+ },
102
+ required: ["selector"],
103
+ },
104
+ },
105
+ {
106
+ name: "evaluate_script",
107
+ description:
108
+ "Execute arbitrary JavaScript in the page context and return the result.",
109
+ inputSchema: {
110
+ type: "object",
111
+ properties: {
112
+ script: {
113
+ type: "string",
114
+ description: "JavaScript code to execute.",
115
+ },
116
+ },
117
+ required: ["script"],
118
+ },
119
+ },
120
+ {
121
+ name: "get_screenshot",
122
+ description: "Capture a PNG screenshot of the current page (base64 encoded).",
123
+ inputSchema: {
124
+ type: "object",
125
+ properties: {},
126
+ },
127
+ },
128
+ {
129
+ name: "get_interactive_elements",
130
+ description:
131
+ "Scan the page and return all interactive elements AND text content with CSS selectors, " +
132
+ "text, and bounding boxes. Draws visual overlays: yellow dashed boxes for clickable elements, " +
133
+ "green dashed boxes for typeable elements (each with index badge), and cyan thin solid boxes " +
134
+ "(50% opacity) on all text content elements (p, h1-h6, span, li, etc.). Overlays auto-remove after 4s.",
135
+ inputSchema: {
136
+ type: "object",
137
+ properties: {},
138
+ },
139
+ },
140
+ {
141
+ name: "clear_overlays",
142
+ description:
143
+ "Remove all visual overlay boxes from the page. Use this if overlays " +
144
+ "are cluttering the view or before taking a clean screenshot.",
145
+ inputSchema: {
146
+ type: "object",
147
+ properties: {},
148
+ },
149
+ },
150
+ ],
151
+ }));
152
+
153
+ /* ─── Tool execution ────────────────────────────────────────────── */
154
+
155
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
156
+ const { name, arguments: args } = request.params;
157
+
158
+ try {
159
+ let result;
160
+
161
+ switch (name) {
162
+ case "navigate":
163
+ await bridgeServer.navigate(args.url);
164
+ result = `Navigated to ${args.url}`;
165
+ break;
166
+
167
+ case "use_current_tab": {
168
+ const info = await bridgeServer.useCurrentTab();
169
+ result = `Using active tab — URL: ${info.url || "unknown"}, Title: ${info.title || "unknown"}`;
170
+ break;
171
+ }
172
+
173
+ case "click":
174
+ result = await bridgeServer.click(args.selector);
175
+ break;
176
+
177
+ case "type_text":
178
+ result = await bridgeServer.type(args.selector, args.text);
179
+ break;
180
+
181
+ case "get_text":
182
+ result = await bridgeServer.getText(args.selector);
183
+ break;
184
+
185
+ case "evaluate_script":
186
+ result = await bridgeServer.evaluate(args.script);
187
+ if (typeof result !== "string") result = JSON.stringify(result);
188
+ break;
189
+
190
+ case "get_screenshot": {
191
+ const data = await bridgeServer.getScreenshot();
192
+ return {
193
+ content: [{ type: "image", data, mimeType: "image/png" }],
194
+ };
195
+ }
196
+
197
+ case "get_interactive_elements":
198
+ result = await bridgeServer.getInteractiveElements();
199
+ if (typeof result !== "string") result = JSON.stringify(result, null, 2);
200
+ break;
201
+
202
+ case "clear_overlays":
203
+ result = await bridgeServer.clearOverlays();
204
+ break;
205
+
206
+ default:
207
+ throw new Error(`Unknown tool: ${name}`);
208
+ }
209
+
210
+ return {
211
+ content: [{ type: "text", text: String(result) }],
212
+ };
213
+ } catch (error) {
214
+ return {
215
+ content: [{ type: "text", text: `Error: ${error.message}` }],
216
+ isError: true,
217
+ };
218
+ }
219
+ });
220
+
221
+ /* ─── Start ─────────────────────────────────────────────────────── */
222
+
223
+ const transport = new StdioServerTransport();
224
+ server.connect(transport).catch((error) => {
225
+ console.error("Server error:", error);
226
+ process.exit(1);
227
+ });
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "domagent",
3
+ "version": "1.0.6",
4
+ "description": "MCP server for DOMAgent — let AI agents control your real browser via Chrome or Firefox extension",
5
+ "main": "index.js",
6
+ "type": "module",
7
+ "bin": {
8
+ "domagent": "./index.js"
9
+ },
10
+ "scripts": {
11
+ "start": "node index.js"
12
+ },
13
+ "files": [
14
+ "index.js",
15
+ "server.js",
16
+ "README.md"
17
+ ],
18
+ "keywords": [
19
+ "mcp",
20
+ "model-context-protocol",
21
+ "browser-automation",
22
+ "ai",
23
+ "chrome",
24
+ "firefox",
25
+ "dom",
26
+ "domagent",
27
+ "cdp",
28
+ "devtools-protocol"
29
+ ],
30
+ "license": "MIT",
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "git+https://github.com/vaishnavucv/domagent.git"
34
+ },
35
+ "homepage": "https://github.com/vaishnavucv/domagent#readme",
36
+ "bugs": {
37
+ "url": "https://github.com/vaishnavucv/domagent/issues"
38
+ },
39
+ "engines": {
40
+ "node": ">=18.0.0"
41
+ },
42
+ "dependencies": {
43
+ "@modelcontextprotocol/sdk": "0.6.0",
44
+ "ws": "^8.18.0"
45
+ }
46
+ }
package/server.js ADDED
@@ -0,0 +1,587 @@
1
+ import { WebSocketServer } from 'ws';
2
+ import { createServer } from 'http';
3
+
4
+ function escapeJS(str) {
5
+ return String(str)
6
+ .replace(/\\/g, '\\\\')
7
+ .replace(/'/g, "\\'")
8
+ .replace(/\n/g, '\\n')
9
+ .replace(/\r/g, '\\r')
10
+ .replace(/\t/g, '\\t');
11
+ }
12
+
13
+ const OVERLAY_CSS = `
14
+ .__da-scan-box {
15
+ position: fixed !important;
16
+ pointer-events: none !important;
17
+ z-index: 2147483640 !important;
18
+ border: 1.5px dashed !important;
19
+ border-radius: 3px !important;
20
+ box-sizing: border-box !important;
21
+ transition: opacity 0.4s ease !important;
22
+ }
23
+ .__da-scan-box[data-kind="click"] {
24
+ border-color: rgba(234, 179, 8, 0.75) !important;
25
+ background: rgba(234, 179, 8, 0.04) !important;
26
+ }
27
+ .__da-scan-box[data-kind="type"] {
28
+ border-color: rgba(34, 197, 94, 0.75) !important;
29
+ background: rgba(34, 197, 94, 0.04) !important;
30
+ }
31
+ .__da-scan-box[data-kind="text"] {
32
+ border: 1px solid rgba(0, 210, 255, 0.50) !important;
33
+ background: rgba(0, 210, 255, 0.05) !important;
34
+ }
35
+ .__da-idx {
36
+ position: absolute !important;
37
+ top: -1px !important;
38
+ left: -1px !important;
39
+ background: rgba(255, 90, 54, 0.92) !important;
40
+ color: #fff !important;
41
+ font: bold 9px/1 system-ui, sans-serif !important;
42
+ padding: 1px 4px 2px !important;
43
+ border-radius: 0 0 4px 0 !important;
44
+ pointer-events: none !important;
45
+ letter-spacing: 0.3px !important;
46
+ }
47
+ .__da-action-hl {
48
+ position: fixed !important;
49
+ pointer-events: none !important;
50
+ z-index: 2147483645 !important;
51
+ border-radius: 4px !important;
52
+ box-sizing: border-box !important;
53
+ animation: __da-pulse 0.5s ease-in-out 3 !important;
54
+ }
55
+ .__da-action-hl[data-action="click"] {
56
+ border: 2.5px solid rgba(234, 179, 8, 0.95) !important;
57
+ background: rgba(234, 179, 8, 0.10) !important;
58
+ box-shadow: 0 0 8px rgba(234, 179, 8, 0.35) !important;
59
+ }
60
+ .__da-action-hl[data-action="type"] {
61
+ border: 2.5px solid rgba(34, 197, 94, 0.95) !important;
62
+ background: rgba(34, 197, 94, 0.10) !important;
63
+ box-shadow: 0 0 8px rgba(34, 197, 94, 0.35) !important;
64
+ }
65
+ @keyframes __da-pulse {
66
+ 0%, 100% { opacity: 1; }
67
+ 50% { opacity: 0.4; }
68
+ }
69
+ `;
70
+
71
+ export class BridgeServer {
72
+ constructor(port = 18792, path = '/extension') {
73
+ this.port = port;
74
+ this.path = path;
75
+ this.wss = null;
76
+ this.httpServer = null;
77
+ this.activeConnection = null;
78
+ this.pendingRequests = new Map();
79
+ this.nextId = 1;
80
+ this.activeSessionId = null;
81
+ }
82
+
83
+ start() {
84
+ return new Promise((resolve) => {
85
+ this.httpServer = createServer((req, res) => {
86
+ if ((req.method === 'HEAD' || req.method === 'GET') && (req.url === '/' || req.url === '/health')) {
87
+ res.writeHead(200, { 'Content-Type': 'text/plain' });
88
+ res.end('OK');
89
+ return;
90
+ }
91
+ res.writeHead(404);
92
+ res.end();
93
+ });
94
+
95
+ this.wss = new WebSocketServer({ server: this.httpServer });
96
+
97
+ this.wss.on('connection', (ws, req) => {
98
+ if (req.url !== this.path) { ws.close(); return; }
99
+
100
+ console.error('Extension connected');
101
+ this.activeConnection = ws;
102
+
103
+ ws.on('message', (raw) => {
104
+ try { this.handleMessage(JSON.parse(raw)); }
105
+ catch (e) { console.error('Parse error:', e); }
106
+ });
107
+
108
+ ws.on('close', () => {
109
+ console.error('Extension disconnected');
110
+ this.activeConnection = null;
111
+ this.activeSessionId = null;
112
+ });
113
+
114
+ ws.on('error', (err) => console.error('WS error:', err));
115
+ });
116
+
117
+ this.httpServer.listen(this.port, '127.0.0.1', () => {
118
+ console.error(`DOMAgent Bridge running on ws://127.0.0.1:${this.port}${this.path}`);
119
+ resolve();
120
+ });
121
+ });
122
+ }
123
+
124
+ handleMessage(data) {
125
+ if (data.method === 'ping') { this.send({ method: 'pong' }); return; }
126
+
127
+ if (data.id && (data.result !== undefined || data.error !== undefined)) {
128
+ const p = this.pendingRequests.get(data.id);
129
+ if (p) {
130
+ this.pendingRequests.delete(data.id);
131
+ data.error ? p.reject(new Error(data.error)) : p.resolve(data.result);
132
+ }
133
+ }
134
+ }
135
+
136
+ send(payload) {
137
+ if (!this.activeConnection || this.activeConnection.readyState !== 1) {
138
+ throw new Error('Extension not connected');
139
+ }
140
+ this.activeConnection.send(JSON.stringify(payload));
141
+ }
142
+
143
+ async sendCommand(method, params = {}) {
144
+ if (!this.activeConnection) throw new Error('Extension not connected');
145
+
146
+ const id = this.nextId++;
147
+ const cmdParams = { method, params };
148
+
149
+ if (this.activeSessionId) {
150
+ cmdParams.sessionId = this.activeSessionId;
151
+ }
152
+
153
+ const payload = { id, method: 'forwardCDPCommand', params: cmdParams };
154
+
155
+ return new Promise((resolve, reject) => {
156
+ const timeout = setTimeout(() => {
157
+ if (this.pendingRequests.has(id)) {
158
+ this.pendingRequests.delete(id);
159
+ reject(new Error(`Command ${method} timed out`));
160
+ }
161
+ }, 30000);
162
+
163
+ this.pendingRequests.set(id, {
164
+ resolve: (res) => { clearTimeout(timeout); resolve(res); },
165
+ reject: (err) => { clearTimeout(timeout); reject(err); },
166
+ });
167
+
168
+ try { this.send(payload); }
169
+ catch (err) { clearTimeout(timeout); this.pendingRequests.delete(id); reject(err); }
170
+ });
171
+ }
172
+
173
+ async navigate(url) {
174
+ console.error(`Navigating to: ${url}`);
175
+ const result = await this.sendCommand('Browser.ensureTab', { url });
176
+ if (result?.sessionId) this.activeSessionId = result.sessionId;
177
+ return result;
178
+ }
179
+
180
+ async useCurrentTab() {
181
+ console.error('Adopting current tab as automation target');
182
+ const result = await this.sendCommand('Browser.useCurrentTab', {});
183
+ if (result?.sessionId) this.activeSessionId = result.sessionId;
184
+ return result;
185
+ }
186
+
187
+ async getScreenshot() {
188
+ const result = await this.sendCommand('Page.captureScreenshot', { format: 'png' });
189
+ return result.data;
190
+ }
191
+
192
+ async evaluate(expression) {
193
+ const result = await this.sendCommand('Runtime.evaluate', {
194
+ expression,
195
+ returnByValue: true,
196
+ awaitPromise: true,
197
+ });
198
+ if (result.exceptionDetails) {
199
+ throw new Error(`Evaluation failed: ${result.exceptionDetails.text}`);
200
+ }
201
+ return result.result.value;
202
+ }
203
+
204
+ async getOverlaySettings() {
205
+ try {
206
+ const settings = await this.sendCommand('Browser.getOverlaySettings', {});
207
+ return {
208
+ overlayClickEnabled: settings.overlayClickEnabled !== false,
209
+ overlayClickOpacity: Number(settings.overlayClickOpacity) || 75,
210
+ overlayTypeEnabled: settings.overlayTypeEnabled !== false,
211
+ overlayTypeOpacity: Number(settings.overlayTypeOpacity) || 75,
212
+ overlayTextEnabled: settings.overlayTextEnabled !== false,
213
+ overlayTextOpacity: Number(settings.overlayTextOpacity) || 50,
214
+ };
215
+ } catch {
216
+ return {
217
+ overlayClickEnabled: true, overlayClickOpacity: 75,
218
+ overlayTypeEnabled: true, overlayTypeOpacity: 75,
219
+ overlayTextEnabled: true, overlayTextOpacity: 50,
220
+ };
221
+ }
222
+ }
223
+ async _ensureOverlayStyles() {
224
+ const cssEscaped = escapeJS(OVERLAY_CSS);
225
+ try {
226
+ await this.evaluate(`(function(){
227
+ if (document.getElementById('__da-style')) return 'exists';
228
+ var s = document.createElement('style');
229
+ s.id = '__da-style';
230
+ s.textContent = '${cssEscaped}';
231
+ (document.head || document.documentElement).appendChild(s);
232
+ return 'injected';
233
+ })()`);
234
+ } catch {
235
+ console.error('Warning: overlay CSS injection failed (CSP or page not ready)');
236
+ }
237
+ }
238
+ async clearOverlays() {
239
+ return this.evaluate(`(function(){
240
+ document.querySelectorAll('.__da-scan-box, .__da-action-hl, .__da-dot').forEach(function(el){ el.remove(); });
241
+ return 'cleared';
242
+ })()`);
243
+ }
244
+ async click(selector) {
245
+ await this._ensureOverlayStyles();
246
+ await this.clearOverlays().catch(() => { });
247
+
248
+ const cfg = await this.getOverlaySettings();
249
+ const showHL = cfg.overlayClickEnabled !== false;
250
+ const op = (cfg.overlayClickOpacity || 75) / 100;
251
+
252
+ const safe = escapeJS(selector);
253
+ const code = `(function(){
254
+ var el = document.querySelector('${safe}');
255
+ if (!el) throw new Error('Element not found: ${safe}');
256
+ var rect = el.getBoundingClientRect();
257
+ var cx = rect.left + rect.width / 2, cy = rect.top + rect.height / 2;
258
+ var container = document.body || document.documentElement;
259
+
260
+ if (${showHL}) {
261
+ var hl = document.createElement('div');
262
+ hl.className = '__da-action-hl';
263
+ hl.setAttribute('data-action', 'click');
264
+ hl.style.cssText = 'position:fixed;pointer-events:none;z-index:2147483645;'
265
+ + 'border-radius:4px;box-sizing:border-box;'
266
+ + 'left:' + (rect.left - 3) + 'px;top:' + (rect.top - 3) + 'px;'
267
+ + 'width:' + (rect.width + 6) + 'px;height:' + (rect.height + 6) + 'px;'
268
+ + 'border:2.5px solid rgba(234,179,8,' + ${op} + ');'
269
+ + 'background:rgba(234,179,8,' + ${op * 0.1} + ');'
270
+ + 'box-shadow:0 0 8px rgba(234,179,8,' + ${op * 0.35} + ');';
271
+ container.appendChild(hl);
272
+
273
+ var dot = document.createElement('div');
274
+ dot.className = '__da-dot';
275
+ dot.style.cssText = 'position:fixed;z-index:2147483647;pointer-events:none;'
276
+ + 'width:18px;height:18px;border-radius:50%;'
277
+ + 'background:rgba(255,90,54,0.85);'
278
+ + 'box-shadow:0 0 0 4px rgba(255,90,54,0.35),0 0 12px rgba(255,90,54,0.5);'
279
+ + 'left:' + (cx - 9) + 'px;top:' + (cy - 9) + 'px;'
280
+ + 'transition:transform .3s ease,opacity .4s ease;'
281
+ + 'transform:scale(1);opacity:1';
282
+ container.appendChild(dot);
283
+
284
+ requestAnimationFrame(function(){
285
+ setTimeout(function(){ dot.style.transform='scale(2.2)'; dot.style.opacity='0'; }, 150);
286
+ setTimeout(function(){ dot.remove(); }, 650);
287
+ setTimeout(function(){ hl.style.opacity='0'; hl.style.transition='opacity 0.4s ease'; }, 1200);
288
+ setTimeout(function(){ hl.remove(); }, 1700);
289
+ });
290
+ }
291
+
292
+ var evOpts = { bubbles: true, cancelable: true, view: window,
293
+ clientX: cx, clientY: cy, button: 0 };
294
+ el.dispatchEvent(new MouseEvent('pointerdown', evOpts));
295
+ el.dispatchEvent(new MouseEvent('mousedown', evOpts));
296
+ el.dispatchEvent(new MouseEvent('pointerup', evOpts));
297
+ el.dispatchEvent(new MouseEvent('mouseup', evOpts));
298
+ el.dispatchEvent(new MouseEvent('click', evOpts));
299
+ return 'Clicked: ${safe}';
300
+ })()`;
301
+ return this.evaluate(code);
302
+ }
303
+ async type(selector, text) {
304
+ await this._ensureOverlayStyles();
305
+ await this.clearOverlays().catch(() => { });
306
+
307
+ const cfg = await this.getOverlaySettings();
308
+ const showHL = cfg.overlayTypeEnabled !== false;
309
+ const op = (cfg.overlayTypeOpacity || 75) / 100;
310
+
311
+ const safeSel = escapeJS(selector);
312
+ const safeText = escapeJS(text);
313
+ const code = `(function(){
314
+ var el = document.querySelector('${safeSel}');
315
+ if (!el) throw new Error('Element not found: ${safeSel}');
316
+ var rect = el.getBoundingClientRect();
317
+ var cx = rect.left + rect.width / 2, cy = rect.top + rect.height / 2;
318
+ var container = document.body || document.documentElement;
319
+
320
+ if (${showHL}) {
321
+ var hl = document.createElement('div');
322
+ hl.className = '__da-action-hl';
323
+ hl.setAttribute('data-action', 'type');
324
+ hl.style.cssText = 'position:fixed;pointer-events:none;z-index:2147483645;'
325
+ + 'border-radius:4px;box-sizing:border-box;'
326
+ + 'left:' + (rect.left - 3) + 'px;top:' + (rect.top - 3) + 'px;'
327
+ + 'width:' + (rect.width + 6) + 'px;height:' + (rect.height + 6) + 'px;'
328
+ + 'border:2.5px solid rgba(34,197,94,' + ${op} + ');'
329
+ + 'background:rgba(34,197,94,' + ${op * 0.1} + ');'
330
+ + 'box-shadow:0 0 8px rgba(34,197,94,' + ${op * 0.35} + ');';
331
+ container.appendChild(hl);
332
+
333
+ var dot = document.createElement('div');
334
+ dot.className = '__da-dot';
335
+ dot.style.cssText = 'position:fixed;z-index:2147483647;pointer-events:none;'
336
+ + 'width:14px;height:14px;border-radius:50%;'
337
+ + 'background:rgba(59,130,246,0.85);'
338
+ + 'box-shadow:0 0 0 3px rgba(59,130,246,0.3),0 0 10px rgba(59,130,246,0.4);'
339
+ + 'left:' + (cx - 7) + 'px;top:' + (cy - 7) + 'px;'
340
+ + 'transition:transform .3s ease,opacity .5s ease;'
341
+ + 'transform:scale(1);opacity:1';
342
+ container.appendChild(dot);
343
+
344
+ requestAnimationFrame(function(){
345
+ setTimeout(function(){ dot.style.transform='scale(1.8)'; dot.style.opacity='0'; }, 350);
346
+ setTimeout(function(){ dot.remove(); }, 850);
347
+ setTimeout(function(){ hl.style.opacity='0'; hl.style.transition='opacity 0.4s ease'; }, 1500);
348
+ setTimeout(function(){ hl.remove(); }, 2000);
349
+ });
350
+ }
351
+
352
+ el.focus();
353
+
354
+ var proto = el.tagName === 'TEXTAREA'
355
+ ? Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value')
356
+ : Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value');
357
+ if (proto && proto.set) {
358
+ proto.set.call(el, '${safeText}');
359
+ } else {
360
+ el.value = '${safeText}';
361
+ }
362
+
363
+ el.dispatchEvent(new Event('input', { bubbles: true, cancelable: true }));
364
+ el.dispatchEvent(new Event('change', { bubbles: true, cancelable: true }));
365
+ return 'Typed into: ${safeSel}';
366
+ })()`;
367
+ return this.evaluate(code);
368
+ }
369
+ async getText(selector) {
370
+ const safe = escapeJS(selector);
371
+ return this.evaluate(`(function(){
372
+ var el = document.querySelector('${safe}');
373
+ return el ? el.innerText : null;
374
+ })()`);
375
+ }
376
+
377
+ async getInteractiveElements() {
378
+ await this._ensureOverlayStyles();
379
+ await this.clearOverlays().catch(() => { });
380
+
381
+ const cfg = await this.getOverlaySettings();
382
+ const showClick = cfg.overlayClickEnabled !== false;
383
+ const showType = cfg.overlayTypeEnabled !== false;
384
+ const showText = cfg.overlayTextEnabled !== false;
385
+ const opClick = (cfg.overlayClickOpacity || 75) / 100;
386
+ const opType = (cfg.overlayTypeOpacity || 75) / 100;
387
+ const opText = (cfg.overlayTextOpacity || 50) / 100;
388
+
389
+ return this.evaluate(`(function(){
390
+ var CFG = {
391
+ showClick: ${showClick}, showType: ${showType}, showText: ${showText},
392
+ opClick: ${opClick}, opType: ${opType}, opText: ${opText}
393
+ };
394
+
395
+ var gen = (window.__daOverlayGen || 0) + 1;
396
+ window.__daOverlayGen = gen;
397
+
398
+ var vw = window.innerWidth, vh = window.innerHeight;
399
+
400
+ var container = document.body || document.documentElement;
401
+ if (!container) return [];
402
+
403
+ function isVisible(el) {
404
+ if (!el) return false;
405
+ var s = getComputedStyle(el);
406
+ if (s.display === 'none' || s.visibility === 'hidden' || s.opacity === '0') return false;
407
+ if (el.offsetParent === null
408
+ && s.position !== 'fixed'
409
+ && s.position !== 'sticky'
410
+ && el.tagName !== 'BODY'
411
+ && el.tagName !== 'HTML') {
412
+ return false;
413
+ }
414
+ var r = el.getBoundingClientRect();
415
+ if (r.width <= 0 || r.height <= 0) return false;
416
+ if (r.right < 0 || r.bottom < 0 || r.left > vw || r.top > vh) return false;
417
+ return true;
418
+ }
419
+
420
+ function getPath(el) {
421
+ if (!(el instanceof Element)) return '';
422
+ var parts = [];
423
+ while (el && el.nodeType === 1) {
424
+ var tag = el.nodeName.toLowerCase();
425
+ if (el.id) {
426
+ var eid = typeof CSS !== 'undefined' && typeof CSS.escape === 'function'
427
+ ? CSS.escape(el.id)
428
+ : el.id.replace(/([^\\w-])/g, '\\\\$1');
429
+ parts.unshift(tag + '#' + eid);
430
+ break;
431
+ }
432
+ var sib = el, nth = 1;
433
+ while ((sib = sib.previousElementSibling)) {
434
+ if (sib.nodeName.toLowerCase() === tag) nth++;
435
+ }
436
+ if (nth > 1) tag += ':nth-of-type(' + nth + ')';
437
+ parts.unshift(tag);
438
+ el = el.parentNode;
439
+ }
440
+ return parts.join(' > ');
441
+ }
442
+
443
+ var typeableTags = { INPUT:1, TEXTAREA:1, SELECT:1 };
444
+ function isTypeable(el) {
445
+ if (typeableTags[el.tagName]) {
446
+ var t = (el.type || '').toLowerCase();
447
+ if (el.tagName === 'INPUT' && (t==='button'||t==='submit'||t==='reset'||t==='image'||t==='hidden')) return false;
448
+ return true;
449
+ }
450
+ if (el.getAttribute('contenteditable') === 'true') return true;
451
+ if (el.getAttribute('role') === 'textbox' || el.getAttribute('role') === 'combobox' || el.getAttribute('role') === 'searchbox') return true;
452
+ return false;
453
+ }
454
+
455
+ function hasDirectText(el) {
456
+ for (var i = 0; i < el.childNodes.length; i++) {
457
+ if (el.childNodes[i].nodeType === 3 && el.childNodes[i].textContent.trim().length > 0) return true;
458
+ }
459
+ return false;
460
+ }
461
+
462
+ var seen = new Set();
463
+
464
+ var sels = 'a[href],button,input:not([type=hidden]),textarea,select,'
465
+ + '[role=button],[role=link],[role=menuitem],[role=textbox],[role=combobox],[role=searchbox],[onclick],[tabindex],label[for]';
466
+ var interactive = Array.from(document.querySelectorAll(sels)).filter(isVisible);
467
+ interactive = interactive.slice(0, 100);
468
+
469
+ var textTags = 'p,h1,h2,h3,h4,h5,h6,span,li,td,th,label,blockquote,figcaption,caption,legend,dt,dd,em,strong,b,i,mark,small,del,ins,sub,sup,cite,code,pre,abbr,time,address';
470
+ var textEls = Array.from(document.querySelectorAll(textTags)).filter(function(el) {
471
+ return isVisible(el) && hasDirectText(el);
472
+ });
473
+ textEls = textEls.slice(0, 150);
474
+
475
+ var results = [];
476
+ var idx = 0;
477
+ for (var i = 0; i < interactive.length; i++) {
478
+ var el = interactive[i];
479
+ seen.add(el);
480
+ var r = el.getBoundingClientRect();
481
+ var kind = isTypeable(el) ? 'type' : 'click';
482
+
483
+ var shouldDraw = (kind === 'click' && CFG.showClick) || (kind === 'type' && CFG.showType);
484
+ var opacity = kind === 'click' ? CFG.opClick : CFG.opType;
485
+
486
+ if (shouldDraw) {
487
+ var box = document.createElement('div');
488
+ box.className = '__da-scan-box';
489
+ box.setAttribute('data-kind', kind);
490
+ box.setAttribute('data-gen', gen);
491
+ var borderColor = kind === 'click'
492
+ ? 'rgba(234,179,8,' + opacity + ')'
493
+ : 'rgba(34,197,94,' + opacity + ')';
494
+ var bgColor = kind === 'click'
495
+ ? 'rgba(234,179,8,' + (opacity * 0.08) + ')'
496
+ : 'rgba(34,197,94,' + (opacity * 0.08) + ')';
497
+ var borderStyle = kind === 'click'
498
+ ? '1.5px dashed ' + borderColor
499
+ : '1.5px dashed ' + borderColor;
500
+ box.style.cssText = 'position:fixed;pointer-events:none;z-index:2147483640;'
501
+ + 'box-sizing:border-box;border-radius:3px;'
502
+ + 'border:' + borderStyle + ';'
503
+ + 'left:' + r.left + 'px;top:' + r.top + 'px;'
504
+ + 'width:' + r.width + 'px;height:' + r.height + 'px;'
505
+ + 'background:' + bgColor + ';';
506
+
507
+ var badge = document.createElement('span');
508
+ badge.className = '__da-idx';
509
+ badge.style.cssText = 'position:absolute;top:-1px;left:-1px;'
510
+ + 'background:rgba(255,90,54,0.92);color:#fff;'
511
+ + 'font:bold 9px/1 system-ui,sans-serif;'
512
+ + 'padding:1px 4px 2px;border-radius:0 0 4px 0;pointer-events:none;letter-spacing:0.3px;';
513
+ badge.textContent = String(idx);
514
+ box.appendChild(badge);
515
+ container.appendChild(box);
516
+ }
517
+
518
+ var txt = (el.innerText||el.value||el.placeholder||el.getAttribute('aria-label')||'')
519
+ .substring(0,100).replace(/\\s+/g,' ').trim();
520
+ results.push({
521
+ index: idx,
522
+ tag: el.tagName.toLowerCase(),
523
+ kind: kind,
524
+ text: txt,
525
+ selector: getPath(el),
526
+ attributes: {
527
+ id: el.id || undefined,
528
+ name: el.name || undefined,
529
+ type: el.type || undefined,
530
+ placeholder: el.placeholder || undefined,
531
+ role: el.getAttribute('role') || undefined
532
+ },
533
+ box: { x: Math.round(r.x), y: Math.round(r.y), w: Math.round(r.width), h: Math.round(r.height) }
534
+ });
535
+ idx++;
536
+ }
537
+
538
+ for (var j = 0; j < textEls.length; j++) {
539
+ var tel = textEls[j];
540
+ if (seen.has(tel)) continue;
541
+ seen.add(tel);
542
+ var tr = tel.getBoundingClientRect();
543
+
544
+ if (CFG.showText) {
545
+ var tbox = document.createElement('div');
546
+ tbox.className = '__da-scan-box';
547
+ tbox.setAttribute('data-kind', 'text');
548
+ tbox.setAttribute('data-gen', gen);
549
+ tbox.style.cssText = 'position:fixed;pointer-events:none;z-index:2147483640;'
550
+ + 'box-sizing:border-box;border-radius:3px;'
551
+ + 'border:1px solid rgba(0,210,255,' + CFG.opText + ');'
552
+ + 'left:' + tr.left + 'px;top:' + tr.top + 'px;'
553
+ + 'width:' + tr.width + 'px;height:' + tr.height + 'px;'
554
+ + 'background:rgba(0,210,255,' + (CFG.opText * 0.07) + ');';
555
+ container.appendChild(tbox);
556
+ }
557
+
558
+ var ttxt = (tel.innerText || '').substring(0, 200).replace(/\\s+/g, ' ').trim();
559
+ if (ttxt) {
560
+ results.push({
561
+ index: idx,
562
+ tag: tel.tagName.toLowerCase(),
563
+ kind: 'text',
564
+ text: ttxt,
565
+ selector: getPath(tel),
566
+ attributes: { id: tel.id || undefined },
567
+ box: { x: Math.round(tr.x), y: Math.round(tr.y), w: Math.round(tr.width), h: Math.round(tr.height) }
568
+ });
569
+ idx++;
570
+ }
571
+ }
572
+
573
+ var thisGen = gen;
574
+ setTimeout(function(){
575
+ document.querySelectorAll('.__da-scan-box[data-gen="' + thisGen + '"]').forEach(function(el){
576
+ el.style.transition = 'opacity 0.4s ease';
577
+ el.style.opacity = '0';
578
+ });
579
+ setTimeout(function(){
580
+ document.querySelectorAll('.__da-scan-box[data-gen="' + thisGen + '"]').forEach(function(el){ el.remove(); });
581
+ }, 500);
582
+ }, 4000);
583
+
584
+ return results;
585
+ })()`);
586
+ }
587
+ }