browsirai 0.1.1 → 0.2.1
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 +661 -78
- package/README.md +120 -6
- package/dist/bin.js +7 -17
- package/dist/bin.js.map +1 -1
- package/dist/cli/commands/act.js +1226 -0
- package/dist/cli/commands/act.js.map +1 -0
- package/dist/cli/commands/nav.js +739 -0
- package/dist/cli/commands/nav.js.map +1 -0
- package/dist/cli/commands/net.js +556 -0
- package/dist/cli/commands/net.js.map +1 -0
- package/dist/cli/commands/obs.js +1049 -0
- package/dist/cli/commands/obs.js.map +1 -0
- package/dist/cli/run.js +728 -0
- package/dist/cli/run.js.map +1 -0
- package/dist/cli.js +7 -17
- package/dist/cli.js.map +1 -1
- package/dist/server.js +4 -2
- package/dist/server.js.map +1 -1
- package/package.json +2 -2
package/dist/cli/run.js
ADDED
|
@@ -0,0 +1,728 @@
|
|
|
1
|
+
// src/cli/run.ts
|
|
2
|
+
import pc from "picocolors";
|
|
3
|
+
|
|
4
|
+
// src/chrome-launcher.ts
|
|
5
|
+
import { execSync, spawn } from "child_process";
|
|
6
|
+
import { existsSync, readFileSync, mkdirSync, copyFileSync, readdirSync, statSync } from "fs";
|
|
7
|
+
import http from "http";
|
|
8
|
+
import { join } from "path";
|
|
9
|
+
import { homedir, tmpdir } from "os";
|
|
10
|
+
import { createConnection } from "net";
|
|
11
|
+
var CHROME_PATHS = {
|
|
12
|
+
darwin: [
|
|
13
|
+
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
14
|
+
"/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary",
|
|
15
|
+
"/Applications/Chromium.app/Contents/MacOS/Chromium",
|
|
16
|
+
"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
|
|
17
|
+
"/Applications/Brave Browser.app/Contents/MacOS/Brave Browser"
|
|
18
|
+
],
|
|
19
|
+
linux: [
|
|
20
|
+
"google-chrome",
|
|
21
|
+
"google-chrome-stable",
|
|
22
|
+
"chromium",
|
|
23
|
+
"chromium-browser",
|
|
24
|
+
"microsoft-edge",
|
|
25
|
+
"brave-browser"
|
|
26
|
+
],
|
|
27
|
+
win32: [
|
|
28
|
+
"C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
|
|
29
|
+
"C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe",
|
|
30
|
+
"C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe",
|
|
31
|
+
"C:\\Program Files\\BraveSoftware\\Brave-Browser\\Application\\brave.exe"
|
|
32
|
+
]
|
|
33
|
+
};
|
|
34
|
+
function getDefaultChromeDataDir() {
|
|
35
|
+
const home = homedir();
|
|
36
|
+
switch (process.platform) {
|
|
37
|
+
case "darwin":
|
|
38
|
+
return join(home, "Library", "Application Support", "Google", "Chrome");
|
|
39
|
+
case "win32":
|
|
40
|
+
return join(home, "AppData", "Local", "Google", "Chrome", "User Data");
|
|
41
|
+
default:
|
|
42
|
+
return join(home, ".config", "google-chrome");
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function findChrome() {
|
|
46
|
+
const platform = process.platform;
|
|
47
|
+
const candidates = CHROME_PATHS[platform] ?? [];
|
|
48
|
+
for (const candidate of candidates) {
|
|
49
|
+
if (platform === "darwin" || platform === "win32") {
|
|
50
|
+
if (existsSync(candidate)) return candidate;
|
|
51
|
+
} else {
|
|
52
|
+
try {
|
|
53
|
+
const result = execSync(`which ${candidate}`, { stdio: "pipe" });
|
|
54
|
+
const path = result.toString().trim();
|
|
55
|
+
if (path) return path;
|
|
56
|
+
} catch {
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
function isCDPHealthy(port, host = "127.0.0.1") {
|
|
63
|
+
return new Promise((resolve) => {
|
|
64
|
+
const req = http.get(`http://${host}:${port}/json/version`, (res) => {
|
|
65
|
+
resolve(res.statusCode === 200);
|
|
66
|
+
res.resume();
|
|
67
|
+
});
|
|
68
|
+
req.setTimeout(3e3, () => {
|
|
69
|
+
req.destroy();
|
|
70
|
+
resolve(false);
|
|
71
|
+
});
|
|
72
|
+
req.on("error", () => resolve(false));
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
function readDevToolsActivePort(chromeDataDir) {
|
|
76
|
+
const dataDir = chromeDataDir ?? getDefaultChromeDataDir();
|
|
77
|
+
const portFile = join(dataDir, "DevToolsActivePort");
|
|
78
|
+
if (!existsSync(portFile)) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
const content = readFileSync(portFile, "utf-8");
|
|
83
|
+
const lines = content.split("\n").map((l) => l.trim()).filter((l) => l.length > 0);
|
|
84
|
+
if (lines.length < 2) return null;
|
|
85
|
+
const port = parseInt(lines[0], 10);
|
|
86
|
+
const wsPath = lines[1];
|
|
87
|
+
if (isNaN(port) || !wsPath.startsWith("/")) return null;
|
|
88
|
+
return {
|
|
89
|
+
port,
|
|
90
|
+
wsPath,
|
|
91
|
+
wsEndpoint: `ws://127.0.0.1:${port}${wsPath}`
|
|
92
|
+
};
|
|
93
|
+
} catch {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function isChromeRunning() {
|
|
98
|
+
try {
|
|
99
|
+
if (process.platform === "win32") {
|
|
100
|
+
const r2 = execSync('tasklist /FI "IMAGENAME eq chrome.exe" /NH', { stdio: "pipe" }).toString();
|
|
101
|
+
return r2.includes("chrome.exe");
|
|
102
|
+
}
|
|
103
|
+
const r = execSync("pgrep -x 'Google Chrome' || pgrep -x chrome || pgrep -x chromium", { stdio: "pipe" }).toString().trim();
|
|
104
|
+
return r.length > 0;
|
|
105
|
+
} catch {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
var SEPARATE_PORT = 9444;
|
|
110
|
+
async function launchChromeWithDebugging(port = 9222, headless = false) {
|
|
111
|
+
const healthy = await isCDPHealthy(port);
|
|
112
|
+
if (healthy) {
|
|
113
|
+
const ws = await getWsEndpoint(port);
|
|
114
|
+
return { success: true, port, wsEndpoint: ws };
|
|
115
|
+
}
|
|
116
|
+
const sepHealthy = await isCDPHealthy(SEPARATE_PORT);
|
|
117
|
+
if (sepHealthy) {
|
|
118
|
+
const ws = await getWsEndpoint(SEPARATE_PORT);
|
|
119
|
+
return { success: true, port: SEPARATE_PORT, wsEndpoint: ws };
|
|
120
|
+
}
|
|
121
|
+
const chromePath = findChrome();
|
|
122
|
+
if (!chromePath) {
|
|
123
|
+
return { success: false, port, error: "Chrome not found. Install Chrome and try again." };
|
|
124
|
+
}
|
|
125
|
+
const usesSeparateInstance = isChromeRunning();
|
|
126
|
+
const targetPort = usesSeparateInstance ? SEPARATE_PORT : port;
|
|
127
|
+
const dataDir = usesSeparateInstance ? join(tmpdir(), "browsirai-normal") : void 0;
|
|
128
|
+
if (dataDir) {
|
|
129
|
+
mkdirSync(dataDir, { recursive: true });
|
|
130
|
+
syncCookiesToHeadless(dataDir);
|
|
131
|
+
}
|
|
132
|
+
const args = [
|
|
133
|
+
`--remote-debugging-port=${targetPort}`,
|
|
134
|
+
"--remote-allow-origins=*",
|
|
135
|
+
"--no-sandbox"
|
|
136
|
+
];
|
|
137
|
+
if (dataDir) {
|
|
138
|
+
args.push(`--user-data-dir=${dataDir}`, "--no-first-run", "--no-default-browser-check", "--disable-extensions");
|
|
139
|
+
}
|
|
140
|
+
if (headless) {
|
|
141
|
+
args.push("--headless=new");
|
|
142
|
+
}
|
|
143
|
+
const child = spawn(chromePath, args, {
|
|
144
|
+
detached: true,
|
|
145
|
+
stdio: "ignore"
|
|
146
|
+
});
|
|
147
|
+
child.unref();
|
|
148
|
+
for (let i = 0; i < 75; i++) {
|
|
149
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
150
|
+
const ok = await isCDPHealthy(targetPort);
|
|
151
|
+
if (ok) {
|
|
152
|
+
const ws = await getWsEndpoint(targetPort);
|
|
153
|
+
return { success: true, port: targetPort, wsEndpoint: ws };
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
success: false,
|
|
158
|
+
port: targetPort,
|
|
159
|
+
error: "Chrome launched but CDP port not reachable after 15s. Check if another Chrome instance is blocking the profile."
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
async function getWsEndpoint(port) {
|
|
163
|
+
return new Promise((resolve) => {
|
|
164
|
+
const req = http.get(`http://127.0.0.1:${port}/json/version`, (res) => {
|
|
165
|
+
let body = "";
|
|
166
|
+
res.on("data", (c) => {
|
|
167
|
+
body += c.toString();
|
|
168
|
+
});
|
|
169
|
+
res.on("end", () => {
|
|
170
|
+
try {
|
|
171
|
+
const data = JSON.parse(body);
|
|
172
|
+
resolve(data.webSocketDebuggerUrl);
|
|
173
|
+
} catch {
|
|
174
|
+
resolve(void 0);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
req.setTimeout(3e3, () => {
|
|
179
|
+
req.destroy();
|
|
180
|
+
resolve(void 0);
|
|
181
|
+
});
|
|
182
|
+
req.on("error", () => resolve(void 0));
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
var cookieSyncState = null;
|
|
186
|
+
function syncCookiesAndTrack(destDataDir, chromeDataDir) {
|
|
187
|
+
const dataDir = chromeDataDir ?? getDefaultChromeDataDir();
|
|
188
|
+
try {
|
|
189
|
+
const localStatePath = join(dataDir, "Local State");
|
|
190
|
+
if (!existsSync(localStatePath)) return;
|
|
191
|
+
const localState = JSON.parse(readFileSync(localStatePath, "utf-8"));
|
|
192
|
+
const profileName = localState.profile?.last_used ?? "Default";
|
|
193
|
+
const srcProfileDir = join(dataDir, profileName);
|
|
194
|
+
if (!existsSync(join(srcProfileDir, "Cookies"))) return;
|
|
195
|
+
const destProfileDir = join(destDataDir, "Default");
|
|
196
|
+
mkdirSync(destProfileDir, { recursive: true });
|
|
197
|
+
const files = readdirSync(srcProfileDir).filter((f) => f.startsWith("Cookies"));
|
|
198
|
+
for (const file of files) {
|
|
199
|
+
copyFileSync(join(srcProfileDir, file), join(destProfileDir, file));
|
|
200
|
+
}
|
|
201
|
+
const mtime = statSync(join(srcProfileDir, "Cookies")).mtimeMs;
|
|
202
|
+
cookieSyncState = { profileName, cookieMtime: mtime };
|
|
203
|
+
} catch {
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
function syncCookiesToHeadless(headlessDataDir) {
|
|
207
|
+
syncCookiesAndTrack(headlessDataDir);
|
|
208
|
+
}
|
|
209
|
+
async function connectChrome(options = {}) {
|
|
210
|
+
const targetPort = options.port ?? 9222;
|
|
211
|
+
const activePort = readDevToolsActivePort();
|
|
212
|
+
if (activePort) {
|
|
213
|
+
const healthy2 = await isCDPHealthy(activePort.port);
|
|
214
|
+
if (healthy2) {
|
|
215
|
+
return {
|
|
216
|
+
success: true,
|
|
217
|
+
port: activePort.port,
|
|
218
|
+
wsEndpoint: activePort.wsEndpoint,
|
|
219
|
+
activePortFound: true
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
const healthy = await isCDPHealthy(targetPort);
|
|
224
|
+
if (healthy) {
|
|
225
|
+
return {
|
|
226
|
+
success: true,
|
|
227
|
+
port: targetPort,
|
|
228
|
+
activePortFound: false
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
if (options.autoLaunch) {
|
|
232
|
+
const launch = await launchChromeWithDebugging(targetPort, options.headless);
|
|
233
|
+
if (launch.success) {
|
|
234
|
+
return {
|
|
235
|
+
success: true,
|
|
236
|
+
port: launch.port,
|
|
237
|
+
wsEndpoint: launch.wsEndpoint,
|
|
238
|
+
activePortFound: false
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
return {
|
|
242
|
+
success: false,
|
|
243
|
+
port: targetPort,
|
|
244
|
+
activePortFound: false,
|
|
245
|
+
error: launch.error
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
return {
|
|
249
|
+
success: false,
|
|
250
|
+
port: targetPort,
|
|
251
|
+
activePortFound: activePort !== null,
|
|
252
|
+
error: "Chrome remote debugging is not enabled. Enable it at chrome://inspect/#remote-debugging"
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// src/cdp/connection.ts
|
|
257
|
+
var TIMEOUT = 15e3;
|
|
258
|
+
var DAEMON_CONNECT_RETRIES = 20;
|
|
259
|
+
var DAEMON_CONNECT_DELAY = 300;
|
|
260
|
+
var CDPConnection = class {
|
|
261
|
+
wsUrl;
|
|
262
|
+
ws = null;
|
|
263
|
+
nextId = 1;
|
|
264
|
+
pending = /* @__PURE__ */ new Map();
|
|
265
|
+
eventHandlers = /* @__PURE__ */ new Map();
|
|
266
|
+
closed = false;
|
|
267
|
+
reconnecting = false;
|
|
268
|
+
_connected = false;
|
|
269
|
+
// Bound listener references for cleanup
|
|
270
|
+
boundOnMessage = null;
|
|
271
|
+
boundOnClose = null;
|
|
272
|
+
boundOnError = null;
|
|
273
|
+
constructor(wsUrl) {
|
|
274
|
+
this.wsUrl = wsUrl;
|
|
275
|
+
}
|
|
276
|
+
// -----------------------------------------------------------------------
|
|
277
|
+
// Public API
|
|
278
|
+
// -----------------------------------------------------------------------
|
|
279
|
+
/** Whether the underlying WebSocket is currently open. */
|
|
280
|
+
get isConnected() {
|
|
281
|
+
return this._connected;
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Open the WebSocket connection.
|
|
285
|
+
* Resolves on the `open` event; rejects on `error`.
|
|
286
|
+
*/
|
|
287
|
+
async connect() {
|
|
288
|
+
let WS = globalThis.WebSocket;
|
|
289
|
+
if (!WS) {
|
|
290
|
+
try {
|
|
291
|
+
const wsModule = await import("ws");
|
|
292
|
+
WS = wsModule.default ?? wsModule.WebSocket ?? wsModule;
|
|
293
|
+
} catch {
|
|
294
|
+
throw new Error(
|
|
295
|
+
"No WebSocket implementation found. Install the `ws` package or use Node 22+."
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
const ws = new WS(this.wsUrl);
|
|
300
|
+
this.ws = ws;
|
|
301
|
+
await new Promise((resolve, reject) => {
|
|
302
|
+
const onOpen = () => {
|
|
303
|
+
ws.removeEventListener("open", onOpen);
|
|
304
|
+
ws.removeEventListener("error", onError);
|
|
305
|
+
resolve();
|
|
306
|
+
};
|
|
307
|
+
const onError = (ev) => {
|
|
308
|
+
ws.removeEventListener("open", onOpen);
|
|
309
|
+
ws.removeEventListener("error", onError);
|
|
310
|
+
const msg = ev?.message ?? "WebSocket error";
|
|
311
|
+
reject(new Error(msg));
|
|
312
|
+
};
|
|
313
|
+
ws.addEventListener("open", onOpen);
|
|
314
|
+
ws.addEventListener("error", onError);
|
|
315
|
+
});
|
|
316
|
+
this._connected = true;
|
|
317
|
+
this.attachListeners(ws);
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Send a CDP command and await its result.
|
|
321
|
+
*
|
|
322
|
+
* @param method CDP method (e.g. `"Runtime.evaluate"`)
|
|
323
|
+
* @param params Optional method parameters
|
|
324
|
+
* @param options Optional timeout / sessionId overrides
|
|
325
|
+
*/
|
|
326
|
+
send(method, params, options) {
|
|
327
|
+
if (this.closed || !this._connected || !this.ws) {
|
|
328
|
+
return Promise.reject(
|
|
329
|
+
new Error(`Connection closed \u2014 cannot send ${method}`)
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
const id = this.nextId++;
|
|
333
|
+
const timeout = options?.timeout ?? TIMEOUT;
|
|
334
|
+
const message = { id, method };
|
|
335
|
+
if (params !== void 0) {
|
|
336
|
+
message.params = params;
|
|
337
|
+
}
|
|
338
|
+
if (options?.sessionId !== void 0) {
|
|
339
|
+
message.sessionId = options.sessionId;
|
|
340
|
+
}
|
|
341
|
+
this.ws.send(JSON.stringify(message));
|
|
342
|
+
return new Promise((resolve, reject) => {
|
|
343
|
+
const timer = setTimeout(() => {
|
|
344
|
+
this.pending.delete(id);
|
|
345
|
+
reject(new Error(`CDP command timeout: ${method} (${timeout}ms)`));
|
|
346
|
+
}, timeout);
|
|
347
|
+
this.pending.set(id, { resolve, reject, method, timer });
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Register an event handler for a CDP event or lifecycle event.
|
|
352
|
+
*
|
|
353
|
+
* CDP events are dispatched as `handler(params, { method })`.
|
|
354
|
+
* Lifecycle events: `disconnected`, `browserCrashed`, `reconnected`,
|
|
355
|
+
* `reconnectionFailed`.
|
|
356
|
+
*/
|
|
357
|
+
on(event, handler) {
|
|
358
|
+
let handlers = this.eventHandlers.get(event);
|
|
359
|
+
if (!handlers) {
|
|
360
|
+
handlers = /* @__PURE__ */ new Set();
|
|
361
|
+
this.eventHandlers.set(event, handlers);
|
|
362
|
+
}
|
|
363
|
+
handlers.add(handler);
|
|
364
|
+
}
|
|
365
|
+
/** Remove a previously registered event handler. */
|
|
366
|
+
off(event, handler) {
|
|
367
|
+
this.eventHandlers.get(event)?.delete(handler);
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Close the connection. Suppresses reconnection.
|
|
371
|
+
* Safe to call multiple times.
|
|
372
|
+
*/
|
|
373
|
+
close() {
|
|
374
|
+
this.closed = true;
|
|
375
|
+
this._connected = false;
|
|
376
|
+
this.rejectAllPending(new Error("Connection closed"));
|
|
377
|
+
if (this.ws) {
|
|
378
|
+
this.detachListeners(this.ws);
|
|
379
|
+
try {
|
|
380
|
+
this.ws.close();
|
|
381
|
+
} catch {
|
|
382
|
+
}
|
|
383
|
+
this.ws = null;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
// -----------------------------------------------------------------------
|
|
387
|
+
// Private
|
|
388
|
+
// -----------------------------------------------------------------------
|
|
389
|
+
/** Attach message / close / error listeners to the WebSocket. */
|
|
390
|
+
attachListeners(ws) {
|
|
391
|
+
this.boundOnMessage = (event) => {
|
|
392
|
+
const data = event?.data;
|
|
393
|
+
if (typeof data !== "string") return;
|
|
394
|
+
let msg;
|
|
395
|
+
try {
|
|
396
|
+
msg = JSON.parse(data);
|
|
397
|
+
} catch {
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
if (msg.id !== void 0) {
|
|
401
|
+
const entry = this.pending.get(msg.id);
|
|
402
|
+
if (entry) {
|
|
403
|
+
this.pending.delete(msg.id);
|
|
404
|
+
clearTimeout(entry.timer);
|
|
405
|
+
if (msg.error) {
|
|
406
|
+
entry.reject(new Error(msg.error.message));
|
|
407
|
+
} else {
|
|
408
|
+
entry.resolve(msg.result);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
if (msg.method) {
|
|
414
|
+
this.emit(msg.method, msg.params ?? {}, { method: msg.method });
|
|
415
|
+
}
|
|
416
|
+
};
|
|
417
|
+
this.boundOnClose = (event) => {
|
|
418
|
+
const code = event?.code ?? 1006;
|
|
419
|
+
this._connected = false;
|
|
420
|
+
this.rejectAllPending(new Error("WebSocket disconnected \u2014 connection closed"));
|
|
421
|
+
if (code !== 1e3) {
|
|
422
|
+
this.emit("browserCrashed");
|
|
423
|
+
}
|
|
424
|
+
this.emit("disconnected");
|
|
425
|
+
if (!this.closed && code !== 1e3) {
|
|
426
|
+
this.attemptReconnection().catch(() => {
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
};
|
|
430
|
+
this.boundOnError = () => {
|
|
431
|
+
};
|
|
432
|
+
ws.addEventListener("message", this.boundOnMessage);
|
|
433
|
+
ws.addEventListener("close", this.boundOnClose);
|
|
434
|
+
ws.addEventListener("error", this.boundOnError);
|
|
435
|
+
}
|
|
436
|
+
/** Detach WebSocket listeners. */
|
|
437
|
+
detachListeners(ws) {
|
|
438
|
+
if (this.boundOnMessage) ws.removeEventListener("message", this.boundOnMessage);
|
|
439
|
+
if (this.boundOnClose) ws.removeEventListener("close", this.boundOnClose);
|
|
440
|
+
if (this.boundOnError) ws.removeEventListener("error", this.boundOnError);
|
|
441
|
+
}
|
|
442
|
+
/** Reject every pending command. */
|
|
443
|
+
rejectAllPending(error) {
|
|
444
|
+
for (const [id, entry] of this.pending) {
|
|
445
|
+
clearTimeout(entry.timer);
|
|
446
|
+
entry.reject(error);
|
|
447
|
+
this.pending.delete(id);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
/** Dispatch an event to all registered handlers. */
|
|
451
|
+
emit(event, ...args) {
|
|
452
|
+
const handlers = this.eventHandlers.get(event);
|
|
453
|
+
if (!handlers) return;
|
|
454
|
+
for (const handler of handlers) {
|
|
455
|
+
try {
|
|
456
|
+
handler(...args);
|
|
457
|
+
} catch {
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
/** Attempt reconnection with retries after abnormal close. */
|
|
462
|
+
async attemptReconnection() {
|
|
463
|
+
if (this.reconnecting || this.closed) return;
|
|
464
|
+
this.reconnecting = true;
|
|
465
|
+
for (let attempt = 0; attempt < DAEMON_CONNECT_RETRIES; attempt++) {
|
|
466
|
+
if (this.closed) {
|
|
467
|
+
this.reconnecting = false;
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
await this.delay(DAEMON_CONNECT_DELAY);
|
|
471
|
+
if (this.closed) {
|
|
472
|
+
this.reconnecting = false;
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
try {
|
|
476
|
+
await this.connect();
|
|
477
|
+
this.reconnecting = false;
|
|
478
|
+
this.emit("reconnected");
|
|
479
|
+
return;
|
|
480
|
+
} catch {
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
this.reconnecting = false;
|
|
484
|
+
this.emit("reconnectionFailed");
|
|
485
|
+
}
|
|
486
|
+
/** Promise-based delay. */
|
|
487
|
+
delay(ms) {
|
|
488
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
489
|
+
}
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
// src/cli/run.ts
|
|
493
|
+
function parseFlags(args) {
|
|
494
|
+
const flags = {};
|
|
495
|
+
let positionalIndex = 0;
|
|
496
|
+
for (let i = 0; i < args.length; i++) {
|
|
497
|
+
const arg = args[i];
|
|
498
|
+
if (arg.startsWith("--")) {
|
|
499
|
+
const eqIdx = arg.indexOf("=");
|
|
500
|
+
if (eqIdx !== -1) {
|
|
501
|
+
const key = arg.slice(2, eqIdx);
|
|
502
|
+
const value = arg.slice(eqIdx + 1);
|
|
503
|
+
flags[key] = value;
|
|
504
|
+
} else {
|
|
505
|
+
const key = arg.slice(2);
|
|
506
|
+
const next = args[i + 1];
|
|
507
|
+
if (next && !next.startsWith("-")) {
|
|
508
|
+
flags[key] = next;
|
|
509
|
+
i++;
|
|
510
|
+
} else {
|
|
511
|
+
flags[key] = "true";
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
} else if (arg.startsWith("-") && arg.length > 1 && !/^-\d/.test(arg)) {
|
|
515
|
+
const chars = arg.slice(1);
|
|
516
|
+
if (chars.length === 1) {
|
|
517
|
+
const next = args[i + 1];
|
|
518
|
+
if (next && !next.startsWith("-")) {
|
|
519
|
+
flags[chars] = next;
|
|
520
|
+
i++;
|
|
521
|
+
} else {
|
|
522
|
+
flags[chars] = "true";
|
|
523
|
+
}
|
|
524
|
+
} else {
|
|
525
|
+
for (const ch of chars) {
|
|
526
|
+
flags[ch] = "true";
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
} else {
|
|
530
|
+
flags[`_${positionalIndex}`] = arg;
|
|
531
|
+
positionalIndex++;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
return flags;
|
|
535
|
+
}
|
|
536
|
+
function printResult(data) {
|
|
537
|
+
if (data === void 0 || data === null) return;
|
|
538
|
+
if (typeof data === "string") {
|
|
539
|
+
console.log(data);
|
|
540
|
+
} else if (typeof data === "object") {
|
|
541
|
+
console.log(JSON.stringify(data, null, 2));
|
|
542
|
+
} else {
|
|
543
|
+
console.log(String(data));
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
async function loadCommands() {
|
|
547
|
+
const categories = [];
|
|
548
|
+
const imports = [
|
|
549
|
+
{ name: "Navigation", path: "./commands/nav.js", key: "navCommands" },
|
|
550
|
+
{ name: "Observation", path: "./commands/obs.js", key: "obsCommands" },
|
|
551
|
+
{ name: "Actions", path: "./commands/act.js", key: "actCommands" },
|
|
552
|
+
{ name: "Network", path: "./commands/net.js", key: "netCommands" }
|
|
553
|
+
];
|
|
554
|
+
const base = new URL(".", import.meta.url);
|
|
555
|
+
for (const entry of imports) {
|
|
556
|
+
try {
|
|
557
|
+
const url = new URL(entry.path, base).href;
|
|
558
|
+
const mod = await import(url);
|
|
559
|
+
const commands = mod[entry.key];
|
|
560
|
+
if (commands && Array.isArray(commands) && commands.length > 0) {
|
|
561
|
+
categories.push({ name: entry.name, commands });
|
|
562
|
+
}
|
|
563
|
+
} catch {
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
return categories;
|
|
567
|
+
}
|
|
568
|
+
function buildRegistry(categories) {
|
|
569
|
+
const registry = /* @__PURE__ */ new Map();
|
|
570
|
+
for (const cat of categories) {
|
|
571
|
+
for (const cmd of cat.commands) {
|
|
572
|
+
registry.set(cmd.name, cmd);
|
|
573
|
+
if (cmd.aliases) {
|
|
574
|
+
for (const alias of cmd.aliases) {
|
|
575
|
+
registry.set(alias, cmd);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
return registry;
|
|
581
|
+
}
|
|
582
|
+
function printHelp(categories) {
|
|
583
|
+
console.log();
|
|
584
|
+
console.log(pc.bold("browsirai") + " \u2014 Browser automation from the terminal");
|
|
585
|
+
console.log();
|
|
586
|
+
console.log(pc.dim("Usage:") + " browsirai <command> [args...] [--flags]");
|
|
587
|
+
console.log();
|
|
588
|
+
if (categories.length === 0) {
|
|
589
|
+
console.log(
|
|
590
|
+
pc.yellow(" No commands available yet. Command modules have not been installed.")
|
|
591
|
+
);
|
|
592
|
+
console.log();
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
for (const cat of categories) {
|
|
596
|
+
console.log(pc.cyan(pc.bold(` ${cat.name}`)));
|
|
597
|
+
for (const cmd of cat.commands) {
|
|
598
|
+
const aliasStr = cmd.aliases?.length ? pc.dim(` (${cmd.aliases.join(", ")})`) : "";
|
|
599
|
+
const name = pc.green(cmd.name.padEnd(20));
|
|
600
|
+
console.log(` ${name} ${pc.dim(cmd.description)}${aliasStr}`);
|
|
601
|
+
}
|
|
602
|
+
console.log();
|
|
603
|
+
}
|
|
604
|
+
console.log(pc.dim(" Examples:"));
|
|
605
|
+
console.log(pc.dim(" browsirai open example.com"));
|
|
606
|
+
console.log(pc.dim(" browsirai snapshot -i"));
|
|
607
|
+
console.log(pc.dim(" browsirai click @e5"));
|
|
608
|
+
console.log(pc.dim(' browsirai fill @e2 "hello world"'));
|
|
609
|
+
console.log(pc.dim(" browsirai press Enter"));
|
|
610
|
+
console.log(pc.dim(' browsirai eval "document.title"'));
|
|
611
|
+
console.log();
|
|
612
|
+
}
|
|
613
|
+
async function connectCDP() {
|
|
614
|
+
const result = await connectChrome({ autoLaunch: true });
|
|
615
|
+
if (!result.success) {
|
|
616
|
+
const msg = result.error ?? "Could not connect to Chrome via CDP.";
|
|
617
|
+
throw new Error(msg);
|
|
618
|
+
}
|
|
619
|
+
const wsUrl = result.wsEndpoint ?? `ws://127.0.0.1:${result.port}/devtools/browser`;
|
|
620
|
+
const browser = new CDPConnection(wsUrl);
|
|
621
|
+
await browser.connect();
|
|
622
|
+
const targets = await browser.send("Target.getTargets");
|
|
623
|
+
let page = targets.targetInfos.find(
|
|
624
|
+
(t) => t.type === "page" && !t.url.startsWith("chrome://")
|
|
625
|
+
) ?? targets.targetInfos.find(
|
|
626
|
+
(t) => t.type === "page"
|
|
627
|
+
);
|
|
628
|
+
if (!page) {
|
|
629
|
+
const created = await browser.send("Target.createTarget", { url: "about:blank" });
|
|
630
|
+
page = { targetId: created.targetId, type: "page", url: "about:blank" };
|
|
631
|
+
}
|
|
632
|
+
const attached = await browser.send("Target.attachToTarget", {
|
|
633
|
+
targetId: page.targetId,
|
|
634
|
+
flatten: true
|
|
635
|
+
});
|
|
636
|
+
const sessionId = attached.sessionId;
|
|
637
|
+
const session = Object.create(browser);
|
|
638
|
+
const originalSend = browser.send.bind(browser);
|
|
639
|
+
session.send = (method, params, options) => {
|
|
640
|
+
return originalSend(method, params, {
|
|
641
|
+
...options,
|
|
642
|
+
sessionId: options?.sessionId ?? sessionId
|
|
643
|
+
});
|
|
644
|
+
};
|
|
645
|
+
session.close = () => browser.close();
|
|
646
|
+
await Promise.all([
|
|
647
|
+
session.send("Page.enable"),
|
|
648
|
+
session.send("Runtime.enable")
|
|
649
|
+
]).catch(() => {
|
|
650
|
+
});
|
|
651
|
+
return session;
|
|
652
|
+
}
|
|
653
|
+
async function runCLI(args) {
|
|
654
|
+
const commandName = args[0];
|
|
655
|
+
const remainingArgs = args.slice(1);
|
|
656
|
+
const categories = await loadCommands();
|
|
657
|
+
const registry = buildRegistry(categories);
|
|
658
|
+
if (!commandName || commandName === "--help" || commandName === "-h") {
|
|
659
|
+
printHelp(categories);
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
const command = registry.get(commandName);
|
|
663
|
+
if (!command) {
|
|
664
|
+
console.error(
|
|
665
|
+
pc.red(`Unknown command: ${pc.bold(commandName)}`)
|
|
666
|
+
);
|
|
667
|
+
console.log();
|
|
668
|
+
console.log(
|
|
669
|
+
pc.dim("Run ") + pc.bold("browsirai --help") + pc.dim(" to see available commands.")
|
|
670
|
+
);
|
|
671
|
+
const similar = findSimilar(commandName, registry);
|
|
672
|
+
if (similar.length > 0) {
|
|
673
|
+
console.log();
|
|
674
|
+
console.log(pc.dim("Did you mean?"));
|
|
675
|
+
for (const s of similar) {
|
|
676
|
+
console.log(` ${pc.green(s)}`);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
console.log();
|
|
680
|
+
process.exit(1);
|
|
681
|
+
}
|
|
682
|
+
let cdp = null;
|
|
683
|
+
try {
|
|
684
|
+
cdp = await connectCDP();
|
|
685
|
+
await command.run(cdp, remainingArgs);
|
|
686
|
+
} catch (err) {
|
|
687
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
688
|
+
console.error(pc.red(`Error: ${message}`));
|
|
689
|
+
process.exit(1);
|
|
690
|
+
} finally {
|
|
691
|
+
if (cdp?.isConnected) {
|
|
692
|
+
cdp.close();
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
function findSimilar(input, registry) {
|
|
697
|
+
const names = Array.from(registry.keys());
|
|
698
|
+
return names.filter((name) => {
|
|
699
|
+
return name.includes(input) || input.includes(name) || levenshtein(input, name) <= 3;
|
|
700
|
+
}).slice(0, 3);
|
|
701
|
+
}
|
|
702
|
+
function levenshtein(a, b) {
|
|
703
|
+
const m = a.length;
|
|
704
|
+
const n = b.length;
|
|
705
|
+
const dp = Array.from(
|
|
706
|
+
{ length: m + 1 },
|
|
707
|
+
() => Array(n + 1).fill(0)
|
|
708
|
+
);
|
|
709
|
+
for (let i = 0; i <= m; i++) dp[i][0] = i;
|
|
710
|
+
for (let j = 0; j <= n; j++) dp[0][j] = j;
|
|
711
|
+
for (let i = 1; i <= m; i++) {
|
|
712
|
+
for (let j = 1; j <= n; j++) {
|
|
713
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
714
|
+
dp[i][j] = Math.min(
|
|
715
|
+
dp[i - 1][j] + 1,
|
|
716
|
+
dp[i][j - 1] + 1,
|
|
717
|
+
dp[i - 1][j - 1] + cost
|
|
718
|
+
);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
return dp[m][n];
|
|
722
|
+
}
|
|
723
|
+
export {
|
|
724
|
+
parseFlags,
|
|
725
|
+
printResult,
|
|
726
|
+
runCLI
|
|
727
|
+
};
|
|
728
|
+
//# sourceMappingURL=run.js.map
|