neuro-cli 4.1.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/LICENSE +21 -0
- package/README.md +510 -0
- package/dist/advisor/advisor.d.ts +50 -0
- package/dist/advisor/advisor.js +178 -0
- package/dist/agents/base.d.ts +62 -0
- package/dist/agents/base.js +215 -0
- package/dist/agents/orchestrator.d.ts +46 -0
- package/dist/agents/orchestrator.js +192 -0
- package/dist/agents/team.d.ts +51 -0
- package/dist/agents/team.js +210 -0
- package/dist/api/models.d.ts +23 -0
- package/dist/api/models.js +514 -0
- package/dist/api/ollama.d.ts +153 -0
- package/dist/api/ollama.js +751 -0
- package/dist/api/openrouter.d.ts +55 -0
- package/dist/api/openrouter.js +223 -0
- package/dist/commands/commands.d.ts +43 -0
- package/dist/commands/commands.js +308 -0
- package/dist/config/config.d.ts +8 -0
- package/dist/config/config.js +311 -0
- package/dist/context/compaction.d.ts +54 -0
- package/dist/context/compaction.js +251 -0
- package/dist/context/custom-agents.d.ts +107 -0
- package/dist/context/custom-agents.js +397 -0
- package/dist/context/custom-tools.d.ts +120 -0
- package/dist/context/custom-tools.js +564 -0
- package/dist/context/git-checkpoint.d.ts +52 -0
- package/dist/context/git-checkpoint.js +240 -0
- package/dist/context/neuro-md.d.ts +48 -0
- package/dist/context/neuro-md.js +202 -0
- package/dist/context/neuroignore.d.ts +102 -0
- package/dist/context/neuroignore.js +441 -0
- package/dist/context/repo-map.d.ts +38 -0
- package/dist/context/repo-map.js +220 -0
- package/dist/context/skill-standard.d.ts +262 -0
- package/dist/context/skill-standard.js +1156 -0
- package/dist/context/skill-system.d.ts +75 -0
- package/dist/context/skill-system.js +578 -0
- package/dist/context/tree-sitter.d.ts +99 -0
- package/dist/context/tree-sitter.js +1956 -0
- package/dist/core/acp.d.ts +325 -0
- package/dist/core/acp.js +1498 -0
- package/dist/core/api-server.d.ts +143 -0
- package/dist/core/api-server.js +550 -0
- package/dist/core/approval.d.ts +81 -0
- package/dist/core/approval.js +432 -0
- package/dist/core/auto-compact.d.ts +127 -0
- package/dist/core/auto-compact.js +436 -0
- package/dist/core/auto-mode.d.ts +232 -0
- package/dist/core/auto-mode.js +831 -0
- package/dist/core/background-session.d.ts +166 -0
- package/dist/core/background-session.js +696 -0
- package/dist/core/cicd.d.ts +257 -0
- package/dist/core/cicd.js +1443 -0
- package/dist/core/cloud-sync.d.ts +156 -0
- package/dist/core/cloud-sync.js +582 -0
- package/dist/core/code-review.d.ts +132 -0
- package/dist/core/code-review.js +1191 -0
- package/dist/core/completion.d.ts +49 -0
- package/dist/core/completion.js +384 -0
- package/dist/core/context.d.ts +38 -0
- package/dist/core/context.js +144 -0
- package/dist/core/diff-preview.d.ts +35 -0
- package/dist/core/diff-preview.js +173 -0
- package/dist/core/doom-loop.d.ts +51 -0
- package/dist/core/doom-loop.js +179 -0
- package/dist/core/engine.d.ts +183 -0
- package/dist/core/engine.js +942 -0
- package/dist/core/extended-thinking.d.ts +103 -0
- package/dist/core/extended-thinking.js +269 -0
- package/dist/core/fallback.d.ts +54 -0
- package/dist/core/fallback.js +104 -0
- package/dist/core/git-worktree.d.ts +139 -0
- package/dist/core/git-worktree.js +614 -0
- package/dist/core/headless.d.ts +30 -0
- package/dist/core/headless.js +101 -0
- package/dist/core/i18n.d.ts +83 -0
- package/dist/core/i18n.js +739 -0
- package/dist/core/linting.d.ts +129 -0
- package/dist/core/linting.js +699 -0
- package/dist/core/model-router.d.ts +109 -0
- package/dist/core/model-router.js +581 -0
- package/dist/core/multi-model.d.ts +243 -0
- package/dist/core/multi-model.js +1099 -0
- package/dist/core/multi-session.d.ts +144 -0
- package/dist/core/multi-session.js +442 -0
- package/dist/core/multimodal.d.ts +125 -0
- package/dist/core/multimodal.js +286 -0
- package/dist/core/observability.d.ts +93 -0
- package/dist/core/observability.js +737 -0
- package/dist/core/os-sandbox.d.ts +122 -0
- package/dist/core/os-sandbox.js +1193 -0
- package/dist/core/outcome-grading.d.ts +228 -0
- package/dist/core/outcome-grading.js +1123 -0
- package/dist/core/output-styles.d.ts +57 -0
- package/dist/core/output-styles.js +382 -0
- package/dist/core/parallel-agents.d.ts +183 -0
- package/dist/core/parallel-agents.js +563 -0
- package/dist/core/plugin-bundle.d.ts +236 -0
- package/dist/core/plugin-bundle.js +887 -0
- package/dist/core/plugin-sdk.d.ts +139 -0
- package/dist/core/plugin-sdk.js +273 -0
- package/dist/core/prompt-cache.d.ts +163 -0
- package/dist/core/prompt-cache.js +599 -0
- package/dist/core/sandbox.d.ts +127 -0
- package/dist/core/sandbox.js +369 -0
- package/dist/core/scheduled-tasks.d.ts +151 -0
- package/dist/core/scheduled-tasks.js +502 -0
- package/dist/core/security-scanner.d.ts +160 -0
- package/dist/core/security-scanner.js +1494 -0
- package/dist/core/session.d.ts +83 -0
- package/dist/core/session.js +269 -0
- package/dist/core/shell-completion.d.ts +51 -0
- package/dist/core/shell-completion.js +674 -0
- package/dist/core/smart-monitor.d.ts +146 -0
- package/dist/core/smart-monitor.js +1199 -0
- package/dist/core/spec-driven.d.ts +233 -0
- package/dist/core/spec-driven.js +1485 -0
- package/dist/core/spending-warnings.d.ts +123 -0
- package/dist/core/spending-warnings.js +456 -0
- package/dist/core/sub-agent.d.ts +298 -0
- package/dist/core/sub-agent.js +1023 -0
- package/dist/core/telemetry.d.ts +157 -0
- package/dist/core/telemetry.js +412 -0
- package/dist/core/terminal-ux.d.ts +134 -0
- package/dist/core/terminal-ux.js +649 -0
- package/dist/core/testing.d.ts +146 -0
- package/dist/core/testing.js +1200 -0
- package/dist/core/types.d.ts +439 -0
- package/dist/core/types.js +6 -0
- package/dist/core/undo-redo.d.ts +112 -0
- package/dist/core/undo-redo.js +290 -0
- package/dist/core/updater.d.ts +159 -0
- package/dist/core/updater.js +608 -0
- package/dist/core/vim-mode.d.ts +151 -0
- package/dist/core/vim-mode.js +771 -0
- package/dist/core/voice.d.ts +137 -0
- package/dist/core/voice.js +538 -0
- package/dist/core/web-dashboard.d.ts +109 -0
- package/dist/core/web-dashboard.js +484 -0
- package/dist/hooks/hooks.d.ts +74 -0
- package/dist/hooks/hooks.js +160 -0
- package/dist/hooks/llm-evaluator.d.ts +165 -0
- package/dist/hooks/llm-evaluator.js +560 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1186 -0
- package/dist/lsp/lsp-manager.d.ts +63 -0
- package/dist/lsp/lsp-manager.js +351 -0
- package/dist/mcp/client.d.ts +133 -0
- package/dist/mcp/client.js +684 -0
- package/dist/mcp/mcp-apps.d.ts +70 -0
- package/dist/mcp/mcp-apps.js +1007 -0
- package/dist/tools/bash.d.ts +5 -0
- package/dist/tools/bash.js +195 -0
- package/dist/tools/browser.d.ts +92 -0
- package/dist/tools/browser.js +1570 -0
- package/dist/tools/extended.d.ts +6 -0
- package/dist/tools/extended.js +191 -0
- package/dist/tools/file.d.ts +10 -0
- package/dist/tools/file.js +382 -0
- package/dist/tools/github.d.ts +389 -0
- package/dist/tools/github.js +759 -0
- package/dist/tools/index.d.ts +9 -0
- package/dist/tools/index.js +40 -0
- package/dist/tools/memory.d.ts +6 -0
- package/dist/tools/memory.js +197 -0
- package/dist/tools/registry.d.ts +29 -0
- package/dist/tools/registry.js +64 -0
- package/dist/tools/web.d.ts +6 -0
- package/dist/tools/web.js +150 -0
- package/dist/ui/renderer.d.ts +97 -0
- package/dist/ui/renderer.js +279 -0
- package/dist/ui/theme.d.ts +27 -0
- package/dist/ui/theme.js +106 -0
- package/package.json +83 -0
|
@@ -0,0 +1,1570 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// NeuroCLI - Browser Automation Tool
|
|
3
|
+
// Headless Chrome control via Chrome DevTools Protocol
|
|
4
|
+
// Fallback: curl-based HTTP requests when no browser available
|
|
5
|
+
// ============================================================
|
|
6
|
+
import { spawn, execSync } from 'child_process';
|
|
7
|
+
import { existsSync, mkdirSync, writeFileSync } from 'fs';
|
|
8
|
+
import { join, resolve } from 'path';
|
|
9
|
+
import { createHash } from 'crypto';
|
|
10
|
+
// ============================================================
|
|
11
|
+
// Default Configuration
|
|
12
|
+
// ============================================================
|
|
13
|
+
const DEFAULT_CONFIG = {
|
|
14
|
+
headless: true,
|
|
15
|
+
defaultTimeout: 30000,
|
|
16
|
+
defaultViewport: { width: 1280, height: 720 },
|
|
17
|
+
blockImages: false,
|
|
18
|
+
blockCSS: false,
|
|
19
|
+
stealth: false,
|
|
20
|
+
};
|
|
21
|
+
const DEVICE_PRESETS = {
|
|
22
|
+
'iphone-x': {
|
|
23
|
+
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1',
|
|
24
|
+
viewport: { width: 375, height: 812 },
|
|
25
|
+
deviceScaleFactor: 3,
|
|
26
|
+
isMobile: true,
|
|
27
|
+
hasTouch: true,
|
|
28
|
+
},
|
|
29
|
+
'iphone-se': {
|
|
30
|
+
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1',
|
|
31
|
+
viewport: { width: 375, height: 667 },
|
|
32
|
+
deviceScaleFactor: 2,
|
|
33
|
+
isMobile: true,
|
|
34
|
+
hasTouch: true,
|
|
35
|
+
},
|
|
36
|
+
'ipad-pro': {
|
|
37
|
+
userAgent: 'Mozilla/5.0 (iPad; CPU OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1',
|
|
38
|
+
viewport: { width: 1024, height: 1366 },
|
|
39
|
+
deviceScaleFactor: 2,
|
|
40
|
+
isMobile: true,
|
|
41
|
+
hasTouch: true,
|
|
42
|
+
},
|
|
43
|
+
'pixel-7': {
|
|
44
|
+
userAgent: 'Mozilla/5.0 (Linux; Android 13; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Mobile Safari/537.36',
|
|
45
|
+
viewport: { width: 412, height: 915 },
|
|
46
|
+
deviceScaleFactor: 2.625,
|
|
47
|
+
isMobile: true,
|
|
48
|
+
hasTouch: true,
|
|
49
|
+
},
|
|
50
|
+
'galaxy-s21': {
|
|
51
|
+
userAgent: 'Mozilla/5.0 (Linux; Android 13; SM-G991B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Mobile Safari/537.36',
|
|
52
|
+
viewport: { width: 360, height: 800 },
|
|
53
|
+
deviceScaleFactor: 3,
|
|
54
|
+
isMobile: true,
|
|
55
|
+
hasTouch: true,
|
|
56
|
+
},
|
|
57
|
+
'desktop-1080p': {
|
|
58
|
+
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36',
|
|
59
|
+
viewport: { width: 1920, height: 1080 },
|
|
60
|
+
deviceScaleFactor: 1,
|
|
61
|
+
isMobile: false,
|
|
62
|
+
hasTouch: false,
|
|
63
|
+
},
|
|
64
|
+
'desktop-1440p': {
|
|
65
|
+
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36',
|
|
66
|
+
viewport: { width: 2560, height: 1440 },
|
|
67
|
+
deviceScaleFactor: 1,
|
|
68
|
+
isMobile: false,
|
|
69
|
+
hasTouch: false,
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
// ============================================================
|
|
73
|
+
// CDP Connection Helper
|
|
74
|
+
// ============================================================
|
|
75
|
+
class CDPConnection {
|
|
76
|
+
wsUrl;
|
|
77
|
+
ws = null;
|
|
78
|
+
requestId = 0;
|
|
79
|
+
pendingRequests = new Map();
|
|
80
|
+
eventHandlers = new Map();
|
|
81
|
+
buffer = '';
|
|
82
|
+
connected = false;
|
|
83
|
+
constructor(wsUrl) {
|
|
84
|
+
this.wsUrl = wsUrl;
|
|
85
|
+
}
|
|
86
|
+
async connect() {
|
|
87
|
+
// Dynamic import of ws module - only if available
|
|
88
|
+
try {
|
|
89
|
+
// @ts-ignore
|
|
90
|
+
const wsModule = await import('ws');
|
|
91
|
+
const WebSocket = wsModule.default || wsModule.WebSocket || wsModule;
|
|
92
|
+
return new Promise((resolve, reject) => {
|
|
93
|
+
this.ws = new WebSocket(this.wsUrl);
|
|
94
|
+
this.ws.on('open', () => {
|
|
95
|
+
this.connected = true;
|
|
96
|
+
resolve();
|
|
97
|
+
});
|
|
98
|
+
this.ws.on('message', (data) => {
|
|
99
|
+
this.handleMessage(data.toString());
|
|
100
|
+
});
|
|
101
|
+
this.ws.on('error', (err) => {
|
|
102
|
+
if (!this.connected)
|
|
103
|
+
reject(err);
|
|
104
|
+
});
|
|
105
|
+
this.ws.on('close', () => {
|
|
106
|
+
this.connected = false;
|
|
107
|
+
// Reject all pending requests
|
|
108
|
+
this.pendingRequests.forEach((pending) => {
|
|
109
|
+
pending.reject(new Error('Connection closed'));
|
|
110
|
+
});
|
|
111
|
+
this.pendingRequests.clear();
|
|
112
|
+
});
|
|
113
|
+
// Timeout
|
|
114
|
+
setTimeout(() => {
|
|
115
|
+
if (!this.connected) {
|
|
116
|
+
reject(new Error('Connection timeout'));
|
|
117
|
+
}
|
|
118
|
+
}, 10000);
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
throw new Error('WebSocket module not available. Install "ws" package or use curl fallback.');
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
async send(method, params) {
|
|
126
|
+
if (!this.connected || !this.ws) {
|
|
127
|
+
throw new Error('Not connected to browser');
|
|
128
|
+
}
|
|
129
|
+
const id = ++this.requestId;
|
|
130
|
+
const message = { id, method, params: params || {} };
|
|
131
|
+
return new Promise((resolve, reject) => {
|
|
132
|
+
const timeout = setTimeout(() => {
|
|
133
|
+
this.pendingRequests.delete(id);
|
|
134
|
+
reject(new Error(`CDP request timeout: ${method}`));
|
|
135
|
+
}, 30000);
|
|
136
|
+
this.pendingRequests.set(id, {
|
|
137
|
+
resolve: (value) => {
|
|
138
|
+
clearTimeout(timeout);
|
|
139
|
+
resolve(value);
|
|
140
|
+
},
|
|
141
|
+
reject: (reason) => {
|
|
142
|
+
clearTimeout(timeout);
|
|
143
|
+
reject(reason);
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
this.ws.send(JSON.stringify(message));
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
on(event, handler) {
|
|
150
|
+
if (!this.eventHandlers.has(event)) {
|
|
151
|
+
this.eventHandlers.set(event, []);
|
|
152
|
+
}
|
|
153
|
+
this.eventHandlers.get(event).push(handler);
|
|
154
|
+
}
|
|
155
|
+
removeListener(event, handler) {
|
|
156
|
+
const handlers = this.eventHandlers.get(event);
|
|
157
|
+
if (handlers) {
|
|
158
|
+
const idx = handlers.indexOf(handler);
|
|
159
|
+
if (idx >= 0)
|
|
160
|
+
handlers.splice(idx, 1);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
disconnect() {
|
|
164
|
+
if (this.ws) {
|
|
165
|
+
try {
|
|
166
|
+
this.ws.close();
|
|
167
|
+
}
|
|
168
|
+
catch { }
|
|
169
|
+
this.ws = null;
|
|
170
|
+
this.connected = false;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
get isConnected() {
|
|
174
|
+
return this.connected;
|
|
175
|
+
}
|
|
176
|
+
handleMessage(data) {
|
|
177
|
+
try {
|
|
178
|
+
const message = JSON.parse(data);
|
|
179
|
+
// Response to a request
|
|
180
|
+
if (message.id !== undefined) {
|
|
181
|
+
const pending = this.pendingRequests.get(message.id);
|
|
182
|
+
if (pending) {
|
|
183
|
+
this.pendingRequests.delete(message.id);
|
|
184
|
+
if (message.error) {
|
|
185
|
+
pending.reject(new Error(`CDP error: ${message.error.message}`));
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
pending.resolve(message.result);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
// Event from browser
|
|
194
|
+
if (message.method) {
|
|
195
|
+
const handlers = this.eventHandlers.get(message.method);
|
|
196
|
+
if (handlers) {
|
|
197
|
+
for (const handler of handlers) {
|
|
198
|
+
try {
|
|
199
|
+
handler(message.params);
|
|
200
|
+
}
|
|
201
|
+
catch { }
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
catch { }
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
// ============================================================
|
|
210
|
+
// Browser Tool Class
|
|
211
|
+
// ============================================================
|
|
212
|
+
export class BrowserTool {
|
|
213
|
+
config;
|
|
214
|
+
browserProcess = null;
|
|
215
|
+
cdp = null;
|
|
216
|
+
sessionId;
|
|
217
|
+
session;
|
|
218
|
+
chromePath = null;
|
|
219
|
+
debugPort = 0;
|
|
220
|
+
useCurlFallback = false;
|
|
221
|
+
screenshotDir;
|
|
222
|
+
constructor(config) {
|
|
223
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
224
|
+
this.sessionId = createHash('md5').update(`browser-${Date.now()}-${Math.random()}`).digest('hex').slice(0, 12);
|
|
225
|
+
this.screenshotDir = join(process.cwd(), '.neuro-screenshots');
|
|
226
|
+
this.session = {
|
|
227
|
+
id: this.sessionId,
|
|
228
|
+
startedAt: Date.now(),
|
|
229
|
+
currentUrl: '',
|
|
230
|
+
title: '',
|
|
231
|
+
statusCode: 0,
|
|
232
|
+
viewport: { ...this.config.defaultViewport },
|
|
233
|
+
actions: [],
|
|
234
|
+
cookies: this.config.cookies ? { ...this.config.cookies } : {},
|
|
235
|
+
history: [],
|
|
236
|
+
historyIndex: -1,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
// ---- Lifecycle ----
|
|
240
|
+
async launch(options) {
|
|
241
|
+
if (options) {
|
|
242
|
+
this.config = { ...this.config, ...options };
|
|
243
|
+
}
|
|
244
|
+
// Try to find a Chrome/Chromium executable
|
|
245
|
+
this.chromePath = this.findChrome();
|
|
246
|
+
if (!this.chromePath) {
|
|
247
|
+
this.useCurlFallback = true;
|
|
248
|
+
return 'Browser not found. Falling back to curl-based HTTP mode. Limited functionality available (navigate, getContent, getLinks, download).';
|
|
249
|
+
}
|
|
250
|
+
// Find a free debug port
|
|
251
|
+
this.debugPort = this.findFreePort();
|
|
252
|
+
const args = [
|
|
253
|
+
`--remote-debugging-port=${this.debugPort}`,
|
|
254
|
+
'--no-first-run',
|
|
255
|
+
'--no-default-browser-check',
|
|
256
|
+
'--disable-background-networking',
|
|
257
|
+
'--disable-client-side-phishing-detection',
|
|
258
|
+
'--disable-default-apps',
|
|
259
|
+
'--disable-extensions',
|
|
260
|
+
'--disable-hang-monitor',
|
|
261
|
+
'--disable-popup-blocking',
|
|
262
|
+
'--disable-prompt-on-repost',
|
|
263
|
+
'--disable-sync',
|
|
264
|
+
'--disable-translate',
|
|
265
|
+
'--metrics-recording-only',
|
|
266
|
+
'--safebrowsing-disable-auto-update',
|
|
267
|
+
];
|
|
268
|
+
if (this.config.headless) {
|
|
269
|
+
args.push('--headless=new');
|
|
270
|
+
}
|
|
271
|
+
if (this.config.proxy) {
|
|
272
|
+
args.push(`--proxy-server=${this.config.proxy}`);
|
|
273
|
+
}
|
|
274
|
+
// Stealth mode: reduce automation detection
|
|
275
|
+
if (this.config.stealth) {
|
|
276
|
+
args.push('--disable-blink-features=AutomationControlled', '--excludeSwitches=enable-automation', '--disable-features=IsolateOrigins,site-per-process');
|
|
277
|
+
}
|
|
278
|
+
// Block images
|
|
279
|
+
if (this.config.blockImages) {
|
|
280
|
+
args.push('--blink-settings=imagesEnabled=false');
|
|
281
|
+
}
|
|
282
|
+
// Set viewport
|
|
283
|
+
args.push(`--window-size=${this.config.defaultViewport.width},${this.config.defaultViewport.height}`);
|
|
284
|
+
return new Promise((resolve, reject) => {
|
|
285
|
+
try {
|
|
286
|
+
this.browserProcess = spawn(this.chromePath, args, {
|
|
287
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
288
|
+
detached: false,
|
|
289
|
+
});
|
|
290
|
+
let stderr = '';
|
|
291
|
+
this.browserProcess.stderr?.on('data', (data) => {
|
|
292
|
+
stderr += data.toString();
|
|
293
|
+
// Detect when DevTools is ready
|
|
294
|
+
if (stderr.includes('DevTools listening on')) {
|
|
295
|
+
this.connectCDP().then(resolve).catch(reject);
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
this.browserProcess.on('error', (err) => {
|
|
299
|
+
this.useCurlFallback = true;
|
|
300
|
+
resolve(`Browser launch failed: ${err.message}. Falling back to curl mode.`);
|
|
301
|
+
});
|
|
302
|
+
this.browserProcess.on('exit', () => {
|
|
303
|
+
this.browserProcess = null;
|
|
304
|
+
this.cdp = null;
|
|
305
|
+
});
|
|
306
|
+
// Timeout for CDP connection
|
|
307
|
+
setTimeout(() => {
|
|
308
|
+
if (!this.cdp || !this.cdp.isConnected) {
|
|
309
|
+
this.connectCDP().then(resolve).catch(() => {
|
|
310
|
+
this.useCurlFallback = true;
|
|
311
|
+
resolve('Browser CDP connection timed out. Falling back to curl mode.');
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
}, 5000);
|
|
315
|
+
}
|
|
316
|
+
catch (err) {
|
|
317
|
+
this.useCurlFallback = true;
|
|
318
|
+
resolve(`Browser launch error. Falling back to curl mode.`);
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
async close() {
|
|
323
|
+
if (this.cdp) {
|
|
324
|
+
try {
|
|
325
|
+
await this.cdp.send('Browser.close');
|
|
326
|
+
}
|
|
327
|
+
catch { }
|
|
328
|
+
this.cdp.disconnect();
|
|
329
|
+
this.cdp = null;
|
|
330
|
+
}
|
|
331
|
+
if (this.browserProcess) {
|
|
332
|
+
try {
|
|
333
|
+
this.browserProcess.kill('SIGTERM');
|
|
334
|
+
// Force kill after 2 seconds
|
|
335
|
+
setTimeout(() => {
|
|
336
|
+
try {
|
|
337
|
+
this.browserProcess?.kill('SIGKILL');
|
|
338
|
+
}
|
|
339
|
+
catch { }
|
|
340
|
+
}, 2000);
|
|
341
|
+
}
|
|
342
|
+
catch { }
|
|
343
|
+
this.browserProcess = null;
|
|
344
|
+
}
|
|
345
|
+
return `Browser session ${this.sessionId} closed. ${this.session.actions.length} actions recorded.`;
|
|
346
|
+
}
|
|
347
|
+
// ---- Navigation ----
|
|
348
|
+
async navigate(url) {
|
|
349
|
+
const actionStart = Date.now();
|
|
350
|
+
const action = {
|
|
351
|
+
type: 'navigate',
|
|
352
|
+
target: url,
|
|
353
|
+
timestamp: actionStart,
|
|
354
|
+
};
|
|
355
|
+
try {
|
|
356
|
+
if (this.useCurlFallback) {
|
|
357
|
+
return this.curlNavigate(url);
|
|
358
|
+
}
|
|
359
|
+
this.ensureConnected();
|
|
360
|
+
// Set cookies before navigation if configured
|
|
361
|
+
if (this.config.cookies && Object.keys(this.config.cookies).length > 0) {
|
|
362
|
+
for (const [name, value] of Object.entries(this.config.cookies)) {
|
|
363
|
+
await this.cdp.send('Network.setCookie', {
|
|
364
|
+
name,
|
|
365
|
+
value,
|
|
366
|
+
domain: new URL(url).hostname,
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
// Enable network tracking for status code
|
|
371
|
+
await this.cdp.send('Network.enable');
|
|
372
|
+
let statusCode = 0;
|
|
373
|
+
const responseHandler = (params) => {
|
|
374
|
+
if (params.response?.url === url) {
|
|
375
|
+
statusCode = params.response.status;
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
this.cdp.on('Network.responseReceived', responseHandler);
|
|
379
|
+
const result = await this.cdp.send('Page.navigate', { url });
|
|
380
|
+
await this.cdp.send('Page.loadEventFired');
|
|
381
|
+
this.cdp.removeListener('Network.responseReceived', responseHandler);
|
|
382
|
+
// Update session
|
|
383
|
+
this.session.currentUrl = url;
|
|
384
|
+
this.session.statusCode = statusCode || 200;
|
|
385
|
+
this.session.history = this.session.history.slice(0, this.session.historyIndex + 1);
|
|
386
|
+
this.session.history.push(url);
|
|
387
|
+
this.session.historyIndex = this.session.history.length - 1;
|
|
388
|
+
// Get page title
|
|
389
|
+
const titleResult = await this.cdp.send('Runtime.evaluate', {
|
|
390
|
+
expression: 'document.title',
|
|
391
|
+
});
|
|
392
|
+
this.session.title = titleResult?.result?.value || '';
|
|
393
|
+
action.duration = Date.now() - actionStart;
|
|
394
|
+
action.result = 'success';
|
|
395
|
+
this.session.actions.push(action);
|
|
396
|
+
return `Navigated to: ${url}\nTitle: ${this.session.title}\nStatus: ${this.session.statusCode}`;
|
|
397
|
+
}
|
|
398
|
+
catch (err) {
|
|
399
|
+
action.duration = Date.now() - actionStart;
|
|
400
|
+
action.result = 'error';
|
|
401
|
+
action.error = err.message;
|
|
402
|
+
this.session.actions.push(action);
|
|
403
|
+
return `Navigation error: ${err.message}`;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
async goBack() {
|
|
407
|
+
if (this.useCurlFallback) {
|
|
408
|
+
if (this.session.historyIndex > 0) {
|
|
409
|
+
this.session.historyIndex--;
|
|
410
|
+
return this.navigate(this.session.history[this.session.historyIndex]);
|
|
411
|
+
}
|
|
412
|
+
return 'No previous page in history';
|
|
413
|
+
}
|
|
414
|
+
this.ensureConnected();
|
|
415
|
+
try {
|
|
416
|
+
await this.cdp.send('Page.goBack');
|
|
417
|
+
await this.cdp.send('Page.loadEventFired');
|
|
418
|
+
if (this.session.historyIndex > 0) {
|
|
419
|
+
this.session.historyIndex--;
|
|
420
|
+
this.session.currentUrl = this.session.history[this.session.historyIndex];
|
|
421
|
+
}
|
|
422
|
+
return `Navigated back to: ${this.session.currentUrl}`;
|
|
423
|
+
}
|
|
424
|
+
catch (err) {
|
|
425
|
+
return `Go back error: ${err.message}`;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
async goForward() {
|
|
429
|
+
if (this.useCurlFallback) {
|
|
430
|
+
if (this.session.historyIndex < this.session.history.length - 1) {
|
|
431
|
+
this.session.historyIndex++;
|
|
432
|
+
return this.navigate(this.session.history[this.session.historyIndex]);
|
|
433
|
+
}
|
|
434
|
+
return 'No next page in history';
|
|
435
|
+
}
|
|
436
|
+
this.ensureConnected();
|
|
437
|
+
try {
|
|
438
|
+
await this.cdp.send('Page.goForward');
|
|
439
|
+
await this.cdp.send('Page.loadEventFired');
|
|
440
|
+
if (this.session.historyIndex < this.session.history.length - 1) {
|
|
441
|
+
this.session.historyIndex++;
|
|
442
|
+
this.session.currentUrl = this.session.history[this.session.historyIndex];
|
|
443
|
+
}
|
|
444
|
+
return `Navigated forward to: ${this.session.currentUrl}`;
|
|
445
|
+
}
|
|
446
|
+
catch (err) {
|
|
447
|
+
return `Go forward error: ${err.message}`;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
// ---- Page Interaction ----
|
|
451
|
+
async screenshot(selector) {
|
|
452
|
+
if (this.useCurlFallback) {
|
|
453
|
+
return 'Screenshot not available in curl fallback mode';
|
|
454
|
+
}
|
|
455
|
+
this.ensureConnected();
|
|
456
|
+
const actionStart = Date.now();
|
|
457
|
+
const action = {
|
|
458
|
+
type: 'screenshot',
|
|
459
|
+
target: selector,
|
|
460
|
+
timestamp: actionStart,
|
|
461
|
+
};
|
|
462
|
+
try {
|
|
463
|
+
if (!existsSync(this.screenshotDir)) {
|
|
464
|
+
mkdirSync(this.screenshotDir, { recursive: true });
|
|
465
|
+
}
|
|
466
|
+
let screenshotData;
|
|
467
|
+
if (selector) {
|
|
468
|
+
// Screenshot a specific element
|
|
469
|
+
const elementResult = await this.cdp.send('Runtime.evaluate', {
|
|
470
|
+
expression: `
|
|
471
|
+
(function() {
|
|
472
|
+
const el = document.querySelector(${JSON.stringify(selector)});
|
|
473
|
+
if (!el) return null;
|
|
474
|
+
const rect = el.getBoundingClientRect();
|
|
475
|
+
return JSON.stringify({
|
|
476
|
+
x: rect.x,
|
|
477
|
+
y: rect.y,
|
|
478
|
+
width: rect.width,
|
|
479
|
+
height: rect.height,
|
|
480
|
+
scale: window.devicePixelRatio || 1
|
|
481
|
+
});
|
|
482
|
+
})()
|
|
483
|
+
`,
|
|
484
|
+
});
|
|
485
|
+
if (!elementResult?.result?.value) {
|
|
486
|
+
return `Element not found: ${selector}`;
|
|
487
|
+
}
|
|
488
|
+
const clip = JSON.parse(elementResult.result.value);
|
|
489
|
+
const result = await this.cdp.send('Page.captureScreenshot', {
|
|
490
|
+
format: 'png',
|
|
491
|
+
clip: {
|
|
492
|
+
x: clip.x,
|
|
493
|
+
y: clip.y,
|
|
494
|
+
width: clip.width,
|
|
495
|
+
height: clip.height,
|
|
496
|
+
scale: clip.scale,
|
|
497
|
+
},
|
|
498
|
+
});
|
|
499
|
+
screenshotData = result.data;
|
|
500
|
+
}
|
|
501
|
+
else {
|
|
502
|
+
// Full page screenshot
|
|
503
|
+
const metrics = await this.cdp.send('Page.getLayoutMetrics');
|
|
504
|
+
const width = Math.ceil(metrics.cssContentSize?.width || this.config.defaultViewport.width);
|
|
505
|
+
const height = Math.ceil(metrics.cssContentSize?.height || this.config.defaultViewport.height);
|
|
506
|
+
await this.cdp.send('Emulation.setDeviceMetricsOverride', {
|
|
507
|
+
width,
|
|
508
|
+
height,
|
|
509
|
+
deviceScaleFactor: 1,
|
|
510
|
+
mobile: false,
|
|
511
|
+
});
|
|
512
|
+
const result = await this.cdp.send('Page.captureScreenshot', {
|
|
513
|
+
format: 'png',
|
|
514
|
+
});
|
|
515
|
+
screenshotData = result.data;
|
|
516
|
+
// Reset viewport
|
|
517
|
+
await this.cdp.send('Emulation.setDeviceMetricsOverride', {
|
|
518
|
+
width: this.session.viewport.width,
|
|
519
|
+
height: this.session.viewport.height,
|
|
520
|
+
deviceScaleFactor: 1,
|
|
521
|
+
mobile: false,
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
// Save to file
|
|
525
|
+
const filename = `screenshot-${Date.now()}.png`;
|
|
526
|
+
const filepath = join(this.screenshotDir, filename);
|
|
527
|
+
const buffer = Buffer.from(screenshotData, 'base64');
|
|
528
|
+
writeFileSync(filepath, buffer);
|
|
529
|
+
action.duration = Date.now() - actionStart;
|
|
530
|
+
action.result = 'success';
|
|
531
|
+
this.session.actions.push(action);
|
|
532
|
+
return `Screenshot saved: ${filepath} (${(buffer.length / 1024).toFixed(1)}KB)`;
|
|
533
|
+
}
|
|
534
|
+
catch (err) {
|
|
535
|
+
action.duration = Date.now() - actionStart;
|
|
536
|
+
action.result = 'error';
|
|
537
|
+
action.error = err.message;
|
|
538
|
+
this.session.actions.push(action);
|
|
539
|
+
return `Screenshot error: ${err.message}`;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
async click(selector) {
|
|
543
|
+
if (this.useCurlFallback) {
|
|
544
|
+
return 'Click not available in curl fallback mode';
|
|
545
|
+
}
|
|
546
|
+
this.ensureConnected();
|
|
547
|
+
const actionStart = Date.now();
|
|
548
|
+
const action = {
|
|
549
|
+
type: 'click',
|
|
550
|
+
target: selector,
|
|
551
|
+
timestamp: actionStart,
|
|
552
|
+
};
|
|
553
|
+
try {
|
|
554
|
+
// Get element position
|
|
555
|
+
const elementResult = await this.cdp.send('Runtime.evaluate', {
|
|
556
|
+
expression: `
|
|
557
|
+
(function() {
|
|
558
|
+
const el = document.querySelector(${JSON.stringify(selector)});
|
|
559
|
+
if (!el) return null;
|
|
560
|
+
const rect = el.getBoundingClientRect();
|
|
561
|
+
return JSON.stringify({
|
|
562
|
+
x: rect.x + rect.width / 2,
|
|
563
|
+
y: rect.y + rect.height / 2
|
|
564
|
+
});
|
|
565
|
+
})()
|
|
566
|
+
`,
|
|
567
|
+
});
|
|
568
|
+
if (!elementResult?.result?.value) {
|
|
569
|
+
action.result = 'error';
|
|
570
|
+
action.error = `Element not found: ${selector}`;
|
|
571
|
+
this.session.actions.push(action);
|
|
572
|
+
return `Element not found: ${selector}`;
|
|
573
|
+
}
|
|
574
|
+
const { x, y } = JSON.parse(elementResult.result.value);
|
|
575
|
+
// Dispatch mouse events
|
|
576
|
+
await this.cdp.send('Input.dispatchMouseEvent', {
|
|
577
|
+
type: 'mousePressed',
|
|
578
|
+
x,
|
|
579
|
+
y,
|
|
580
|
+
button: 'left',
|
|
581
|
+
clickCount: 1,
|
|
582
|
+
});
|
|
583
|
+
await this.cdp.send('Input.dispatchMouseEvent', {
|
|
584
|
+
type: 'mouseReleased',
|
|
585
|
+
x,
|
|
586
|
+
y,
|
|
587
|
+
button: 'left',
|
|
588
|
+
clickCount: 1,
|
|
589
|
+
});
|
|
590
|
+
// Small delay for any JS handlers
|
|
591
|
+
await this.delay(100);
|
|
592
|
+
action.duration = Date.now() - actionStart;
|
|
593
|
+
action.result = 'success';
|
|
594
|
+
this.session.actions.push(action);
|
|
595
|
+
return `Clicked: ${selector} at (${x.toFixed(0)}, ${y.toFixed(0)})`;
|
|
596
|
+
}
|
|
597
|
+
catch (err) {
|
|
598
|
+
action.duration = Date.now() - actionStart;
|
|
599
|
+
action.result = 'error';
|
|
600
|
+
action.error = err.message;
|
|
601
|
+
this.session.actions.push(action);
|
|
602
|
+
return `Click error: ${err.message}`;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
async type(selector, text) {
|
|
606
|
+
if (this.useCurlFallback) {
|
|
607
|
+
return 'Type not available in curl fallback mode';
|
|
608
|
+
}
|
|
609
|
+
this.ensureConnected();
|
|
610
|
+
const actionStart = Date.now();
|
|
611
|
+
const action = {
|
|
612
|
+
type: 'type',
|
|
613
|
+
target: selector,
|
|
614
|
+
value: text,
|
|
615
|
+
timestamp: actionStart,
|
|
616
|
+
};
|
|
617
|
+
try {
|
|
618
|
+
// Focus the element first
|
|
619
|
+
await this.cdp.send('Runtime.evaluate', {
|
|
620
|
+
expression: `document.querySelector(${JSON.stringify(selector)})?.focus()`,
|
|
621
|
+
});
|
|
622
|
+
// Clear existing content
|
|
623
|
+
await this.cdp.send('Runtime.evaluate', {
|
|
624
|
+
expression: `
|
|
625
|
+
(function() {
|
|
626
|
+
const el = document.querySelector(${JSON.stringify(selector)});
|
|
627
|
+
if (el) {
|
|
628
|
+
el.value = '';
|
|
629
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
630
|
+
}
|
|
631
|
+
})()
|
|
632
|
+
`,
|
|
633
|
+
});
|
|
634
|
+
// Type each character for realistic input
|
|
635
|
+
for (const char of text) {
|
|
636
|
+
await this.cdp.send('Input.dispatchKeyEvent', {
|
|
637
|
+
type: 'keyDown',
|
|
638
|
+
text: char,
|
|
639
|
+
key: char,
|
|
640
|
+
});
|
|
641
|
+
await this.delay(10);
|
|
642
|
+
await this.cdp.send('Input.dispatchKeyEvent', {
|
|
643
|
+
type: 'keyUp',
|
|
644
|
+
key: char,
|
|
645
|
+
});
|
|
646
|
+
await this.delay(10);
|
|
647
|
+
}
|
|
648
|
+
// Trigger change event
|
|
649
|
+
await this.cdp.send('Runtime.evaluate', {
|
|
650
|
+
expression: `
|
|
651
|
+
document.querySelector(${JSON.stringify(selector)})?.dispatchEvent(new Event('change', { bubbles: true }))
|
|
652
|
+
`,
|
|
653
|
+
});
|
|
654
|
+
action.duration = Date.now() - actionStart;
|
|
655
|
+
action.result = 'success';
|
|
656
|
+
this.session.actions.push(action);
|
|
657
|
+
return `Typed "${text.length > 50 ? text.slice(0, 50) + '...' : text}" into ${selector}`;
|
|
658
|
+
}
|
|
659
|
+
catch (err) {
|
|
660
|
+
action.duration = Date.now() - actionStart;
|
|
661
|
+
action.result = 'error';
|
|
662
|
+
action.error = err.message;
|
|
663
|
+
this.session.actions.push(action);
|
|
664
|
+
return `Type error: ${err.message}`;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
async select(selector, value) {
|
|
668
|
+
if (this.useCurlFallback) {
|
|
669
|
+
return 'Select not available in curl fallback mode';
|
|
670
|
+
}
|
|
671
|
+
this.ensureConnected();
|
|
672
|
+
const actionStart = Date.now();
|
|
673
|
+
const action = {
|
|
674
|
+
type: 'select',
|
|
675
|
+
target: selector,
|
|
676
|
+
value,
|
|
677
|
+
timestamp: actionStart,
|
|
678
|
+
};
|
|
679
|
+
try {
|
|
680
|
+
const result = await this.cdp.send('Runtime.evaluate', {
|
|
681
|
+
expression: `
|
|
682
|
+
(function() {
|
|
683
|
+
const el = document.querySelector(${JSON.stringify(selector)});
|
|
684
|
+
if (!el || el.tagName !== 'SELECT') return { error: 'Not a select element' };
|
|
685
|
+
el.value = ${JSON.stringify(value)};
|
|
686
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
687
|
+
return { selectedValue: el.value };
|
|
688
|
+
})()
|
|
689
|
+
`,
|
|
690
|
+
returnByValue: true,
|
|
691
|
+
});
|
|
692
|
+
action.duration = Date.now() - actionStart;
|
|
693
|
+
if (result?.result?.value?.error) {
|
|
694
|
+
action.result = 'error';
|
|
695
|
+
action.error = result.result.value.error;
|
|
696
|
+
this.session.actions.push(action);
|
|
697
|
+
return `Select error: ${result.result.value.error}`;
|
|
698
|
+
}
|
|
699
|
+
action.result = 'success';
|
|
700
|
+
this.session.actions.push(action);
|
|
701
|
+
return `Selected "${value}" in ${selector}`;
|
|
702
|
+
}
|
|
703
|
+
catch (err) {
|
|
704
|
+
action.duration = Date.now() - actionStart;
|
|
705
|
+
action.result = 'error';
|
|
706
|
+
action.error = err.message;
|
|
707
|
+
this.session.actions.push(action);
|
|
708
|
+
return `Select error: ${err.message}`;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
async wait(selector, timeout) {
|
|
712
|
+
if (this.useCurlFallback) {
|
|
713
|
+
return 'Wait not available in curl fallback mode';
|
|
714
|
+
}
|
|
715
|
+
this.ensureConnected();
|
|
716
|
+
const actionStart = Date.now();
|
|
717
|
+
const waitTimeout = timeout || this.config.defaultTimeout;
|
|
718
|
+
const action = {
|
|
719
|
+
type: 'wait',
|
|
720
|
+
target: selector,
|
|
721
|
+
timestamp: actionStart,
|
|
722
|
+
};
|
|
723
|
+
try {
|
|
724
|
+
const result = await this.cdp.send('Runtime.evaluate', {
|
|
725
|
+
expression: `
|
|
726
|
+
new Promise((resolve, reject) => {
|
|
727
|
+
const timeout = setTimeout(() => reject(new Error('Timeout waiting for ${selector}')), ${waitTimeout});
|
|
728
|
+
const check = () => {
|
|
729
|
+
if (document.querySelector(${JSON.stringify(selector)})) {
|
|
730
|
+
clearTimeout(timeout);
|
|
731
|
+
resolve(true);
|
|
732
|
+
} else {
|
|
733
|
+
requestAnimationFrame(check);
|
|
734
|
+
}
|
|
735
|
+
};
|
|
736
|
+
check();
|
|
737
|
+
})
|
|
738
|
+
`,
|
|
739
|
+
awaitPromise: true,
|
|
740
|
+
returnByValue: true,
|
|
741
|
+
});
|
|
742
|
+
action.duration = Date.now() - actionStart;
|
|
743
|
+
action.result = 'success';
|
|
744
|
+
this.session.actions.push(action);
|
|
745
|
+
return `Element found: ${selector} (waited ${action.duration}ms)`;
|
|
746
|
+
}
|
|
747
|
+
catch (err) {
|
|
748
|
+
action.duration = Date.now() - actionStart;
|
|
749
|
+
action.result = 'error';
|
|
750
|
+
action.error = err.message;
|
|
751
|
+
this.session.actions.push(action);
|
|
752
|
+
return `Wait error: ${err.message}`;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
// ---- Page Evaluation ----
|
|
756
|
+
async evaluate(script) {
|
|
757
|
+
if (this.useCurlFallback) {
|
|
758
|
+
return 'JavaScript evaluation not available in curl fallback mode';
|
|
759
|
+
}
|
|
760
|
+
this.ensureConnected();
|
|
761
|
+
const actionStart = Date.now();
|
|
762
|
+
const action = {
|
|
763
|
+
type: 'evaluate',
|
|
764
|
+
value: script.slice(0, 200),
|
|
765
|
+
timestamp: actionStart,
|
|
766
|
+
};
|
|
767
|
+
try {
|
|
768
|
+
const result = await this.cdp.send('Runtime.evaluate', {
|
|
769
|
+
expression: script,
|
|
770
|
+
returnByValue: true,
|
|
771
|
+
awaitPromise: true,
|
|
772
|
+
});
|
|
773
|
+
action.duration = Date.now() - actionStart;
|
|
774
|
+
if (result?.exceptionDetails) {
|
|
775
|
+
action.result = 'error';
|
|
776
|
+
action.error = result.exceptionDetails.text || 'Evaluation error';
|
|
777
|
+
this.session.actions.push(action);
|
|
778
|
+
return `Evaluation error: ${result.exceptionDetails.text}\n${result.exceptionDetails.exception?.description || ''}`;
|
|
779
|
+
}
|
|
780
|
+
action.result = 'success';
|
|
781
|
+
this.session.actions.push(action);
|
|
782
|
+
const value = result?.result?.value;
|
|
783
|
+
const output = typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value ?? 'undefined');
|
|
784
|
+
return truncateOutput(`Result: ${output}`);
|
|
785
|
+
}
|
|
786
|
+
catch (err) {
|
|
787
|
+
action.duration = Date.now() - actionStart;
|
|
788
|
+
action.result = 'error';
|
|
789
|
+
action.error = err.message;
|
|
790
|
+
this.session.actions.push(action);
|
|
791
|
+
return `Evaluate error: ${err.message}`;
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
// ---- Content Extraction ----
|
|
795
|
+
async getContent(selector) {
|
|
796
|
+
if (this.useCurlFallback) {
|
|
797
|
+
return this.curlGetContent();
|
|
798
|
+
}
|
|
799
|
+
this.ensureConnected();
|
|
800
|
+
try {
|
|
801
|
+
const expression = selector
|
|
802
|
+
? `document.querySelector(${JSON.stringify(selector)})?.innerText || document.querySelector(${JSON.stringify(selector)})?.outerHTML || ''`
|
|
803
|
+
: `document.body.innerText`;
|
|
804
|
+
const result = await this.cdp.send('Runtime.evaluate', {
|
|
805
|
+
expression,
|
|
806
|
+
returnByValue: true,
|
|
807
|
+
});
|
|
808
|
+
const content = result?.result?.value || '';
|
|
809
|
+
if (selector && !content) {
|
|
810
|
+
// Try outerHTML
|
|
811
|
+
const htmlResult = await this.cdp.send('Runtime.evaluate', {
|
|
812
|
+
expression: `document.querySelector(${JSON.stringify(selector)})?.outerHTML || ''`,
|
|
813
|
+
returnByValue: true,
|
|
814
|
+
});
|
|
815
|
+
return truncateOutput(htmlResult?.result?.value || `Element not found: ${selector}`);
|
|
816
|
+
}
|
|
817
|
+
return truncateOutput(content || 'No content found');
|
|
818
|
+
}
|
|
819
|
+
catch (err) {
|
|
820
|
+
return `Get content error: ${err.message}`;
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
async getLinks() {
|
|
824
|
+
if (this.useCurlFallback) {
|
|
825
|
+
return this.curlGetLinks();
|
|
826
|
+
}
|
|
827
|
+
this.ensureConnected();
|
|
828
|
+
try {
|
|
829
|
+
const result = await this.cdp.send('Runtime.evaluate', {
|
|
830
|
+
expression: `
|
|
831
|
+
Array.from(document.querySelectorAll('a[href]')).map(a => ({
|
|
832
|
+
text: a.innerText.trim().slice(0, 100),
|
|
833
|
+
href: a.href,
|
|
834
|
+
target: a.target
|
|
835
|
+
}))
|
|
836
|
+
`,
|
|
837
|
+
returnByValue: true,
|
|
838
|
+
});
|
|
839
|
+
const links = result?.result?.value || [];
|
|
840
|
+
if (links.length === 0) {
|
|
841
|
+
return 'No links found on this page';
|
|
842
|
+
}
|
|
843
|
+
const formatted = links.map((link, i) => {
|
|
844
|
+
const text = link.text || '(no text)';
|
|
845
|
+
return `${i + 1}. ${text}\n ${link.href}${link.target ? ` [target=${link.target}]` : ''}`;
|
|
846
|
+
}).join('\n\n');
|
|
847
|
+
return `Found ${links.length} links:\n\n${truncateOutput(formatted)}`;
|
|
848
|
+
}
|
|
849
|
+
catch (err) {
|
|
850
|
+
return `Get links error: ${err.message}`;
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
async getForms() {
|
|
854
|
+
if (this.useCurlFallback) {
|
|
855
|
+
return 'Form extraction not available in curl fallback mode';
|
|
856
|
+
}
|
|
857
|
+
this.ensureConnected();
|
|
858
|
+
try {
|
|
859
|
+
const result = await this.cdp.send('Runtime.evaluate', {
|
|
860
|
+
expression: `
|
|
861
|
+
Array.from(document.forms).map((form, i) => ({
|
|
862
|
+
index: i,
|
|
863
|
+
id: form.id,
|
|
864
|
+
name: form.name,
|
|
865
|
+
action: form.action,
|
|
866
|
+
method: form.method,
|
|
867
|
+
fields: Array.from(form.elements).map(el => ({
|
|
868
|
+
tag: el.tagName,
|
|
869
|
+
type: el.type,
|
|
870
|
+
name: el.name,
|
|
871
|
+
id: el.id,
|
|
872
|
+
value: el.type === 'password' ? '***' : (el.value || ''),
|
|
873
|
+
required: el.required,
|
|
874
|
+
placeholder: el.placeholder
|
|
875
|
+
}))
|
|
876
|
+
}))
|
|
877
|
+
`,
|
|
878
|
+
returnByValue: true,
|
|
879
|
+
});
|
|
880
|
+
const forms = result?.result?.value || [];
|
|
881
|
+
if (forms.length === 0) {
|
|
882
|
+
return 'No forms found on this page';
|
|
883
|
+
}
|
|
884
|
+
const formatted = forms.map(form => {
|
|
885
|
+
const header = `Form ${form.index + 1}: ${form.name || form.id || '(unnamed)'}`;
|
|
886
|
+
const meta = ` Action: ${form.action} | Method: ${form.method.toUpperCase()}`;
|
|
887
|
+
const fields = form.fields.map(f => {
|
|
888
|
+
const label = f.name || f.id || f.type;
|
|
889
|
+
const details = [f.tag.toLowerCase()];
|
|
890
|
+
if (f.type)
|
|
891
|
+
details.push(`type=${f.type}`);
|
|
892
|
+
if (f.required)
|
|
893
|
+
details.push('required');
|
|
894
|
+
if (f.placeholder)
|
|
895
|
+
details.push(`placeholder="${f.placeholder}"`);
|
|
896
|
+
if (f.value)
|
|
897
|
+
details.push(`value="${f.value.slice(0, 50)}"`);
|
|
898
|
+
return ` - ${label}: ${details.join(', ')}`;
|
|
899
|
+
}).join('\n');
|
|
900
|
+
return `${header}\n${meta}\n Fields:\n${fields}`;
|
|
901
|
+
}).join('\n\n');
|
|
902
|
+
return `Found ${forms.length} form(s):\n\n${formatted}`;
|
|
903
|
+
}
|
|
904
|
+
catch (err) {
|
|
905
|
+
return `Get forms error: ${err.message}`;
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
async fillForm(selector, data) {
|
|
909
|
+
if (this.useCurlFallback) {
|
|
910
|
+
return 'Form filling not available in curl fallback mode';
|
|
911
|
+
}
|
|
912
|
+
this.ensureConnected();
|
|
913
|
+
const actionStart = Date.now();
|
|
914
|
+
const action = {
|
|
915
|
+
type: 'type',
|
|
916
|
+
target: selector,
|
|
917
|
+
value: JSON.stringify(data),
|
|
918
|
+
timestamp: actionStart,
|
|
919
|
+
};
|
|
920
|
+
try {
|
|
921
|
+
// Fill each field
|
|
922
|
+
const results = [];
|
|
923
|
+
for (const [name, value] of Object.entries(data)) {
|
|
924
|
+
const fillResult = await this.cdp.send('Runtime.evaluate', {
|
|
925
|
+
expression: `
|
|
926
|
+
(function() {
|
|
927
|
+
const form = document.querySelector(${JSON.stringify(selector)});
|
|
928
|
+
if (!form) return { error: 'Form not found' };
|
|
929
|
+
|
|
930
|
+
// Try finding by name, then id
|
|
931
|
+
let el = form.elements.namedItem(${JSON.stringify(name)}) ||
|
|
932
|
+
form.querySelector('#' + ${JSON.stringify(name)}) ||
|
|
933
|
+
form.querySelector('[name="' + ${JSON.stringify(name)} + '"]');
|
|
934
|
+
|
|
935
|
+
if (!el) return { error: 'Field not found: ${name}' };
|
|
936
|
+
|
|
937
|
+
if (el.tagName === 'SELECT') {
|
|
938
|
+
el.value = ${JSON.stringify(value)};
|
|
939
|
+
} else if (el.type === 'checkbox') {
|
|
940
|
+
el.checked = ${JSON.stringify(value)} === 'true' || ${JSON.stringify(value)} === 'on';
|
|
941
|
+
} else if (el.type === 'radio') {
|
|
942
|
+
const radio = form.querySelector('input[name="${name}"][value="${value}"]');
|
|
943
|
+
if (radio) radio.checked = true;
|
|
944
|
+
} else {
|
|
945
|
+
el.value = ${JSON.stringify(value)};
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
949
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
950
|
+
return { success: true, field: '${name}' };
|
|
951
|
+
})()
|
|
952
|
+
`,
|
|
953
|
+
returnByValue: true,
|
|
954
|
+
});
|
|
955
|
+
if (fillResult?.result?.value?.error) {
|
|
956
|
+
results.push(` ⚠ ${name}: ${fillResult.result.value.error}`);
|
|
957
|
+
}
|
|
958
|
+
else {
|
|
959
|
+
results.push(` ✓ ${name}: set to "${value.slice(0, 50)}"`);
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
action.duration = Date.now() - actionStart;
|
|
963
|
+
action.result = 'success';
|
|
964
|
+
this.session.actions.push(action);
|
|
965
|
+
return `Form fill results:\n${results.join('\n')}`;
|
|
966
|
+
}
|
|
967
|
+
catch (err) {
|
|
968
|
+
action.duration = Date.now() - actionStart;
|
|
969
|
+
action.result = 'error';
|
|
970
|
+
action.error = err.message;
|
|
971
|
+
this.session.actions.push(action);
|
|
972
|
+
return `Fill form error: ${err.message}`;
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
// ---- File Download ----
|
|
976
|
+
async download(url, outputPath) {
|
|
977
|
+
const actionStart = Date.now();
|
|
978
|
+
const action = {
|
|
979
|
+
type: 'download',
|
|
980
|
+
target: url,
|
|
981
|
+
value: outputPath,
|
|
982
|
+
timestamp: actionStart,
|
|
983
|
+
};
|
|
984
|
+
try {
|
|
985
|
+
const absolutePath = resolve(outputPath);
|
|
986
|
+
const dir = join(absolutePath, '..');
|
|
987
|
+
if (!existsSync(dir)) {
|
|
988
|
+
mkdirSync(dir, { recursive: true });
|
|
989
|
+
}
|
|
990
|
+
// Use curl for downloads (works in both modes)
|
|
991
|
+
const result = execSync(`curl -L -s -o "${absolutePath}" -w "%{http_code}|%{size_download}|%{content_type}" "${url}"`, { encoding: 'utf-8', timeout: 120000 });
|
|
992
|
+
const [statusCode, size, contentType] = result.split('|');
|
|
993
|
+
action.duration = Date.now() - actionStart;
|
|
994
|
+
action.result = 'success';
|
|
995
|
+
this.session.actions.push(action);
|
|
996
|
+
return `Downloaded: ${url}\n → ${absolutePath}\n Size: ${(Number(size) / 1024).toFixed(1)}KB | Status: ${statusCode} | Type: ${contentType}`;
|
|
997
|
+
}
|
|
998
|
+
catch (err) {
|
|
999
|
+
action.duration = Date.now() - actionStart;
|
|
1000
|
+
action.result = 'error';
|
|
1001
|
+
action.error = err.message;
|
|
1002
|
+
this.session.actions.push(action);
|
|
1003
|
+
return `Download error: ${err.message}`;
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
// ---- Scroll ----
|
|
1007
|
+
async scroll(direction, amount = 300) {
|
|
1008
|
+
if (this.useCurlFallback) {
|
|
1009
|
+
return 'Scroll not available in curl fallback mode';
|
|
1010
|
+
}
|
|
1011
|
+
this.ensureConnected();
|
|
1012
|
+
const actionStart = Date.now();
|
|
1013
|
+
const action = {
|
|
1014
|
+
type: 'scroll',
|
|
1015
|
+
value: `${direction} ${amount}`,
|
|
1016
|
+
timestamp: actionStart,
|
|
1017
|
+
};
|
|
1018
|
+
try {
|
|
1019
|
+
let scrollExpr;
|
|
1020
|
+
switch (direction) {
|
|
1021
|
+
case 'down':
|
|
1022
|
+
scrollExpr = `window.scrollBy(0, ${amount})`;
|
|
1023
|
+
break;
|
|
1024
|
+
case 'up':
|
|
1025
|
+
scrollExpr = `window.scrollBy(0, -${amount})`;
|
|
1026
|
+
break;
|
|
1027
|
+
case 'right':
|
|
1028
|
+
scrollExpr = `window.scrollBy(${amount}, 0)`;
|
|
1029
|
+
break;
|
|
1030
|
+
case 'left':
|
|
1031
|
+
scrollExpr = `window.scrollBy(-${amount}, 0)`;
|
|
1032
|
+
break;
|
|
1033
|
+
}
|
|
1034
|
+
await this.cdp.send('Runtime.evaluate', {
|
|
1035
|
+
expression: scrollExpr,
|
|
1036
|
+
});
|
|
1037
|
+
// Get scroll position
|
|
1038
|
+
const posResult = await this.cdp.send('Runtime.evaluate', {
|
|
1039
|
+
expression: `JSON.stringify({ x: window.scrollX, y: window.scrollY })`,
|
|
1040
|
+
returnByValue: true,
|
|
1041
|
+
});
|
|
1042
|
+
const pos = JSON.parse(posResult?.result?.value || '{}');
|
|
1043
|
+
action.duration = Date.now() - actionStart;
|
|
1044
|
+
action.result = 'success';
|
|
1045
|
+
this.session.actions.push(action);
|
|
1046
|
+
return `Scrolled ${direction} by ${amount}px. Position: (${pos.x}, ${pos.y})`;
|
|
1047
|
+
}
|
|
1048
|
+
catch (err) {
|
|
1049
|
+
action.duration = Date.now() - actionStart;
|
|
1050
|
+
action.result = 'error';
|
|
1051
|
+
action.error = err.message;
|
|
1052
|
+
this.session.actions.push(action);
|
|
1053
|
+
return `Scroll error: ${err.message}`;
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
// ---- Page Info ----
|
|
1057
|
+
async getPageInfo() {
|
|
1058
|
+
if (this.useCurlFallback) {
|
|
1059
|
+
return `Session: ${this.session.id}\nMode: curl fallback\nURL: ${this.session.currentUrl || 'none'}\nActions: ${this.session.actions.length}`;
|
|
1060
|
+
}
|
|
1061
|
+
try {
|
|
1062
|
+
this.ensureConnected();
|
|
1063
|
+
const infoResult = await this.cdp.send('Runtime.evaluate', {
|
|
1064
|
+
expression: `
|
|
1065
|
+
JSON.stringify({
|
|
1066
|
+
url: window.location.href,
|
|
1067
|
+
title: document.title,
|
|
1068
|
+
charset: document.characterSet,
|
|
1069
|
+
language: document.documentElement.lang,
|
|
1070
|
+
referrer: document.referrer,
|
|
1071
|
+
readyState: document.readyState,
|
|
1072
|
+
viewport: { width: window.innerWidth, height: window.innerHeight },
|
|
1073
|
+
scrollPosition: { x: window.scrollX, y: window.scrollY },
|
|
1074
|
+
documentHeight: document.documentElement.scrollHeight,
|
|
1075
|
+
cookieCount: document.cookie.split(';').filter(c => c.trim()).length,
|
|
1076
|
+
linkCount: document.querySelectorAll('a').length,
|
|
1077
|
+
imageCount: document.querySelectorAll('img').length,
|
|
1078
|
+
formCount: document.forms.length,
|
|
1079
|
+
scriptCount: document.querySelectorAll('script').length,
|
|
1080
|
+
})
|
|
1081
|
+
`,
|
|
1082
|
+
returnByValue: true,
|
|
1083
|
+
});
|
|
1084
|
+
const info = JSON.parse(infoResult?.result?.value || '{}');
|
|
1085
|
+
return [
|
|
1086
|
+
`Page Info:`,
|
|
1087
|
+
` URL: ${info.url || this.session.currentUrl}`,
|
|
1088
|
+
` Title: ${info.title || this.session.title}`,
|
|
1089
|
+
` Status: ${this.session.statusCode}`,
|
|
1090
|
+
` Charset: ${info.charset || 'unknown'}`,
|
|
1091
|
+
` Language: ${info.language || 'unknown'}`,
|
|
1092
|
+
` Ready State: ${info.readyState}`,
|
|
1093
|
+
` Viewport: ${info.viewport?.width}x${info.viewport?.height}`,
|
|
1094
|
+
` Scroll: (${info.scrollPosition?.x}, ${info.scrollPosition?.y})`,
|
|
1095
|
+
` Document Height: ${info.documentHeight}px`,
|
|
1096
|
+
` Links: ${info.linkCount} | Images: ${info.imageCount} | Forms: ${info.formCount} | Scripts: ${info.scriptCount}`,
|
|
1097
|
+
` Cookies: ${info.cookieCount}`,
|
|
1098
|
+
``,
|
|
1099
|
+
`Session: ${this.session.id}`,
|
|
1100
|
+
` Actions: ${this.session.actions.length}`,
|
|
1101
|
+
` Duration: ${((Date.now() - this.session.startedAt) / 1000).toFixed(1)}s`,
|
|
1102
|
+
].join('\n');
|
|
1103
|
+
}
|
|
1104
|
+
catch (err) {
|
|
1105
|
+
return `Page info error: ${err.message}`;
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
// ---- Viewport & Device Emulation ----
|
|
1109
|
+
async setViewport(width, height) {
|
|
1110
|
+
this.session.viewport = { width, height };
|
|
1111
|
+
if (this.useCurlFallback || !this.cdp?.isConnected) {
|
|
1112
|
+
return `Viewport set to ${width}x${height} (will apply on next navigation)`;
|
|
1113
|
+
}
|
|
1114
|
+
try {
|
|
1115
|
+
await this.cdp.send('Emulation.setDeviceMetricsOverride', {
|
|
1116
|
+
width,
|
|
1117
|
+
height,
|
|
1118
|
+
deviceScaleFactor: 1,
|
|
1119
|
+
mobile: false,
|
|
1120
|
+
});
|
|
1121
|
+
return `Viewport set to ${width}x${height}`;
|
|
1122
|
+
}
|
|
1123
|
+
catch (err) {
|
|
1124
|
+
return `Set viewport error: ${err.message}`;
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
async emulateDevice(device) {
|
|
1128
|
+
const preset = DEVICE_PRESETS[device.toLowerCase()];
|
|
1129
|
+
if (!preset) {
|
|
1130
|
+
const available = Object.keys(DEVICE_PRESETS).join(', ');
|
|
1131
|
+
return `Unknown device: ${device}. Available: ${available}`;
|
|
1132
|
+
}
|
|
1133
|
+
this.session.viewport = { ...preset.viewport };
|
|
1134
|
+
if (this.config.stealth || preset.userAgent) {
|
|
1135
|
+
this.config.userAgent = preset.userAgent;
|
|
1136
|
+
}
|
|
1137
|
+
if (this.useCurlFallback || !this.cdp?.isConnected) {
|
|
1138
|
+
return `Device emulation set to ${device} (${preset.viewport.width}x${preset.viewport.height}). Will apply on next navigation.`;
|
|
1139
|
+
}
|
|
1140
|
+
try {
|
|
1141
|
+
await this.cdp.send('Emulation.setDeviceMetricsOverride', {
|
|
1142
|
+
width: preset.viewport.width,
|
|
1143
|
+
height: preset.viewport.height,
|
|
1144
|
+
deviceScaleFactor: preset.deviceScaleFactor,
|
|
1145
|
+
mobile: preset.isMobile,
|
|
1146
|
+
});
|
|
1147
|
+
await this.cdp.send('Network.setUserAgentOverride', {
|
|
1148
|
+
userAgent: preset.userAgent,
|
|
1149
|
+
});
|
|
1150
|
+
if (preset.hasTouch) {
|
|
1151
|
+
await this.cdp.send('Emulation.setTouchEmulationEnabled', {
|
|
1152
|
+
enabled: true,
|
|
1153
|
+
maxTouchPoints: 5,
|
|
1154
|
+
});
|
|
1155
|
+
}
|
|
1156
|
+
return `Emulating ${device}: ${preset.viewport.width}x${preset.viewport.height}, mobile=${preset.isMobile}, touch=${preset.hasTouch}`;
|
|
1157
|
+
}
|
|
1158
|
+
catch (err) {
|
|
1159
|
+
return `Emulate device error: ${err.message}`;
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
// ---- Session Management ----
|
|
1163
|
+
getSession() {
|
|
1164
|
+
return { ...this.session, actions: [...this.session.actions] };
|
|
1165
|
+
}
|
|
1166
|
+
getActionHistory() {
|
|
1167
|
+
return [...this.session.actions];
|
|
1168
|
+
}
|
|
1169
|
+
isUsingFallback() {
|
|
1170
|
+
return this.useCurlFallback;
|
|
1171
|
+
}
|
|
1172
|
+
// ---- Private Helpers ----
|
|
1173
|
+
findChrome() {
|
|
1174
|
+
const candidates = [];
|
|
1175
|
+
switch (process.platform) {
|
|
1176
|
+
case 'darwin':
|
|
1177
|
+
candidates.push('/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', '/Applications/Chromium.app/Contents/MacOS/Chromium', '/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary');
|
|
1178
|
+
break;
|
|
1179
|
+
case 'linux':
|
|
1180
|
+
candidates.push('/usr/bin/google-chrome', '/usr/bin/google-chrome-stable', '/usr/bin/chromium-browser', '/usr/bin/chromium', '/snap/bin/chromium', '/usr/bin/brave-browser');
|
|
1181
|
+
break;
|
|
1182
|
+
case 'win32':
|
|
1183
|
+
candidates.push('C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe', 'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe', 'C:\\Program Files\\BraveSoftware\\Brave-Browser\\Application\\brave.exe', `${process.env.LOCALAPPDATA || ''}\\Google\\Chrome\\Application\\chrome.exe`, `${process.env.PROGRAMFILES || ''}\\Google\\Chrome\\Application\\chrome.exe`);
|
|
1184
|
+
break;
|
|
1185
|
+
}
|
|
1186
|
+
// Check CHROME_PATH env var
|
|
1187
|
+
const envPath = process.env.CHROME_PATH || process.env.CHROMIUM_PATH;
|
|
1188
|
+
if (envPath && existsSync(envPath)) {
|
|
1189
|
+
return envPath;
|
|
1190
|
+
}
|
|
1191
|
+
// Try which/where command
|
|
1192
|
+
try {
|
|
1193
|
+
const cmd = process.platform === 'win32' ? 'where chrome' : 'which google-chrome || which chromium-browser || which chromium';
|
|
1194
|
+
const result = execSync(cmd, { encoding: 'utf-8', timeout: 5000 }).trim();
|
|
1195
|
+
if (result && existsSync(result.split('\n')[0].trim())) {
|
|
1196
|
+
return result.split('\n')[0].trim();
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
catch { }
|
|
1200
|
+
// Check candidate paths
|
|
1201
|
+
for (const candidate of candidates) {
|
|
1202
|
+
if (existsSync(candidate)) {
|
|
1203
|
+
return candidate;
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
return null;
|
|
1207
|
+
}
|
|
1208
|
+
findFreePort() {
|
|
1209
|
+
// Generate a random port in the dynamic range
|
|
1210
|
+
return 9222 + Math.floor(Math.random() * 1000);
|
|
1211
|
+
}
|
|
1212
|
+
async connectCDP() {
|
|
1213
|
+
try {
|
|
1214
|
+
// Get browser info from debug endpoint
|
|
1215
|
+
const response = await fetch(`http://127.0.0.1:${this.debugPort}/json/version`);
|
|
1216
|
+
const info = await response.json();
|
|
1217
|
+
const wsUrl = info.webSocketDebuggerUrl;
|
|
1218
|
+
if (!wsUrl) {
|
|
1219
|
+
throw new Error('No WebSocket URL found');
|
|
1220
|
+
}
|
|
1221
|
+
// Connect via CDP
|
|
1222
|
+
this.cdp = new CDPConnection(wsUrl);
|
|
1223
|
+
await this.cdp.connect();
|
|
1224
|
+
// Set up stealth overrides
|
|
1225
|
+
if (this.config.stealth) {
|
|
1226
|
+
await this.cdp.send('Page.addScriptToEvaluateOnNewDocument', {
|
|
1227
|
+
source: `
|
|
1228
|
+
Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
|
|
1229
|
+
Object.defineProperty(navigator, 'plugins', { get: () => [1, 2, 3, 4, 5] });
|
|
1230
|
+
Object.defineProperty(navigator, 'languages', { get: () => ['en-US', 'en'] });
|
|
1231
|
+
window.chrome = { runtime: {} };
|
|
1232
|
+
`,
|
|
1233
|
+
});
|
|
1234
|
+
}
|
|
1235
|
+
// Set user agent if specified
|
|
1236
|
+
if (this.config.userAgent) {
|
|
1237
|
+
await this.cdp.send('Network.setUserAgentOverride', {
|
|
1238
|
+
userAgent: this.config.userAgent,
|
|
1239
|
+
});
|
|
1240
|
+
}
|
|
1241
|
+
// Block resources if configured
|
|
1242
|
+
if (this.config.blockImages || this.config.blockCSS) {
|
|
1243
|
+
await this.cdp.send('Network.setBlockedURLs', {
|
|
1244
|
+
urls: [
|
|
1245
|
+
...(this.config.blockImages ? ['*.png', '*.jpg', '*.jpeg', '*.gif', '*.webp', '*.svg'] : []),
|
|
1246
|
+
...(this.config.blockCSS ? ['*.css'] : []),
|
|
1247
|
+
],
|
|
1248
|
+
});
|
|
1249
|
+
}
|
|
1250
|
+
this.useCurlFallback = false;
|
|
1251
|
+
return `Browser connected via CDP on port ${this.debugPort}`;
|
|
1252
|
+
}
|
|
1253
|
+
catch (err) {
|
|
1254
|
+
this.useCurlFallback = true;
|
|
1255
|
+
return `CDP connection failed: ${err.message}. Using curl fallback.`;
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
ensureConnected() {
|
|
1259
|
+
if (!this.cdp || !this.cdp.isConnected) {
|
|
1260
|
+
throw new Error('Browser not connected. Call launch() first.');
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
delay(ms) {
|
|
1264
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
1265
|
+
}
|
|
1266
|
+
// ---- Curl Fallback Methods ----
|
|
1267
|
+
async curlNavigate(url) {
|
|
1268
|
+
try {
|
|
1269
|
+
const result = execSync(`curl -L -s -o /dev/null -w "%{http_code}|%{url_effective}|%{content_type}" "${url}"`, { encoding: 'utf-8', timeout: this.config.defaultTimeout });
|
|
1270
|
+
const [statusCode, effectiveUrl, contentType] = result.split('|');
|
|
1271
|
+
this.session.currentUrl = effectiveUrl || url;
|
|
1272
|
+
this.session.statusCode = parseInt(statusCode, 10);
|
|
1273
|
+
this.session.history.push(this.session.currentUrl);
|
|
1274
|
+
this.session.historyIndex = this.session.history.length - 1;
|
|
1275
|
+
const action = {
|
|
1276
|
+
type: 'navigate',
|
|
1277
|
+
target: url,
|
|
1278
|
+
timestamp: Date.now(),
|
|
1279
|
+
result: 'success',
|
|
1280
|
+
duration: 0,
|
|
1281
|
+
};
|
|
1282
|
+
this.session.actions.push(action);
|
|
1283
|
+
return `Navigated to: ${this.session.currentUrl}\nStatus: ${this.session.statusCode}\nContent-Type: ${contentType}`;
|
|
1284
|
+
}
|
|
1285
|
+
catch (err) {
|
|
1286
|
+
const action = {
|
|
1287
|
+
type: 'navigate',
|
|
1288
|
+
target: url,
|
|
1289
|
+
timestamp: Date.now(),
|
|
1290
|
+
result: 'error',
|
|
1291
|
+
error: err.message,
|
|
1292
|
+
};
|
|
1293
|
+
this.session.actions.push(action);
|
|
1294
|
+
return `Navigation error: ${err.message}`;
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
async curlGetContent() {
|
|
1298
|
+
if (!this.session.currentUrl) {
|
|
1299
|
+
return 'No URL loaded. Navigate first.';
|
|
1300
|
+
}
|
|
1301
|
+
try {
|
|
1302
|
+
const result = execSync(`curl -L -s --max-time 30 "${this.session.currentUrl}"`, { encoding: 'utf-8', maxBuffer: 10 * 1024 * 1024, timeout: 35000 });
|
|
1303
|
+
// Basic HTML to text conversion
|
|
1304
|
+
const text = result
|
|
1305
|
+
.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
|
|
1306
|
+
.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
|
|
1307
|
+
.replace(/<nav[^>]*>[\s\S]*?<\/nav>/gi, '')
|
|
1308
|
+
.replace(/<footer[^>]*>[\s\S]*?<\/footer>/gi, '')
|
|
1309
|
+
.replace(/<header[^>]*>[\s\S]*?<\/header>/gi, '')
|
|
1310
|
+
.replace(/<[^>]+>/g, ' ')
|
|
1311
|
+
.replace(/ /g, ' ')
|
|
1312
|
+
.replace(/&/g, '&')
|
|
1313
|
+
.replace(/</g, '<')
|
|
1314
|
+
.replace(/>/g, '>')
|
|
1315
|
+
.replace(/"/g, '"')
|
|
1316
|
+
.replace(/'/g, "'")
|
|
1317
|
+
.replace(/\s+/g, ' ')
|
|
1318
|
+
.trim();
|
|
1319
|
+
return truncateOutput(text);
|
|
1320
|
+
}
|
|
1321
|
+
catch (err) {
|
|
1322
|
+
return `Get content error: ${err.message}`;
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
async curlGetLinks() {
|
|
1326
|
+
if (!this.session.currentUrl) {
|
|
1327
|
+
return 'No URL loaded. Navigate first.';
|
|
1328
|
+
}
|
|
1329
|
+
try {
|
|
1330
|
+
const result = execSync(`curl -L -s --max-time 30 "${this.session.currentUrl}"`, { encoding: 'utf-8', maxBuffer: 10 * 1024 * 1024, timeout: 35000 });
|
|
1331
|
+
// Extract links from HTML
|
|
1332
|
+
const linkRegex = /<a[^>]+href=["']([^"']+)["'][^>]*>([^<]*)<\/a>/gi;
|
|
1333
|
+
const links = [];
|
|
1334
|
+
let match;
|
|
1335
|
+
while ((match = linkRegex.exec(result)) !== null) {
|
|
1336
|
+
const href = match[1];
|
|
1337
|
+
const text = match[2].trim();
|
|
1338
|
+
if (href && !href.startsWith('#') && !href.startsWith('javascript:')) {
|
|
1339
|
+
// Resolve relative URLs
|
|
1340
|
+
const resolved = new URL(href, this.session.currentUrl).href;
|
|
1341
|
+
links.push({ text: text.slice(0, 100) || '(no text)', href: resolved });
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
if (links.length === 0) {
|
|
1345
|
+
return 'No links found on this page';
|
|
1346
|
+
}
|
|
1347
|
+
const formatted = links.slice(0, 100).map((link, i) => {
|
|
1348
|
+
return `${i + 1}. ${link.text}\n ${link.href}`;
|
|
1349
|
+
}).join('\n\n');
|
|
1350
|
+
return `Found ${links.length} links:\n\n${truncateOutput(formatted)}`;
|
|
1351
|
+
}
|
|
1352
|
+
catch (err) {
|
|
1353
|
+
return `Get links error: ${err.message}`;
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
// ============================================================
|
|
1358
|
+
// Tool Registration (compatible with NeuroCLI ToolExecutor)
|
|
1359
|
+
// ============================================================
|
|
1360
|
+
function truncateOutput(output, maxLength = 30000) {
|
|
1361
|
+
if (output.length <= maxLength)
|
|
1362
|
+
return output;
|
|
1363
|
+
const half = Math.floor(maxLength / 2);
|
|
1364
|
+
return output.slice(0, half) + '\n\n... [truncated] ...\n\n' + output.slice(-half);
|
|
1365
|
+
}
|
|
1366
|
+
// Singleton browser instance for tool use
|
|
1367
|
+
let browserInstance = null;
|
|
1368
|
+
async function getBrowser() {
|
|
1369
|
+
if (!browserInstance) {
|
|
1370
|
+
browserInstance = new BrowserTool();
|
|
1371
|
+
await browserInstance.launch();
|
|
1372
|
+
}
|
|
1373
|
+
return browserInstance;
|
|
1374
|
+
}
|
|
1375
|
+
// ---- Browser Navigate Tool ----
|
|
1376
|
+
const browserNavigateDef = {
|
|
1377
|
+
name: 'browser_navigate',
|
|
1378
|
+
description: 'Navigate the headless browser to a URL. Launches browser if not already running.',
|
|
1379
|
+
parameters: {
|
|
1380
|
+
type: 'object',
|
|
1381
|
+
properties: {
|
|
1382
|
+
url: { type: 'string', description: 'URL to navigate to' },
|
|
1383
|
+
},
|
|
1384
|
+
required: ['url'],
|
|
1385
|
+
},
|
|
1386
|
+
};
|
|
1387
|
+
export const browserNavigateTool = {
|
|
1388
|
+
name: 'browser_navigate',
|
|
1389
|
+
definition: browserNavigateDef,
|
|
1390
|
+
risk: 'low',
|
|
1391
|
+
async execute(args) {
|
|
1392
|
+
const browser = await getBrowser();
|
|
1393
|
+
return browser.navigate(args.url);
|
|
1394
|
+
},
|
|
1395
|
+
};
|
|
1396
|
+
// ---- Browser Screenshot Tool ----
|
|
1397
|
+
const browserScreenshotDef = {
|
|
1398
|
+
name: 'browser_screenshot',
|
|
1399
|
+
description: 'Take a screenshot of the current page or a specific element.',
|
|
1400
|
+
parameters: {
|
|
1401
|
+
type: 'object',
|
|
1402
|
+
properties: {
|
|
1403
|
+
selector: { type: 'string', description: 'CSS selector for element screenshot (optional, full page if omitted)' },
|
|
1404
|
+
},
|
|
1405
|
+
required: [],
|
|
1406
|
+
},
|
|
1407
|
+
};
|
|
1408
|
+
export const browserScreenshotTool = {
|
|
1409
|
+
name: 'browser_screenshot',
|
|
1410
|
+
definition: browserScreenshotDef,
|
|
1411
|
+
risk: 'low',
|
|
1412
|
+
async execute(args) {
|
|
1413
|
+
const browser = await getBrowser();
|
|
1414
|
+
return browser.screenshot(args.selector);
|
|
1415
|
+
},
|
|
1416
|
+
};
|
|
1417
|
+
// ---- Browser Click Tool ----
|
|
1418
|
+
const browserClickDef = {
|
|
1419
|
+
name: 'browser_click',
|
|
1420
|
+
description: 'Click an element on the page by CSS selector.',
|
|
1421
|
+
parameters: {
|
|
1422
|
+
type: 'object',
|
|
1423
|
+
properties: {
|
|
1424
|
+
selector: { type: 'string', description: 'CSS selector of the element to click' },
|
|
1425
|
+
},
|
|
1426
|
+
required: ['selector'],
|
|
1427
|
+
},
|
|
1428
|
+
};
|
|
1429
|
+
export const browserClickTool = {
|
|
1430
|
+
name: 'browser_click',
|
|
1431
|
+
definition: browserClickDef,
|
|
1432
|
+
risk: 'medium',
|
|
1433
|
+
async execute(args) {
|
|
1434
|
+
const browser = await getBrowser();
|
|
1435
|
+
return browser.click(args.selector);
|
|
1436
|
+
},
|
|
1437
|
+
};
|
|
1438
|
+
// ---- Browser Type Tool ----
|
|
1439
|
+
const browserTypeDef = {
|
|
1440
|
+
name: 'browser_type',
|
|
1441
|
+
description: 'Type text into a form element by CSS selector.',
|
|
1442
|
+
parameters: {
|
|
1443
|
+
type: 'object',
|
|
1444
|
+
properties: {
|
|
1445
|
+
selector: { type: 'string', description: 'CSS selector of the input element' },
|
|
1446
|
+
text: { type: 'string', description: 'Text to type' },
|
|
1447
|
+
},
|
|
1448
|
+
required: ['selector', 'text'],
|
|
1449
|
+
},
|
|
1450
|
+
};
|
|
1451
|
+
export const browserTypeTool = {
|
|
1452
|
+
name: 'browser_type',
|
|
1453
|
+
definition: browserTypeDef,
|
|
1454
|
+
risk: 'medium',
|
|
1455
|
+
async execute(args) {
|
|
1456
|
+
const browser = await getBrowser();
|
|
1457
|
+
return browser.type(args.selector, args.text);
|
|
1458
|
+
},
|
|
1459
|
+
};
|
|
1460
|
+
// ---- Browser Evaluate Tool ----
|
|
1461
|
+
const browserEvaluateDef = {
|
|
1462
|
+
name: 'browser_evaluate',
|
|
1463
|
+
description: 'Execute JavaScript in the browser page context.',
|
|
1464
|
+
parameters: {
|
|
1465
|
+
type: 'object',
|
|
1466
|
+
properties: {
|
|
1467
|
+
script: { type: 'string', description: 'JavaScript code to execute' },
|
|
1468
|
+
},
|
|
1469
|
+
required: ['script'],
|
|
1470
|
+
},
|
|
1471
|
+
};
|
|
1472
|
+
export const browserEvaluateTool = {
|
|
1473
|
+
name: 'browser_evaluate',
|
|
1474
|
+
definition: browserEvaluateDef,
|
|
1475
|
+
risk: 'high',
|
|
1476
|
+
getApprovalRequest(args) {
|
|
1477
|
+
return {
|
|
1478
|
+
toolName: 'browser_evaluate',
|
|
1479
|
+
args,
|
|
1480
|
+
risk: 'high',
|
|
1481
|
+
description: `Execute JS in browser: ${args.script.slice(0, 100)}...`,
|
|
1482
|
+
};
|
|
1483
|
+
},
|
|
1484
|
+
async execute(args) {
|
|
1485
|
+
const browser = await getBrowser();
|
|
1486
|
+
return browser.evaluate(args.script);
|
|
1487
|
+
},
|
|
1488
|
+
};
|
|
1489
|
+
// ---- Browser Get Content Tool ----
|
|
1490
|
+
const browserGetContentDef = {
|
|
1491
|
+
name: 'browser_get_content',
|
|
1492
|
+
description: 'Get the text content of the current page or a specific element.',
|
|
1493
|
+
parameters: {
|
|
1494
|
+
type: 'object',
|
|
1495
|
+
properties: {
|
|
1496
|
+
selector: { type: 'string', description: 'CSS selector (optional, gets full page text if omitted)' },
|
|
1497
|
+
},
|
|
1498
|
+
required: [],
|
|
1499
|
+
},
|
|
1500
|
+
};
|
|
1501
|
+
export const browserGetContentTool = {
|
|
1502
|
+
name: 'browser_get_content',
|
|
1503
|
+
definition: browserGetContentDef,
|
|
1504
|
+
risk: 'low',
|
|
1505
|
+
async execute(args) {
|
|
1506
|
+
const browser = await getBrowser();
|
|
1507
|
+
return browser.getContent(args.selector);
|
|
1508
|
+
},
|
|
1509
|
+
};
|
|
1510
|
+
// ---- Browser Get Links Tool ----
|
|
1511
|
+
const browserGetLinksDef = {
|
|
1512
|
+
name: 'browser_get_links',
|
|
1513
|
+
description: 'Extract all links from the current page.',
|
|
1514
|
+
parameters: {
|
|
1515
|
+
type: 'object',
|
|
1516
|
+
properties: {},
|
|
1517
|
+
required: [],
|
|
1518
|
+
},
|
|
1519
|
+
};
|
|
1520
|
+
export const browserGetLinksTool = {
|
|
1521
|
+
name: 'browser_get_links',
|
|
1522
|
+
definition: browserGetLinksDef,
|
|
1523
|
+
risk: 'low',
|
|
1524
|
+
async execute() {
|
|
1525
|
+
const browser = await getBrowser();
|
|
1526
|
+
return browser.getLinks();
|
|
1527
|
+
},
|
|
1528
|
+
};
|
|
1529
|
+
// ---- Browser Download Tool ----
|
|
1530
|
+
const browserDownloadDef = {
|
|
1531
|
+
name: 'browser_download',
|
|
1532
|
+
description: 'Download a file from a URL to a local path.',
|
|
1533
|
+
parameters: {
|
|
1534
|
+
type: 'object',
|
|
1535
|
+
properties: {
|
|
1536
|
+
url: { type: 'string', description: 'URL to download from' },
|
|
1537
|
+
output_path: { type: 'string', description: 'Local file path to save to' },
|
|
1538
|
+
},
|
|
1539
|
+
required: ['url', 'output_path'],
|
|
1540
|
+
},
|
|
1541
|
+
};
|
|
1542
|
+
export const browserDownloadTool = {
|
|
1543
|
+
name: 'browser_download',
|
|
1544
|
+
definition: browserDownloadDef,
|
|
1545
|
+
risk: 'medium',
|
|
1546
|
+
getApprovalRequest(args) {
|
|
1547
|
+
return {
|
|
1548
|
+
toolName: 'browser_download',
|
|
1549
|
+
args,
|
|
1550
|
+
risk: 'medium',
|
|
1551
|
+
description: `Download ${args.url} to ${args.output_path}`,
|
|
1552
|
+
};
|
|
1553
|
+
},
|
|
1554
|
+
async execute(args) {
|
|
1555
|
+
const browser = await getBrowser();
|
|
1556
|
+
return browser.download(args.url, args.output_path);
|
|
1557
|
+
},
|
|
1558
|
+
};
|
|
1559
|
+
// Export all browser tools
|
|
1560
|
+
export const browserTools = [
|
|
1561
|
+
browserNavigateTool,
|
|
1562
|
+
browserScreenshotTool,
|
|
1563
|
+
browserClickTool,
|
|
1564
|
+
browserTypeTool,
|
|
1565
|
+
browserEvaluateTool,
|
|
1566
|
+
browserGetContentTool,
|
|
1567
|
+
browserGetLinksTool,
|
|
1568
|
+
browserDownloadTool,
|
|
1569
|
+
];
|
|
1570
|
+
//# sourceMappingURL=browser.js.map
|