@monoes/monobrowse 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/browser/action-builder/analyzer.d.ts +11 -0
- package/dist/src/browser/action-builder/analyzer.d.ts.map +1 -0
- package/dist/src/browser/action-builder/analyzer.js +93 -0
- package/dist/src/browser/action-builder/analyzer.js.map +1 -0
- package/dist/src/browser/action-builder/types.d.ts +46 -0
- package/dist/src/browser/action-builder/types.d.ts.map +1 -0
- package/dist/src/browser/action-builder/types.js +2 -0
- package/dist/src/browser/action-builder/types.js.map +1 -0
- package/dist/src/browser/actions.d.ts +29 -0
- package/dist/src/browser/actions.d.ts.map +1 -0
- package/dist/src/browser/actions.js +398 -0
- package/dist/src/browser/actions.js.map +1 -0
- package/dist/src/browser/adapters/gemini.d.ts +3 -0
- package/dist/src/browser/adapters/gemini.d.ts.map +1 -0
- package/dist/src/browser/adapters/gemini.js +13 -0
- package/dist/src/browser/adapters/gemini.js.map +1 -0
- package/dist/src/browser/adapters/google.d.ts +3 -0
- package/dist/src/browser/adapters/google.d.ts.map +1 -0
- package/dist/src/browser/adapters/google.js +17 -0
- package/dist/src/browser/adapters/google.js.map +1 -0
- package/dist/src/browser/adapters/index.d.ts +15 -0
- package/dist/src/browser/adapters/index.d.ts.map +1 -0
- package/dist/src/browser/adapters/index.js +18 -0
- package/dist/src/browser/adapters/index.js.map +1 -0
- package/dist/src/browser/adapters/instagram.d.ts +3 -0
- package/dist/src/browser/adapters/instagram.d.ts.map +1 -0
- package/dist/src/browser/adapters/instagram.js +14 -0
- package/dist/src/browser/adapters/instagram.js.map +1 -0
- package/dist/src/browser/adapters/linkedin.d.ts +3 -0
- package/dist/src/browser/adapters/linkedin.d.ts.map +1 -0
- package/dist/src/browser/adapters/linkedin.js +13 -0
- package/dist/src/browser/adapters/linkedin.js.map +1 -0
- package/dist/src/browser/adapters/microsoft.d.ts +3 -0
- package/dist/src/browser/adapters/microsoft.d.ts.map +1 -0
- package/dist/src/browser/adapters/microsoft.js +16 -0
- package/dist/src/browser/adapters/microsoft.js.map +1 -0
- package/dist/src/browser/adapters/x.d.ts +3 -0
- package/dist/src/browser/adapters/x.d.ts.map +1 -0
- package/dist/src/browser/adapters/x.js +13 -0
- package/dist/src/browser/adapters/x.js.map +1 -0
- package/dist/src/browser/batch.d.ts +5 -0
- package/dist/src/browser/batch.d.ts.map +1 -0
- package/dist/src/browser/batch.js +2 -0
- package/dist/src/browser/batch.js.map +1 -0
- package/dist/src/browser/browser.d.ts +15 -0
- package/dist/src/browser/browser.d.ts.map +1 -0
- package/dist/src/browser/browser.js +256 -0
- package/dist/src/browser/browser.js.map +1 -0
- package/dist/src/browser/cdp.d.ts +18 -0
- package/dist/src/browser/cdp.d.ts.map +1 -0
- package/dist/src/browser/cdp.js +153 -0
- package/dist/src/browser/cdp.js.map +1 -0
- package/dist/src/browser/console-log.d.ts +23 -0
- package/dist/src/browser/console-log.d.ts.map +1 -0
- package/dist/src/browser/console-log.js +104 -0
- package/dist/src/browser/console-log.js.map +1 -0
- package/dist/src/browser/dashboard/api-types.d.ts +50 -0
- package/dist/src/browser/dashboard/api-types.d.ts.map +1 -0
- package/dist/src/browser/dashboard/api-types.js +14 -0
- package/dist/src/browser/dashboard/api-types.js.map +1 -0
- package/dist/src/browser/dashboard/server.d.ts +11 -0
- package/dist/src/browser/dashboard/server.d.ts.map +1 -0
- package/dist/src/browser/dashboard/server.js +191 -0
- package/dist/src/browser/dashboard/server.js.map +1 -0
- package/dist/src/browser/dialog.d.ts +12 -0
- package/dist/src/browser/dialog.d.ts.map +1 -0
- package/dist/src/browser/dialog.js +53 -0
- package/dist/src/browser/dialog.js.map +1 -0
- package/dist/src/browser/emulation.d.ts +15 -0
- package/dist/src/browser/emulation.d.ts.map +1 -0
- package/dist/src/browser/emulation.js +62 -0
- package/dist/src/browser/emulation.js.map +1 -0
- package/dist/src/browser/find.d.ts +22 -0
- package/dist/src/browser/find.d.ts.map +1 -0
- package/dist/src/browser/find.js +169 -0
- package/dist/src/browser/find.js.map +1 -0
- package/dist/src/browser/har.d.ts +27 -0
- package/dist/src/browser/har.d.ts.map +1 -0
- package/dist/src/browser/har.js +161 -0
- package/dist/src/browser/har.js.map +1 -0
- package/dist/src/browser/index.d.ts +23 -0
- package/dist/src/browser/index.d.ts.map +1 -0
- package/dist/src/browser/index.js +23 -0
- package/dist/src/browser/index.js.map +1 -0
- package/dist/src/browser/network.d.ts +32 -0
- package/dist/src/browser/network.d.ts.map +1 -0
- package/dist/src/browser/network.js +214 -0
- package/dist/src/browser/network.js.map +1 -0
- package/dist/src/browser/pdf.d.ts +15 -0
- package/dist/src/browser/pdf.d.ts.map +1 -0
- package/dist/src/browser/pdf.js +27 -0
- package/dist/src/browser/pdf.js.map +1 -0
- package/dist/src/browser/profiler.d.ts +10 -0
- package/dist/src/browser/profiler.d.ts.map +1 -0
- package/dist/src/browser/profiler.js +77 -0
- package/dist/src/browser/profiler.js.map +1 -0
- package/dist/src/browser/record.d.ts +21 -0
- package/dist/src/browser/record.d.ts.map +1 -0
- package/dist/src/browser/record.js +59 -0
- package/dist/src/browser/record.js.map +1 -0
- package/dist/src/browser/screenshot.d.ts +15 -0
- package/dist/src/browser/screenshot.d.ts.map +1 -0
- package/dist/src/browser/screenshot.js +38 -0
- package/dist/src/browser/screenshot.js.map +1 -0
- package/dist/src/browser/session.d.ts +8 -0
- package/dist/src/browser/session.d.ts.map +1 -0
- package/dist/src/browser/session.js +91 -0
- package/dist/src/browser/session.js.map +1 -0
- package/dist/src/browser/snapshot.d.ts +12 -0
- package/dist/src/browser/snapshot.d.ts.map +1 -0
- package/dist/src/browser/snapshot.js +179 -0
- package/dist/src/browser/snapshot.js.map +1 -0
- package/dist/src/browser/storage.d.ts +12 -0
- package/dist/src/browser/storage.d.ts.map +1 -0
- package/dist/src/browser/storage.js +46 -0
- package/dist/src/browser/storage.js.map +1 -0
- package/dist/src/browser/tabs.d.ts +8 -0
- package/dist/src/browser/tabs.d.ts.map +1 -0
- package/dist/src/browser/tabs.js +25 -0
- package/dist/src/browser/tabs.js.map +1 -0
- package/dist/src/browser/trace.d.ts +10 -0
- package/dist/src/browser/trace.d.ts.map +1 -0
- package/dist/src/browser/trace.js +87 -0
- package/dist/src/browser/trace.js.map +1 -0
- package/dist/src/browser/types.d.ts +111 -0
- package/dist/src/browser/types.d.ts.map +1 -0
- package/dist/src/browser/types.js +16 -0
- package/dist/src/browser/types.js.map +1 -0
- package/dist/src/browser/vitals.d.ts +15 -0
- package/dist/src/browser/vitals.d.ts.map +1 -0
- package/dist/src/browser/vitals.js +116 -0
- package/dist/src/browser/vitals.js.map +1 -0
- package/dist/src/browser/wait.d.ts +4 -0
- package/dist/src/browser/wait.d.ts.map +1 -0
- package/dist/src/browser/wait.js +146 -0
- package/dist/src/browser/wait.js.map +1 -0
- package/dist/src/browser/workflow/builtin-handlers.d.ts +3 -0
- package/dist/src/browser/workflow/builtin-handlers.d.ts.map +1 -0
- package/dist/src/browser/workflow/builtin-handlers.js +343 -0
- package/dist/src/browser/workflow/builtin-handlers.js.map +1 -0
- package/dist/src/browser/workflow/engine.d.ts +10 -0
- package/dist/src/browser/workflow/engine.d.ts.map +1 -0
- package/dist/src/browser/workflow/engine.js +174 -0
- package/dist/src/browser/workflow/engine.js.map +1 -0
- package/dist/src/browser/workflow/expression.d.ts +4 -0
- package/dist/src/browser/workflow/expression.d.ts.map +1 -0
- package/dist/src/browser/workflow/expression.js +89 -0
- package/dist/src/browser/workflow/expression.js.map +1 -0
- package/dist/src/browser/workflow/index.d.ts +6 -0
- package/dist/src/browser/workflow/index.d.ts.map +1 -0
- package/dist/src/browser/workflow/index.js +6 -0
- package/dist/src/browser/workflow/index.js.map +1 -0
- package/dist/src/browser/workflow/store.d.ts +8 -0
- package/dist/src/browser/workflow/store.d.ts.map +1 -0
- package/dist/src/browser/workflow/store.js +87 -0
- package/dist/src/browser/workflow/store.js.map +1 -0
- package/dist/src/browser/workflow/types.d.ts +62 -0
- package/dist/src/browser/workflow/types.d.ts.map +1 -0
- package/dist/src/browser/workflow/types.js +2 -0
- package/dist/src/browser/workflow/types.js.map +1 -0
- package/dist/src/index.d.ts +7 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +7 -0
- package/dist/src/index.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +56 -0
- package/src/browser/action-builder/analyzer.ts +127 -0
- package/src/browser/action-builder/types.ts +17 -0
- package/src/browser/actions.ts +518 -0
- package/src/browser/adapters/gemini.ts +15 -0
- package/src/browser/adapters/google.ts +20 -0
- package/src/browser/adapters/index.ts +33 -0
- package/src/browser/adapters/instagram.ts +16 -0
- package/src/browser/adapters/linkedin.ts +15 -0
- package/src/browser/adapters/microsoft.ts +19 -0
- package/src/browser/adapters/x.ts +15 -0
- package/src/browser/batch.ts +4 -0
- package/src/browser/browser.ts +265 -0
- package/src/browser/cdp.ts +156 -0
- package/src/browser/console-log.ts +120 -0
- package/src/browser/dashboard/api-types.ts +71 -0
- package/src/browser/dashboard/server.ts +206 -0
- package/src/browser/dashboard/ui.html +444 -0
- package/src/browser/dialog.ts +63 -0
- package/src/browser/emulation.ts +94 -0
- package/src/browser/find.ts +225 -0
- package/src/browser/har.ts +202 -0
- package/src/browser/index.ts +22 -0
- package/src/browser/network.ts +244 -0
- package/src/browser/pdf.ts +41 -0
- package/src/browser/profiler.ts +96 -0
- package/src/browser/record.ts +92 -0
- package/src/browser/screenshot.ts +67 -0
- package/src/browser/session.ts +118 -0
- package/src/browser/snapshot.ts +248 -0
- package/src/browser/storage.ts +54 -0
- package/src/browser/tabs.ts +37 -0
- package/src/browser/trace.ts +106 -0
- package/src/browser/types.ts +135 -0
- package/src/browser/vitals.ts +122 -0
- package/src/browser/wait.ts +178 -0
- package/src/browser/workflow/builtin-handlers.ts +379 -0
- package/src/browser/workflow/engine.ts +202 -0
- package/src/browser/workflow/expression.ts +107 -0
- package/src/browser/workflow/index.ts +5 -0
- package/src/browser/workflow/store.ts +87 -0
- package/src/browser/workflow/types.ts +62 -0
- package/src/index.ts +6 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { CdpClient } from './cdp.js';
|
|
2
|
+
import { writeFile } from 'fs/promises';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { tmpdir } from 'os';
|
|
5
|
+
|
|
6
|
+
export interface PdfOptions {
|
|
7
|
+
path?: string;
|
|
8
|
+
landscape?: boolean;
|
|
9
|
+
paperWidth?: number;
|
|
10
|
+
paperHeight?: number;
|
|
11
|
+
marginTop?: number;
|
|
12
|
+
marginBottom?: number;
|
|
13
|
+
marginLeft?: number;
|
|
14
|
+
marginRight?: number;
|
|
15
|
+
printBackground?: boolean;
|
|
16
|
+
scale?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function capturePdf(
|
|
20
|
+
client: CdpClient,
|
|
21
|
+
sessionId: string,
|
|
22
|
+
options: PdfOptions = {}
|
|
23
|
+
): Promise<string> {
|
|
24
|
+
const params: Record<string, unknown> = {
|
|
25
|
+
landscape: options.landscape ?? false,
|
|
26
|
+
printBackground: options.printBackground ?? true,
|
|
27
|
+
scale: options.scale ?? 1,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
if (options.paperWidth) params.paperWidth = options.paperWidth;
|
|
31
|
+
if (options.paperHeight) params.paperHeight = options.paperHeight;
|
|
32
|
+
if (options.marginTop !== undefined) params.marginTop = options.marginTop;
|
|
33
|
+
if (options.marginBottom !== undefined) params.marginBottom = options.marginBottom;
|
|
34
|
+
if (options.marginLeft !== undefined) params.marginLeft = options.marginLeft;
|
|
35
|
+
if (options.marginRight !== undefined) params.marginRight = options.marginRight;
|
|
36
|
+
|
|
37
|
+
const result = await client.send<{ data: string }>('Page.printToPDF', params, sessionId);
|
|
38
|
+
const outputPath = options.path ?? join(tmpdir(), `monomind-pdf-${Date.now()}.pdf`);
|
|
39
|
+
await writeFile(outputPath, Buffer.from(result.data, 'base64'));
|
|
40
|
+
return outputPath;
|
|
41
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { CdpClient } from './cdp.js';
|
|
2
|
+
import { writeFile } from 'fs/promises';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { tmpdir } from 'os';
|
|
5
|
+
|
|
6
|
+
export interface ProfilerOptions {
|
|
7
|
+
path?: string;
|
|
8
|
+
samplingInterval?: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const _sessions = new Set<string>();
|
|
12
|
+
const _heapSessions = new Set<string>();
|
|
13
|
+
|
|
14
|
+
export async function startCpuProfile(
|
|
15
|
+
client: CdpClient,
|
|
16
|
+
sessionId: string,
|
|
17
|
+
options: ProfilerOptions = {}
|
|
18
|
+
): Promise<void> {
|
|
19
|
+
if (_sessions.has(sessionId)) {
|
|
20
|
+
throw new Error('CPU profiler already running for this session');
|
|
21
|
+
}
|
|
22
|
+
await client.send('Profiler.enable', {}, sessionId);
|
|
23
|
+
if (options.samplingInterval !== undefined) {
|
|
24
|
+
await client.send('Profiler.setSamplingInterval', { interval: options.samplingInterval }, sessionId);
|
|
25
|
+
}
|
|
26
|
+
await client.send('Profiler.start', {}, sessionId);
|
|
27
|
+
_sessions.add(sessionId);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function stopCpuProfile(
|
|
31
|
+
client: CdpClient,
|
|
32
|
+
sessionId: string,
|
|
33
|
+
outputPath?: string
|
|
34
|
+
): Promise<string> {
|
|
35
|
+
if (!_sessions.has(sessionId)) {
|
|
36
|
+
throw new Error('No active CPU profiler for this session');
|
|
37
|
+
}
|
|
38
|
+
let result: { profile: unknown };
|
|
39
|
+
try {
|
|
40
|
+
result = await client.send<{ profile: unknown }>('Profiler.stop', {}, sessionId);
|
|
41
|
+
} finally {
|
|
42
|
+
_sessions.delete(sessionId);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Disable after capturing the profile so a disable failure doesn't discard valid data
|
|
46
|
+
await client.send('Profiler.disable', {}, sessionId).catch(() => {});
|
|
47
|
+
|
|
48
|
+
const path = outputPath ?? join(tmpdir(), `monomind-profile-${Date.now()}.cpuprofile`);
|
|
49
|
+
await writeFile(path, JSON.stringify(result!.profile));
|
|
50
|
+
return path;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function isProfilingActive(sessionId: string): boolean {
|
|
54
|
+
return _sessions.has(sessionId);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function startHeapSnapshot(
|
|
58
|
+
client: CdpClient,
|
|
59
|
+
sessionId: string,
|
|
60
|
+
outputPath?: string
|
|
61
|
+
): Promise<string> {
|
|
62
|
+
if (_heapSessions.has(sessionId)) {
|
|
63
|
+
throw new Error('Heap snapshot already in progress for this session');
|
|
64
|
+
}
|
|
65
|
+
_heapSessions.add(sessionId);
|
|
66
|
+
await client.send('HeapProfiler.enable', {}, sessionId);
|
|
67
|
+
|
|
68
|
+
const chunks: string[] = [];
|
|
69
|
+
const off = client.on('HeapProfiler.addHeapSnapshotChunk', (params, sid) => {
|
|
70
|
+
if (sid !== sessionId) return;
|
|
71
|
+
chunks.push(params.chunk as string);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
await new Promise<void>((resolve, reject) => {
|
|
76
|
+
const timeoutHandle = setTimeout(() => {
|
|
77
|
+
off2();
|
|
78
|
+
reject(new Error('Timeout waiting for heap snapshot (120s)'));
|
|
79
|
+
}, 120_000);
|
|
80
|
+
const off2 = client.on('HeapProfiler.reportHeapSnapshotProgress', (params, sid) => {
|
|
81
|
+
if (sid !== sessionId) return;
|
|
82
|
+
if (params.finished) { clearTimeout(timeoutHandle); off2(); resolve(); }
|
|
83
|
+
});
|
|
84
|
+
client.send('HeapProfiler.takeHeapSnapshot', { reportProgress: true }, sessionId)
|
|
85
|
+
.catch((err) => { clearTimeout(timeoutHandle); off2(); reject(err); });
|
|
86
|
+
});
|
|
87
|
+
} finally {
|
|
88
|
+
off();
|
|
89
|
+
_heapSessions.delete(sessionId);
|
|
90
|
+
await client.send('HeapProfiler.disable', {}, sessionId).catch(() => {});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const path = outputPath ?? join(tmpdir(), `monomind-heap-${Date.now()}.heapsnapshot`);
|
|
94
|
+
await writeFile(path, chunks.join(''));
|
|
95
|
+
return path;
|
|
96
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { CdpClient } from './cdp.js';
|
|
2
|
+
import { writeFile } from 'fs/promises';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { tmpdir } from 'os';
|
|
5
|
+
|
|
6
|
+
export interface RecordOptions {
|
|
7
|
+
path?: string;
|
|
8
|
+
format?: 'jpeg' | 'png' | 'webp';
|
|
9
|
+
quality?: number;
|
|
10
|
+
everyNthFrame?: number;
|
|
11
|
+
maxWidth?: number;
|
|
12
|
+
maxHeight?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface RecordingState {
|
|
16
|
+
frames: string[];
|
|
17
|
+
offScreencast: (() => void) | null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const _sessions = new Map<string, RecordingState>();
|
|
21
|
+
|
|
22
|
+
export async function startRecording(
|
|
23
|
+
client: CdpClient,
|
|
24
|
+
sessionId: string,
|
|
25
|
+
options: RecordOptions = {}
|
|
26
|
+
): Promise<void> {
|
|
27
|
+
if (_sessions.has(sessionId)) {
|
|
28
|
+
throw new Error('Recording already in progress for this session');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const state: RecordingState = { frames: [], offScreencast: null };
|
|
32
|
+
_sessions.set(sessionId, state);
|
|
33
|
+
|
|
34
|
+
state.offScreencast = client.on('Page.screencastFrame', async (params, sid) => {
|
|
35
|
+
if (sid !== sessionId) return;
|
|
36
|
+
const { data, sessionId: frameSessionId } = params as { data: string; sessionId: number };
|
|
37
|
+
state.frames.push(data);
|
|
38
|
+
await client.send('Page.screencastFrameAck', { sessionId: frameSessionId }, sessionId).catch(() => {});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
await client.send('Page.startScreencast', {
|
|
43
|
+
format: options.format ?? 'jpeg',
|
|
44
|
+
quality: options.quality ?? 80,
|
|
45
|
+
everyNthFrame: options.everyNthFrame ?? 1,
|
|
46
|
+
...(options.maxWidth ? { maxWidth: options.maxWidth } : {}),
|
|
47
|
+
...(options.maxHeight ? { maxHeight: options.maxHeight } : {}),
|
|
48
|
+
}, sessionId);
|
|
49
|
+
} catch (err) {
|
|
50
|
+
state.offScreencast?.();
|
|
51
|
+
_sessions.delete(sessionId);
|
|
52
|
+
throw err;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function stopRecording(
|
|
57
|
+
client: CdpClient,
|
|
58
|
+
sessionId: string,
|
|
59
|
+
outputPath?: string
|
|
60
|
+
): Promise<string> {
|
|
61
|
+
const state = _sessions.get(sessionId);
|
|
62
|
+
if (!state) throw new Error('No active recording for this session');
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
await client.send('Page.stopScreencast', {}, sessionId);
|
|
66
|
+
} finally {
|
|
67
|
+
state.offScreencast?.();
|
|
68
|
+
_sessions.delete(sessionId);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const path = outputPath ?? join(tmpdir(), `monomind-screencast-${Date.now()}.frames.json`);
|
|
72
|
+
await writeFile(path, JSON.stringify({ frameCount: state.frames.length, frames: state.frames }));
|
|
73
|
+
return path;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function getRecordingStatus(sessionId: string): { recording: boolean; frames: number } {
|
|
77
|
+
const state = _sessions.get(sessionId);
|
|
78
|
+
return { recording: !!state, frames: state?.frames.length ?? 0 };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function saveFrameAsPng(
|
|
82
|
+
client: CdpClient,
|
|
83
|
+
sessionId: string,
|
|
84
|
+
frameIndex: number,
|
|
85
|
+
outputPath: string
|
|
86
|
+
): Promise<void> {
|
|
87
|
+
const state = _sessions.get(sessionId);
|
|
88
|
+
if (!state || frameIndex >= state.frames.length) {
|
|
89
|
+
throw new Error(`Frame ${frameIndex} not available`);
|
|
90
|
+
}
|
|
91
|
+
await writeFile(outputPath, Buffer.from(state.frames[frameIndex], 'base64'));
|
|
92
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { CdpClient } from './cdp.js';
|
|
2
|
+
import { writeFile, mkdir } from 'fs/promises';
|
|
3
|
+
import { join, dirname } from 'path';
|
|
4
|
+
import { tmpdir } from 'os';
|
|
5
|
+
|
|
6
|
+
export interface ScreenshotOptions {
|
|
7
|
+
path?: string;
|
|
8
|
+
fullPage?: boolean;
|
|
9
|
+
quality?: number;
|
|
10
|
+
format?: 'jpeg' | 'png' | 'webp';
|
|
11
|
+
annotate?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function captureScreenshot(
|
|
15
|
+
client: CdpClient,
|
|
16
|
+
sessionId: string,
|
|
17
|
+
options: ScreenshotOptions = {}
|
|
18
|
+
): Promise<{ path: string; dataUrl: string }> {
|
|
19
|
+
const format = options.format ?? 'png';
|
|
20
|
+
const params: Record<string, unknown> = { format };
|
|
21
|
+
|
|
22
|
+
if (format === 'jpeg' || format === 'webp') {
|
|
23
|
+
params.quality = options.quality ?? 80;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (options.fullPage) {
|
|
27
|
+
const dims = await client.send<{ result: { value: string } }>('Runtime.evaluate', {
|
|
28
|
+
expression: 'JSON.stringify({w: document.documentElement.scrollWidth, h: document.documentElement.scrollHeight})',
|
|
29
|
+
returnByValue: true,
|
|
30
|
+
}, sessionId);
|
|
31
|
+
const { w, h } = JSON.parse(dims.result?.value ?? '{"w":1280,"h":720}');
|
|
32
|
+
params.clip = { x: 0, y: 0, width: w, height: h, scale: 1 };
|
|
33
|
+
params.captureBeyondViewport = true;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const result = await client.send<{ data: string }>('Page.captureScreenshot', params, sessionId);
|
|
37
|
+
const data = result.data;
|
|
38
|
+
const dataUrl = `data:image/${format};base64,${data}`;
|
|
39
|
+
|
|
40
|
+
const outputPath = options.path ?? join(tmpdir(), `monomind-screenshot-${Date.now()}.${format}`);
|
|
41
|
+
await mkdir(dirname(outputPath), { recursive: true });
|
|
42
|
+
await writeFile(outputPath, Buffer.from(data, 'base64'));
|
|
43
|
+
|
|
44
|
+
return { path: outputPath, dataUrl };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function setViewport(
|
|
48
|
+
client: CdpClient,
|
|
49
|
+
sessionId: string,
|
|
50
|
+
width: number,
|
|
51
|
+
height: number
|
|
52
|
+
): Promise<void> {
|
|
53
|
+
await client.send('Emulation.setDeviceMetricsOverride', {
|
|
54
|
+
width,
|
|
55
|
+
height,
|
|
56
|
+
deviceScaleFactor: 1,
|
|
57
|
+
mobile: false,
|
|
58
|
+
}, sessionId);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function setUserAgent(
|
|
62
|
+
client: CdpClient,
|
|
63
|
+
sessionId: string,
|
|
64
|
+
userAgent: string
|
|
65
|
+
): Promise<void> {
|
|
66
|
+
await client.send('Emulation.setUserAgentOverride', { userAgent }, sessionId);
|
|
67
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { readFile, writeFile, mkdir } from 'fs/promises';
|
|
2
|
+
import { join, dirname } from 'path';
|
|
3
|
+
import { homedir } from 'os';
|
|
4
|
+
import type { SessionState, CdpCookie } from './types.js';
|
|
5
|
+
import type { CdpClient } from './cdp.js';
|
|
6
|
+
import { getCookies, setCookies, getLocalStorage, setLocalStorage, getSessionStorage, setSessionStorage } from './network.js';
|
|
7
|
+
|
|
8
|
+
const SESSION_DIR = join(homedir(), '.monomind', 'browser-sessions');
|
|
9
|
+
|
|
10
|
+
function validateSessionName(name: string): void {
|
|
11
|
+
if (!name || /[/\\\x00]/.test(name) || name === '..' || name === '.' || name.startsWith('..')) {
|
|
12
|
+
throw new Error(`Invalid session name: ${JSON.stringify(name)}`);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function validateFilePath(filePath: string): void {
|
|
17
|
+
if (!filePath || filePath.includes('\x00')) {
|
|
18
|
+
throw new Error(`Invalid file path: ${JSON.stringify(filePath)}`);
|
|
19
|
+
}
|
|
20
|
+
if (filePath.includes('/../') || filePath.startsWith('../') || filePath.endsWith('/..') || filePath.includes('\\')) {
|
|
21
|
+
throw new Error(`Invalid file path (traversal not allowed): ${JSON.stringify(filePath)}`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function saveSession(
|
|
26
|
+
client: CdpClient,
|
|
27
|
+
sessionId: string,
|
|
28
|
+
targetId: string,
|
|
29
|
+
name: string,
|
|
30
|
+
url: string,
|
|
31
|
+
title: string
|
|
32
|
+
): Promise<string> {
|
|
33
|
+
validateSessionName(name);
|
|
34
|
+
await mkdir(SESSION_DIR, { recursive: true });
|
|
35
|
+
|
|
36
|
+
const cookies = await getCookies(client, sessionId);
|
|
37
|
+
const localStorage = await getLocalStorage(client, sessionId);
|
|
38
|
+
const sessionStorage = await getSessionStorage(client, sessionId);
|
|
39
|
+
|
|
40
|
+
const state: SessionState = { targetId, sessionId, url, title, cookies, localStorage, sessionStorage };
|
|
41
|
+
const filePath = join(SESSION_DIR, `${name}.json`);
|
|
42
|
+
await writeFile(filePath, JSON.stringify(state, null, 2));
|
|
43
|
+
return filePath;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function loadSession(
|
|
47
|
+
client: CdpClient,
|
|
48
|
+
sessionId: string,
|
|
49
|
+
name: string
|
|
50
|
+
): Promise<SessionState> {
|
|
51
|
+
validateSessionName(name);
|
|
52
|
+
const filePath = join(SESSION_DIR, `${name}.json`);
|
|
53
|
+
const raw = await readFile(filePath, 'utf8').catch((err: NodeJS.ErrnoException) => {
|
|
54
|
+
if (err.code === 'ENOENT') throw new Error(`Session not found: ${name}`);
|
|
55
|
+
throw err;
|
|
56
|
+
});
|
|
57
|
+
const state: SessionState = JSON.parse(raw);
|
|
58
|
+
if (!Array.isArray(state.cookies)) throw new Error(`Invalid session file: cookies is not an array`);
|
|
59
|
+
|
|
60
|
+
await setCookies(client, sessionId, state.cookies);
|
|
61
|
+
if (state.localStorage) {
|
|
62
|
+
if (typeof state.localStorage !== 'object' || Array.isArray(state.localStorage)) throw new Error('Invalid session file: localStorage is not a plain object');
|
|
63
|
+
await setLocalStorage(client, sessionId, state.localStorage);
|
|
64
|
+
}
|
|
65
|
+
if (state.sessionStorage) {
|
|
66
|
+
if (typeof state.sessionStorage !== 'object' || Array.isArray(state.sessionStorage)) throw new Error('Invalid session file: sessionStorage is not a plain object');
|
|
67
|
+
await setSessionStorage(client, sessionId, state.sessionStorage);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return state;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function saveStateFile(
|
|
74
|
+
client: CdpClient,
|
|
75
|
+
sessionId: string,
|
|
76
|
+
targetId: string,
|
|
77
|
+
filePath: string,
|
|
78
|
+
url: string,
|
|
79
|
+
title: string
|
|
80
|
+
): Promise<void> {
|
|
81
|
+
validateFilePath(filePath);
|
|
82
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
83
|
+
const cookies = await getCookies(client, sessionId);
|
|
84
|
+
const localStorage = await getLocalStorage(client, sessionId);
|
|
85
|
+
const sessionStorage = await getSessionStorage(client, sessionId);
|
|
86
|
+
const state: SessionState = { targetId, sessionId, url, title, cookies, localStorage, sessionStorage };
|
|
87
|
+
await writeFile(filePath, JSON.stringify(state, null, 2));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function loadStateFile(
|
|
91
|
+
client: CdpClient,
|
|
92
|
+
sessionId: string,
|
|
93
|
+
filePath: string
|
|
94
|
+
): Promise<SessionState> {
|
|
95
|
+
validateFilePath(filePath);
|
|
96
|
+
const raw = await readFile(filePath, 'utf8');
|
|
97
|
+
const state: SessionState = JSON.parse(raw);
|
|
98
|
+
if (!Array.isArray(state.cookies)) throw new Error(`Invalid state file: cookies is not an array`);
|
|
99
|
+
await setCookies(client, sessionId, state.cookies);
|
|
100
|
+
if (state.localStorage) {
|
|
101
|
+
if (typeof state.localStorage !== 'object' || Array.isArray(state.localStorage)) throw new Error('Invalid state file: localStorage is not a plain object');
|
|
102
|
+
await setLocalStorage(client, sessionId, state.localStorage);
|
|
103
|
+
}
|
|
104
|
+
if (state.sessionStorage) {
|
|
105
|
+
if (typeof state.sessionStorage !== 'object' || Array.isArray(state.sessionStorage)) throw new Error('Invalid state file: sessionStorage is not a plain object');
|
|
106
|
+
await setSessionStorage(client, sessionId, state.sessionStorage);
|
|
107
|
+
}
|
|
108
|
+
return state;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export async function listSessions(): Promise<string[]> {
|
|
112
|
+
const { readdir } = await import('fs/promises');
|
|
113
|
+
const files = await readdir(SESSION_DIR).catch((err: NodeJS.ErrnoException) => {
|
|
114
|
+
if (err.code === 'ENOENT') return [] as string[];
|
|
115
|
+
throw err;
|
|
116
|
+
});
|
|
117
|
+
return files.filter((f) => f.endsWith('.json')).map((f) => f.replace(/\.json$/, ''));
|
|
118
|
+
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import type { CdpClient } from './cdp.js';
|
|
2
|
+
import type { ElementRef, SnapshotOptions, SnapshotResult } from './types.js';
|
|
3
|
+
import { INTERACTIVE_ROLES } from './types.js';
|
|
4
|
+
import { getCurrentUrl, getCurrentTitle } from './browser.js';
|
|
5
|
+
|
|
6
|
+
interface AXNode {
|
|
7
|
+
nodeId: number;
|
|
8
|
+
role?: { value: string };
|
|
9
|
+
name?: { value: string };
|
|
10
|
+
description?: { value: string };
|
|
11
|
+
value?: { value: string };
|
|
12
|
+
properties?: Array<{ name: string; value: { value: unknown } }>;
|
|
13
|
+
backendDOMNodeId?: number;
|
|
14
|
+
parentId?: number;
|
|
15
|
+
childIds?: number[];
|
|
16
|
+
ignored?: boolean;
|
|
17
|
+
ignoredReasons?: unknown[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function captureSnapshot(
|
|
21
|
+
client: CdpClient,
|
|
22
|
+
sessionId: string,
|
|
23
|
+
options: SnapshotOptions = {}
|
|
24
|
+
): Promise<SnapshotResult> {
|
|
25
|
+
const { interactiveOnly = false, compact = false, maxDepth, selector } = options;
|
|
26
|
+
|
|
27
|
+
// If a selector scope is requested, resolve it to a backendDOMNodeId and use getPartialAXTree
|
|
28
|
+
let nodes: AXNode[];
|
|
29
|
+
if (selector) {
|
|
30
|
+
const doc = await client.send<{ root: { nodeId: number } }>('DOM.getDocument', {}, sessionId);
|
|
31
|
+
const found = await client.send<{ nodeId: number }>(
|
|
32
|
+
'DOM.querySelector',
|
|
33
|
+
{ nodeId: doc.root.nodeId, selector },
|
|
34
|
+
sessionId
|
|
35
|
+
).catch(() => ({ nodeId: 0 }));
|
|
36
|
+
|
|
37
|
+
if (found.nodeId) {
|
|
38
|
+
const partial = await client.send<{ nodes: AXNode[] }>(
|
|
39
|
+
'Accessibility.getPartialAXTree',
|
|
40
|
+
{ nodeId: found.nodeId, fetchRelatives: false },
|
|
41
|
+
sessionId
|
|
42
|
+
).catch(async () => client.send<{ nodes: AXNode[] }>('Accessibility.getFullAXTree', {}, sessionId));
|
|
43
|
+
nodes = partial.nodes;
|
|
44
|
+
} else {
|
|
45
|
+
const full = await client.send<{ nodes: AXNode[] }>('Accessibility.getFullAXTree', {}, sessionId);
|
|
46
|
+
nodes = full.nodes;
|
|
47
|
+
}
|
|
48
|
+
} else {
|
|
49
|
+
const full = await client.send<{ nodes: AXNode[] }>('Accessibility.getFullAXTree', {}, sessionId);
|
|
50
|
+
nodes = full.nodes;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const url = await getCurrentUrl(client, sessionId);
|
|
54
|
+
const title = await getCurrentTitle(client, sessionId);
|
|
55
|
+
|
|
56
|
+
const refs = new Map<string, ElementRef>();
|
|
57
|
+
const nodeMap = new Map<number, AXNode>();
|
|
58
|
+
for (const node of nodes) nodeMap.set(node.nodeId, node);
|
|
59
|
+
|
|
60
|
+
let refCounter = 1;
|
|
61
|
+
const lines: string[] = [];
|
|
62
|
+
|
|
63
|
+
const processNode = (node: AXNode, depth: number) => {
|
|
64
|
+
// Transparent/structural nodes don't consume depth budget — check before maxDepth
|
|
65
|
+
if (node.ignored) {
|
|
66
|
+
if (node.childIds) {
|
|
67
|
+
for (const childId of node.childIds) {
|
|
68
|
+
const child = nodeMap.get(childId);
|
|
69
|
+
if (child) processNode(child, depth);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const role = node.role?.value ?? 'generic';
|
|
76
|
+
|
|
77
|
+
if (role === 'none' || role === 'generic' || role === 'inlineTextBox') {
|
|
78
|
+
if (node.childIds) {
|
|
79
|
+
for (const childId of node.childIds) {
|
|
80
|
+
const child = nodeMap.get(childId);
|
|
81
|
+
if (child) processNode(child, depth);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Only enforce maxDepth for real rendered roles
|
|
88
|
+
if (maxDepth !== undefined && depth > maxDepth) return;
|
|
89
|
+
|
|
90
|
+
const name = node.name?.value ?? '';
|
|
91
|
+
const description = node.description?.value;
|
|
92
|
+
|
|
93
|
+
const isInteractive = INTERACTIVE_ROLES.has(role.toLowerCase());
|
|
94
|
+
|
|
95
|
+
if (interactiveOnly && !isInteractive) {
|
|
96
|
+
if (node.childIds) {
|
|
97
|
+
for (const childId of node.childIds) {
|
|
98
|
+
const child = nodeMap.get(childId);
|
|
99
|
+
if (child) processNode(child, depth);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const props = extractProperties(node);
|
|
106
|
+
const refKey = `e${refCounter++}`;
|
|
107
|
+
const elemRef: ElementRef = {
|
|
108
|
+
ref: refKey,
|
|
109
|
+
role,
|
|
110
|
+
name,
|
|
111
|
+
description,
|
|
112
|
+
placeholder: props.placeholder,
|
|
113
|
+
value: props.value,
|
|
114
|
+
disabled: props.disabled,
|
|
115
|
+
checked: props.checked,
|
|
116
|
+
expanded: props.expanded,
|
|
117
|
+
nodeId: node.nodeId,
|
|
118
|
+
backendDOMNodeId: node.backendDOMNodeId,
|
|
119
|
+
};
|
|
120
|
+
refs.set(refKey, elemRef);
|
|
121
|
+
|
|
122
|
+
const indent = compact ? '' : ' '.repeat(depth);
|
|
123
|
+
const attrParts: string[] = [`ref=${refKey}`];
|
|
124
|
+
if (props.value !== undefined) attrParts.push(`value="${props.value}"`);
|
|
125
|
+
if (props.disabled) attrParts.push('disabled');
|
|
126
|
+
if (props.checked !== undefined) attrParts.push(`checked=${props.checked}`);
|
|
127
|
+
if (props.expanded !== undefined) attrParts.push(`expanded=${props.expanded}`);
|
|
128
|
+
if (props.placeholder) attrParts.push(`placeholder="${props.placeholder}"`);
|
|
129
|
+
if (props.required) attrParts.push('required');
|
|
130
|
+
|
|
131
|
+
const nameStr = name ? ` "${name}"` : '';
|
|
132
|
+
const descStr = description && !compact ? ` (${description})` : '';
|
|
133
|
+
const attrsStr = ` [${attrParts.join(', ')}]`;
|
|
134
|
+
|
|
135
|
+
lines.push(`${indent}${role}${nameStr}${descStr}${attrsStr}`);
|
|
136
|
+
|
|
137
|
+
if (node.childIds) {
|
|
138
|
+
for (const childId of node.childIds) {
|
|
139
|
+
const child = nodeMap.get(childId);
|
|
140
|
+
if (child) processNode(child, depth + 1);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
// Find root nodes — for partial trees parentId may point outside the set; use filter to handle forests
|
|
146
|
+
const ids = new Set(nodes.map((n) => n.nodeId));
|
|
147
|
+
const roots = nodes.filter((n) => n.parentId === undefined || !ids.has(n.parentId));
|
|
148
|
+
for (const root of roots) processNode(root, 0);
|
|
149
|
+
|
|
150
|
+
return { text: lines.join('\n'), refs, url, title };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function extractProperties(node: AXNode): {
|
|
154
|
+
value?: string;
|
|
155
|
+
disabled?: boolean;
|
|
156
|
+
checked?: boolean;
|
|
157
|
+
expanded?: boolean;
|
|
158
|
+
placeholder?: string;
|
|
159
|
+
required?: boolean;
|
|
160
|
+
} {
|
|
161
|
+
const result: ReturnType<typeof extractProperties> = {};
|
|
162
|
+
if (!node.properties) return result;
|
|
163
|
+
|
|
164
|
+
for (const prop of node.properties) {
|
|
165
|
+
switch (prop.name) {
|
|
166
|
+
case 'value':
|
|
167
|
+
if (prop.value.value != null) result.value = String(prop.value.value);
|
|
168
|
+
break;
|
|
169
|
+
case 'disabled':
|
|
170
|
+
result.disabled = Boolean(prop.value.value);
|
|
171
|
+
break;
|
|
172
|
+
case 'checked':
|
|
173
|
+
result.checked = prop.value.value === 'true' || prop.value.value === true;
|
|
174
|
+
break;
|
|
175
|
+
case 'expanded':
|
|
176
|
+
result.expanded = prop.value.value === 'true' || prop.value.value === true;
|
|
177
|
+
break;
|
|
178
|
+
case 'placeholder':
|
|
179
|
+
result.placeholder = String(prop.value.value ?? '');
|
|
180
|
+
break;
|
|
181
|
+
case 'required':
|
|
182
|
+
result.required = Boolean(prop.value.value);
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (node.value?.value !== undefined && result.value === undefined) {
|
|
188
|
+
result.value = String(node.value.value);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return result;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export async function resolveRef(
|
|
195
|
+
client: CdpClient,
|
|
196
|
+
sessionId: string,
|
|
197
|
+
refs: Map<string, ElementRef>,
|
|
198
|
+
refKey: string
|
|
199
|
+
): Promise<ElementRef> {
|
|
200
|
+
const ref = refs.get(refKey);
|
|
201
|
+
if (!ref) throw new Error(`Element ref @${refKey} not found. Run snapshot first.`);
|
|
202
|
+
return ref;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export async function getObjectIdForRef(
|
|
206
|
+
client: CdpClient,
|
|
207
|
+
sessionId: string,
|
|
208
|
+
ref: ElementRef
|
|
209
|
+
): Promise<string | null> {
|
|
210
|
+
if (!ref.backendDOMNodeId) return null;
|
|
211
|
+
|
|
212
|
+
const result = await client.send<{ object: { objectId?: string } }>(
|
|
213
|
+
'DOM.resolveNode',
|
|
214
|
+
{ backendNodeId: ref.backendDOMNodeId },
|
|
215
|
+
sessionId
|
|
216
|
+
);
|
|
217
|
+
return result.object?.objectId ?? null;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export async function getElementBox(
|
|
221
|
+
client: CdpClient,
|
|
222
|
+
sessionId: string,
|
|
223
|
+
ref: ElementRef
|
|
224
|
+
): Promise<{ x: number; y: number; width: number; height: number } | null> {
|
|
225
|
+
if (!ref.backendDOMNodeId) return null;
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
const result = await client.send<{
|
|
229
|
+
model: {
|
|
230
|
+
content: number[];
|
|
231
|
+
border: number[];
|
|
232
|
+
margin: number[];
|
|
233
|
+
padding: number[];
|
|
234
|
+
width: number;
|
|
235
|
+
height: number;
|
|
236
|
+
};
|
|
237
|
+
}>('DOM.getBoxModel', { backendNodeId: ref.backendDOMNodeId }, sessionId);
|
|
238
|
+
|
|
239
|
+
const content = result.model?.content;
|
|
240
|
+
if (!content || content.length < 8) return null;
|
|
241
|
+
|
|
242
|
+
const x = (content[0] + content[2] + content[4] + content[6]) / 4;
|
|
243
|
+
const y = (content[1] + content[3] + content[5] + content[7]) / 4;
|
|
244
|
+
return { x, y, width: result.model.width, height: result.model.height };
|
|
245
|
+
} catch {
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
}
|