@bubstack/moe-glass 0.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/README.md +29 -0
- package/agents/browser-user.md +105 -0
- package/dist/LICENSE +25 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +22517 -0
- package/dist/index.js.map +1 -0
- package/dist/payload.d.ts +214 -0
- package/dist/payload.d.ts.map +1 -0
- package/dist/payload.js +325 -0
- package/dist/payload.js.map +1 -0
- package/package.json +59 -0
- package/skills/browsing/COMMANDLINE-USAGE.md +595 -0
- package/skills/browsing/EXAMPLES.md +717 -0
- package/skills/browsing/README.md +55 -0
- package/skills/browsing/SKILL.md +478 -0
- package/skills/browsing/chrome-ws +1021 -0
- package/skills/browsing/chrome-ws-lib.js +461 -0
- package/skills/browsing/host-override.js +98 -0
- package/skills/browsing/lib/browser-bridge.js +175 -0
- package/skills/browsing/lib/browser-session.js +137 -0
- package/skills/browsing/lib/capture.js +499 -0
- package/skills/browsing/lib/cdp-router.js +72 -0
- package/skills/browsing/lib/cdp-utils.js +18 -0
- package/skills/browsing/lib/chrome-launcher-helpers.js +374 -0
- package/skills/browsing/lib/chrome-process.js +464 -0
- package/skills/browsing/lib/console-logging.js +70 -0
- package/skills/browsing/lib/cookies.js +17 -0
- package/skills/browsing/lib/dialogs-render.js +154 -0
- package/skills/browsing/lib/dialogs-router.js +117 -0
- package/skills/browsing/lib/dialogs.js +254 -0
- package/skills/browsing/lib/element-selector.js +91 -0
- package/skills/browsing/lib/evaluation.js +85 -0
- package/skills/browsing/lib/extraction.js +55 -0
- package/skills/browsing/lib/file-upload.js +56 -0
- package/skills/browsing/lib/html-diff.js +122 -0
- package/skills/browsing/lib/key-definitions.js +149 -0
- package/skills/browsing/lib/keyboard-input.js +288 -0
- package/skills/browsing/lib/mouse.js +423 -0
- package/skills/browsing/lib/navigation.js +272 -0
- package/skills/browsing/lib/page-scripts/dom-summary.js +31 -0
- package/skills/browsing/lib/page-scripts/markdown.js +85 -0
- package/skills/browsing/lib/page-scripts/permission-shim.js +80 -0
- package/skills/browsing/lib/page-session.js +106 -0
- package/skills/browsing/lib/profile-lock.js +179 -0
- package/skills/browsing/lib/screenshot.js +171 -0
- package/skills/browsing/lib/select-option.js +99 -0
- package/skills/browsing/lib/session-state.js +66 -0
- package/skills/browsing/lib/tabs.js +144 -0
- package/skills/browsing/lib/viewport.js +103 -0
- package/skills/browsing/lib/websocket-client.js +162 -0
- package/skills/browsing/package.json +11 -0
- package/skills/browsing/test-chrome-args.js +81 -0
- package/skills/browsing/test-cookies.js +21 -0
- package/skills/browsing/test-e2e.sh +51 -0
- package/skills/browsing/test-extract.sh +17 -0
- package/skills/browsing/test-interact.sh +11 -0
- package/skills/browsing/test-navigate.sh +9 -0
- package/skills/browsing/test-raw.sh +8 -0
- package/skills/browsing/test-tabs.sh +15 -0
- package/skills/browsing/test-viewport.js +27 -0
- package/skills/browsing/test-wait.sh +9 -0
|
@@ -0,0 +1,1021 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const process = require('process');
|
|
4
|
+
|
|
5
|
+
// Parse --port=N flag from anywhere in argv (filter it out of positional args)
|
|
6
|
+
const allArgs = process.argv.slice(2);
|
|
7
|
+
const portArg = allArgs.find(a => a.startsWith('--port='));
|
|
8
|
+
const positionalArgs = allArgs.filter(a => !a.startsWith('--port='));
|
|
9
|
+
const [command, wsUrlOrIndex, ...args] = positionalArgs;
|
|
10
|
+
|
|
11
|
+
// Handle --help and --version before any other processing
|
|
12
|
+
if (command === '--help' || command === '-h' || !command) {
|
|
13
|
+
console.log(`Usage: chrome-ws <command> [args]
|
|
14
|
+
|
|
15
|
+
Commands:
|
|
16
|
+
start [port] Start Chrome with remote debugging
|
|
17
|
+
stop Kill Chrome
|
|
18
|
+
pid Print Chrome PID
|
|
19
|
+
info Print Chrome info (JSON)
|
|
20
|
+
tabs List open tabs
|
|
21
|
+
new <url> Open a new tab
|
|
22
|
+
close <tab> Close a tab
|
|
23
|
+
navigate <tab> <url> Navigate tab to URL
|
|
24
|
+
extract <tab> <selector> Extract element text content
|
|
25
|
+
attr <tab> <selector> <attribute> Get element attribute
|
|
26
|
+
html <tab> [selector] Get HTML content
|
|
27
|
+
click <tab> <selector> Click an element
|
|
28
|
+
fill <tab> <selector> <text> Fill an input field
|
|
29
|
+
select <tab> <selector> <value> Select a dropdown option
|
|
30
|
+
eval <tab> <js> Evaluate JavaScript
|
|
31
|
+
wait-for <tab> <selector> [timeout-ms] Wait for element to appear
|
|
32
|
+
wait-text <tab> <text> [timeout-ms] Wait for text to appear
|
|
33
|
+
screenshot <tab> <filename.png> [--fullpage] Take a screenshot
|
|
34
|
+
markdown <tab> <filename.md> Save page as markdown
|
|
35
|
+
har <tab> <filename.har> Export HAR (after har-start)
|
|
36
|
+
raw <ws-url> <json-rpc-payload> Send raw CDP command
|
|
37
|
+
|
|
38
|
+
--help, -h Show this help
|
|
39
|
+
--version, -v Show version
|
|
40
|
+
--port=N Override CHROME_WS_PORT env var
|
|
41
|
+
|
|
42
|
+
Tab arg: numeric index (0, 1, 2...) or full ws:// URL.
|
|
43
|
+
`);
|
|
44
|
+
process.exit(0);
|
|
45
|
+
}
|
|
46
|
+
if (command === '--version' || command === '-v') {
|
|
47
|
+
const pkg = require('../../package.json');
|
|
48
|
+
console.log(pkg.version);
|
|
49
|
+
process.exit(0);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const hostOverride = require('./host-override').createOverride();
|
|
53
|
+
const { createSession } = require('./chrome-ws-lib');
|
|
54
|
+
const { buildChromeArgs } = require('./lib/chrome-launcher-helpers');
|
|
55
|
+
const CHROME_DEBUG_HOST = hostOverride.getHost();
|
|
56
|
+
const CHROME_DEBUG_PORT = hostOverride.getPort();
|
|
57
|
+
const WS_OVERRIDE_ENABLED = hostOverride.isOverrideEnabled();
|
|
58
|
+
const rewriteWsUrl = hostOverride.rewriteWsUrl;
|
|
59
|
+
|
|
60
|
+
// Effective port: --port=N flag overrides CHROME_WS_PORT env / default 9222
|
|
61
|
+
const effectivePort = portArg ? parseInt(portArg.split('=')[1], 10) : CHROME_DEBUG_PORT;
|
|
62
|
+
|
|
63
|
+
// Session pointed at the effective port. Built after effectivePort is known
|
|
64
|
+
// so the lib's pooled connections target the right Chrome instance.
|
|
65
|
+
const session = createSession({ host: CHROME_DEBUG_HOST, port: effectivePort });
|
|
66
|
+
|
|
67
|
+
// Minimal WebSocket client implementation (dependency-free)
|
|
68
|
+
class WebSocketClient {
|
|
69
|
+
constructor(url) {
|
|
70
|
+
this.url = new URL(url);
|
|
71
|
+
this.callbacks = {};
|
|
72
|
+
this.socket = null;
|
|
73
|
+
this.buffer = Buffer.alloc(0);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
on(event, callback) {
|
|
77
|
+
this.callbacks[event] = callback;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
connect() {
|
|
81
|
+
return new Promise((resolve, reject) => {
|
|
82
|
+
const http = require('http');
|
|
83
|
+
const crypto = require('crypto');
|
|
84
|
+
|
|
85
|
+
const key = crypto.randomBytes(16).toString('base64');
|
|
86
|
+
|
|
87
|
+
const options = {
|
|
88
|
+
hostname: this.url.hostname,
|
|
89
|
+
port: this.url.port || 80,
|
|
90
|
+
path: this.url.pathname + this.url.search,
|
|
91
|
+
headers: {
|
|
92
|
+
'Upgrade': 'websocket',
|
|
93
|
+
'Connection': 'Upgrade',
|
|
94
|
+
'Sec-WebSocket-Key': key,
|
|
95
|
+
'Sec-WebSocket-Version': '13'
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const req = http.request(options);
|
|
100
|
+
|
|
101
|
+
req.on('upgrade', (res, socket) => {
|
|
102
|
+
this.socket = socket;
|
|
103
|
+
|
|
104
|
+
socket.on('data', (data) => {
|
|
105
|
+
this.buffer = Buffer.concat([this.buffer, data]);
|
|
106
|
+
this.processFrames();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
socket.on('error', (err) => {
|
|
110
|
+
if (this.callbacks.error) this.callbacks.error(err);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
if (this.callbacks.open) this.callbacks.open();
|
|
114
|
+
resolve();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
req.on('error', reject);
|
|
118
|
+
req.end();
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
processFrames() {
|
|
123
|
+
while (this.buffer.length >= 2) {
|
|
124
|
+
const firstByte = this.buffer[0];
|
|
125
|
+
const secondByte = this.buffer[1];
|
|
126
|
+
|
|
127
|
+
const fin = (firstByte & 0x80) !== 0;
|
|
128
|
+
const opcode = firstByte & 0x0F;
|
|
129
|
+
const masked = (secondByte & 0x80) !== 0;
|
|
130
|
+
let payloadLen = secondByte & 0x7F;
|
|
131
|
+
|
|
132
|
+
let offset = 2;
|
|
133
|
+
|
|
134
|
+
if (payloadLen === 126) {
|
|
135
|
+
if (this.buffer.length < 4) return;
|
|
136
|
+
payloadLen = this.buffer.readUInt16BE(2);
|
|
137
|
+
offset = 4;
|
|
138
|
+
} else if (payloadLen === 127) {
|
|
139
|
+
if (this.buffer.length < 10) return;
|
|
140
|
+
payloadLen = Number(this.buffer.readBigUInt64BE(2));
|
|
141
|
+
offset = 10;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (this.buffer.length < offset + payloadLen) return;
|
|
145
|
+
|
|
146
|
+
let payload = this.buffer.slice(offset, offset + payloadLen);
|
|
147
|
+
this.buffer = this.buffer.slice(offset + payloadLen);
|
|
148
|
+
|
|
149
|
+
if (opcode === 0x1 && this.callbacks.message) {
|
|
150
|
+
this.callbacks.message(payload.toString('utf8'));
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
send(data) {
|
|
156
|
+
const payload = Buffer.from(data, 'utf8');
|
|
157
|
+
const payloadLen = payload.length;
|
|
158
|
+
|
|
159
|
+
let frame;
|
|
160
|
+
let offset = 2;
|
|
161
|
+
|
|
162
|
+
if (payloadLen < 126) {
|
|
163
|
+
frame = Buffer.alloc(payloadLen + 6);
|
|
164
|
+
frame[1] = payloadLen | 0x80;
|
|
165
|
+
} else if (payloadLen < 65536) {
|
|
166
|
+
frame = Buffer.alloc(payloadLen + 8);
|
|
167
|
+
frame[1] = 126 | 0x80;
|
|
168
|
+
frame.writeUInt16BE(payloadLen, 2);
|
|
169
|
+
offset = 4;
|
|
170
|
+
} else {
|
|
171
|
+
frame = Buffer.alloc(payloadLen + 14);
|
|
172
|
+
frame[1] = 127 | 0x80;
|
|
173
|
+
frame.writeBigUInt64BE(BigInt(payloadLen), 2);
|
|
174
|
+
offset = 10;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
frame[0] = 0x81; // FIN + text frame
|
|
178
|
+
|
|
179
|
+
const mask = Buffer.alloc(4);
|
|
180
|
+
require('crypto').randomFillSync(mask);
|
|
181
|
+
mask.copy(frame, offset);
|
|
182
|
+
offset += 4;
|
|
183
|
+
|
|
184
|
+
for (let i = 0; i < payloadLen; i++) {
|
|
185
|
+
frame[offset + i] = payload[i] ^ mask[i % 4];
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
this.socket.write(frame);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
close() {
|
|
192
|
+
if (this.socket) {
|
|
193
|
+
this.socket.end();
|
|
194
|
+
this.socket = null;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Helper to convert string tab specifier to the type expected by session methods.
|
|
200
|
+
// session.fill/evaluate/etc. use getPageSession which accepts a number (index) or
|
|
201
|
+
// a ws:// string — but NOT a numeric string like "0".
|
|
202
|
+
function resolveTabArg(wsUrlOrIndex) {
|
|
203
|
+
if (wsUrlOrIndex && wsUrlOrIndex.startsWith('ws://')) {
|
|
204
|
+
return wsUrlOrIndex; // Already a ws URL string
|
|
205
|
+
}
|
|
206
|
+
const index = parseInt(wsUrlOrIndex, 10);
|
|
207
|
+
if (!isNaN(index)) {
|
|
208
|
+
return index; // Numeric tab index as a number
|
|
209
|
+
}
|
|
210
|
+
throw new Error(`Invalid tab specifier: ${wsUrlOrIndex}`);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Helper to resolve tab index or ws URL to actual ws URL
|
|
214
|
+
async function resolveWsUrl(wsUrlOrIndex) {
|
|
215
|
+
// If it's already a WebSocket URL, return it
|
|
216
|
+
if (wsUrlOrIndex && wsUrlOrIndex.startsWith('ws://')) {
|
|
217
|
+
return wsUrlOrIndex;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// If it's a number (tab index), resolve it
|
|
221
|
+
const index = parseInt(wsUrlOrIndex);
|
|
222
|
+
if (!isNaN(index)) {
|
|
223
|
+
const tabs = await chromeHttp('/json');
|
|
224
|
+
const pageTabs = Array.isArray(tabs)
|
|
225
|
+
? tabs
|
|
226
|
+
.filter(t => t.type === 'page')
|
|
227
|
+
.map(tab => WS_OVERRIDE_ENABLED
|
|
228
|
+
? { ...tab, webSocketDebuggerUrl: rewriteWsUrl(tab.webSocketDebuggerUrl) }
|
|
229
|
+
: tab
|
|
230
|
+
)
|
|
231
|
+
: [];
|
|
232
|
+
|
|
233
|
+
// Auto-create tab if none exist (similar to auto-start Chrome behavior)
|
|
234
|
+
if (pageTabs.length === 0) {
|
|
235
|
+
const newTabInfo = await chromeHttp('/json/new?about:blank', 'PUT');
|
|
236
|
+
return WS_OVERRIDE_ENABLED ? rewriteWsUrl(newTabInfo.webSocketDebuggerUrl) : newTabInfo.webSocketDebuggerUrl;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (index < 0 || index >= pageTabs.length) {
|
|
240
|
+
throw new Error(`Tab index ${index} out of range (0-${pageTabs.length - 1})`);
|
|
241
|
+
}
|
|
242
|
+
return WS_OVERRIDE_ENABLED ? rewriteWsUrl(pageTabs[index].webSocketDebuggerUrl) : pageTabs[index].webSocketDebuggerUrl;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
throw new Error(`Invalid tab specifier: ${wsUrlOrIndex}`);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Helper to make HTTP requests to Chrome on the effective port
|
|
249
|
+
async function chromeHttp(path, method = 'GET') {
|
|
250
|
+
const http = require('http');
|
|
251
|
+
|
|
252
|
+
return new Promise((resolve, reject) => {
|
|
253
|
+
const options = {
|
|
254
|
+
hostname: CHROME_DEBUG_HOST,
|
|
255
|
+
port: effectivePort,
|
|
256
|
+
path,
|
|
257
|
+
method: method
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const req = http.request(options, (res) => {
|
|
261
|
+
let data = '';
|
|
262
|
+
res.on('data', chunk => data += chunk);
|
|
263
|
+
res.on('end', () => {
|
|
264
|
+
if (!data) {
|
|
265
|
+
resolve({});
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
try {
|
|
269
|
+
resolve(JSON.parse(data));
|
|
270
|
+
} catch (e) {
|
|
271
|
+
// Some endpoints return plain text (e.g., "Target is closing")
|
|
272
|
+
resolve({ message: data });
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
req.on('error', reject);
|
|
278
|
+
req.end();
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Command: start - launch Chrome with remote debugging
|
|
283
|
+
if (command === 'start') {
|
|
284
|
+
const { spawn } = require('child_process');
|
|
285
|
+
const { existsSync } = require('fs');
|
|
286
|
+
const os = require('os');
|
|
287
|
+
|
|
288
|
+
// Platform-specific Chrome paths
|
|
289
|
+
const chromePaths = {
|
|
290
|
+
darwin: [
|
|
291
|
+
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
|
292
|
+
'/Applications/Chromium.app/Contents/MacOS/Chromium'
|
|
293
|
+
],
|
|
294
|
+
linux: [
|
|
295
|
+
'/usr/bin/google-chrome',
|
|
296
|
+
'/usr/bin/google-chrome-stable',
|
|
297
|
+
'/usr/bin/chromium',
|
|
298
|
+
'/usr/bin/chromium-browser'
|
|
299
|
+
],
|
|
300
|
+
win32: [
|
|
301
|
+
'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
|
|
302
|
+
'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
|
|
303
|
+
'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe',
|
|
304
|
+
'C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe'
|
|
305
|
+
]
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
const platform = os.platform();
|
|
309
|
+
const paths = chromePaths[platform];
|
|
310
|
+
|
|
311
|
+
if (!paths) {
|
|
312
|
+
console.error(`Unsupported platform: ${platform}`);
|
|
313
|
+
process.exit(1);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Find Chrome executable (CHROME_WS_BROWSER env var overrides auto-detection)
|
|
317
|
+
const chromePath = process.env.CHROME_WS_BROWSER || paths.find(p => existsSync(p));
|
|
318
|
+
|
|
319
|
+
if (!chromePath) {
|
|
320
|
+
console.error('Chrome not found. Searched:');
|
|
321
|
+
paths.forEach(p => console.error(` ${p}`));
|
|
322
|
+
process.exit(1);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Launch Chrome
|
|
326
|
+
const userDataDir = platform === 'win32'
|
|
327
|
+
? 'C:\\temp\\chrome-debug'
|
|
328
|
+
: '/tmp/chrome-debug';
|
|
329
|
+
|
|
330
|
+
const chromeArgs = buildChromeArgs({
|
|
331
|
+
chosenPort: effectivePort,
|
|
332
|
+
chromeUserDataDir: userDataDir,
|
|
333
|
+
chromeHeadless: false,
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
console.log(`Starting Chrome: ${chromePath}`);
|
|
337
|
+
|
|
338
|
+
// Capture Chrome's stderr to a tempfile (issue #35): with stdio 'ignore',
|
|
339
|
+
// Chrome's own complaint (sandbox, missing libs, profile lock) vanished and
|
|
340
|
+
// every launch failure looked like an ambiguous port timeout.
|
|
341
|
+
const { openSync, closeSync, readFileSync: readFile, unlinkSync } = require('fs');
|
|
342
|
+
const pathMod = require('path');
|
|
343
|
+
const stderrLogPath = pathMod.join(os.tmpdir(), `chrome-ws-start-${process.pid}.log`);
|
|
344
|
+
const stderrFd = openSync(stderrLogPath, 'w');
|
|
345
|
+
|
|
346
|
+
const chrome = spawn(chromePath, chromeArgs, {
|
|
347
|
+
detached: true,
|
|
348
|
+
stdio: ['ignore', 'ignore', stderrFd]
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
// Race Chrome's exit against the port poll so "Chrome died at launch" is
|
|
352
|
+
// reported as that, not as "debug port not accessible".
|
|
353
|
+
let exited = null;
|
|
354
|
+
chrome.on('exit', (code, signal) => { exited = { code, signal }; });
|
|
355
|
+
chrome.on('error', (err) => {
|
|
356
|
+
console.error(`Failed to launch Chrome (${chromePath}): ${err.message}`);
|
|
357
|
+
process.exit(1);
|
|
358
|
+
});
|
|
359
|
+
chrome.unref();
|
|
360
|
+
closeSync(stderrFd); // the child keeps its own descriptor
|
|
361
|
+
|
|
362
|
+
const stderrTail = () => {
|
|
363
|
+
try {
|
|
364
|
+
const lines = readFile(stderrLogPath, 'utf8').trim().split('\n');
|
|
365
|
+
return lines.slice(-20).join('\n').trim();
|
|
366
|
+
} catch (_e) {
|
|
367
|
+
return '';
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
const debugBase = `http://${CHROME_DEBUG_HOST}:${effectivePort}`;
|
|
372
|
+
|
|
373
|
+
// Wait and verify
|
|
374
|
+
setTimeout(async () => {
|
|
375
|
+
try {
|
|
376
|
+
const version = await chromeHttp('/json/version');
|
|
377
|
+
console.log(`Chrome started: ${version.Browser}`);
|
|
378
|
+
console.log(`Remote debugging: ${debugBase}`);
|
|
379
|
+
try { unlinkSync(stderrLogPath); } catch (_e) { /* best-effort */ }
|
|
380
|
+
} catch (e) {
|
|
381
|
+
if (exited) {
|
|
382
|
+
const how = exited.signal ? `signal ${exited.signal}` : `code ${exited.code}`;
|
|
383
|
+
console.error(`Chrome exited with ${how} before opening the debug port`);
|
|
384
|
+
} else {
|
|
385
|
+
console.error(`Chrome is running but the debug port at ${debugBase} is not responding`);
|
|
386
|
+
console.error(`Try: curl ${debugBase}/json/version`);
|
|
387
|
+
}
|
|
388
|
+
const tail = stderrTail();
|
|
389
|
+
if (tail) {
|
|
390
|
+
console.error(`Chrome stderr (full log: ${stderrLogPath}):`);
|
|
391
|
+
console.error(tail);
|
|
392
|
+
}
|
|
393
|
+
process.exit(1);
|
|
394
|
+
}
|
|
395
|
+
}, 2000);
|
|
396
|
+
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Command: stop - kill the Chrome process this session manages
|
|
401
|
+
if (command === 'stop') {
|
|
402
|
+
(async () => {
|
|
403
|
+
try {
|
|
404
|
+
await session.killChrome();
|
|
405
|
+
console.log('Chrome stopped');
|
|
406
|
+
} catch (e) {
|
|
407
|
+
console.error('Failed to stop Chrome:', e.message);
|
|
408
|
+
process.exit(1);
|
|
409
|
+
}
|
|
410
|
+
})();
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Command: pid - print Chrome PID (for X11 window management, etc.)
|
|
415
|
+
if (command === 'pid') {
|
|
416
|
+
const pid = session.getChromePid();
|
|
417
|
+
if (pid === null) {
|
|
418
|
+
console.error('Chrome is not running (started via MCP). PID is only available when Chrome was started in this process.');
|
|
419
|
+
process.exit(1);
|
|
420
|
+
}
|
|
421
|
+
console.log(pid);
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Command: info - print Chrome info as JSON (pid, mode, profile, port)
|
|
426
|
+
if (command === 'info') {
|
|
427
|
+
(async () => {
|
|
428
|
+
try {
|
|
429
|
+
const mode = await session.getBrowserMode();
|
|
430
|
+
// Also try to get PID from meta.json if available
|
|
431
|
+
const meta = session.readProfileMeta ? session.readProfileMeta(mode.profile) : null;
|
|
432
|
+
const info = {
|
|
433
|
+
pid: meta ? meta.pid : mode.pid,
|
|
434
|
+
port: meta ? meta.port : mode.port,
|
|
435
|
+
mode: meta ? (meta.headless ? 'headless' : 'headed') : mode.mode,
|
|
436
|
+
profile: mode.profile,
|
|
437
|
+
profileDir: mode.profileDir,
|
|
438
|
+
running: meta !== null
|
|
439
|
+
};
|
|
440
|
+
console.log(JSON.stringify(info, null, 2));
|
|
441
|
+
} catch (e) {
|
|
442
|
+
console.error('Failed to get Chrome info:', e.message);
|
|
443
|
+
process.exit(1);
|
|
444
|
+
}
|
|
445
|
+
})();
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Command: tabs - list all tabs
|
|
450
|
+
if (command === 'tabs') {
|
|
451
|
+
(async () => {
|
|
452
|
+
try {
|
|
453
|
+
const tabs = await chromeHttp('/json');
|
|
454
|
+
tabs.forEach(tab => {
|
|
455
|
+
if (tab.type === 'page') {
|
|
456
|
+
console.log(`${tab.id}\t${tab.url}\t${tab.title}`);
|
|
457
|
+
}
|
|
458
|
+
});
|
|
459
|
+
} catch (e) {
|
|
460
|
+
console.error('Failed to list tabs:', e.message);
|
|
461
|
+
process.exit(1);
|
|
462
|
+
}
|
|
463
|
+
})();
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Command: new - create new tab
|
|
468
|
+
if (command === 'new') {
|
|
469
|
+
// For this command, wsUrlOrIndex variable contains the URL parameter
|
|
470
|
+
if (!wsUrlOrIndex) {
|
|
471
|
+
console.error('Usage: chrome-ws new <url>');
|
|
472
|
+
process.exit(1);
|
|
473
|
+
}
|
|
474
|
+
const url = wsUrlOrIndex;
|
|
475
|
+
(async () => {
|
|
476
|
+
try {
|
|
477
|
+
const encoded = encodeURIComponent(url);
|
|
478
|
+
const tab = await chromeHttp(`/json/new?${encoded}`, 'PUT');
|
|
479
|
+
const wsUrl = WS_OVERRIDE_ENABLED ? rewriteWsUrl(tab.webSocketDebuggerUrl) : tab.webSocketDebuggerUrl;
|
|
480
|
+
console.log(wsUrl);
|
|
481
|
+
} catch (e) {
|
|
482
|
+
console.error('Failed to create tab:', e.message);
|
|
483
|
+
process.exit(1);
|
|
484
|
+
}
|
|
485
|
+
})();
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Command: close - close tab by ws URL or numeric index
|
|
490
|
+
if (command === 'close') {
|
|
491
|
+
if (!wsUrlOrIndex) {
|
|
492
|
+
console.error('Usage: chrome-ws close <tab>');
|
|
493
|
+
process.exit(1);
|
|
494
|
+
}
|
|
495
|
+
(async () => {
|
|
496
|
+
try {
|
|
497
|
+
const tabWsUrl = await resolveWsUrl(wsUrlOrIndex);
|
|
498
|
+
// Extract tab ID from ws URL
|
|
499
|
+
const match = tabWsUrl.match(/\/devtools\/page\/([A-F0-9-]+)/i);
|
|
500
|
+
if (!match) {
|
|
501
|
+
console.error('Invalid WebSocket URL');
|
|
502
|
+
process.exit(1);
|
|
503
|
+
}
|
|
504
|
+
await chromeHttp(`/json/close/${match[1]}`);
|
|
505
|
+
console.log('Tab closed');
|
|
506
|
+
} catch (e) {
|
|
507
|
+
console.error('Failed to close tab:', e.message);
|
|
508
|
+
process.exit(1);
|
|
509
|
+
}
|
|
510
|
+
})();
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Helper to send CDP command via WebSocket
|
|
515
|
+
async function sendCdpCommand(wsUrl, method, params = {}) {
|
|
516
|
+
return new Promise(async (resolve, reject) => {
|
|
517
|
+
const ws = new WebSocketClient(wsUrl);
|
|
518
|
+
const id = Math.floor(Math.random() * 1000000);
|
|
519
|
+
|
|
520
|
+
const timeout = setTimeout(() => {
|
|
521
|
+
ws.close();
|
|
522
|
+
reject(new Error('Timeout after 30s'));
|
|
523
|
+
}, 30000);
|
|
524
|
+
|
|
525
|
+
ws.on('message', (data) => {
|
|
526
|
+
const response = JSON.parse(data);
|
|
527
|
+
if (response.id === id) {
|
|
528
|
+
clearTimeout(timeout);
|
|
529
|
+
if (response.error) {
|
|
530
|
+
ws.close();
|
|
531
|
+
reject(new Error(response.error.message));
|
|
532
|
+
} else {
|
|
533
|
+
ws.close();
|
|
534
|
+
resolve(response.result);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
ws.on('error', (err) => {
|
|
540
|
+
clearTimeout(timeout);
|
|
541
|
+
reject(err);
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
try {
|
|
545
|
+
await ws.connect();
|
|
546
|
+
ws.send(JSON.stringify({ id, method, params }));
|
|
547
|
+
} catch (err) {
|
|
548
|
+
clearTimeout(timeout);
|
|
549
|
+
reject(err);
|
|
550
|
+
}
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Command: navigate
|
|
555
|
+
if (command === 'navigate') {
|
|
556
|
+
const [url] = args;
|
|
557
|
+
if (!wsUrlOrIndex || !url) {
|
|
558
|
+
console.error('Usage: chrome-ws navigate <tab-index-or-ws-url> <url>');
|
|
559
|
+
process.exit(1);
|
|
560
|
+
}
|
|
561
|
+
(async () => {
|
|
562
|
+
try {
|
|
563
|
+
const wsUrl = await resolveWsUrl(wsUrlOrIndex);
|
|
564
|
+
await sendCdpCommand(wsUrl, 'Page.navigate', { url });
|
|
565
|
+
console.log(`Navigated to ${url}`);
|
|
566
|
+
} catch (e) {
|
|
567
|
+
console.error('Navigation failed:', e.message);
|
|
568
|
+
process.exit(1);
|
|
569
|
+
}
|
|
570
|
+
})();
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// Command: wait-for - wait for selector to appear
|
|
575
|
+
if (command === 'wait-for') {
|
|
576
|
+
const [selector, timeoutArg] = args;
|
|
577
|
+
if (!wsUrlOrIndex || !selector) {
|
|
578
|
+
console.error('Usage: chrome-ws wait-for <tab-index-or-ws-url> <selector> [timeout-ms]');
|
|
579
|
+
process.exit(1);
|
|
580
|
+
}
|
|
581
|
+
const timeout = timeoutArg ? parseInt(timeoutArg, 10) : 5000;
|
|
582
|
+
if (Number.isNaN(timeout) || timeout < 0) {
|
|
583
|
+
console.error(`Invalid timeout: ${timeoutArg}`);
|
|
584
|
+
process.exit(1);
|
|
585
|
+
}
|
|
586
|
+
(async () => {
|
|
587
|
+
try {
|
|
588
|
+
await session.waitForElement(resolveTabArg(wsUrlOrIndex), selector, timeout);
|
|
589
|
+
console.log(`Element found: ${selector}`);
|
|
590
|
+
process.exit(0);
|
|
591
|
+
} catch (e) {
|
|
592
|
+
console.error('Wait failed:', e.message);
|
|
593
|
+
process.exit(1);
|
|
594
|
+
}
|
|
595
|
+
})();
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// Command: click
|
|
600
|
+
if (command === 'click') {
|
|
601
|
+
const [selector] = args;
|
|
602
|
+
if (!wsUrlOrIndex || !selector) {
|
|
603
|
+
console.error('Usage: chrome-ws click <tab-index-or-ws-url> <selector>');
|
|
604
|
+
process.exit(1);
|
|
605
|
+
}
|
|
606
|
+
(async () => {
|
|
607
|
+
try {
|
|
608
|
+
const wsUrl = await resolveWsUrl(wsUrlOrIndex);
|
|
609
|
+
const js = `document.querySelector(${JSON.stringify(selector)}).click()`;
|
|
610
|
+
await sendCdpCommand(wsUrl, 'Runtime.evaluate', { expression: js });
|
|
611
|
+
console.log(`Clicked: ${selector}`);
|
|
612
|
+
} catch (e) {
|
|
613
|
+
console.error('Click failed:', e.message);
|
|
614
|
+
process.exit(1);
|
|
615
|
+
}
|
|
616
|
+
})();
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// Command: fill
|
|
621
|
+
if (command === 'fill') {
|
|
622
|
+
const [selector, value] = args;
|
|
623
|
+
if (!wsUrlOrIndex || !selector || value === undefined) {
|
|
624
|
+
console.error('Usage: chrome-ws fill <tab-index-or-ws-url> <selector> <value>');
|
|
625
|
+
process.exit(1);
|
|
626
|
+
}
|
|
627
|
+
(async () => {
|
|
628
|
+
try {
|
|
629
|
+
await session.fill(resolveTabArg(wsUrlOrIndex), selector, value);
|
|
630
|
+
console.log(`Filled: ${selector}`);
|
|
631
|
+
process.exit(0);
|
|
632
|
+
} catch (e) {
|
|
633
|
+
console.error('Fill failed:', e.message);
|
|
634
|
+
process.exit(1);
|
|
635
|
+
}
|
|
636
|
+
})();
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// Command: select - select dropdown option
|
|
641
|
+
if (command === 'select') {
|
|
642
|
+
const [selector, value] = args;
|
|
643
|
+
if (!wsUrlOrIndex || !selector || value === undefined) {
|
|
644
|
+
console.error('Usage: chrome-ws select <tab-index-or-ws-url> <selector> <value-or-label-or-json-array>');
|
|
645
|
+
process.exit(1);
|
|
646
|
+
}
|
|
647
|
+
(async () => {
|
|
648
|
+
try {
|
|
649
|
+
// Accept JSON array (multi-select) or plain string (value or label).
|
|
650
|
+
let selectValue = value;
|
|
651
|
+
if (typeof value === 'string' && value.trim().startsWith('[')) {
|
|
652
|
+
try {
|
|
653
|
+
const parsed = JSON.parse(value);
|
|
654
|
+
if (Array.isArray(parsed) && parsed.every(v => typeof v === 'string')) {
|
|
655
|
+
selectValue = parsed;
|
|
656
|
+
}
|
|
657
|
+
} catch (_e) { /* not JSON, treat as plain string */ }
|
|
658
|
+
}
|
|
659
|
+
const result = await session.selectOption(resolveTabArg(wsUrlOrIndex), selector, selectValue);
|
|
660
|
+
console.log(JSON.stringify(result.matched.map(o => o.value)));
|
|
661
|
+
process.exit(0);
|
|
662
|
+
} catch (e) {
|
|
663
|
+
console.error('Select failed:', e.message);
|
|
664
|
+
process.exit(1);
|
|
665
|
+
}
|
|
666
|
+
})();
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// Command: eval - evaluate JavaScript
|
|
671
|
+
if (command === 'eval') {
|
|
672
|
+
const expression = args.join(' ');
|
|
673
|
+
if (!wsUrlOrIndex || !expression) {
|
|
674
|
+
console.error('Usage: chrome-ws eval <tab-index-or-ws-url> <js-expression>');
|
|
675
|
+
process.exit(1);
|
|
676
|
+
}
|
|
677
|
+
(async () => {
|
|
678
|
+
try {
|
|
679
|
+
const value = await session.evaluate(resolveTabArg(wsUrlOrIndex), expression);
|
|
680
|
+
console.log(JSON.stringify(value, null, 2));
|
|
681
|
+
process.exit(0);
|
|
682
|
+
} catch (e) {
|
|
683
|
+
console.error('Eval failed:', e.message);
|
|
684
|
+
process.exit(1);
|
|
685
|
+
}
|
|
686
|
+
})();
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// Command: extract - get element text content
|
|
691
|
+
if (command === 'extract') {
|
|
692
|
+
const [selector] = args;
|
|
693
|
+
if (!wsUrlOrIndex || !selector) {
|
|
694
|
+
console.error('Usage: chrome-ws extract <tab-index-or-ws-url> <selector>');
|
|
695
|
+
process.exit(1);
|
|
696
|
+
}
|
|
697
|
+
(async () => {
|
|
698
|
+
try {
|
|
699
|
+
const wsUrl = await resolveWsUrl(wsUrlOrIndex);
|
|
700
|
+
const js = `document.querySelector(${JSON.stringify(selector)})?.textContent`;
|
|
701
|
+
const result = await sendCdpCommand(wsUrl, 'Runtime.evaluate', {
|
|
702
|
+
expression: js,
|
|
703
|
+
returnByValue: true
|
|
704
|
+
});
|
|
705
|
+
console.log(result.result.value);
|
|
706
|
+
} catch (e) {
|
|
707
|
+
console.error('Extract failed:', e.message);
|
|
708
|
+
process.exit(1);
|
|
709
|
+
}
|
|
710
|
+
})();
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// Command: attr - get element attribute
|
|
715
|
+
if (command === 'attr') {
|
|
716
|
+
const [selector, attrName] = args;
|
|
717
|
+
if (!wsUrlOrIndex || !selector || !attrName) {
|
|
718
|
+
console.error('Usage: chrome-ws attr <tab-index-or-ws-url> <selector> <attribute>');
|
|
719
|
+
process.exit(1);
|
|
720
|
+
}
|
|
721
|
+
(async () => {
|
|
722
|
+
try {
|
|
723
|
+
const wsUrl = await resolveWsUrl(wsUrlOrIndex);
|
|
724
|
+
const js = `document.querySelector(${JSON.stringify(selector)})?.getAttribute(${JSON.stringify(attrName)})`;
|
|
725
|
+
const result = await sendCdpCommand(wsUrl, 'Runtime.evaluate', {
|
|
726
|
+
expression: js,
|
|
727
|
+
returnByValue: true
|
|
728
|
+
});
|
|
729
|
+
console.log(result.result.value);
|
|
730
|
+
} catch (e) {
|
|
731
|
+
console.error('Attr failed:', e.message);
|
|
732
|
+
process.exit(1);
|
|
733
|
+
}
|
|
734
|
+
})();
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// Command: html - get HTML content
|
|
739
|
+
if (command === 'html') {
|
|
740
|
+
const [selector] = args;
|
|
741
|
+
if (!wsUrlOrIndex) {
|
|
742
|
+
console.error('Usage: chrome-ws html <tab-index-or-ws-url> [selector]');
|
|
743
|
+
process.exit(1);
|
|
744
|
+
}
|
|
745
|
+
(async () => {
|
|
746
|
+
try {
|
|
747
|
+
const wsUrl = await resolveWsUrl(wsUrlOrIndex);
|
|
748
|
+
const js = selector
|
|
749
|
+
? `document.querySelector(${JSON.stringify(selector)})?.innerHTML`
|
|
750
|
+
: 'document.documentElement.outerHTML';
|
|
751
|
+
const result = await sendCdpCommand(wsUrl, 'Runtime.evaluate', {
|
|
752
|
+
expression: js,
|
|
753
|
+
returnByValue: true
|
|
754
|
+
});
|
|
755
|
+
console.log(result.result.value);
|
|
756
|
+
} catch (e) {
|
|
757
|
+
console.error('HTML failed:', e.message);
|
|
758
|
+
process.exit(1);
|
|
759
|
+
}
|
|
760
|
+
})();
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// Command: wait-text - wait for text to appear
|
|
765
|
+
if (command === 'wait-text') {
|
|
766
|
+
// Last positional arg is treated as timeout if it parses as a non-negative
|
|
767
|
+
// integer; otherwise everything is text. This handles both:
|
|
768
|
+
// wait-text 0 "the text" 3000
|
|
769
|
+
// wait-text 0 "text without timeout"
|
|
770
|
+
if (!wsUrlOrIndex || args.length === 0) {
|
|
771
|
+
console.error('Usage: chrome-ws wait-text <tab-index-or-ws-url> <text> [timeout-ms]');
|
|
772
|
+
process.exit(1);
|
|
773
|
+
}
|
|
774
|
+
let textArgs = args;
|
|
775
|
+
let timeout = 5000;
|
|
776
|
+
const last = args[args.length - 1];
|
|
777
|
+
const parsedLast = parseInt(last, 10);
|
|
778
|
+
if (args.length >= 2 && Number.isFinite(parsedLast) && parsedLast >= 0 && String(parsedLast) === last.trim()) {
|
|
779
|
+
timeout = parsedLast;
|
|
780
|
+
textArgs = args.slice(0, -1);
|
|
781
|
+
}
|
|
782
|
+
const text = textArgs.join(' ');
|
|
783
|
+
if (!text) {
|
|
784
|
+
console.error('Usage: chrome-ws wait-text <tab-index-or-ws-url> <text> [timeout-ms]');
|
|
785
|
+
process.exit(1);
|
|
786
|
+
}
|
|
787
|
+
(async () => {
|
|
788
|
+
try {
|
|
789
|
+
await session.waitForText(resolveTabArg(wsUrlOrIndex), text, timeout);
|
|
790
|
+
console.log(`Text found: ${text}`);
|
|
791
|
+
process.exit(0);
|
|
792
|
+
} catch (e) {
|
|
793
|
+
console.error('Wait failed:', e.message);
|
|
794
|
+
process.exit(1);
|
|
795
|
+
}
|
|
796
|
+
})();
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
// Command: screenshot - capture screenshot
|
|
801
|
+
if (command === 'screenshot') {
|
|
802
|
+
const fullPage = args.includes('--fullpage');
|
|
803
|
+
const cleanArgs = args.filter(a => a !== '--fullpage');
|
|
804
|
+
const [filename] = cleanArgs;
|
|
805
|
+
if (!wsUrlOrIndex || !filename) {
|
|
806
|
+
console.error('Usage: chrome-ws screenshot <tab-index-or-ws-url> <filename.png> [--fullpage]');
|
|
807
|
+
process.exit(1);
|
|
808
|
+
}
|
|
809
|
+
(async () => {
|
|
810
|
+
try {
|
|
811
|
+
const savedPath = await session.screenshot(resolveTabArg(wsUrlOrIndex), filename, null, fullPage);
|
|
812
|
+
console.log(`Screenshot saved to ${savedPath}`);
|
|
813
|
+
process.exit(0);
|
|
814
|
+
} catch (e) {
|
|
815
|
+
console.error('Screenshot failed:', e.message);
|
|
816
|
+
process.exit(1);
|
|
817
|
+
}
|
|
818
|
+
})();
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
// Command: markdown - save page as markdown
|
|
823
|
+
if (command === 'markdown') {
|
|
824
|
+
const [filename] = args;
|
|
825
|
+
if (!wsUrlOrIndex || !filename) {
|
|
826
|
+
console.error('Usage: chrome-ws markdown <tab-index-or-ws-url> <filename.md>');
|
|
827
|
+
process.exit(1);
|
|
828
|
+
}
|
|
829
|
+
(async () => {
|
|
830
|
+
try {
|
|
831
|
+
const wsUrl = await resolveWsUrl(wsUrlOrIndex);
|
|
832
|
+
|
|
833
|
+
// Extract page content intelligently
|
|
834
|
+
const js = `
|
|
835
|
+
(() => {
|
|
836
|
+
const title = document.title;
|
|
837
|
+
const url = window.location.href;
|
|
838
|
+
|
|
839
|
+
// Get main content (try article, main, or body)
|
|
840
|
+
let content = document.querySelector('article') ||
|
|
841
|
+
document.querySelector('main') ||
|
|
842
|
+
document.body;
|
|
843
|
+
|
|
844
|
+
// Convert to markdown-ish text
|
|
845
|
+
function nodeToMarkdown(node, level = 0) {
|
|
846
|
+
let md = '';
|
|
847
|
+
|
|
848
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
849
|
+
const text = node.textContent.trim();
|
|
850
|
+
return text ? text + ' ' : '';
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
if (node.nodeType !== Node.ELEMENT_NODE) return '';
|
|
854
|
+
|
|
855
|
+
const tag = node.tagName.toLowerCase();
|
|
856
|
+
|
|
857
|
+
// Headers
|
|
858
|
+
if (/^h[1-6]$/.test(tag)) {
|
|
859
|
+
const hLevel = parseInt(tag[1]);
|
|
860
|
+
md += '\\n' + '#'.repeat(hLevel) + ' ' + node.textContent.trim() + '\\n\\n';
|
|
861
|
+
return md;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// Paragraphs
|
|
865
|
+
if (tag === 'p') {
|
|
866
|
+
md += node.textContent.trim() + '\\n\\n';
|
|
867
|
+
return md;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// Links
|
|
871
|
+
if (tag === 'a') {
|
|
872
|
+
const href = node.getAttribute('href') || '';
|
|
873
|
+
const text = node.textContent.trim();
|
|
874
|
+
return \`[\${text}](\${href}) \`;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// Lists
|
|
878
|
+
if (tag === 'li') {
|
|
879
|
+
return '- ' + node.textContent.trim() + '\\n';
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// Code
|
|
883
|
+
if (tag === 'code' || tag === 'pre') {
|
|
884
|
+
return '\`' + node.textContent.trim() + '\` ';
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
// Recurse for other elements
|
|
888
|
+
for (const child of node.childNodes) {
|
|
889
|
+
md += nodeToMarkdown(child, level + 1);
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
if (tag === 'div' || tag === 'section') md += '\\n';
|
|
893
|
+
|
|
894
|
+
return md;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
const markdown = nodeToMarkdown(content);
|
|
898
|
+
|
|
899
|
+
return \`# \${title}\\n\\nSource: \${url}\\n\\n\${markdown}\`;
|
|
900
|
+
})()
|
|
901
|
+
`;
|
|
902
|
+
|
|
903
|
+
const result = await sendCdpCommand(wsUrl, 'Runtime.evaluate', {
|
|
904
|
+
expression: js,
|
|
905
|
+
returnByValue: true
|
|
906
|
+
});
|
|
907
|
+
|
|
908
|
+
const fs = require('fs');
|
|
909
|
+
fs.writeFileSync(filename, result.result.value);
|
|
910
|
+
console.log(`Markdown saved to ${filename}`);
|
|
911
|
+
} catch (e) {
|
|
912
|
+
console.error('Markdown conversion failed:', e.message);
|
|
913
|
+
process.exit(1);
|
|
914
|
+
}
|
|
915
|
+
})();
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// Command: har - save network traffic as HAR
|
|
920
|
+
if (command === 'har') {
|
|
921
|
+
const [filename] = args;
|
|
922
|
+
if (!wsUrlOrIndex || !filename) {
|
|
923
|
+
console.error('Usage: chrome-ws har <tab-index-or-ws-url> <filename.har>');
|
|
924
|
+
console.error('Note: Start recording with "chrome-ws har-start <tab>" first');
|
|
925
|
+
process.exit(1);
|
|
926
|
+
}
|
|
927
|
+
(async () => {
|
|
928
|
+
try {
|
|
929
|
+
const wsUrl = await resolveWsUrl(wsUrlOrIndex);
|
|
930
|
+
|
|
931
|
+
// Get HAR data
|
|
932
|
+
const js = `window.__chrome_ws_har__ || []`;
|
|
933
|
+
const result = await sendCdpCommand(wsUrl, 'Runtime.evaluate', {
|
|
934
|
+
expression: js,
|
|
935
|
+
returnByValue: true
|
|
936
|
+
});
|
|
937
|
+
|
|
938
|
+
const har = {
|
|
939
|
+
log: {
|
|
940
|
+
version: '1.2',
|
|
941
|
+
creator: { name: 'chrome-ws', version: '1.0.0' },
|
|
942
|
+
entries: result.result.value || []
|
|
943
|
+
}
|
|
944
|
+
};
|
|
945
|
+
|
|
946
|
+
const fs = require('fs');
|
|
947
|
+
fs.writeFileSync(filename, JSON.stringify(har, null, 2));
|
|
948
|
+
console.log(`HAR saved to ${filename} (${har.log.entries.length} entries)`);
|
|
949
|
+
} catch (e) {
|
|
950
|
+
console.error('HAR export failed:', e.message);
|
|
951
|
+
process.exit(1);
|
|
952
|
+
}
|
|
953
|
+
})();
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
// Past all the named-command dispatches without a return → either it's the
|
|
958
|
+
// raw escape hatch or it's a typo. Separate those cases so users get an
|
|
959
|
+
// actionable error instead of a confusing "Usage: chrome-ws raw ..." banner.
|
|
960
|
+
if (command !== 'raw') {
|
|
961
|
+
console.error(`Unknown command: ${command}`);
|
|
962
|
+
console.error(`Run 'chrome-ws --help' for the list of commands.`);
|
|
963
|
+
process.exit(1);
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
if (!wsUrlOrIndex || args.length === 0) {
|
|
967
|
+
console.error('Usage: chrome-ws raw <tab-index-or-ws-url> <json-rpc-payload>');
|
|
968
|
+
process.exit(1);
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
const payload = args.join(' ');
|
|
972
|
+
let message;
|
|
973
|
+
try {
|
|
974
|
+
message = JSON.parse(payload);
|
|
975
|
+
} catch (e) {
|
|
976
|
+
console.error('Invalid JSON payload:', e.message);
|
|
977
|
+
process.exit(1);
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
// For raw command, wsUrlOrIndex must be a full WebSocket URL (not an index)
|
|
981
|
+
// since this is the low-level escape hatch
|
|
982
|
+
if (!wsUrlOrIndex.startsWith('ws://')) {
|
|
983
|
+
console.error('raw command requires full WebSocket URL, not tab index');
|
|
984
|
+
console.error('Use: chrome-ws tabs # to get WebSocket URLs');
|
|
985
|
+
process.exit(1);
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
(async () => {
|
|
989
|
+
const ws = new WebSocketClient(wsUrlOrIndex);
|
|
990
|
+
|
|
991
|
+
const timeout = setTimeout(() => {
|
|
992
|
+
console.error('Timeout after 30s');
|
|
993
|
+
ws.close();
|
|
994
|
+
process.exit(1);
|
|
995
|
+
}, 30000);
|
|
996
|
+
|
|
997
|
+
ws.on('message', (data) => {
|
|
998
|
+
const response = JSON.parse(data);
|
|
999
|
+
if (response.id === message.id) {
|
|
1000
|
+
clearTimeout(timeout);
|
|
1001
|
+
console.log(JSON.stringify(response, null, 2));
|
|
1002
|
+
ws.close();
|
|
1003
|
+
process.exit(0);
|
|
1004
|
+
}
|
|
1005
|
+
});
|
|
1006
|
+
|
|
1007
|
+
ws.on('error', (error) => {
|
|
1008
|
+
clearTimeout(timeout);
|
|
1009
|
+
console.error('WebSocket error:', error.message);
|
|
1010
|
+
process.exit(1);
|
|
1011
|
+
});
|
|
1012
|
+
|
|
1013
|
+
try {
|
|
1014
|
+
await ws.connect();
|
|
1015
|
+
ws.send(JSON.stringify(message));
|
|
1016
|
+
} catch (err) {
|
|
1017
|
+
clearTimeout(timeout);
|
|
1018
|
+
console.error('Connection failed:', err.message);
|
|
1019
|
+
process.exit(1);
|
|
1020
|
+
}
|
|
1021
|
+
})();
|