@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.
Files changed (208) hide show
  1. package/dist/src/browser/action-builder/analyzer.d.ts +11 -0
  2. package/dist/src/browser/action-builder/analyzer.d.ts.map +1 -0
  3. package/dist/src/browser/action-builder/analyzer.js +93 -0
  4. package/dist/src/browser/action-builder/analyzer.js.map +1 -0
  5. package/dist/src/browser/action-builder/types.d.ts +46 -0
  6. package/dist/src/browser/action-builder/types.d.ts.map +1 -0
  7. package/dist/src/browser/action-builder/types.js +2 -0
  8. package/dist/src/browser/action-builder/types.js.map +1 -0
  9. package/dist/src/browser/actions.d.ts +29 -0
  10. package/dist/src/browser/actions.d.ts.map +1 -0
  11. package/dist/src/browser/actions.js +398 -0
  12. package/dist/src/browser/actions.js.map +1 -0
  13. package/dist/src/browser/adapters/gemini.d.ts +3 -0
  14. package/dist/src/browser/adapters/gemini.d.ts.map +1 -0
  15. package/dist/src/browser/adapters/gemini.js +13 -0
  16. package/dist/src/browser/adapters/gemini.js.map +1 -0
  17. package/dist/src/browser/adapters/google.d.ts +3 -0
  18. package/dist/src/browser/adapters/google.d.ts.map +1 -0
  19. package/dist/src/browser/adapters/google.js +17 -0
  20. package/dist/src/browser/adapters/google.js.map +1 -0
  21. package/dist/src/browser/adapters/index.d.ts +15 -0
  22. package/dist/src/browser/adapters/index.d.ts.map +1 -0
  23. package/dist/src/browser/adapters/index.js +18 -0
  24. package/dist/src/browser/adapters/index.js.map +1 -0
  25. package/dist/src/browser/adapters/instagram.d.ts +3 -0
  26. package/dist/src/browser/adapters/instagram.d.ts.map +1 -0
  27. package/dist/src/browser/adapters/instagram.js +14 -0
  28. package/dist/src/browser/adapters/instagram.js.map +1 -0
  29. package/dist/src/browser/adapters/linkedin.d.ts +3 -0
  30. package/dist/src/browser/adapters/linkedin.d.ts.map +1 -0
  31. package/dist/src/browser/adapters/linkedin.js +13 -0
  32. package/dist/src/browser/adapters/linkedin.js.map +1 -0
  33. package/dist/src/browser/adapters/microsoft.d.ts +3 -0
  34. package/dist/src/browser/adapters/microsoft.d.ts.map +1 -0
  35. package/dist/src/browser/adapters/microsoft.js +16 -0
  36. package/dist/src/browser/adapters/microsoft.js.map +1 -0
  37. package/dist/src/browser/adapters/x.d.ts +3 -0
  38. package/dist/src/browser/adapters/x.d.ts.map +1 -0
  39. package/dist/src/browser/adapters/x.js +13 -0
  40. package/dist/src/browser/adapters/x.js.map +1 -0
  41. package/dist/src/browser/batch.d.ts +5 -0
  42. package/dist/src/browser/batch.d.ts.map +1 -0
  43. package/dist/src/browser/batch.js +2 -0
  44. package/dist/src/browser/batch.js.map +1 -0
  45. package/dist/src/browser/browser.d.ts +15 -0
  46. package/dist/src/browser/browser.d.ts.map +1 -0
  47. package/dist/src/browser/browser.js +256 -0
  48. package/dist/src/browser/browser.js.map +1 -0
  49. package/dist/src/browser/cdp.d.ts +18 -0
  50. package/dist/src/browser/cdp.d.ts.map +1 -0
  51. package/dist/src/browser/cdp.js +153 -0
  52. package/dist/src/browser/cdp.js.map +1 -0
  53. package/dist/src/browser/console-log.d.ts +23 -0
  54. package/dist/src/browser/console-log.d.ts.map +1 -0
  55. package/dist/src/browser/console-log.js +104 -0
  56. package/dist/src/browser/console-log.js.map +1 -0
  57. package/dist/src/browser/dashboard/api-types.d.ts +50 -0
  58. package/dist/src/browser/dashboard/api-types.d.ts.map +1 -0
  59. package/dist/src/browser/dashboard/api-types.js +14 -0
  60. package/dist/src/browser/dashboard/api-types.js.map +1 -0
  61. package/dist/src/browser/dashboard/server.d.ts +11 -0
  62. package/dist/src/browser/dashboard/server.d.ts.map +1 -0
  63. package/dist/src/browser/dashboard/server.js +191 -0
  64. package/dist/src/browser/dashboard/server.js.map +1 -0
  65. package/dist/src/browser/dialog.d.ts +12 -0
  66. package/dist/src/browser/dialog.d.ts.map +1 -0
  67. package/dist/src/browser/dialog.js +53 -0
  68. package/dist/src/browser/dialog.js.map +1 -0
  69. package/dist/src/browser/emulation.d.ts +15 -0
  70. package/dist/src/browser/emulation.d.ts.map +1 -0
  71. package/dist/src/browser/emulation.js +62 -0
  72. package/dist/src/browser/emulation.js.map +1 -0
  73. package/dist/src/browser/find.d.ts +22 -0
  74. package/dist/src/browser/find.d.ts.map +1 -0
  75. package/dist/src/browser/find.js +169 -0
  76. package/dist/src/browser/find.js.map +1 -0
  77. package/dist/src/browser/har.d.ts +27 -0
  78. package/dist/src/browser/har.d.ts.map +1 -0
  79. package/dist/src/browser/har.js +161 -0
  80. package/dist/src/browser/har.js.map +1 -0
  81. package/dist/src/browser/index.d.ts +23 -0
  82. package/dist/src/browser/index.d.ts.map +1 -0
  83. package/dist/src/browser/index.js +23 -0
  84. package/dist/src/browser/index.js.map +1 -0
  85. package/dist/src/browser/network.d.ts +32 -0
  86. package/dist/src/browser/network.d.ts.map +1 -0
  87. package/dist/src/browser/network.js +214 -0
  88. package/dist/src/browser/network.js.map +1 -0
  89. package/dist/src/browser/pdf.d.ts +15 -0
  90. package/dist/src/browser/pdf.d.ts.map +1 -0
  91. package/dist/src/browser/pdf.js +27 -0
  92. package/dist/src/browser/pdf.js.map +1 -0
  93. package/dist/src/browser/profiler.d.ts +10 -0
  94. package/dist/src/browser/profiler.d.ts.map +1 -0
  95. package/dist/src/browser/profiler.js +77 -0
  96. package/dist/src/browser/profiler.js.map +1 -0
  97. package/dist/src/browser/record.d.ts +21 -0
  98. package/dist/src/browser/record.d.ts.map +1 -0
  99. package/dist/src/browser/record.js +59 -0
  100. package/dist/src/browser/record.js.map +1 -0
  101. package/dist/src/browser/screenshot.d.ts +15 -0
  102. package/dist/src/browser/screenshot.d.ts.map +1 -0
  103. package/dist/src/browser/screenshot.js +38 -0
  104. package/dist/src/browser/screenshot.js.map +1 -0
  105. package/dist/src/browser/session.d.ts +8 -0
  106. package/dist/src/browser/session.d.ts.map +1 -0
  107. package/dist/src/browser/session.js +91 -0
  108. package/dist/src/browser/session.js.map +1 -0
  109. package/dist/src/browser/snapshot.d.ts +12 -0
  110. package/dist/src/browser/snapshot.d.ts.map +1 -0
  111. package/dist/src/browser/snapshot.js +179 -0
  112. package/dist/src/browser/snapshot.js.map +1 -0
  113. package/dist/src/browser/storage.d.ts +12 -0
  114. package/dist/src/browser/storage.d.ts.map +1 -0
  115. package/dist/src/browser/storage.js +46 -0
  116. package/dist/src/browser/storage.js.map +1 -0
  117. package/dist/src/browser/tabs.d.ts +8 -0
  118. package/dist/src/browser/tabs.d.ts.map +1 -0
  119. package/dist/src/browser/tabs.js +25 -0
  120. package/dist/src/browser/tabs.js.map +1 -0
  121. package/dist/src/browser/trace.d.ts +10 -0
  122. package/dist/src/browser/trace.d.ts.map +1 -0
  123. package/dist/src/browser/trace.js +87 -0
  124. package/dist/src/browser/trace.js.map +1 -0
  125. package/dist/src/browser/types.d.ts +111 -0
  126. package/dist/src/browser/types.d.ts.map +1 -0
  127. package/dist/src/browser/types.js +16 -0
  128. package/dist/src/browser/types.js.map +1 -0
  129. package/dist/src/browser/vitals.d.ts +15 -0
  130. package/dist/src/browser/vitals.d.ts.map +1 -0
  131. package/dist/src/browser/vitals.js +116 -0
  132. package/dist/src/browser/vitals.js.map +1 -0
  133. package/dist/src/browser/wait.d.ts +4 -0
  134. package/dist/src/browser/wait.d.ts.map +1 -0
  135. package/dist/src/browser/wait.js +146 -0
  136. package/dist/src/browser/wait.js.map +1 -0
  137. package/dist/src/browser/workflow/builtin-handlers.d.ts +3 -0
  138. package/dist/src/browser/workflow/builtin-handlers.d.ts.map +1 -0
  139. package/dist/src/browser/workflow/builtin-handlers.js +343 -0
  140. package/dist/src/browser/workflow/builtin-handlers.js.map +1 -0
  141. package/dist/src/browser/workflow/engine.d.ts +10 -0
  142. package/dist/src/browser/workflow/engine.d.ts.map +1 -0
  143. package/dist/src/browser/workflow/engine.js +174 -0
  144. package/dist/src/browser/workflow/engine.js.map +1 -0
  145. package/dist/src/browser/workflow/expression.d.ts +4 -0
  146. package/dist/src/browser/workflow/expression.d.ts.map +1 -0
  147. package/dist/src/browser/workflow/expression.js +89 -0
  148. package/dist/src/browser/workflow/expression.js.map +1 -0
  149. package/dist/src/browser/workflow/index.d.ts +6 -0
  150. package/dist/src/browser/workflow/index.d.ts.map +1 -0
  151. package/dist/src/browser/workflow/index.js +6 -0
  152. package/dist/src/browser/workflow/index.js.map +1 -0
  153. package/dist/src/browser/workflow/store.d.ts +8 -0
  154. package/dist/src/browser/workflow/store.d.ts.map +1 -0
  155. package/dist/src/browser/workflow/store.js +87 -0
  156. package/dist/src/browser/workflow/store.js.map +1 -0
  157. package/dist/src/browser/workflow/types.d.ts +62 -0
  158. package/dist/src/browser/workflow/types.d.ts.map +1 -0
  159. package/dist/src/browser/workflow/types.js +2 -0
  160. package/dist/src/browser/workflow/types.js.map +1 -0
  161. package/dist/src/index.d.ts +7 -0
  162. package/dist/src/index.d.ts.map +1 -0
  163. package/dist/src/index.js +7 -0
  164. package/dist/src/index.js.map +1 -0
  165. package/dist/tsconfig.tsbuildinfo +1 -0
  166. package/package.json +56 -0
  167. package/src/browser/action-builder/analyzer.ts +127 -0
  168. package/src/browser/action-builder/types.ts +17 -0
  169. package/src/browser/actions.ts +518 -0
  170. package/src/browser/adapters/gemini.ts +15 -0
  171. package/src/browser/adapters/google.ts +20 -0
  172. package/src/browser/adapters/index.ts +33 -0
  173. package/src/browser/adapters/instagram.ts +16 -0
  174. package/src/browser/adapters/linkedin.ts +15 -0
  175. package/src/browser/adapters/microsoft.ts +19 -0
  176. package/src/browser/adapters/x.ts +15 -0
  177. package/src/browser/batch.ts +4 -0
  178. package/src/browser/browser.ts +265 -0
  179. package/src/browser/cdp.ts +156 -0
  180. package/src/browser/console-log.ts +120 -0
  181. package/src/browser/dashboard/api-types.ts +71 -0
  182. package/src/browser/dashboard/server.ts +206 -0
  183. package/src/browser/dashboard/ui.html +444 -0
  184. package/src/browser/dialog.ts +63 -0
  185. package/src/browser/emulation.ts +94 -0
  186. package/src/browser/find.ts +225 -0
  187. package/src/browser/har.ts +202 -0
  188. package/src/browser/index.ts +22 -0
  189. package/src/browser/network.ts +244 -0
  190. package/src/browser/pdf.ts +41 -0
  191. package/src/browser/profiler.ts +96 -0
  192. package/src/browser/record.ts +92 -0
  193. package/src/browser/screenshot.ts +67 -0
  194. package/src/browser/session.ts +118 -0
  195. package/src/browser/snapshot.ts +248 -0
  196. package/src/browser/storage.ts +54 -0
  197. package/src/browser/tabs.ts +37 -0
  198. package/src/browser/trace.ts +106 -0
  199. package/src/browser/types.ts +135 -0
  200. package/src/browser/vitals.ts +122 -0
  201. package/src/browser/wait.ts +178 -0
  202. package/src/browser/workflow/builtin-handlers.ts +379 -0
  203. package/src/browser/workflow/engine.ts +202 -0
  204. package/src/browser/workflow/expression.ts +107 -0
  205. package/src/browser/workflow/index.ts +5 -0
  206. package/src/browser/workflow/store.ts +87 -0
  207. package/src/browser/workflow/types.ts +62 -0
  208. package/src/index.ts +6 -0
@@ -0,0 +1,54 @@
1
+ import type { CdpClient } from './cdp.js';
2
+ import { evaluateJs } from './actions.js';
3
+
4
+ export async function getLocalStorageKey(client: CdpClient, sessionId: string, key: string): Promise<string | null> {
5
+ const result = await evaluateJs(client, sessionId, `localStorage.getItem(${JSON.stringify(key)})`);
6
+ return result as string | null;
7
+ }
8
+
9
+ export async function setLocalStorageKey(client: CdpClient, sessionId: string, key: string, value: string): Promise<void> {
10
+ await evaluateJs(client, sessionId, `localStorage.setItem(${JSON.stringify(key)}, ${JSON.stringify(value)})`);
11
+ }
12
+
13
+ export async function removeLocalStorageKey(client: CdpClient, sessionId: string, key: string): Promise<void> {
14
+ await evaluateJs(client, sessionId, `localStorage.removeItem(${JSON.stringify(key)})`);
15
+ }
16
+
17
+ export async function clearLocalStorage(client: CdpClient, sessionId: string): Promise<void> {
18
+ await evaluateJs(client, sessionId, 'localStorage.clear()');
19
+ }
20
+
21
+ export async function getAllLocalStorage(client: CdpClient, sessionId: string): Promise<Record<string, string>> {
22
+ const result = await evaluateJs(
23
+ client,
24
+ sessionId,
25
+ 'JSON.stringify(Object.fromEntries(Object.entries(localStorage)))'
26
+ );
27
+ try { return JSON.parse(result as string); } catch { return {}; }
28
+ }
29
+
30
+ export async function getSessionStorageKey(client: CdpClient, sessionId: string, key: string): Promise<string | null> {
31
+ const result = await evaluateJs(client, sessionId, `sessionStorage.getItem(${JSON.stringify(key)})`);
32
+ return result as string | null;
33
+ }
34
+
35
+ export async function setSessionStorageKey(client: CdpClient, sessionId: string, key: string, value: string): Promise<void> {
36
+ await evaluateJs(client, sessionId, `sessionStorage.setItem(${JSON.stringify(key)}, ${JSON.stringify(value)})`);
37
+ }
38
+
39
+ export async function removeSessionStorageKey(client: CdpClient, sessionId: string, key: string): Promise<void> {
40
+ await evaluateJs(client, sessionId, `sessionStorage.removeItem(${JSON.stringify(key)})`);
41
+ }
42
+
43
+ export async function clearSessionStorage(client: CdpClient, sessionId: string): Promise<void> {
44
+ await evaluateJs(client, sessionId, 'sessionStorage.clear()');
45
+ }
46
+
47
+ export async function getAllSessionStorage(client: CdpClient, sessionId: string): Promise<Record<string, string>> {
48
+ const result = await evaluateJs(
49
+ client,
50
+ sessionId,
51
+ 'JSON.stringify(Object.fromEntries(Object.entries(sessionStorage)))'
52
+ );
53
+ try { return JSON.parse(result as string); } catch { return {}; }
54
+ }
@@ -0,0 +1,37 @@
1
+ import type { CdpClient } from './cdp.js';
2
+ import type { CdpTarget } from './types.js';
3
+ import { fetchTargets, fetchNewTarget } from './cdp.js';
4
+
5
+ export async function listTabs(port: number): Promise<CdpTarget[]> {
6
+ const targets = await fetchTargets(port);
7
+ return targets.filter((t) => t.type === 'page');
8
+ }
9
+
10
+ export async function newTab(port: number, url = 'about:blank'): Promise<CdpTarget> {
11
+ return fetchNewTarget(port, url);
12
+ }
13
+
14
+ export async function closeTab(client: CdpClient, _sessionId: string, targetId: string): Promise<void> {
15
+ await client.send('Target.closeTarget', { targetId });
16
+ }
17
+
18
+ export async function activateTab(client: CdpClient, oldSessionId: string, targetId: string): Promise<string> {
19
+ if (oldSessionId) {
20
+ await client.send('Target.detachFromTarget', { sessionId: oldSessionId }).catch(() => {});
21
+ }
22
+ await client.send('Target.activateTarget', { targetId });
23
+ const result = await client.send<{ sessionId: string }>('Target.attachToTarget', { targetId, flatten: true });
24
+ return result.sessionId;
25
+ }
26
+
27
+ export async function switchToFrame(
28
+ _client: CdpClient,
29
+ _sessionId: string,
30
+ _frameSelector: string
31
+ ): Promise<string | null> {
32
+ throw new Error(
33
+ 'switchToFrame is not yet implemented. ' +
34
+ 'For same-origin frames, CDP commands already apply to the whole page. ' +
35
+ 'For cross-origin (OOPIF) frames, call Target.attachToTarget with the frame target ID.'
36
+ );
37
+ }
@@ -0,0 +1,106 @@
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 TraceOptions {
7
+ path?: string;
8
+ categories?: string[];
9
+ screenshots?: boolean;
10
+ }
11
+
12
+ const DEFAULT_CATEGORIES = [
13
+ '-*',
14
+ 'devtools.timeline',
15
+ 'v8.execute',
16
+ 'disabled-by-default-devtools.timeline',
17
+ 'disabled-by-default-devtools.timeline.frame',
18
+ 'toplevel',
19
+ 'blink.console',
20
+ 'blink.user_timing',
21
+ 'latencyInfo',
22
+ 'disabled-by-default-devtools.timeline.stack',
23
+ ];
24
+
25
+ const _sessions = new Map<string, { events: unknown[]; offData: () => void }>();
26
+
27
+ export async function startTrace(
28
+ client: CdpClient,
29
+ sessionId: string,
30
+ options: TraceOptions = {}
31
+ ): Promise<void> {
32
+ if (_sessions.has(sessionId)) {
33
+ throw new Error('Trace already in progress for this session');
34
+ }
35
+
36
+ const events: unknown[] = [];
37
+
38
+ const offData = client.on('Tracing.dataCollected', (params, sid) => {
39
+ if (sid !== sessionId) return;
40
+ const value = params.value as unknown[];
41
+ if (Array.isArray(value)) events.push(...value);
42
+ });
43
+
44
+ _sessions.set(sessionId, { events, offData });
45
+
46
+ const cats = [...(options.categories ?? DEFAULT_CATEGORIES)];
47
+ if (options.screenshots) cats.push('disabled-by-default-devtools.screenshot');
48
+
49
+ try {
50
+ await client.send('Tracing.start', {
51
+ traceConfig: {
52
+ includedCategories: cats.filter((c) => !c.startsWith('-')),
53
+ excludedCategories: cats.filter((c) => c.startsWith('-')).map((c) => c.slice(1)),
54
+ },
55
+ }, sessionId);
56
+ } catch (err) {
57
+ offData();
58
+ _sessions.delete(sessionId);
59
+ throw err;
60
+ }
61
+ }
62
+
63
+ export async function stopTrace(
64
+ client: CdpClient,
65
+ sessionId: string,
66
+ outputPath?: string
67
+ ): Promise<string> {
68
+ const state = _sessions.get(sessionId);
69
+ if (!state) throw new Error('No active trace for this session');
70
+
71
+ try {
72
+ const [completePromise, cancelOnce] = client.onceWithOff('Tracing.tracingComplete', sessionId);
73
+ let timeoutHandle: ReturnType<typeof setTimeout> | undefined;
74
+ const timeoutPromise = new Promise<void>((_, reject) => {
75
+ timeoutHandle = setTimeout(() => {
76
+ cancelOnce();
77
+ reject(new Error('Timeout waiting for Tracing.tracingComplete'));
78
+ }, 30_000);
79
+ });
80
+ try {
81
+ await client.send('Tracing.end', {}, sessionId);
82
+ await Promise.race([completePromise, timeoutPromise]);
83
+ } catch (err) {
84
+ cancelOnce();
85
+ throw err;
86
+ } finally {
87
+ clearTimeout(timeoutHandle);
88
+ }
89
+ } finally {
90
+ state.offData();
91
+ _sessions.delete(sessionId);
92
+ }
93
+
94
+ const trace = {
95
+ traceEvents: state.events,
96
+ metadata: { 'clock-offset-since-epoch': Date.now() },
97
+ };
98
+
99
+ const path = outputPath ?? join(tmpdir(), `monomind-trace-${Date.now()}.json`);
100
+ await writeFile(path, JSON.stringify(trace));
101
+ return path;
102
+ }
103
+
104
+ export function getTraceStatus(sessionId: string): boolean {
105
+ return _sessions.has(sessionId);
106
+ }
@@ -0,0 +1,135 @@
1
+ export interface BrowserConfig {
2
+ port?: number;
3
+ headless?: boolean;
4
+ executablePath?: string;
5
+ userDataDir?: string;
6
+ args?: string[];
7
+ }
8
+
9
+ export interface SessionState {
10
+ targetId: string;
11
+ sessionId: string;
12
+ url: string;
13
+ title: string;
14
+ cookies: CdpCookie[];
15
+ localStorage?: Record<string, string>;
16
+ sessionStorage?: Record<string, string>;
17
+ }
18
+
19
+ export interface CdpCookie {
20
+ name: string;
21
+ value: string;
22
+ domain?: string;
23
+ path?: string;
24
+ expires?: number;
25
+ httpOnly?: boolean;
26
+ secure?: boolean;
27
+ sameSite?: 'Strict' | 'Lax' | 'None';
28
+ }
29
+
30
+ export interface ElementRef {
31
+ ref: string;
32
+ role: string;
33
+ name: string;
34
+ description?: string;
35
+ placeholder?: string;
36
+ value?: string;
37
+ disabled?: boolean;
38
+ checked?: boolean;
39
+ expanded?: boolean;
40
+ nodeId: number;
41
+ backendDOMNodeId?: number;
42
+ objectId?: string;
43
+ }
44
+
45
+ export interface SnapshotOptions {
46
+ interactiveOnly?: boolean;
47
+ compact?: boolean;
48
+ maxDepth?: number;
49
+ selector?: string;
50
+ }
51
+
52
+ export interface SnapshotResult {
53
+ text: string;
54
+ refs: Map<string, ElementRef>;
55
+ url: string;
56
+ title: string;
57
+ }
58
+
59
+ export interface ClickOptions {
60
+ button?: 'left' | 'right' | 'middle';
61
+ clickCount?: number;
62
+ modifiers?: number;
63
+ }
64
+
65
+ export interface WaitOptions {
66
+ timeout?: number;
67
+ url?: string;
68
+ text?: string;
69
+ selector?: string;
70
+ load?: 'load' | 'networkidle' | 'domcontentloaded';
71
+ }
72
+
73
+ export interface NetworkRoute {
74
+ pattern: string;
75
+ action: 'abort' | 'fulfill' | 'continue';
76
+ response?: {
77
+ status?: number;
78
+ headers?: Record<string, string>;
79
+ body?: string;
80
+ };
81
+ }
82
+
83
+ export interface ViewportSize {
84
+ width: number;
85
+ height: number;
86
+ }
87
+
88
+ export interface CdpTarget {
89
+ id: string;
90
+ type: string;
91
+ title: string;
92
+ url: string;
93
+ webSocketDebuggerUrl?: string;
94
+ }
95
+
96
+ export interface CdpCommand {
97
+ id: number;
98
+ method: string;
99
+ params?: Record<string, unknown>;
100
+ sessionId?: string;
101
+ }
102
+
103
+ export interface CdpResponse {
104
+ id?: number;
105
+ method?: string;
106
+ params?: Record<string, unknown>;
107
+ result?: Record<string, unknown>;
108
+ error?: { code: number; message: string };
109
+ sessionId?: string;
110
+ }
111
+
112
+ export interface BrowserSession {
113
+ targetId: string;
114
+ sessionId: string;
115
+ refs: Map<string, ElementRef>;
116
+ routes: NetworkRoute[];
117
+ viewport: ViewportSize;
118
+ }
119
+
120
+ export const INTERACTIVE_ROLES = new Set([
121
+ 'button', 'link', 'textbox', 'combobox', 'listbox', 'checkbox', 'radio',
122
+ 'slider', 'spinbutton', 'searchbox', 'option', 'menuitem', 'tab', 'switch',
123
+ 'treeitem', 'menuitemcheckbox', 'menuitemradio', 'row', 'gridcell', 'cell',
124
+ 'columnheader', 'rowheader', 'scrollbar',
125
+ ]);
126
+
127
+ export const CHROME_EXECUTABLES = [
128
+ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
129
+ '/Applications/Chromium.app/Contents/MacOS/Chromium',
130
+ '/usr/bin/google-chrome',
131
+ '/usr/bin/chromium-browser',
132
+ '/usr/bin/chromium',
133
+ 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
134
+ 'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
135
+ ];
@@ -0,0 +1,122 @@
1
+ import type { CdpClient } from './cdp.js';
2
+ import { evaluateJs } from './actions.js';
3
+
4
+ export interface WebVitals {
5
+ lcp?: number;
6
+ fcp?: number;
7
+ cls?: number;
8
+ ttfb?: number;
9
+ inp?: number;
10
+ domInteractive?: number;
11
+ domContentLoaded?: number;
12
+ loadTime?: number;
13
+ resources?: number;
14
+ }
15
+
16
+ export async function collectVitals(
17
+ client: CdpClient,
18
+ sessionId: string,
19
+ waitMs = 2000
20
+ ): Promise<WebVitals> {
21
+ // Inject PerformanceObserver collectors and wait for data
22
+ const script = `
23
+ new Promise((resolve) => {
24
+ const vitals = {};
25
+
26
+ // Navigation timing
27
+ const nav = performance.getEntriesByType('navigation')[0];
28
+ if (nav) {
29
+ vitals.ttfb = nav.responseStart - nav.requestStart;
30
+ vitals.domInteractive = nav.domInteractive;
31
+ vitals.domContentLoaded = nav.domContentLoadedEventEnd;
32
+ vitals.loadTime = nav.loadEventEnd;
33
+ vitals.resources = performance.getEntriesByType('resource').length;
34
+ }
35
+
36
+ // FCP via paint entries
37
+ const paintEntries = performance.getEntriesByType('paint');
38
+ const fcp = paintEntries.find(e => e.name === 'first-contentful-paint');
39
+ if (fcp) vitals.fcp = fcp.startTime;
40
+
41
+ // LCP
42
+ let lcpValue = 0;
43
+ let lcpObs;
44
+ try {
45
+ lcpObs = new PerformanceObserver((list) => {
46
+ const entries = list.getEntries();
47
+ if (entries.length > 0) {
48
+ lcpValue = entries[entries.length - 1].startTime;
49
+ vitals.lcp = lcpValue;
50
+ }
51
+ });
52
+ lcpObs.observe({ type: 'largest-contentful-paint', buffered: true });
53
+ } catch(e) {}
54
+
55
+ // CLS
56
+ let clsValue = 0;
57
+ let clsObs;
58
+ try {
59
+ clsObs = new PerformanceObserver((list) => {
60
+ for (const entry of list.getEntries()) {
61
+ if (!entry.hadRecentInput) {
62
+ clsValue += entry.value;
63
+ }
64
+ }
65
+ vitals.cls = clsValue;
66
+ });
67
+ clsObs.observe({ type: 'layout-shift', buffered: true });
68
+ } catch(e) {}
69
+
70
+ // INP
71
+ let inpValue = 0;
72
+ let inpObs;
73
+ try {
74
+ inpObs = new PerformanceObserver((list) => {
75
+ for (const entry of list.getEntries()) {
76
+ if (entry.duration > inpValue) {
77
+ inpValue = entry.duration;
78
+ vitals.inp = inpValue;
79
+ }
80
+ }
81
+ });
82
+ inpObs.observe({ type: 'event', buffered: true, durationThreshold: 40 });
83
+ } catch(e) {}
84
+
85
+ setTimeout(() => {
86
+ try { lcpObs?.disconnect(); } catch(e) {}
87
+ try { clsObs?.disconnect(); } catch(e) {}
88
+ try { inpObs?.disconnect(); } catch(e) {}
89
+ resolve(vitals);
90
+ }, ${waitMs});
91
+ })
92
+ `;
93
+
94
+ const result = await evaluateJs(client, sessionId, script);
95
+ return (result ?? {}) as WebVitals;
96
+ }
97
+
98
+ export function formatVitals(vitals: WebVitals): string {
99
+ const lines: string[] = [];
100
+ const ms = (v?: number) => v !== undefined ? `${Math.round(v)}ms` : 'n/a';
101
+ const score = (metric: string, v?: number): string => {
102
+ if (v === undefined) return '';
103
+ if (metric === 'lcp') return v < 2500 ? ' ✓ good' : v < 4000 ? ' ~ needs improvement' : ' ✗ poor';
104
+ if (metric === 'fcp') return v < 1800 ? ' ✓ good' : v < 3000 ? ' ~ needs improvement' : ' ✗ poor';
105
+ if (metric === 'cls') return v < 0.1 ? ' ✓ good' : v < 0.25 ? ' ~ needs improvement' : ' ✗ poor';
106
+ if (metric === 'inp') return v < 200 ? ' ✓ good' : v < 500 ? ' ~ needs improvement' : ' ✗ poor';
107
+ if (metric === 'ttfb') return v < 800 ? ' ✓ good' : v < 1800 ? ' ~ needs improvement' : ' ✗ poor';
108
+ return '';
109
+ };
110
+
111
+ if (vitals.lcp !== undefined) lines.push(` LCP (Largest Contentful Paint): ${ms(vitals.lcp)}${score('lcp', vitals.lcp)}`);
112
+ if (vitals.fcp !== undefined) lines.push(` FCP (First Contentful Paint): ${ms(vitals.fcp)}${score('fcp', vitals.fcp)}`);
113
+ if (vitals.cls !== undefined) lines.push(` CLS (Cumulative Layout Shift): ${vitals.cls?.toFixed(4)}${score('cls', vitals.cls)}`);
114
+ if (vitals.inp !== undefined) lines.push(` INP (Interaction to Next Paint): ${ms(vitals.inp)}${score('inp', vitals.inp)}`);
115
+ if (vitals.ttfb !== undefined) lines.push(` TTFB (Time to First Byte): ${ms(vitals.ttfb)}${score('ttfb', vitals.ttfb)}`);
116
+ if (vitals.domInteractive !== undefined) lines.push(` DOM Interactive: ${ms(vitals.domInteractive)}`);
117
+ if (vitals.domContentLoaded !== undefined) lines.push(` DOMContentLoaded: ${ms(vitals.domContentLoaded)}`);
118
+ if (vitals.loadTime !== undefined) lines.push(` Load: ${ms(vitals.loadTime)}`);
119
+ if (vitals.resources !== undefined) lines.push(` Resources loaded: ${vitals.resources}`);
120
+
121
+ return lines.join('\n');
122
+ }
@@ -0,0 +1,178 @@
1
+ import type { CdpClient } from './cdp.js';
2
+ import type { WaitOptions } from './types.js';
3
+ import { getCurrentUrl } from './browser.js';
4
+ import { evaluateJs } from './actions.js';
5
+
6
+ const DEFAULT_TIMEOUT = 30_000;
7
+ const POLL_INTERVAL = 250;
8
+
9
+ export async function waitFor(
10
+ client: CdpClient,
11
+ sessionId: string,
12
+ options: WaitOptions
13
+ ): Promise<void> {
14
+ const timeout = options.timeout ?? DEFAULT_TIMEOUT;
15
+ const deadline = Date.now() + timeout;
16
+
17
+ if (options.load) {
18
+ await waitForLoad(client, sessionId, options.load, timeout);
19
+ return;
20
+ }
21
+
22
+ if (options.url) {
23
+ await waitForUrl(client, sessionId, options.url, deadline);
24
+ return;
25
+ }
26
+
27
+ if (options.text) {
28
+ await waitForText(client, sessionId, options.text, deadline);
29
+ return;
30
+ }
31
+
32
+ if (options.selector) {
33
+ await waitForSelector(client, sessionId, options.selector, deadline);
34
+ return;
35
+ }
36
+
37
+ // Just wait for timeout
38
+ await sleep(timeout);
39
+ }
40
+
41
+ async function waitForLoad(
42
+ client: CdpClient,
43
+ sessionId: string,
44
+ condition: string,
45
+ timeout: number
46
+ ): Promise<void> {
47
+ if (condition === 'networkidle') {
48
+ await waitForNetworkIdle(client, sessionId, 500, timeout);
49
+ return;
50
+ }
51
+
52
+ const event = condition === 'load' ? 'Page.loadEventFired' : 'Page.domContentEventFired';
53
+ const [eventPromise, cancelOnce] = client.onceWithOff(event, sessionId);
54
+ let timedOut = false;
55
+ let timeoutHandle: ReturnType<typeof setTimeout> | undefined;
56
+ const timeoutPromise = new Promise<void>((resolve) => {
57
+ timeoutHandle = setTimeout(() => { timedOut = true; resolve(); }, timeout);
58
+ });
59
+ try {
60
+ await Promise.race([eventPromise, timeoutPromise]);
61
+ if (timedOut) throw new Error(`Timeout waiting for ${condition}`);
62
+ } finally {
63
+ cancelOnce();
64
+ clearTimeout(timeoutHandle);
65
+ }
66
+ }
67
+
68
+ async function waitForNetworkIdle(
69
+ client: CdpClient,
70
+ sessionId: string,
71
+ idleMs: number,
72
+ timeout: number
73
+ ): Promise<void> {
74
+ return new Promise((resolve, reject) => {
75
+ let pending = 0;
76
+ const inflight = new Set<string>();
77
+ let idleTimer: ReturnType<typeof setTimeout> | null = null;
78
+
79
+ const cleanup = () => {
80
+ if (idleTimer) { clearTimeout(idleTimer); idleTimer = null; }
81
+ clearTimeout(killTimer);
82
+ offReq(); offResp(); offFail(); offCache();
83
+ };
84
+
85
+ const killTimer = setTimeout(() => {
86
+ cleanup();
87
+ reject(new Error('Timeout waiting for networkidle'));
88
+ }, timeout);
89
+
90
+ const settle = () => { cleanup(); resolve(); };
91
+
92
+ const check = () => {
93
+ if (pending === 0) {
94
+ if (idleTimer) clearTimeout(idleTimer);
95
+ idleTimer = setTimeout(settle, idleMs);
96
+ } else {
97
+ if (idleTimer) { clearTimeout(idleTimer); idleTimer = null; }
98
+ }
99
+ };
100
+
101
+ const offReq = client.on('Network.requestWillBeSent', (params, sid) => {
102
+ if (sid !== sessionId) return;
103
+ const id = params.requestId as string;
104
+ if (!inflight.has(id)) { inflight.add(id); pending++; check(); }
105
+ });
106
+
107
+ const decrement = (params: Record<string, unknown>, sid?: string) => {
108
+ if (sid !== sessionId) return;
109
+ const id = params.requestId as string;
110
+ if (inflight.delete(id)) { pending = Math.max(0, pending - 1); check(); }
111
+ };
112
+
113
+ const offResp = client.on('Network.loadingFinished', decrement);
114
+ const offFail = client.on('Network.loadingFailed', decrement);
115
+ const offCache = client.on('Network.requestServedFromCache', decrement);
116
+
117
+ check();
118
+ });
119
+ }
120
+
121
+ async function waitForUrl(
122
+ client: CdpClient,
123
+ sessionId: string,
124
+ pattern: string,
125
+ deadline: number
126
+ ): Promise<void> {
127
+ const regex = globToRegex(pattern);
128
+ while (Date.now() < deadline) {
129
+ const url = await getCurrentUrl(client, sessionId);
130
+ if (regex.test(url)) return;
131
+ await sleep(POLL_INTERVAL);
132
+ }
133
+ throw new Error(`Timeout waiting for URL matching: ${pattern}`);
134
+ }
135
+
136
+ async function waitForText(
137
+ client: CdpClient,
138
+ sessionId: string,
139
+ text: string,
140
+ deadline: number
141
+ ): Promise<void> {
142
+ while (Date.now() < deadline) {
143
+ const bodyText = await evaluateJs(client, sessionId, 'document.body?.innerText ?? ""') as string;
144
+ if (bodyText.includes(text)) return;
145
+ await sleep(POLL_INTERVAL);
146
+ }
147
+ throw new Error(`Timeout waiting for text: ${text}`);
148
+ }
149
+
150
+ async function waitForSelector(
151
+ client: CdpClient,
152
+ sessionId: string,
153
+ selector: string,
154
+ deadline: number
155
+ ): Promise<void> {
156
+ while (Date.now() < deadline) {
157
+ const found = await evaluateJs(
158
+ client,
159
+ sessionId,
160
+ `!!document.querySelector(${JSON.stringify(selector)})`
161
+ ) as boolean;
162
+ if (found) return;
163
+ await sleep(POLL_INTERVAL);
164
+ }
165
+ throw new Error(`Timeout waiting for selector: ${selector}`);
166
+ }
167
+
168
+ function globToRegex(pattern: string): RegExp {
169
+ const escaped = pattern
170
+ .replace(/[-[\]{}()+?.,\\^$|#\s]/g, '\\$&')
171
+ .replace(/\*\*/g, '.*')
172
+ .replace(/\*/g, '[^/]*');
173
+ return new RegExp(`^${escaped}$`);
174
+ }
175
+
176
+ function sleep(ms: number): Promise<void> {
177
+ return new Promise((r) => setTimeout(r, ms));
178
+ }