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,1049 @@
|
|
|
1
|
+
// src/cli/commands/obs.ts
|
|
2
|
+
import { writeFileSync } from "fs";
|
|
3
|
+
|
|
4
|
+
// src/cli/run.ts
|
|
5
|
+
import pc from "picocolors";
|
|
6
|
+
|
|
7
|
+
// src/chrome-launcher.ts
|
|
8
|
+
import { execSync, spawn } from "child_process";
|
|
9
|
+
import { existsSync, readFileSync, mkdirSync, copyFileSync, readdirSync, statSync } from "fs";
|
|
10
|
+
import http from "http";
|
|
11
|
+
import { join } from "path";
|
|
12
|
+
import { homedir, tmpdir } from "os";
|
|
13
|
+
import { createConnection } from "net";
|
|
14
|
+
|
|
15
|
+
// src/cli/run.ts
|
|
16
|
+
function parseFlags(args) {
|
|
17
|
+
const flags = {};
|
|
18
|
+
let positionalIndex = 0;
|
|
19
|
+
for (let i = 0; i < args.length; i++) {
|
|
20
|
+
const arg = args[i];
|
|
21
|
+
if (arg.startsWith("--")) {
|
|
22
|
+
const eqIdx = arg.indexOf("=");
|
|
23
|
+
if (eqIdx !== -1) {
|
|
24
|
+
const key = arg.slice(2, eqIdx);
|
|
25
|
+
const value = arg.slice(eqIdx + 1);
|
|
26
|
+
flags[key] = value;
|
|
27
|
+
} else {
|
|
28
|
+
const key = arg.slice(2);
|
|
29
|
+
const next = args[i + 1];
|
|
30
|
+
if (next && !next.startsWith("-")) {
|
|
31
|
+
flags[key] = next;
|
|
32
|
+
i++;
|
|
33
|
+
} else {
|
|
34
|
+
flags[key] = "true";
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
} else if (arg.startsWith("-") && arg.length > 1 && !/^-\d/.test(arg)) {
|
|
38
|
+
const chars = arg.slice(1);
|
|
39
|
+
if (chars.length === 1) {
|
|
40
|
+
const next = args[i + 1];
|
|
41
|
+
if (next && !next.startsWith("-")) {
|
|
42
|
+
flags[chars] = next;
|
|
43
|
+
i++;
|
|
44
|
+
} else {
|
|
45
|
+
flags[chars] = "true";
|
|
46
|
+
}
|
|
47
|
+
} else {
|
|
48
|
+
for (const ch of chars) {
|
|
49
|
+
flags[ch] = "true";
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
} else {
|
|
53
|
+
flags[`_${positionalIndex}`] = arg;
|
|
54
|
+
positionalIndex++;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return flags;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// src/tools/browser-snapshot.ts
|
|
61
|
+
var INTERACTIVE_ROLES = /* @__PURE__ */ new Set([
|
|
62
|
+
"button",
|
|
63
|
+
"link",
|
|
64
|
+
"textbox",
|
|
65
|
+
"checkbox",
|
|
66
|
+
"radio",
|
|
67
|
+
"combobox",
|
|
68
|
+
"listbox",
|
|
69
|
+
"menuitem",
|
|
70
|
+
"menuitemcheckbox",
|
|
71
|
+
"menuitemradio",
|
|
72
|
+
"option",
|
|
73
|
+
"searchbox",
|
|
74
|
+
"slider",
|
|
75
|
+
"spinbutton",
|
|
76
|
+
"switch",
|
|
77
|
+
"tab",
|
|
78
|
+
"treeitem"
|
|
79
|
+
]);
|
|
80
|
+
function shouldShowAxNode(node, options) {
|
|
81
|
+
const role = node.role?.value ?? "";
|
|
82
|
+
if (role === "none") {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
if (role === "generic") {
|
|
86
|
+
const name2 = node.name?.value ?? "";
|
|
87
|
+
if (!name2) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (options?.compact && role === "InlineTextBox") {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
const name = node.name?.value ?? "";
|
|
95
|
+
const value = node.value?.value ?? null;
|
|
96
|
+
if (!name && (value === null || value === void 0 || value === "")) {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
function getChildNodeIds(node) {
|
|
102
|
+
if (node.childIds && node.childIds.length > 0) {
|
|
103
|
+
return node.childIds;
|
|
104
|
+
}
|
|
105
|
+
if (node.children && node.children.length > 0) {
|
|
106
|
+
return node.children.map((c) => c.nodeId);
|
|
107
|
+
}
|
|
108
|
+
return [];
|
|
109
|
+
}
|
|
110
|
+
function formatNodeAttributes(node) {
|
|
111
|
+
const parts = [];
|
|
112
|
+
if (node.value?.value !== void 0 && node.value.value !== "") {
|
|
113
|
+
parts.push(`value="${node.value.value}"`);
|
|
114
|
+
}
|
|
115
|
+
if (node.description?.value) {
|
|
116
|
+
parts.push(`description="${node.description.value}"`);
|
|
117
|
+
}
|
|
118
|
+
if (node.properties) {
|
|
119
|
+
for (const prop of node.properties) {
|
|
120
|
+
switch (prop.name) {
|
|
121
|
+
case "level":
|
|
122
|
+
parts.push(`level=${prop.value.value}`);
|
|
123
|
+
break;
|
|
124
|
+
case "checked": {
|
|
125
|
+
const val = prop.value.value;
|
|
126
|
+
if (val === true || val === "true" || val === "mixed") {
|
|
127
|
+
parts.push("checked");
|
|
128
|
+
}
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
case "selected":
|
|
132
|
+
if (prop.value.value === true) {
|
|
133
|
+
parts.push("selected");
|
|
134
|
+
}
|
|
135
|
+
break;
|
|
136
|
+
case "expanded":
|
|
137
|
+
if (prop.value.value === true || prop.value.value === "true") {
|
|
138
|
+
parts.push("expanded");
|
|
139
|
+
} else {
|
|
140
|
+
parts.push("collapsed");
|
|
141
|
+
}
|
|
142
|
+
break;
|
|
143
|
+
default:
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return parts.join(" ");
|
|
149
|
+
}
|
|
150
|
+
async function browserSnapshot(cdp, params) {
|
|
151
|
+
const options = {
|
|
152
|
+
interactive: params?.interactive,
|
|
153
|
+
cursor: params?.cursor,
|
|
154
|
+
compact: params?.compact,
|
|
155
|
+
depth: params?.depth,
|
|
156
|
+
selector: params?.selector
|
|
157
|
+
};
|
|
158
|
+
await cdp.send("Accessibility.enable");
|
|
159
|
+
let axNodes;
|
|
160
|
+
if (options.selector) {
|
|
161
|
+
const docResult = await cdp.send("DOM.getDocument");
|
|
162
|
+
const queryResult = await cdp.send("DOM.querySelector", {
|
|
163
|
+
nodeId: docResult.root.nodeId,
|
|
164
|
+
selector: options.selector
|
|
165
|
+
});
|
|
166
|
+
if (queryResult.nodeId === 0) {
|
|
167
|
+
return { snapshot: `No element found for selector: ${options.selector}` };
|
|
168
|
+
}
|
|
169
|
+
const partialResult = await cdp.send("Accessibility.getPartialAXTree", {
|
|
170
|
+
nodeId: queryResult.nodeId,
|
|
171
|
+
fetchRelatives: true
|
|
172
|
+
});
|
|
173
|
+
axNodes = partialResult.nodes;
|
|
174
|
+
} else {
|
|
175
|
+
const fullResult = await cdp.send("Accessibility.getFullAXTree");
|
|
176
|
+
axNodes = fullResult.nodes;
|
|
177
|
+
}
|
|
178
|
+
const totalElements = axNodes.filter(
|
|
179
|
+
(n) => n.role?.value !== "WebArea"
|
|
180
|
+
).length;
|
|
181
|
+
const nodeMap = /* @__PURE__ */ new Map();
|
|
182
|
+
for (const node of axNodes) {
|
|
183
|
+
nodeMap.set(node.nodeId, node);
|
|
184
|
+
}
|
|
185
|
+
let root;
|
|
186
|
+
for (const node of axNodes) {
|
|
187
|
+
if (!node.parentId) {
|
|
188
|
+
root = node;
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (!root) {
|
|
193
|
+
root = axNodes[0];
|
|
194
|
+
}
|
|
195
|
+
if (!root) {
|
|
196
|
+
return { snapshot: "" };
|
|
197
|
+
}
|
|
198
|
+
const lines = [];
|
|
199
|
+
let refCounter = 0;
|
|
200
|
+
const visited = /* @__PURE__ */ new Set();
|
|
201
|
+
const maxDepth = options.depth ?? 100;
|
|
202
|
+
function traverse(node, depth) {
|
|
203
|
+
if (visited.has(node.nodeId)) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
visited.add(node.nodeId);
|
|
207
|
+
if (depth > maxDepth) {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
const role = node.role?.value ?? "";
|
|
211
|
+
const isRoot = role === "WebArea";
|
|
212
|
+
const showNode = isRoot || shouldShow(node, options);
|
|
213
|
+
if (showNode && !isRoot) {
|
|
214
|
+
refCounter++;
|
|
215
|
+
const ref = node.backendDOMNodeId ? `@e${node.backendDOMNodeId}` : `@e${refCounter}`;
|
|
216
|
+
const indent = " ".repeat(depth);
|
|
217
|
+
const name = node.name?.value ?? "";
|
|
218
|
+
const attrs = formatNodeAttributes(node);
|
|
219
|
+
let line = `${indent}${ref} ${role}`;
|
|
220
|
+
if (name) {
|
|
221
|
+
line += ` "${name}"`;
|
|
222
|
+
}
|
|
223
|
+
if (attrs) {
|
|
224
|
+
line += ` ${attrs}`;
|
|
225
|
+
}
|
|
226
|
+
lines.push(line);
|
|
227
|
+
}
|
|
228
|
+
const childNodeIds = getChildNodeIds(node);
|
|
229
|
+
const nextDepth = isRoot ? depth : depth + 1;
|
|
230
|
+
for (const childId of childNodeIds) {
|
|
231
|
+
const childNode = nodeMap.get(childId);
|
|
232
|
+
if (childNode) {
|
|
233
|
+
traverse(childNode, nextDepth);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
traverse(root, 0);
|
|
238
|
+
const snapshot = lines.join("\n");
|
|
239
|
+
const result = { snapshot };
|
|
240
|
+
if (totalElements > 1e3) {
|
|
241
|
+
result.truncated = true;
|
|
242
|
+
result.totalElements = totalElements;
|
|
243
|
+
}
|
|
244
|
+
return result;
|
|
245
|
+
}
|
|
246
|
+
function shouldShow(node, options) {
|
|
247
|
+
if (!shouldShowAxNode(node, { compact: options.compact })) {
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
const role = node.role?.value ?? "";
|
|
251
|
+
if (options.interactive) {
|
|
252
|
+
if (INTERACTIVE_ROLES.has(role)) {
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
255
|
+
if (options.cursor) {
|
|
256
|
+
return hasCursorPointer(node);
|
|
257
|
+
}
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
if (options.cursor) {
|
|
261
|
+
if (hasCursorPointer(node)) {
|
|
262
|
+
return true;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return true;
|
|
266
|
+
}
|
|
267
|
+
function hasCursorPointer(node) {
|
|
268
|
+
if (!node.properties) return false;
|
|
269
|
+
return node.properties.some(
|
|
270
|
+
(p) => p.name === "cursor" && p.value.value === "pointer"
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// src/tools/browser-screenshot.ts
|
|
275
|
+
async function detectDPR(cdp) {
|
|
276
|
+
try {
|
|
277
|
+
const metrics = await cdp.send("Page.getLayoutMetrics", {});
|
|
278
|
+
if (metrics.visualViewport && metrics.cssVisualViewport) {
|
|
279
|
+
const physicalWidth = metrics.visualViewport.clientWidth;
|
|
280
|
+
const cssWidth = metrics.cssVisualViewport.clientWidth;
|
|
281
|
+
if (cssWidth > 0 && physicalWidth > 0) {
|
|
282
|
+
const dpr = physicalWidth / cssWidth;
|
|
283
|
+
if (dpr >= 1) {
|
|
284
|
+
return dpr;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
} catch {
|
|
289
|
+
}
|
|
290
|
+
try {
|
|
291
|
+
const evalResult = await cdp.send("Runtime.evaluate", {
|
|
292
|
+
expression: "window.devicePixelRatio",
|
|
293
|
+
returnByValue: true
|
|
294
|
+
});
|
|
295
|
+
if (evalResult.result.type === "number" && typeof evalResult.result.value === "number") {
|
|
296
|
+
return evalResult.result.value;
|
|
297
|
+
}
|
|
298
|
+
} catch {
|
|
299
|
+
}
|
|
300
|
+
return 1;
|
|
301
|
+
}
|
|
302
|
+
async function getElementBoxBySelector(cdp, selector) {
|
|
303
|
+
const doc = await cdp.send("DOM.getDocument", {});
|
|
304
|
+
const queryResult = await cdp.send("DOM.querySelector", {
|
|
305
|
+
nodeId: doc.root.nodeId,
|
|
306
|
+
selector
|
|
307
|
+
});
|
|
308
|
+
if (!queryResult.nodeId) {
|
|
309
|
+
throw new Error(`Element not found: ${selector}`);
|
|
310
|
+
}
|
|
311
|
+
const boxModel = await cdp.send("DOM.getBoxModel", {
|
|
312
|
+
nodeId: queryResult.nodeId
|
|
313
|
+
});
|
|
314
|
+
const content = boxModel.model.content;
|
|
315
|
+
const x = content[0];
|
|
316
|
+
const y = content[1];
|
|
317
|
+
const width = content[2] - content[0];
|
|
318
|
+
const height = content[5] - content[1];
|
|
319
|
+
return { x, y, width, height };
|
|
320
|
+
}
|
|
321
|
+
async function getElementBoxByRef(cdp, ref) {
|
|
322
|
+
const match = /^@e(\d+)$/.exec(ref);
|
|
323
|
+
if (!match) {
|
|
324
|
+
throw new Error(`Invalid ref format: ${ref}`);
|
|
325
|
+
}
|
|
326
|
+
const backendNodeId = parseInt(match[1], 10);
|
|
327
|
+
const resolved = await cdp.send("DOM.resolveNode", {
|
|
328
|
+
backendNodeId
|
|
329
|
+
});
|
|
330
|
+
const boxModel = await cdp.send("DOM.getBoxModel", {
|
|
331
|
+
backendNodeId
|
|
332
|
+
});
|
|
333
|
+
const content = boxModel.model.content;
|
|
334
|
+
const x = content[0];
|
|
335
|
+
const y = content[1];
|
|
336
|
+
const width = content[2] - content[0];
|
|
337
|
+
const height = content[5] - content[1];
|
|
338
|
+
return { x, y, width, height };
|
|
339
|
+
}
|
|
340
|
+
async function buildAnnotations(cdp) {
|
|
341
|
+
const axTree = await cdp.send("Accessibility.getFullAXTree", {}, { timeout: 1e4 });
|
|
342
|
+
const annotations = [];
|
|
343
|
+
let counter = 0;
|
|
344
|
+
for (const node of axTree.nodes) {
|
|
345
|
+
const role = node.role?.value;
|
|
346
|
+
if (role === "WebArea") {
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
counter++;
|
|
350
|
+
const ref = node.backendDOMNodeId ? `@e${node.backendDOMNodeId}` : `@e${counter}`;
|
|
351
|
+
const label = `[${counter}]`;
|
|
352
|
+
annotations.push({
|
|
353
|
+
ref,
|
|
354
|
+
label,
|
|
355
|
+
role: role ?? "unknown",
|
|
356
|
+
name: node.name?.value ?? ""
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
return annotations;
|
|
360
|
+
}
|
|
361
|
+
async function browserScreenshot(cdp, params) {
|
|
362
|
+
const format = params.format ?? "png";
|
|
363
|
+
const captureParams = {
|
|
364
|
+
format
|
|
365
|
+
};
|
|
366
|
+
if (format === "jpeg" && params.quality !== void 0) {
|
|
367
|
+
captureParams.quality = params.quality;
|
|
368
|
+
}
|
|
369
|
+
const _dpr = await detectDPR(cdp);
|
|
370
|
+
if (params.selector) {
|
|
371
|
+
const box = await getElementBoxBySelector(cdp, params.selector);
|
|
372
|
+
captureParams.clip = {
|
|
373
|
+
x: box.x,
|
|
374
|
+
y: box.y,
|
|
375
|
+
width: box.width,
|
|
376
|
+
height: box.height,
|
|
377
|
+
scale: 1
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
if (params.ref) {
|
|
381
|
+
const box = await getElementBoxByRef(cdp, params.ref);
|
|
382
|
+
captureParams.clip = {
|
|
383
|
+
x: box.x,
|
|
384
|
+
y: box.y,
|
|
385
|
+
width: box.width,
|
|
386
|
+
height: box.height,
|
|
387
|
+
scale: 1
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
if (params.fullPage) {
|
|
391
|
+
const metrics = await cdp.send("Page.getLayoutMetrics", {});
|
|
392
|
+
const contentWidth = metrics.cssContentSize?.width ?? metrics.contentSize.width;
|
|
393
|
+
const contentHeight = metrics.cssContentSize?.height ?? metrics.contentSize.height;
|
|
394
|
+
captureParams.clip = {
|
|
395
|
+
x: 0,
|
|
396
|
+
y: 0,
|
|
397
|
+
width: contentWidth,
|
|
398
|
+
height: contentHeight,
|
|
399
|
+
scale: 1
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
const screenshot = await cdp.send("Page.captureScreenshot", captureParams);
|
|
403
|
+
const result = {
|
|
404
|
+
base64: screenshot.data
|
|
405
|
+
};
|
|
406
|
+
if (params.annotate) {
|
|
407
|
+
result.annotations = await buildAnnotations(cdp);
|
|
408
|
+
}
|
|
409
|
+
return result;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// src/tools/browser-html.ts
|
|
413
|
+
async function browserHtml(cdp, params) {
|
|
414
|
+
if (!params.selector) {
|
|
415
|
+
const evalResult = await cdp.send("Runtime.evaluate", {
|
|
416
|
+
expression: "document.documentElement.outerHTML",
|
|
417
|
+
returnByValue: true
|
|
418
|
+
});
|
|
419
|
+
return { html: evalResult.result.value };
|
|
420
|
+
}
|
|
421
|
+
const doc = await cdp.send("DOM.getDocument", {});
|
|
422
|
+
const queryResult = await cdp.send("DOM.querySelector", {
|
|
423
|
+
nodeId: doc.root.nodeId,
|
|
424
|
+
selector: params.selector
|
|
425
|
+
});
|
|
426
|
+
if (!queryResult.nodeId) {
|
|
427
|
+
return {
|
|
428
|
+
html: "",
|
|
429
|
+
error: `Element not found: ${params.selector}`
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
const outerResult = await cdp.send("DOM.getOuterHTML", {
|
|
433
|
+
nodeId: queryResult.nodeId
|
|
434
|
+
});
|
|
435
|
+
return { html: outerResult.outerHTML };
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// src/tools/browser-eval.ts
|
|
439
|
+
function parseRemoteObject(obj) {
|
|
440
|
+
if (obj.type === "undefined") {
|
|
441
|
+
return void 0;
|
|
442
|
+
}
|
|
443
|
+
if (obj.type === "object" && obj.subtype === "null") {
|
|
444
|
+
return null;
|
|
445
|
+
}
|
|
446
|
+
if (obj.type === "object" && obj.subtype === "node") {
|
|
447
|
+
return obj.description ?? `[${obj.className ?? "Node"}]`;
|
|
448
|
+
}
|
|
449
|
+
if (obj.value !== void 0) {
|
|
450
|
+
return obj.value;
|
|
451
|
+
}
|
|
452
|
+
if (obj.description) {
|
|
453
|
+
return obj.description;
|
|
454
|
+
}
|
|
455
|
+
return void 0;
|
|
456
|
+
}
|
|
457
|
+
function formatException(details) {
|
|
458
|
+
if (details.exception?.description) {
|
|
459
|
+
return details.exception.description;
|
|
460
|
+
}
|
|
461
|
+
if (details.exception?.className) {
|
|
462
|
+
return `${details.exception.className}: ${details.text}`;
|
|
463
|
+
}
|
|
464
|
+
return details.text;
|
|
465
|
+
}
|
|
466
|
+
async function browserEval(cdp, params) {
|
|
467
|
+
let expression = params.expression;
|
|
468
|
+
if (params.base64) {
|
|
469
|
+
expression = atob(expression);
|
|
470
|
+
}
|
|
471
|
+
if (params.ref) {
|
|
472
|
+
return evalWithRef(cdp, expression, params.ref);
|
|
473
|
+
}
|
|
474
|
+
return evalGlobal(cdp, expression);
|
|
475
|
+
}
|
|
476
|
+
async function evalGlobal(cdp, expression) {
|
|
477
|
+
const response = await cdp.send("Runtime.evaluate", {
|
|
478
|
+
expression,
|
|
479
|
+
returnByValue: true,
|
|
480
|
+
awaitPromise: true
|
|
481
|
+
});
|
|
482
|
+
if (response.exceptionDetails) {
|
|
483
|
+
return {
|
|
484
|
+
error: formatException(response.exceptionDetails)
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
const value = parseRemoteObject(response.result);
|
|
488
|
+
return { result: value };
|
|
489
|
+
}
|
|
490
|
+
async function evalWithRef(cdp, functionDeclaration, ref) {
|
|
491
|
+
const match = /^@e(\d+)$/.exec(ref);
|
|
492
|
+
if (!match) {
|
|
493
|
+
return { error: `Invalid ref format: ${ref}` };
|
|
494
|
+
}
|
|
495
|
+
const backendNodeId = parseInt(match[1], 10);
|
|
496
|
+
const resolved = await cdp.send("DOM.resolveNode", {
|
|
497
|
+
backendNodeId
|
|
498
|
+
});
|
|
499
|
+
const objectId = resolved.object.objectId;
|
|
500
|
+
const response = await cdp.send("Runtime.callFunctionOn", {
|
|
501
|
+
objectId,
|
|
502
|
+
functionDeclaration,
|
|
503
|
+
returnByValue: true,
|
|
504
|
+
awaitPromise: true
|
|
505
|
+
});
|
|
506
|
+
if (response.exceptionDetails) {
|
|
507
|
+
return {
|
|
508
|
+
error: formatException(response.exceptionDetails)
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
const value = parseRemoteObject(response.result);
|
|
512
|
+
return { result: value };
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// src/tools/browser-find.ts
|
|
516
|
+
async function browserFind(cdp, params) {
|
|
517
|
+
const nth = params.nth ?? 0;
|
|
518
|
+
const response = await cdp.send("Accessibility.getFullAXTree", void 0, {
|
|
519
|
+
timeout: 1e4
|
|
520
|
+
});
|
|
521
|
+
const matches = response.nodes.filter((node) => {
|
|
522
|
+
if (params.role) {
|
|
523
|
+
const nodeRole = node.role?.value ?? "";
|
|
524
|
+
if (nodeRole.toLowerCase() !== params.role.toLowerCase()) {
|
|
525
|
+
return false;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
if (params.name) {
|
|
529
|
+
const nodeName = node.name?.value ?? "";
|
|
530
|
+
if (!nodeName.includes(params.name)) {
|
|
531
|
+
return false;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
if (params.text) {
|
|
535
|
+
const nodeName = node.name?.value ?? "";
|
|
536
|
+
if (!nodeName.includes(params.text)) {
|
|
537
|
+
return false;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
return true;
|
|
541
|
+
});
|
|
542
|
+
const count = matches.length;
|
|
543
|
+
if (nth >= count || count === 0) {
|
|
544
|
+
return {
|
|
545
|
+
found: false,
|
|
546
|
+
ref: null,
|
|
547
|
+
role: null,
|
|
548
|
+
name: null,
|
|
549
|
+
count
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
const match = matches[nth];
|
|
553
|
+
const ref = match.backendDOMNodeId ? `@e${match.backendDOMNodeId}` : null;
|
|
554
|
+
return {
|
|
555
|
+
found: true,
|
|
556
|
+
ref,
|
|
557
|
+
role: match.role?.value ?? null,
|
|
558
|
+
name: match.name?.value ?? null,
|
|
559
|
+
count
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// src/tools/browser-inspect-source.ts
|
|
564
|
+
var REF_PATTERN = /^@?e(\d+)$/;
|
|
565
|
+
async function browserInspectSource(cdp, params) {
|
|
566
|
+
let objectId;
|
|
567
|
+
if (params.ref) {
|
|
568
|
+
const match = REF_PATTERN.exec(params.ref);
|
|
569
|
+
if (!match) throw new Error(`Invalid ref format: ${params.ref}`);
|
|
570
|
+
const backendNodeId = parseInt(match[1], 10);
|
|
571
|
+
const resolved = await cdp.send("DOM.resolveNode", { backendNodeId });
|
|
572
|
+
objectId = resolved.object.objectId;
|
|
573
|
+
} else if (params.selector) {
|
|
574
|
+
const evalResult = await cdp.send("Runtime.evaluate", {
|
|
575
|
+
expression: `document.querySelector(${JSON.stringify(params.selector)})`,
|
|
576
|
+
returnByValue: false
|
|
577
|
+
});
|
|
578
|
+
if (!evalResult.result.objectId || evalResult.result.subtype === "null") {
|
|
579
|
+
throw new Error(`Element not found: ${params.selector}`);
|
|
580
|
+
}
|
|
581
|
+
objectId = evalResult.result.objectId;
|
|
582
|
+
} else {
|
|
583
|
+
throw new Error("Either ref or selector must be provided");
|
|
584
|
+
}
|
|
585
|
+
const cdpResult = await cdp.send("Runtime.callFunctionOn", {
|
|
586
|
+
objectId,
|
|
587
|
+
functionDeclaration: `function() {
|
|
588
|
+
var el = this;
|
|
589
|
+
var tagName = (el.tagName || '').toLowerCase();
|
|
590
|
+
|
|
591
|
+
// Find React Fiber
|
|
592
|
+
var fiberKey = Object.keys(el).find(function(k) { return k.startsWith('__reactFiber'); });
|
|
593
|
+
|
|
594
|
+
// Also check Vue, Svelte
|
|
595
|
+
var vueComp = el.__vueParentComponent;
|
|
596
|
+
var svelteMeta = el.__svelte_meta;
|
|
597
|
+
|
|
598
|
+
if (!fiberKey && !vueComp && !svelteMeta) {
|
|
599
|
+
return JSON.stringify({ tagName: tagName, componentName: null, source: null, stack: [], framework: null });
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// --- React path ---
|
|
603
|
+
if (fiberKey) {
|
|
604
|
+
var fiber = el[fiberKey];
|
|
605
|
+
var stack = [];
|
|
606
|
+
var current = fiber;
|
|
607
|
+
var firstSource = null;
|
|
608
|
+
var firstName = null;
|
|
609
|
+
|
|
610
|
+
while (current && stack.length < 15) {
|
|
611
|
+
if (typeof current.type === 'function' && current.type.name) {
|
|
612
|
+
var fn = current.type;
|
|
613
|
+
var fnStr = fn.toString();
|
|
614
|
+
var fileName = null;
|
|
615
|
+
var lineNumber = null;
|
|
616
|
+
var columnNumber = null;
|
|
617
|
+
|
|
618
|
+
// Parse jsxDEV calls for embedded fileName/lineNumber
|
|
619
|
+
var fileMatch = fnStr.match(/fileName:\\s*"([^"]+)"/);
|
|
620
|
+
if (fileMatch) {
|
|
621
|
+
fileName = fileMatch[1];
|
|
622
|
+
var lineMatch = fnStr.match(/lineNumber:\\s*(\\d+)/);
|
|
623
|
+
if (lineMatch) lineNumber = parseInt(lineMatch[1]);
|
|
624
|
+
var colMatch = fnStr.match(/columnNumber:\\s*(\\d+)/);
|
|
625
|
+
if (colMatch) columnNumber = parseInt(colMatch[1]);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
var entry = {
|
|
629
|
+
filePath: fileName,
|
|
630
|
+
lineNumber: lineNumber,
|
|
631
|
+
columnNumber: columnNumber,
|
|
632
|
+
componentName: fn.name
|
|
633
|
+
};
|
|
634
|
+
|
|
635
|
+
stack.push(entry);
|
|
636
|
+
|
|
637
|
+
if (fileName && !firstSource) {
|
|
638
|
+
firstSource = entry;
|
|
639
|
+
}
|
|
640
|
+
if (!firstName && fn.name.length > 1) {
|
|
641
|
+
firstName = fn.name;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
current = current.return;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
return JSON.stringify({
|
|
648
|
+
tagName: tagName,
|
|
649
|
+
componentName: firstName || null,
|
|
650
|
+
source: firstSource || (stack.length > 0 ? { filePath: null, lineNumber: null, columnNumber: null, componentName: stack[0].componentName } : null),
|
|
651
|
+
stack: stack.filter(function(s) { return s.filePath; }),
|
|
652
|
+
framework: 'react'
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// --- Svelte path ---
|
|
657
|
+
if (svelteMeta) {
|
|
658
|
+
var loc = svelteMeta.loc || {};
|
|
659
|
+
return JSON.stringify({
|
|
660
|
+
tagName: tagName,
|
|
661
|
+
componentName: loc.char ? null : (svelteMeta.component || null),
|
|
662
|
+
source: loc.file ? { filePath: loc.file, lineNumber: loc.line || null, columnNumber: (loc.column || 0) + 1, componentName: null } : null,
|
|
663
|
+
stack: [],
|
|
664
|
+
framework: 'svelte'
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// --- Vue path ---
|
|
669
|
+
if (vueComp) {
|
|
670
|
+
var comp = vueComp;
|
|
671
|
+
var vueName = comp.type?.__name || comp.type?.name || null;
|
|
672
|
+
var vueFile = comp.type?.__file || null;
|
|
673
|
+
return JSON.stringify({
|
|
674
|
+
tagName: tagName,
|
|
675
|
+
componentName: vueName,
|
|
676
|
+
source: vueFile ? { filePath: vueFile, lineNumber: null, columnNumber: null, componentName: vueName } : null,
|
|
677
|
+
stack: [],
|
|
678
|
+
framework: 'vue'
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
return JSON.stringify({ tagName: tagName, componentName: null, source: null, stack: [], framework: null });
|
|
683
|
+
}`,
|
|
684
|
+
returnByValue: true
|
|
685
|
+
});
|
|
686
|
+
return JSON.parse(cdpResult.result.value);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// src/event-buffer.ts
|
|
690
|
+
var EventBuffer = class {
|
|
691
|
+
_buffer;
|
|
692
|
+
_capacity;
|
|
693
|
+
_head;
|
|
694
|
+
// next write index
|
|
695
|
+
_size;
|
|
696
|
+
_totalPushed;
|
|
697
|
+
constructor(capacity = 500) {
|
|
698
|
+
this._capacity = capacity;
|
|
699
|
+
this._buffer = new Array(capacity);
|
|
700
|
+
this._head = 0;
|
|
701
|
+
this._size = 0;
|
|
702
|
+
this._totalPushed = 0;
|
|
703
|
+
}
|
|
704
|
+
/** Append an event, evicting the oldest if at capacity. */
|
|
705
|
+
push(event) {
|
|
706
|
+
this._buffer[this._head] = event;
|
|
707
|
+
this._head = (this._head + 1) % this._capacity;
|
|
708
|
+
if (this._size < this._capacity) {
|
|
709
|
+
this._size++;
|
|
710
|
+
}
|
|
711
|
+
this._totalPushed++;
|
|
712
|
+
}
|
|
713
|
+
/**
|
|
714
|
+
* Return the last `n` events in chronological (oldest-first) order.
|
|
715
|
+
* Defaults to all events when `n` is omitted.
|
|
716
|
+
*/
|
|
717
|
+
last(n) {
|
|
718
|
+
const count = n === void 0 ? this._size : Math.min(n, this._size);
|
|
719
|
+
if (count === 0) return [];
|
|
720
|
+
const result = new Array(count);
|
|
721
|
+
let start = (this._head - count + this._capacity) % this._capacity;
|
|
722
|
+
for (let i = 0; i < count; i++) {
|
|
723
|
+
result[i] = this._buffer[(start + i) % this._capacity];
|
|
724
|
+
}
|
|
725
|
+
return result;
|
|
726
|
+
}
|
|
727
|
+
/** Remove all events from the buffer. */
|
|
728
|
+
clear() {
|
|
729
|
+
this._buffer = new Array(this._capacity);
|
|
730
|
+
this._head = 0;
|
|
731
|
+
this._size = 0;
|
|
732
|
+
}
|
|
733
|
+
/**
|
|
734
|
+
* Return the last `n` events AND remove them from the buffer.
|
|
735
|
+
* Defaults to all events when `n` is omitted.
|
|
736
|
+
*/
|
|
737
|
+
drain(n) {
|
|
738
|
+
const events = this.last(n);
|
|
739
|
+
this.clear();
|
|
740
|
+
return events;
|
|
741
|
+
}
|
|
742
|
+
/** Current number of events stored. */
|
|
743
|
+
get size() {
|
|
744
|
+
return this._size;
|
|
745
|
+
}
|
|
746
|
+
/** Maximum number of events this buffer can hold. */
|
|
747
|
+
get capacity() {
|
|
748
|
+
return this._capacity;
|
|
749
|
+
}
|
|
750
|
+
/** Total number of events ever pushed (including evicted ones). */
|
|
751
|
+
get totalPushed() {
|
|
752
|
+
return this._totalPushed;
|
|
753
|
+
}
|
|
754
|
+
/** Return events matching `predicate` without modifying the buffer. */
|
|
755
|
+
filter(predicate) {
|
|
756
|
+
return this.last().filter(predicate);
|
|
757
|
+
}
|
|
758
|
+
/** Snapshot of buffer statistics. */
|
|
759
|
+
get stats() {
|
|
760
|
+
return {
|
|
761
|
+
size: this._size,
|
|
762
|
+
capacity: this._capacity,
|
|
763
|
+
totalPushed: this._totalPushed,
|
|
764
|
+
evicted: this._totalPushed - this._size
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
};
|
|
768
|
+
|
|
769
|
+
// src/redactor.ts
|
|
770
|
+
var JWT_PATTERN = /eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]+/g;
|
|
771
|
+
var BEARER_PATTERN = /Bearer\s+\S+/gi;
|
|
772
|
+
var REDACTED = "[REDACTED]";
|
|
773
|
+
var REDACTED_JWT = "[REDACTED_JWT]";
|
|
774
|
+
function redactInlineSecrets(text, opts) {
|
|
775
|
+
if (opts?.enabled === false) {
|
|
776
|
+
return text;
|
|
777
|
+
}
|
|
778
|
+
let result = text.replace(JWT_PATTERN, REDACTED_JWT);
|
|
779
|
+
result = result.replace(BEARER_PATTERN, (match) => {
|
|
780
|
+
const bearerWord = match.split(/\s+/)[0];
|
|
781
|
+
return `${bearerWord} ${REDACTED}`;
|
|
782
|
+
});
|
|
783
|
+
return result;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// src/tools/browser-console-messages.ts
|
|
787
|
+
var consoleBuffer = new EventBuffer(500);
|
|
788
|
+
async function browserConsoleMessages(_cdp, params) {
|
|
789
|
+
let messages = consoleBuffer.last();
|
|
790
|
+
messages = messages.map((m) => ({ ...m, text: redactInlineSecrets(m.text) }));
|
|
791
|
+
if (params.level) {
|
|
792
|
+
messages = messages.filter((m) => m.level === params.level);
|
|
793
|
+
}
|
|
794
|
+
const limit = params.limit ?? 100;
|
|
795
|
+
if (messages.length > limit) {
|
|
796
|
+
messages = messages.slice(-limit);
|
|
797
|
+
}
|
|
798
|
+
return { messages };
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// src/tools/browser-network-requests.ts
|
|
802
|
+
var STATIC_TYPES = /* @__PURE__ */ new Set([
|
|
803
|
+
"Image",
|
|
804
|
+
"Stylesheet",
|
|
805
|
+
"Font",
|
|
806
|
+
"Script",
|
|
807
|
+
"Media"
|
|
808
|
+
]);
|
|
809
|
+
var networkBuffer = new EventBuffer(500);
|
|
810
|
+
async function browserNetworkRequests(_cdp, params) {
|
|
811
|
+
let entries = networkBuffer.last();
|
|
812
|
+
if (!params.includeStatic) {
|
|
813
|
+
entries = entries.filter((e) => !STATIC_TYPES.has(e.type));
|
|
814
|
+
}
|
|
815
|
+
if (params.filter) {
|
|
816
|
+
const filterLower = params.filter.toLowerCase();
|
|
817
|
+
entries = entries.filter((e) => e.url.toLowerCase().includes(filterLower));
|
|
818
|
+
}
|
|
819
|
+
const limit = params.limit ?? 100;
|
|
820
|
+
entries = entries.slice(0, limit);
|
|
821
|
+
const requests = entries.map((e) => ({
|
|
822
|
+
url: redactInlineSecrets(e.url),
|
|
823
|
+
method: e.method,
|
|
824
|
+
status: e.status,
|
|
825
|
+
type: e.type
|
|
826
|
+
}));
|
|
827
|
+
return { requests };
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// src/cli/commands/obs.ts
|
|
831
|
+
var snapshotCommand = {
|
|
832
|
+
name: "snapshot",
|
|
833
|
+
description: "Capture the accessibility tree of the page",
|
|
834
|
+
usage: "browsirai snapshot [-i] [-c] [-d N] [-s selector]",
|
|
835
|
+
run: async (cdp, args) => {
|
|
836
|
+
const flags = parseFlags(args);
|
|
837
|
+
const params = {};
|
|
838
|
+
if (flags.compact === "true" || flags.c === "true") params.compact = true;
|
|
839
|
+
if (flags.interactive === "true" || flags.i === "true") params.interactive = true;
|
|
840
|
+
if (flags.selector) params.selector = flags.selector;
|
|
841
|
+
if (flags.s) params.selector = flags.s;
|
|
842
|
+
if (flags.depth) params.depth = parseInt(flags.depth, 10);
|
|
843
|
+
if (flags.d) params.depth = parseInt(flags.d, 10);
|
|
844
|
+
const result = await browserSnapshot(cdp, params);
|
|
845
|
+
if (result.snapshot) {
|
|
846
|
+
console.log(result.snapshot);
|
|
847
|
+
} else {
|
|
848
|
+
console.log("(empty snapshot)");
|
|
849
|
+
}
|
|
850
|
+
if (result.truncated) {
|
|
851
|
+
console.log(`
|
|
852
|
+
(truncated \u2014 ${result.totalElements} total elements)`);
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
};
|
|
856
|
+
var screenshotCommand = {
|
|
857
|
+
name: "screenshot",
|
|
858
|
+
description: "Capture a screenshot of the page",
|
|
859
|
+
usage: "browsirai screenshot [-o file.png] [--fullPage] [--selector=...] [--format=png]",
|
|
860
|
+
run: async (cdp, args) => {
|
|
861
|
+
const flags = parseFlags(args);
|
|
862
|
+
const params = {};
|
|
863
|
+
if (flags.fullPage === "true") params.fullPage = true;
|
|
864
|
+
if (flags.selector) params.selector = flags.selector;
|
|
865
|
+
if (flags.format) params.format = flags.format;
|
|
866
|
+
const result = await browserScreenshot(cdp, params);
|
|
867
|
+
const output = flags.output ?? flags.o;
|
|
868
|
+
if (output) {
|
|
869
|
+
const buffer = Buffer.from(result.base64, "base64");
|
|
870
|
+
writeFileSync(output, buffer);
|
|
871
|
+
console.log(`Screenshot saved to ${output} (${buffer.length} bytes)`);
|
|
872
|
+
} else {
|
|
873
|
+
const sizeKB = Math.round(result.base64.length * 3 / 4 / 1024);
|
|
874
|
+
console.log(`Screenshot taken (~${sizeKB}KB)`);
|
|
875
|
+
console.log("Use --output=file.png to save to disk");
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
};
|
|
879
|
+
var htmlCommand = {
|
|
880
|
+
name: "html",
|
|
881
|
+
description: "Retrieve page or element HTML",
|
|
882
|
+
usage: "browsirai html [--selector=...]",
|
|
883
|
+
run: async (cdp, args) => {
|
|
884
|
+
const flags = parseFlags(args);
|
|
885
|
+
const params = {};
|
|
886
|
+
if (flags.selector) params.selector = flags.selector;
|
|
887
|
+
const result = await browserHtml(cdp, params);
|
|
888
|
+
if (result.error) {
|
|
889
|
+
console.error(`Error: ${result.error}`);
|
|
890
|
+
process.exit(1);
|
|
891
|
+
}
|
|
892
|
+
console.log(result.html);
|
|
893
|
+
}
|
|
894
|
+
};
|
|
895
|
+
var evalCommand = {
|
|
896
|
+
name: "eval",
|
|
897
|
+
description: "Evaluate a JavaScript expression in the browser",
|
|
898
|
+
usage: 'browsirai eval "<expression>"',
|
|
899
|
+
run: async (cdp, args) => {
|
|
900
|
+
const flags = parseFlags(args);
|
|
901
|
+
const expression = flags._0;
|
|
902
|
+
if (!expression) {
|
|
903
|
+
console.error("Error: Missing expression argument");
|
|
904
|
+
console.error('Usage: browsirai eval "<expression>"');
|
|
905
|
+
process.exit(1);
|
|
906
|
+
}
|
|
907
|
+
const result = await browserEval(cdp, { expression });
|
|
908
|
+
if (result.error) {
|
|
909
|
+
console.error(`Error: ${result.error}`);
|
|
910
|
+
process.exit(1);
|
|
911
|
+
}
|
|
912
|
+
if (result.result === void 0) {
|
|
913
|
+
console.log("undefined");
|
|
914
|
+
} else if (result.result === null) {
|
|
915
|
+
console.log("null");
|
|
916
|
+
} else if (typeof result.result === "object") {
|
|
917
|
+
console.log(JSON.stringify(result.result, null, 2));
|
|
918
|
+
} else {
|
|
919
|
+
console.log(String(result.result));
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
};
|
|
923
|
+
var findCommand = {
|
|
924
|
+
name: "find",
|
|
925
|
+
description: "Find elements by ARIA role, name, or text",
|
|
926
|
+
usage: "browsirai find [--role=button] [--name=...] [--text=...]",
|
|
927
|
+
run: async (cdp, args) => {
|
|
928
|
+
const flags = parseFlags(args);
|
|
929
|
+
const params = {};
|
|
930
|
+
if (flags.role) params.role = flags.role;
|
|
931
|
+
if (flags.name) params.name = flags.name;
|
|
932
|
+
if (flags.text) params.text = flags.text;
|
|
933
|
+
if (flags.nth) params.nth = parseInt(flags.nth, 10);
|
|
934
|
+
const result = await browserFind(cdp, params);
|
|
935
|
+
if (result.found) {
|
|
936
|
+
console.log(`Found ${result.ref} (role: ${result.role}, name: "${result.name}")`);
|
|
937
|
+
if (result.count > 1) {
|
|
938
|
+
console.log(` ${result.count} total matches`);
|
|
939
|
+
}
|
|
940
|
+
} else {
|
|
941
|
+
console.log("Not found");
|
|
942
|
+
if (result.count > 0) {
|
|
943
|
+
console.log(` ${result.count} matches exist but index out of range`);
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
};
|
|
948
|
+
var sourceCommand = {
|
|
949
|
+
name: "source",
|
|
950
|
+
description: "Inspect source code location of an element",
|
|
951
|
+
usage: "browsirai source [--ref=@e5] [--selector=h1]",
|
|
952
|
+
run: async (cdp, args) => {
|
|
953
|
+
const flags = parseFlags(args);
|
|
954
|
+
const params = {};
|
|
955
|
+
if (flags.ref) params.ref = flags.ref;
|
|
956
|
+
if (flags.selector) params.selector = flags.selector;
|
|
957
|
+
if (!params.ref && !params.selector) {
|
|
958
|
+
console.error("Error: Either --ref or --selector is required");
|
|
959
|
+
console.error("Usage: browsirai source [--ref=@e5] [--selector=h1]");
|
|
960
|
+
process.exit(1);
|
|
961
|
+
}
|
|
962
|
+
const result = await browserInspectSource(cdp, params);
|
|
963
|
+
if (result.source) {
|
|
964
|
+
const loc = result.source;
|
|
965
|
+
const file = loc.filePath ?? "(unknown file)";
|
|
966
|
+
const line = loc.lineNumber != null ? `:${loc.lineNumber}` : "";
|
|
967
|
+
const col = loc.columnNumber != null ? `:${loc.columnNumber}` : "";
|
|
968
|
+
const component = result.componentName ?? loc.componentName ?? "(anonymous)";
|
|
969
|
+
console.log(`Component: ${component}`);
|
|
970
|
+
console.log(`Source: ${file}${line}${col}`);
|
|
971
|
+
} else {
|
|
972
|
+
console.log(`Tag: <${result.tagName}>`);
|
|
973
|
+
if (result.componentName) {
|
|
974
|
+
console.log(`Component: ${result.componentName}`);
|
|
975
|
+
}
|
|
976
|
+
console.log("Source: not found (dev mode may not be enabled)");
|
|
977
|
+
}
|
|
978
|
+
if (result.stack.length > 0) {
|
|
979
|
+
console.log("\nComponent stack:");
|
|
980
|
+
for (const loc of result.stack) {
|
|
981
|
+
const name = loc.componentName ?? "(anonymous)";
|
|
982
|
+
const line = loc.lineNumber != null ? `:${loc.lineNumber}` : "";
|
|
983
|
+
console.log(` ${name} -> ${loc.filePath}${line}`);
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
};
|
|
988
|
+
var consoleCommand = {
|
|
989
|
+
name: "console",
|
|
990
|
+
description: "View captured console messages",
|
|
991
|
+
usage: "browsirai console [--level=error] [--limit=20]",
|
|
992
|
+
run: async (cdp, args) => {
|
|
993
|
+
const flags = parseFlags(args);
|
|
994
|
+
const params = {};
|
|
995
|
+
if (flags.level) params.level = flags.level;
|
|
996
|
+
if (flags.limit) params.limit = parseInt(flags.limit, 10);
|
|
997
|
+
const result = await browserConsoleMessages(cdp, params);
|
|
998
|
+
if (result.messages.length === 0) {
|
|
999
|
+
console.log("No console messages captured");
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
for (const msg of result.messages) {
|
|
1003
|
+
const level = msg.level.toUpperCase().padEnd(5);
|
|
1004
|
+
const ts = msg.timestamp ? new Date(msg.timestamp).toISOString().slice(11, 23) : "";
|
|
1005
|
+
const prefix = ts ? `[${ts}] ${level}` : level;
|
|
1006
|
+
console.log(`${prefix} ${msg.text}`);
|
|
1007
|
+
}
|
|
1008
|
+
console.log(`
|
|
1009
|
+
(${result.messages.length} messages)`);
|
|
1010
|
+
}
|
|
1011
|
+
};
|
|
1012
|
+
var networkCommand = {
|
|
1013
|
+
name: "network",
|
|
1014
|
+
description: "View captured network requests",
|
|
1015
|
+
usage: "browsirai network [--filter=*api*] [--limit=10] [--includeHeaders]",
|
|
1016
|
+
run: async (cdp, args) => {
|
|
1017
|
+
const flags = parseFlags(args);
|
|
1018
|
+
const params = {};
|
|
1019
|
+
if (flags.filter) params.filter = flags.filter;
|
|
1020
|
+
if (flags.limit) params.limit = parseInt(flags.limit, 10);
|
|
1021
|
+
if (flags.includeHeaders === "true") params.includeHeaders = true;
|
|
1022
|
+
const result = await browserNetworkRequests(cdp, params);
|
|
1023
|
+
if (result.requests.length === 0) {
|
|
1024
|
+
console.log("No network requests captured");
|
|
1025
|
+
return;
|
|
1026
|
+
}
|
|
1027
|
+
for (const req of result.requests) {
|
|
1028
|
+
const status = req.status != null ? String(req.status) : "...";
|
|
1029
|
+
const type = req.type ? `[${req.type}]` : "";
|
|
1030
|
+
console.log(`${req.method.padEnd(6)} ${status.padEnd(3)} ${type.padEnd(10)} ${req.url}`);
|
|
1031
|
+
}
|
|
1032
|
+
console.log(`
|
|
1033
|
+
(${result.requests.length} requests)`);
|
|
1034
|
+
}
|
|
1035
|
+
};
|
|
1036
|
+
var obsCommands = [
|
|
1037
|
+
snapshotCommand,
|
|
1038
|
+
screenshotCommand,
|
|
1039
|
+
htmlCommand,
|
|
1040
|
+
evalCommand,
|
|
1041
|
+
findCommand,
|
|
1042
|
+
sourceCommand,
|
|
1043
|
+
consoleCommand,
|
|
1044
|
+
networkCommand
|
|
1045
|
+
];
|
|
1046
|
+
export {
|
|
1047
|
+
obsCommands
|
|
1048
|
+
};
|
|
1049
|
+
//# sourceMappingURL=obs.js.map
|