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
|
@@ -0,0 +1,739 @@
|
|
|
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
|
+
|
|
12
|
+
// src/cli/run.ts
|
|
13
|
+
function parseFlags(args) {
|
|
14
|
+
const flags = {};
|
|
15
|
+
let positionalIndex = 0;
|
|
16
|
+
for (let i = 0; i < args.length; i++) {
|
|
17
|
+
const arg = args[i];
|
|
18
|
+
if (arg.startsWith("--")) {
|
|
19
|
+
const eqIdx = arg.indexOf("=");
|
|
20
|
+
if (eqIdx !== -1) {
|
|
21
|
+
const key = arg.slice(2, eqIdx);
|
|
22
|
+
const value = arg.slice(eqIdx + 1);
|
|
23
|
+
flags[key] = value;
|
|
24
|
+
} else {
|
|
25
|
+
const key = arg.slice(2);
|
|
26
|
+
const next = args[i + 1];
|
|
27
|
+
if (next && !next.startsWith("-")) {
|
|
28
|
+
flags[key] = next;
|
|
29
|
+
i++;
|
|
30
|
+
} else {
|
|
31
|
+
flags[key] = "true";
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
} else if (arg.startsWith("-") && arg.length > 1 && !/^-\d/.test(arg)) {
|
|
35
|
+
const chars = arg.slice(1);
|
|
36
|
+
if (chars.length === 1) {
|
|
37
|
+
const next = args[i + 1];
|
|
38
|
+
if (next && !next.startsWith("-")) {
|
|
39
|
+
flags[chars] = next;
|
|
40
|
+
i++;
|
|
41
|
+
} else {
|
|
42
|
+
flags[chars] = "true";
|
|
43
|
+
}
|
|
44
|
+
} else {
|
|
45
|
+
for (const ch of chars) {
|
|
46
|
+
flags[ch] = "true";
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
} else {
|
|
50
|
+
flags[`_${positionalIndex}`] = arg;
|
|
51
|
+
positionalIndex++;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return flags;
|
|
55
|
+
}
|
|
56
|
+
function printResult(data) {
|
|
57
|
+
if (data === void 0 || data === null) return;
|
|
58
|
+
if (typeof data === "string") {
|
|
59
|
+
console.log(data);
|
|
60
|
+
} else if (typeof data === "object") {
|
|
61
|
+
console.log(JSON.stringify(data, null, 2));
|
|
62
|
+
} else {
|
|
63
|
+
console.log(String(data));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// src/tools/browser-navigate.ts
|
|
68
|
+
var POLL_INTERVAL_MS = 100;
|
|
69
|
+
async function browserNavigate(cdp, params) {
|
|
70
|
+
const { url, timeout = 8 } = params;
|
|
71
|
+
const timeoutMs = timeout * 1e3;
|
|
72
|
+
await cdp.send("Page.enable");
|
|
73
|
+
const result = await Promise.race([
|
|
74
|
+
performNavigation(cdp, url, params.waitUntil),
|
|
75
|
+
createTimeout(timeoutMs)
|
|
76
|
+
]);
|
|
77
|
+
return result;
|
|
78
|
+
}
|
|
79
|
+
async function performNavigation(cdp, url, waitUntil) {
|
|
80
|
+
const navResponse = await cdp.send("Page.navigate", { url });
|
|
81
|
+
if (navResponse.errorText) {
|
|
82
|
+
throw new Error(`Navigation failed: ${navResponse.errorText}`);
|
|
83
|
+
}
|
|
84
|
+
const hasCrossDocNavigation = Boolean(navResponse.loaderId);
|
|
85
|
+
if (hasCrossDocNavigation) {
|
|
86
|
+
await waitForLoadCompletion(cdp, waitUntil);
|
|
87
|
+
}
|
|
88
|
+
return getPageInfo(cdp);
|
|
89
|
+
}
|
|
90
|
+
function waitForLoadCompletion(cdp, waitUntil) {
|
|
91
|
+
const eventName = waitUntil === "domcontentloaded" ? "Page.domContentEventFired" : "Page.loadEventFired";
|
|
92
|
+
return new Promise((resolve) => {
|
|
93
|
+
let settled = false;
|
|
94
|
+
const handler = () => {
|
|
95
|
+
if (settled) return;
|
|
96
|
+
settled = true;
|
|
97
|
+
cdp.off(eventName, handler);
|
|
98
|
+
resolve();
|
|
99
|
+
};
|
|
100
|
+
cdp.on(eventName, handler);
|
|
101
|
+
const poll = async () => {
|
|
102
|
+
while (!settled) {
|
|
103
|
+
try {
|
|
104
|
+
const response = await cdp.send("Runtime.evaluate", {
|
|
105
|
+
expression: "document.readyState",
|
|
106
|
+
returnByValue: true
|
|
107
|
+
});
|
|
108
|
+
const readyState = response.result.value;
|
|
109
|
+
const isLoadingState = readyState === "loading" || readyState === "interactive";
|
|
110
|
+
if (readyState === "complete" || !isLoadingState) {
|
|
111
|
+
if (!settled) {
|
|
112
|
+
settled = true;
|
|
113
|
+
cdp.off(eventName, handler);
|
|
114
|
+
resolve();
|
|
115
|
+
}
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
} catch {
|
|
119
|
+
}
|
|
120
|
+
if (!settled) {
|
|
121
|
+
await delay(POLL_INTERVAL_MS);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
poll();
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
async function getPageInfo(cdp) {
|
|
129
|
+
const [titleResponse, urlResponse] = await Promise.all([
|
|
130
|
+
cdp.send("Runtime.evaluate", {
|
|
131
|
+
expression: "document.title"
|
|
132
|
+
}),
|
|
133
|
+
cdp.send("Runtime.evaluate", {
|
|
134
|
+
expression: "location.href"
|
|
135
|
+
})
|
|
136
|
+
]);
|
|
137
|
+
return {
|
|
138
|
+
title: titleResponse.result.value ?? "",
|
|
139
|
+
url: urlResponse.result.value ?? ""
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
function createTimeout(ms) {
|
|
143
|
+
return new Promise((_resolve, reject) => {
|
|
144
|
+
setTimeout(() => {
|
|
145
|
+
reject(new Error(`Navigation timeout after ${ms}ms`));
|
|
146
|
+
}, ms);
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
function delay(ms) {
|
|
150
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// src/tools/browser-navigate-back.ts
|
|
154
|
+
async function browserNavigateBack(cdp, params) {
|
|
155
|
+
const direction = params.direction ?? "back";
|
|
156
|
+
const history = await cdp.send("Page.getNavigationHistory");
|
|
157
|
+
const targetIndex = direction === "back" ? history.currentIndex - 1 : history.currentIndex + 1;
|
|
158
|
+
if (targetIndex < 0 || targetIndex >= history.entries.length) {
|
|
159
|
+
return { success: false, url: history.entries[history.currentIndex]?.url };
|
|
160
|
+
}
|
|
161
|
+
const entry = history.entries[targetIndex];
|
|
162
|
+
await cdp.send("Page.navigateToHistoryEntry", { entryId: entry.id });
|
|
163
|
+
return { success: true, url: entry.url };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// src/tools/browser-scroll.ts
|
|
167
|
+
var DEFAULT_SCROLL_AMOUNT = 300;
|
|
168
|
+
async function resolveSelector(cdp, selector) {
|
|
169
|
+
const evalResult = await cdp.send("Runtime.evaluate", {
|
|
170
|
+
expression: `document.querySelector(${JSON.stringify(selector)})`,
|
|
171
|
+
returnByValue: false
|
|
172
|
+
});
|
|
173
|
+
if (evalResult.result.objectId && evalResult.result.subtype !== "null") {
|
|
174
|
+
return { objectId: evalResult.result.objectId };
|
|
175
|
+
}
|
|
176
|
+
if (evalResult.result.subtype === "null" || evalResult.result.value === null) {
|
|
177
|
+
throw new Error(`Element not found: ${selector}`);
|
|
178
|
+
}
|
|
179
|
+
try {
|
|
180
|
+
const resolveResponse = await cdp.send("DOM.resolveNode", {
|
|
181
|
+
backendNodeId: void 0
|
|
182
|
+
});
|
|
183
|
+
if (resolveResponse.object?.objectId) {
|
|
184
|
+
return { objectId: resolveResponse.object.objectId };
|
|
185
|
+
}
|
|
186
|
+
} catch {
|
|
187
|
+
}
|
|
188
|
+
throw new Error(`Could not find element: ${selector}`);
|
|
189
|
+
}
|
|
190
|
+
async function browserScroll(cdp, params) {
|
|
191
|
+
const { direction, selector } = params;
|
|
192
|
+
const amount = params.amount ?? DEFAULT_SCROLL_AMOUNT;
|
|
193
|
+
if (selector && !direction) {
|
|
194
|
+
const { objectId } = await resolveSelector(cdp, selector);
|
|
195
|
+
await cdp.send("Runtime.callFunctionOn", {
|
|
196
|
+
objectId,
|
|
197
|
+
functionDeclaration: `function() { this.scrollIntoView({ behavior: "smooth", block: "center" }); }`,
|
|
198
|
+
returnByValue: true
|
|
199
|
+
});
|
|
200
|
+
return { success: true };
|
|
201
|
+
}
|
|
202
|
+
if (selector && direction) {
|
|
203
|
+
const { objectId } = await resolveSelector(cdp, selector);
|
|
204
|
+
const scrollX2 = direction === "left" ? -amount : direction === "right" ? amount : 0;
|
|
205
|
+
const scrollY2 = direction === "up" ? -amount : direction === "down" ? amount : 0;
|
|
206
|
+
await cdp.send("Runtime.callFunctionOn", {
|
|
207
|
+
objectId,
|
|
208
|
+
functionDeclaration: `function() { this.scrollBy(${scrollX2}, ${scrollY2}); }`,
|
|
209
|
+
returnByValue: true
|
|
210
|
+
});
|
|
211
|
+
return { success: true };
|
|
212
|
+
}
|
|
213
|
+
const scrollX = direction === "left" ? -amount : direction === "right" ? amount : 0;
|
|
214
|
+
const scrollY = direction === "up" ? -amount : direction === "down" ? amount : 0;
|
|
215
|
+
await cdp.send("Runtime.evaluate", {
|
|
216
|
+
expression: `window.scrollBy(${scrollX}, ${scrollY})`,
|
|
217
|
+
returnByValue: true
|
|
218
|
+
});
|
|
219
|
+
return { success: true };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// src/tools/browser-wait-for.ts
|
|
223
|
+
var DEFAULT_TIMEOUT_S = 30;
|
|
224
|
+
var POLL_INTERVAL_MS2 = 100;
|
|
225
|
+
function normalizeTimeoutMs(timeout) {
|
|
226
|
+
if (timeout > 60) {
|
|
227
|
+
return timeout;
|
|
228
|
+
}
|
|
229
|
+
return timeout * 1e3;
|
|
230
|
+
}
|
|
231
|
+
async function browserWaitFor(cdp, params) {
|
|
232
|
+
const timeoutMs = normalizeTimeoutMs(params.timeout ?? DEFAULT_TIMEOUT_S);
|
|
233
|
+
const start = Date.now();
|
|
234
|
+
if (params.time !== void 0) {
|
|
235
|
+
const delayMs = params.time * 1e3;
|
|
236
|
+
await delay2(delayMs);
|
|
237
|
+
return { success: true, elapsed: Date.now() - start };
|
|
238
|
+
}
|
|
239
|
+
const condition = buildCondition(params);
|
|
240
|
+
while (true) {
|
|
241
|
+
const elapsed = Date.now() - start;
|
|
242
|
+
if (elapsed >= timeoutMs) {
|
|
243
|
+
throw new Error(
|
|
244
|
+
`Timeout after ${timeoutMs}ms waiting for condition: ${describeCondition(params)}`
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
let met = false;
|
|
248
|
+
try {
|
|
249
|
+
met = await evaluateCondition(cdp, condition);
|
|
250
|
+
} catch {
|
|
251
|
+
}
|
|
252
|
+
if (met) {
|
|
253
|
+
return { success: true, elapsed: Date.now() - start };
|
|
254
|
+
}
|
|
255
|
+
await delay2(POLL_INTERVAL_MS2);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
function buildCondition(params) {
|
|
259
|
+
if (params.url !== void 0) {
|
|
260
|
+
return { kind: "url", expression: params.url };
|
|
261
|
+
}
|
|
262
|
+
if (params.fn !== void 0) {
|
|
263
|
+
return {
|
|
264
|
+
kind: "fn",
|
|
265
|
+
expression: `Boolean(${params.fn})`
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
if (params.selector !== void 0 && params.state === "hidden") {
|
|
269
|
+
return {
|
|
270
|
+
kind: "selectorHidden",
|
|
271
|
+
expression: buildVisibilityCheck(params.selector)
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
if (params.selector !== void 0 && params.visible) {
|
|
275
|
+
return {
|
|
276
|
+
kind: "selectorVisible",
|
|
277
|
+
expression: buildVisibilityCheck(params.selector)
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
if (params.selector !== void 0) {
|
|
281
|
+
return {
|
|
282
|
+
kind: "selector",
|
|
283
|
+
expression: `document.querySelector(${JSON.stringify(params.selector)})`
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
if (params.text !== void 0) {
|
|
287
|
+
return {
|
|
288
|
+
kind: "text",
|
|
289
|
+
expression: `document.body && document.body.innerText.includes(${JSON.stringify(params.text)})`
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
if (params.textGone !== void 0) {
|
|
293
|
+
return {
|
|
294
|
+
kind: "textGone",
|
|
295
|
+
expression: `document.body && !document.body.innerText.includes(${JSON.stringify(params.textGone)})`
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
if (params.networkIdle) {
|
|
299
|
+
return {
|
|
300
|
+
kind: "networkIdle",
|
|
301
|
+
expression: "true"
|
|
302
|
+
// Simplified: check via Runtime.evaluate
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
if (params.loadState !== void 0) {
|
|
306
|
+
return {
|
|
307
|
+
kind: "loadState",
|
|
308
|
+
expression: params.loadState
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
if (params.load) {
|
|
312
|
+
return {
|
|
313
|
+
kind: "load",
|
|
314
|
+
expression: "document.readyState"
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
throw new Error("browserWaitFor: no wait condition specified");
|
|
318
|
+
}
|
|
319
|
+
function buildVisibilityCheck(selector) {
|
|
320
|
+
const sel = JSON.stringify(selector);
|
|
321
|
+
return `(function() {
|
|
322
|
+
var el = document.querySelector(${sel});
|
|
323
|
+
if (!el) return false;
|
|
324
|
+
var style = window.getComputedStyle(el);
|
|
325
|
+
return style.display !== 'none' && style.visibility !== 'hidden' && el.offsetParent !== null;
|
|
326
|
+
})()`;
|
|
327
|
+
}
|
|
328
|
+
async function evaluateCondition(cdp, condition) {
|
|
329
|
+
if (condition.kind === "url") {
|
|
330
|
+
return evaluateUrlCondition(cdp, condition.expression);
|
|
331
|
+
}
|
|
332
|
+
if (condition.kind === "load") {
|
|
333
|
+
return evaluateLoadCondition(cdp, condition.expression);
|
|
334
|
+
}
|
|
335
|
+
if (condition.kind === "loadState") {
|
|
336
|
+
return evaluateLoadStateCondition(cdp, condition.expression);
|
|
337
|
+
}
|
|
338
|
+
if (condition.kind === "selectorHidden") {
|
|
339
|
+
const response2 = await cdp.send("Runtime.evaluate", {
|
|
340
|
+
expression: condition.expression,
|
|
341
|
+
returnByValue: true
|
|
342
|
+
});
|
|
343
|
+
return response2.result.value === false;
|
|
344
|
+
}
|
|
345
|
+
if (condition.kind === "selector") {
|
|
346
|
+
const response2 = await cdp.send("Runtime.evaluate", {
|
|
347
|
+
expression: condition.expression,
|
|
348
|
+
returnByValue: true
|
|
349
|
+
});
|
|
350
|
+
return response2.result.value !== null && response2.result.subtype !== "null";
|
|
351
|
+
}
|
|
352
|
+
const response = await cdp.send("Runtime.evaluate", {
|
|
353
|
+
expression: condition.expression,
|
|
354
|
+
returnByValue: true
|
|
355
|
+
});
|
|
356
|
+
return response.result.value === true;
|
|
357
|
+
}
|
|
358
|
+
async function evaluateLoadCondition(cdp, expression) {
|
|
359
|
+
const response = await cdp.send("Runtime.evaluate", {
|
|
360
|
+
expression,
|
|
361
|
+
returnByValue: true
|
|
362
|
+
});
|
|
363
|
+
if (response.result.type === "string") {
|
|
364
|
+
return response.result.value === "complete";
|
|
365
|
+
}
|
|
366
|
+
return response.result.value === true;
|
|
367
|
+
}
|
|
368
|
+
async function evaluateLoadStateCondition(cdp, targetState) {
|
|
369
|
+
const response = await cdp.send("Runtime.evaluate", {
|
|
370
|
+
expression: "document.readyState",
|
|
371
|
+
returnByValue: true
|
|
372
|
+
});
|
|
373
|
+
const current = response.result.value;
|
|
374
|
+
if (targetState === "complete") {
|
|
375
|
+
return current === "complete";
|
|
376
|
+
}
|
|
377
|
+
if (targetState === "interactive") {
|
|
378
|
+
return current === "interactive" || current === "complete";
|
|
379
|
+
}
|
|
380
|
+
return current === targetState;
|
|
381
|
+
}
|
|
382
|
+
async function evaluateUrlCondition(cdp, pattern) {
|
|
383
|
+
const response = await cdp.send("Runtime.evaluate", {
|
|
384
|
+
expression: "location.href",
|
|
385
|
+
returnByValue: true
|
|
386
|
+
});
|
|
387
|
+
const currentUrl = response.result.value;
|
|
388
|
+
return globMatch(pattern, currentUrl);
|
|
389
|
+
}
|
|
390
|
+
function globMatch(pattern, value) {
|
|
391
|
+
const regexStr = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "\0").replace(/\*/g, "[^/]*").replace(/\u0000/g, ".*").replace(/\?/g, ".");
|
|
392
|
+
const regex = new RegExp(regexStr);
|
|
393
|
+
return regex.test(value);
|
|
394
|
+
}
|
|
395
|
+
function describeCondition(params) {
|
|
396
|
+
if (params.text) return `text "${params.text}" to appear`;
|
|
397
|
+
if (params.textGone) return `text "${params.textGone}" to disappear`;
|
|
398
|
+
if (params.selector && params.state === "hidden")
|
|
399
|
+
return `selector "${params.selector}" to become hidden`;
|
|
400
|
+
if (params.selector && params.visible)
|
|
401
|
+
return `selector "${params.selector}" to become visible`;
|
|
402
|
+
if (params.selector) return `selector "${params.selector}" to appear`;
|
|
403
|
+
if (params.url) return `URL matching "${params.url}"`;
|
|
404
|
+
if (params.fn) return `JS condition: ${params.fn}`;
|
|
405
|
+
if (params.loadState) return `document.readyState === "${params.loadState}"`;
|
|
406
|
+
if (params.networkIdle) return "network idle";
|
|
407
|
+
if (params.load) return "page load complete";
|
|
408
|
+
return "unknown condition";
|
|
409
|
+
}
|
|
410
|
+
function delay2(ms) {
|
|
411
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// src/tools/browser-close.ts
|
|
415
|
+
async function browserClose(cdp, params) {
|
|
416
|
+
let closedCount = 0;
|
|
417
|
+
if (params.closeAll) {
|
|
418
|
+
const targetsResponse = await cdp.send("Target.getTargets");
|
|
419
|
+
const pageTargets = targetsResponse.targetInfos.filter(
|
|
420
|
+
(t) => t.type === "page"
|
|
421
|
+
);
|
|
422
|
+
for (const target of pageTargets) {
|
|
423
|
+
try {
|
|
424
|
+
await cdp.send("Target.closeTarget", {
|
|
425
|
+
targetId: target.targetId
|
|
426
|
+
});
|
|
427
|
+
closedCount++;
|
|
428
|
+
} catch {
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
} else if (params.targetId) {
|
|
432
|
+
await cdp.send("Target.closeTarget", {
|
|
433
|
+
targetId: params.targetId
|
|
434
|
+
});
|
|
435
|
+
closedCount = 1;
|
|
436
|
+
} else {
|
|
437
|
+
const targetsResponse = await cdp.send("Target.getTargets");
|
|
438
|
+
const pageTargets = targetsResponse.targetInfos.filter(
|
|
439
|
+
(t) => t.type === "page"
|
|
440
|
+
);
|
|
441
|
+
if (pageTargets.length === 0) {
|
|
442
|
+
throw new Error("No page targets found to close");
|
|
443
|
+
}
|
|
444
|
+
const activeTarget = pageTargets.find((t) => t.attached) ?? pageTargets[0];
|
|
445
|
+
await cdp.send("Target.closeTarget", {
|
|
446
|
+
targetId: activeTarget.targetId
|
|
447
|
+
});
|
|
448
|
+
closedCount = 1;
|
|
449
|
+
}
|
|
450
|
+
return {
|
|
451
|
+
success: true,
|
|
452
|
+
closedTargets: closedCount
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// src/tools/browser-tabs.ts
|
|
457
|
+
function globToRegExp(pattern) {
|
|
458
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
459
|
+
const regexStr = escaped.replace(/\*/g, ".*");
|
|
460
|
+
return new RegExp(`^${regexStr}$`, "i");
|
|
461
|
+
}
|
|
462
|
+
async function browserTabs(cdp, params = {}) {
|
|
463
|
+
const response = await cdp.send("Target.getTargets");
|
|
464
|
+
let tabs2 = response.targetInfos.filter(
|
|
465
|
+
(target) => target.type === "page"
|
|
466
|
+
);
|
|
467
|
+
if (params.filter) {
|
|
468
|
+
const regex = globToRegExp(params.filter);
|
|
469
|
+
tabs2 = tabs2.filter((target) => regex.test(target.url));
|
|
470
|
+
}
|
|
471
|
+
return {
|
|
472
|
+
tabs: tabs2.map((target) => ({
|
|
473
|
+
id: target.targetId,
|
|
474
|
+
title: target.title,
|
|
475
|
+
url: target.url
|
|
476
|
+
}))
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// src/tools/browser-resize.ts
|
|
481
|
+
var PRESETS = {
|
|
482
|
+
mobile: { width: 375, height: 667 },
|
|
483
|
+
tablet: { width: 768, height: 1024 },
|
|
484
|
+
desktop: { width: 1280, height: 720 },
|
|
485
|
+
fullhd: { width: 1920, height: 1080 }
|
|
486
|
+
};
|
|
487
|
+
async function browserResize(cdp, params) {
|
|
488
|
+
let width;
|
|
489
|
+
let height;
|
|
490
|
+
if (params.preset?.toLowerCase() === "reset") {
|
|
491
|
+
await cdp.send("Emulation.clearDeviceMetricsOverride");
|
|
492
|
+
return { success: true, width: 0, height: 0 };
|
|
493
|
+
}
|
|
494
|
+
if (params.preset) {
|
|
495
|
+
const preset = PRESETS[params.preset.toLowerCase()];
|
|
496
|
+
if (!preset) {
|
|
497
|
+
throw new Error(
|
|
498
|
+
`Unknown preset "${params.preset}". Available: ${Object.keys(PRESETS).join(", ")}, reset`
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
width = preset.width;
|
|
502
|
+
height = preset.height;
|
|
503
|
+
} else {
|
|
504
|
+
width = params.width ?? 1280;
|
|
505
|
+
height = params.height ?? 720;
|
|
506
|
+
}
|
|
507
|
+
if (params.width !== void 0) width = params.width;
|
|
508
|
+
if (params.height !== void 0) height = params.height;
|
|
509
|
+
const deviceScaleFactor = params.deviceScaleFactor ?? 0;
|
|
510
|
+
const mobile = width < 768;
|
|
511
|
+
await cdp.send("Emulation.setDeviceMetricsOverride", {
|
|
512
|
+
width,
|
|
513
|
+
height,
|
|
514
|
+
deviceScaleFactor,
|
|
515
|
+
mobile
|
|
516
|
+
});
|
|
517
|
+
return {
|
|
518
|
+
success: true,
|
|
519
|
+
width,
|
|
520
|
+
height
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// src/cli/commands/nav.ts
|
|
525
|
+
var navigate = {
|
|
526
|
+
name: "navigate",
|
|
527
|
+
aliases: ["open", "goto"],
|
|
528
|
+
description: "Navigate the browser to a URL",
|
|
529
|
+
usage: "browsirai open <url> [--waitUntil=load]",
|
|
530
|
+
async run(cdp, args) {
|
|
531
|
+
const flags = parseFlags(args);
|
|
532
|
+
const url = flags._0 ?? flags.url;
|
|
533
|
+
if (!url) {
|
|
534
|
+
console.error("Usage: browsirai open <url> [--waitUntil=load]");
|
|
535
|
+
console.error(" Provide a URL as the first argument or via --url=...");
|
|
536
|
+
process.exit(1);
|
|
537
|
+
}
|
|
538
|
+
const fullUrl = /^https?:\/\//i.test(url) ? url : `https://${url}`;
|
|
539
|
+
try {
|
|
540
|
+
const result = await browserNavigate(cdp, {
|
|
541
|
+
url: fullUrl,
|
|
542
|
+
waitUntil: flags.waitUntil,
|
|
543
|
+
timeout: flags.timeout ? Number(flags.timeout) : void 0
|
|
544
|
+
});
|
|
545
|
+
console.log(`Navigated to ${result.url}`);
|
|
546
|
+
if (result.title) {
|
|
547
|
+
console.log(` Title: ${result.title}`);
|
|
548
|
+
}
|
|
549
|
+
} catch (err) {
|
|
550
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
551
|
+
console.error(`Navigate failed: ${msg}`);
|
|
552
|
+
process.exit(1);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
};
|
|
556
|
+
var back = {
|
|
557
|
+
name: "back",
|
|
558
|
+
description: "Navigate back or forward in browser history",
|
|
559
|
+
usage: "browsirai back [--direction=back]",
|
|
560
|
+
async run(cdp, args) {
|
|
561
|
+
const flags = parseFlags(args);
|
|
562
|
+
const direction = flags._0 ?? flags.direction ?? "back";
|
|
563
|
+
try {
|
|
564
|
+
const result = await browserNavigateBack(cdp, { direction });
|
|
565
|
+
if (result.success) {
|
|
566
|
+
console.log(`Navigated ${direction}`);
|
|
567
|
+
if (result.url) {
|
|
568
|
+
console.log(` URL: ${result.url}`);
|
|
569
|
+
}
|
|
570
|
+
} else {
|
|
571
|
+
console.log(`Cannot navigate ${direction} \u2014 no history entry`);
|
|
572
|
+
if (result.url) {
|
|
573
|
+
console.log(` Current URL: ${result.url}`);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
} catch (err) {
|
|
577
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
578
|
+
console.error(`Navigate ${direction} failed: ${msg}`);
|
|
579
|
+
process.exit(1);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
};
|
|
583
|
+
var scroll = {
|
|
584
|
+
name: "scroll",
|
|
585
|
+
description: "Scroll the page in a direction",
|
|
586
|
+
usage: "browsirai scroll <direction> [--pixels=300] [--selector=...]",
|
|
587
|
+
async run(cdp, args) {
|
|
588
|
+
const flags = parseFlags(args);
|
|
589
|
+
const direction = flags._0 ?? flags.direction ?? "down";
|
|
590
|
+
const amount = flags.pixels ? Number(flags.pixels) : flags.amount ? Number(flags.amount) : 300;
|
|
591
|
+
const selector = flags.selector;
|
|
592
|
+
try {
|
|
593
|
+
await browserScroll(cdp, {
|
|
594
|
+
direction,
|
|
595
|
+
amount,
|
|
596
|
+
selector
|
|
597
|
+
});
|
|
598
|
+
console.log(`Scrolled ${direction} ${amount}px`);
|
|
599
|
+
} catch (err) {
|
|
600
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
601
|
+
console.error(`Scroll failed: ${msg}`);
|
|
602
|
+
process.exit(1);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
};
|
|
606
|
+
var wait = {
|
|
607
|
+
name: "wait",
|
|
608
|
+
description: "Wait for a condition on the page",
|
|
609
|
+
usage: "browsirai wait [--text=...] [--selector=...] [--url=...] [--fn=...] [--time=N] [--timeout=30]",
|
|
610
|
+
async run(cdp, args) {
|
|
611
|
+
const flags = parseFlags(args);
|
|
612
|
+
const timeout = flags.timeout ? Number(flags.timeout) : void 0;
|
|
613
|
+
const params = {};
|
|
614
|
+
if (flags.text !== void 0) params.text = flags.text;
|
|
615
|
+
if (flags.selector !== void 0) params.selector = flags.selector;
|
|
616
|
+
if (flags.url !== void 0) params.url = flags.url;
|
|
617
|
+
if (flags.fn !== void 0) params.fn = flags.fn;
|
|
618
|
+
if (flags.time !== void 0) params.time = Number(flags.time);
|
|
619
|
+
if (flags.visible !== void 0) params.visible = flags.visible === "true";
|
|
620
|
+
if (flags.state !== void 0) params.state = flags.state;
|
|
621
|
+
if (flags.networkIdle !== void 0) params.networkIdle = flags.networkIdle === "true";
|
|
622
|
+
if (flags.load !== void 0) params.load = flags.load === "true";
|
|
623
|
+
if (timeout !== void 0) params.timeout = timeout;
|
|
624
|
+
const hasCondition = Object.keys(params).some((k) => k !== "timeout");
|
|
625
|
+
if (!hasCondition && flags._0) {
|
|
626
|
+
params.text = flags._0;
|
|
627
|
+
}
|
|
628
|
+
if (!hasCondition && !flags._0) {
|
|
629
|
+
console.error("Usage: browsirai wait [--text=...] [--selector=...] [--url=...] [--fn=...] [--time=N]");
|
|
630
|
+
console.error(" Provide at least one condition to wait for.");
|
|
631
|
+
process.exit(1);
|
|
632
|
+
}
|
|
633
|
+
try {
|
|
634
|
+
const result = await browserWaitFor(cdp, params);
|
|
635
|
+
console.log(`Condition met (${result.elapsed}ms)`);
|
|
636
|
+
} catch (err) {
|
|
637
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
638
|
+
console.error(`Wait failed: ${msg}`);
|
|
639
|
+
process.exit(1);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
};
|
|
643
|
+
var tabs = {
|
|
644
|
+
name: "tab",
|
|
645
|
+
aliases: ["tabs"],
|
|
646
|
+
description: "List open browser tabs",
|
|
647
|
+
usage: "browsirai tab [--filter=*github*]",
|
|
648
|
+
async run(cdp, args) {
|
|
649
|
+
const flags = parseFlags(args);
|
|
650
|
+
const filter = flags._0 ?? flags.filter;
|
|
651
|
+
try {
|
|
652
|
+
const result = await browserTabs(cdp, { filter });
|
|
653
|
+
if (result.tabs.length === 0) {
|
|
654
|
+
console.log("No tabs found");
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
const lines = result.tabs.map(
|
|
658
|
+
(t) => `[${t.id}] ${t.title}
|
|
659
|
+
${t.url}`
|
|
660
|
+
);
|
|
661
|
+
printResult(lines.join("\n\n"));
|
|
662
|
+
} catch (err) {
|
|
663
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
664
|
+
console.error(`Tabs failed: ${msg}`);
|
|
665
|
+
process.exit(1);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
};
|
|
669
|
+
var close = {
|
|
670
|
+
name: "close",
|
|
671
|
+
description: "Close browser tab(s)",
|
|
672
|
+
usage: "browsirai close [--force] [--targetId=...] [--closeAll]",
|
|
673
|
+
async run(cdp, args) {
|
|
674
|
+
const flags = parseFlags(args);
|
|
675
|
+
try {
|
|
676
|
+
const result = await browserClose(cdp, {
|
|
677
|
+
force: flags.force === "true",
|
|
678
|
+
targetId: flags.targetId,
|
|
679
|
+
closeAll: flags.closeAll === "true"
|
|
680
|
+
});
|
|
681
|
+
if (result.success) {
|
|
682
|
+
console.log(`Closed ${result.closedTargets} tab(s)`);
|
|
683
|
+
} else {
|
|
684
|
+
console.log("Close failed \u2014 no targets matched");
|
|
685
|
+
}
|
|
686
|
+
} catch (err) {
|
|
687
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
688
|
+
console.error(`Close failed: ${msg}`);
|
|
689
|
+
process.exit(1);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
};
|
|
693
|
+
var resize = {
|
|
694
|
+
name: "resize",
|
|
695
|
+
description: "Resize the browser viewport",
|
|
696
|
+
usage: "browsirai resize <width> <height> [--preset=mobile]",
|
|
697
|
+
async run(cdp, args) {
|
|
698
|
+
const flags = parseFlags(args);
|
|
699
|
+
const preset = flags.preset;
|
|
700
|
+
const width = flags._0 ? Number(flags._0) : flags.width ? Number(flags.width) : void 0;
|
|
701
|
+
const height = flags._1 ? Number(flags._1) : flags.height ? Number(flags.height) : void 0;
|
|
702
|
+
const deviceScaleFactor = flags.deviceScaleFactor ? Number(flags.deviceScaleFactor) : void 0;
|
|
703
|
+
if (!preset && width === void 0) {
|
|
704
|
+
console.error("Usage: browsirai resize <width> <height> [--preset=mobile]");
|
|
705
|
+
console.error(" Provide dimensions or a preset (mobile, tablet, desktop, fullhd, reset).");
|
|
706
|
+
process.exit(1);
|
|
707
|
+
}
|
|
708
|
+
try {
|
|
709
|
+
const result = await browserResize(cdp, {
|
|
710
|
+
width,
|
|
711
|
+
height,
|
|
712
|
+
preset,
|
|
713
|
+
deviceScaleFactor
|
|
714
|
+
});
|
|
715
|
+
if (preset?.toLowerCase() === "reset") {
|
|
716
|
+
console.log("Viewport reset to browser defaults");
|
|
717
|
+
} else {
|
|
718
|
+
console.log(`Viewport resized to ${result.width}x${result.height}`);
|
|
719
|
+
}
|
|
720
|
+
} catch (err) {
|
|
721
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
722
|
+
console.error(`Resize failed: ${msg}`);
|
|
723
|
+
process.exit(1);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
};
|
|
727
|
+
var navCommands = [
|
|
728
|
+
navigate,
|
|
729
|
+
back,
|
|
730
|
+
scroll,
|
|
731
|
+
wait,
|
|
732
|
+
tabs,
|
|
733
|
+
close,
|
|
734
|
+
resize
|
|
735
|
+
];
|
|
736
|
+
export {
|
|
737
|
+
navCommands
|
|
738
|
+
};
|
|
739
|
+
//# sourceMappingURL=nav.js.map
|