@everydaydevopsio/pilot 0.5.0 → 0.6.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 (280) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/CLAUDE.md +9 -9
  3. package/README.md +108 -53
  4. package/dist/browser/browser-manager.d.ts +60 -0
  5. package/dist/browser/browser-manager.d.ts.map +1 -0
  6. package/dist/browser/browser-manager.js +348 -0
  7. package/dist/browser/browser-manager.js.map +1 -0
  8. package/dist/browser/chrome-launcher.d.ts +30 -0
  9. package/dist/browser/chrome-launcher.d.ts.map +1 -0
  10. package/dist/browser/chrome-launcher.js +217 -0
  11. package/dist/browser/chrome-launcher.js.map +1 -0
  12. package/dist/browser/connection.d.ts +23 -0
  13. package/dist/browser/connection.d.ts.map +1 -0
  14. package/dist/browser/connection.js +53 -0
  15. package/dist/browser/connection.js.map +1 -0
  16. package/dist/browser/events.d.ts +9 -0
  17. package/dist/browser/events.d.ts.map +1 -0
  18. package/dist/browser/events.js +99 -0
  19. package/dist/browser/events.js.map +1 -0
  20. package/dist/browser/index.d.ts +4 -0
  21. package/dist/browser/index.d.ts.map +1 -0
  22. package/dist/browser/index.js +4 -0
  23. package/dist/browser/index.js.map +1 -0
  24. package/dist/browser/inspect/element-ref.d.ts +23 -0
  25. package/dist/browser/inspect/element-ref.d.ts.map +1 -0
  26. package/dist/browser/inspect/element-ref.js +52 -0
  27. package/dist/browser/inspect/element-ref.js.map +1 -0
  28. package/dist/browser/inspect/snapshot.d.ts +11 -0
  29. package/dist/browser/inspect/snapshot.d.ts.map +1 -0
  30. package/dist/browser/inspect/snapshot.js +282 -0
  31. package/dist/browser/inspect/snapshot.js.map +1 -0
  32. package/dist/browser/inspect/styles.d.ts +45 -0
  33. package/dist/browser/inspect/styles.d.ts.map +1 -0
  34. package/dist/browser/inspect/styles.js +190 -0
  35. package/dist/browser/inspect/styles.js.map +1 -0
  36. package/dist/browser/inspect/types.d.ts +35 -0
  37. package/dist/browser/inspect/types.d.ts.map +1 -0
  38. package/dist/browser/inspect/types.js +2 -0
  39. package/dist/browser/inspect/types.js.map +1 -0
  40. package/dist/browser/interaction/dialogs.d.ts +18 -0
  41. package/dist/browser/interaction/dialogs.d.ts.map +1 -0
  42. package/dist/browser/interaction/dialogs.js +35 -0
  43. package/dist/browser/interaction/dialogs.js.map +1 -0
  44. package/dist/browser/interaction/downloads.d.ts +23 -0
  45. package/dist/browser/interaction/downloads.d.ts.map +1 -0
  46. package/dist/browser/interaction/downloads.js +87 -0
  47. package/dist/browser/interaction/downloads.js.map +1 -0
  48. package/dist/browser/interaction/events.d.ts +17 -0
  49. package/dist/browser/interaction/events.d.ts.map +1 -0
  50. package/dist/browser/interaction/events.js +66 -0
  51. package/dist/browser/interaction/events.js.map +1 -0
  52. package/dist/browser/interaction/ref-resolver.d.ts +18 -0
  53. package/dist/browser/interaction/ref-resolver.d.ts.map +1 -0
  54. package/dist/browser/interaction/ref-resolver.js +32 -0
  55. package/dist/browser/interaction/ref-resolver.js.map +1 -0
  56. package/dist/browser/interaction/uploads.d.ts +4 -0
  57. package/dist/browser/interaction/uploads.d.ts.map +1 -0
  58. package/dist/browser/interaction/uploads.js +43 -0
  59. package/dist/browser/interaction/uploads.js.map +1 -0
  60. package/dist/browser/network/network-buffer.d.ts +19 -0
  61. package/dist/browser/network/network-buffer.d.ts.map +1 -0
  62. package/dist/browser/network/network-buffer.js +118 -0
  63. package/dist/browser/network/network-buffer.js.map +1 -0
  64. package/dist/browser/network/network-monitor.d.ts +8 -0
  65. package/dist/browser/network/network-monitor.d.ts.map +1 -0
  66. package/dist/browser/network/network-monitor.js +85 -0
  67. package/dist/browser/network/network-monitor.js.map +1 -0
  68. package/dist/browser/network/redaction.d.ts +3 -0
  69. package/dist/browser/network/redaction.d.ts.map +1 -0
  70. package/dist/browser/network/redaction.js +36 -0
  71. package/dist/browser/network/redaction.js.map +1 -0
  72. package/dist/browser/network/types.d.ts +45 -0
  73. package/dist/browser/network/types.d.ts.map +1 -0
  74. package/dist/browser/network/types.js +2 -0
  75. package/dist/browser/network/types.js.map +1 -0
  76. package/dist/browser/performance/analyze.d.ts +5 -0
  77. package/dist/browser/performance/analyze.d.ts.map +1 -0
  78. package/dist/browser/performance/analyze.js +230 -0
  79. package/dist/browser/performance/analyze.js.map +1 -0
  80. package/dist/browser/performance/trace.d.ts +10 -0
  81. package/dist/browser/performance/trace.d.ts.map +1 -0
  82. package/dist/browser/performance/trace.js +38 -0
  83. package/dist/browser/performance/trace.js.map +1 -0
  84. package/dist/browser/performance/types.d.ts +31 -0
  85. package/dist/browser/performance/types.d.ts.map +1 -0
  86. package/dist/browser/performance/types.js +2 -0
  87. package/dist/browser/performance/types.js.map +1 -0
  88. package/dist/browser/security/origins.d.ts +18 -0
  89. package/dist/browser/security/origins.d.ts.map +1 -0
  90. package/dist/browser/security/origins.js +84 -0
  91. package/dist/browser/security/origins.js.map +1 -0
  92. package/dist/browser/tabs.d.ts +15 -0
  93. package/dist/browser/tabs.d.ts.map +1 -0
  94. package/dist/browser/tabs.js +71 -0
  95. package/dist/browser/tabs.js.map +1 -0
  96. package/dist/browser/types.d.ts +31 -0
  97. package/dist/browser/types.d.ts.map +1 -0
  98. package/dist/browser/types.js +2 -0
  99. package/dist/browser/types.js.map +1 -0
  100. package/dist/browser.d.ts +2 -99
  101. package/dist/browser.d.ts.map +1 -1
  102. package/dist/browser.js +3 -653
  103. package/dist/browser.js.map +1 -1
  104. package/dist/commands/check.d.ts +17 -0
  105. package/dist/commands/check.d.ts.map +1 -0
  106. package/dist/commands/check.js +38 -0
  107. package/dist/commands/check.js.map +1 -0
  108. package/dist/commands/click.d.ts +3 -1
  109. package/dist/commands/click.d.ts.map +1 -1
  110. package/dist/commands/click.js +16 -4
  111. package/dist/commands/click.js.map +1 -1
  112. package/dist/commands/connect.d.ts +14 -0
  113. package/dist/commands/connect.d.ts.map +1 -0
  114. package/dist/commands/connect.js +19 -0
  115. package/dist/commands/connect.js.map +1 -0
  116. package/dist/commands/dialog.d.ts +19 -0
  117. package/dist/commands/dialog.d.ts.map +1 -0
  118. package/dist/commands/dialog.js +51 -0
  119. package/dist/commands/dialog.js.map +1 -0
  120. package/dist/commands/downloads.d.ts +30 -0
  121. package/dist/commands/downloads.d.ts.map +1 -0
  122. package/dist/commands/downloads.js +66 -0
  123. package/dist/commands/downloads.js.map +1 -0
  124. package/dist/commands/fill.d.ts +16 -0
  125. package/dist/commands/fill.d.ts.map +1 -0
  126. package/dist/commands/fill.js +15 -0
  127. package/dist/commands/fill.js.map +1 -0
  128. package/dist/commands/find.d.ts +14 -0
  129. package/dist/commands/find.d.ts.map +1 -0
  130. package/dist/commands/find.js +26 -0
  131. package/dist/commands/find.js.map +1 -0
  132. package/dist/commands/hover.d.ts +13 -0
  133. package/dist/commands/hover.d.ts.map +1 -0
  134. package/dist/commands/hover.js +24 -0
  135. package/dist/commands/hover.js.map +1 -0
  136. package/dist/commands/navigate.d.ts.map +1 -1
  137. package/dist/commands/navigate.js +13 -0
  138. package/dist/commands/navigate.js.map +1 -1
  139. package/dist/commands/network.d.ts +33 -0
  140. package/dist/commands/network.d.ts.map +1 -0
  141. package/dist/commands/network.js +84 -0
  142. package/dist/commands/network.js.map +1 -0
  143. package/dist/commands/performance.d.ts +12 -0
  144. package/dist/commands/performance.d.ts.map +1 -0
  145. package/dist/commands/performance.js +26 -0
  146. package/dist/commands/performance.js.map +1 -0
  147. package/dist/commands/press_key.d.ts +10 -0
  148. package/dist/commands/press_key.d.ts.map +1 -0
  149. package/dist/commands/press_key.js +103 -0
  150. package/dist/commands/press_key.js.map +1 -0
  151. package/dist/commands/scroll.d.ts +21 -0
  152. package/dist/commands/scroll.d.ts.map +1 -0
  153. package/dist/commands/scroll.js +54 -0
  154. package/dist/commands/scroll.js.map +1 -0
  155. package/dist/commands/select.d.ts +18 -0
  156. package/dist/commands/select.d.ts.map +1 -0
  157. package/dist/commands/select.js +53 -0
  158. package/dist/commands/select.js.map +1 -0
  159. package/dist/commands/snapshot.d.ts +9 -0
  160. package/dist/commands/snapshot.d.ts.map +1 -0
  161. package/dist/commands/snapshot.js +7 -0
  162. package/dist/commands/snapshot.js.map +1 -0
  163. package/dist/commands/styles.d.ts +13 -0
  164. package/dist/commands/styles.d.ts.map +1 -0
  165. package/dist/commands/styles.js +7 -0
  166. package/dist/commands/styles.js.map +1 -0
  167. package/dist/commands/type.d.ts +3 -1
  168. package/dist/commands/type.d.ts.map +1 -1
  169. package/dist/commands/type.js +11 -2
  170. package/dist/commands/type.js.map +1 -1
  171. package/dist/commands/upload.d.ts +12 -0
  172. package/dist/commands/upload.d.ts.map +1 -0
  173. package/dist/commands/upload.js +13 -0
  174. package/dist/commands/upload.js.map +1 -0
  175. package/dist/mcp/console-buffer.d.ts +9 -0
  176. package/dist/mcp/console-buffer.d.ts.map +1 -1
  177. package/dist/mcp/console-buffer.js.map +1 -1
  178. package/dist/mcp/server.d.ts +10 -0
  179. package/dist/mcp/server.d.ts.map +1 -1
  180. package/dist/mcp/server.js +63 -2
  181. package/dist/mcp/server.js.map +1 -1
  182. package/dist/mcp/tools/browser.d.ts.map +1 -1
  183. package/dist/mcp/tools/browser.js +99 -8
  184. package/dist/mcp/tools/browser.js.map +1 -1
  185. package/dist/mcp/tools/connect.d.ts +5 -0
  186. package/dist/mcp/tools/connect.d.ts.map +1 -0
  187. package/dist/mcp/tools/connect.js +48 -0
  188. package/dist/mcp/tools/connect.js.map +1 -0
  189. package/dist/mcp/tools/files.d.ts +4 -0
  190. package/dist/mcp/tools/files.d.ts.map +1 -0
  191. package/dist/mcp/tools/files.js +85 -0
  192. package/dist/mcp/tools/files.js.map +1 -0
  193. package/dist/mcp/tools/interaction.d.ts +4 -0
  194. package/dist/mcp/tools/interaction.d.ts.map +1 -0
  195. package/dist/mcp/tools/interaction.js +176 -0
  196. package/dist/mcp/tools/interaction.js.map +1 -0
  197. package/dist/mcp/tools/names.d.ts +1 -1
  198. package/dist/mcp/tools/names.d.ts.map +1 -1
  199. package/dist/mcp/tools/names.js +19 -1
  200. package/dist/mcp/tools/names.js.map +1 -1
  201. package/dist/mcp/tools/network.d.ts +4 -0
  202. package/dist/mcp/tools/network.d.ts.map +1 -0
  203. package/dist/mcp/tools/network.js +68 -0
  204. package/dist/mcp/tools/network.js.map +1 -0
  205. package/dist/mcp/tools/performance.d.ts +4 -0
  206. package/dist/mcp/tools/performance.d.ts.map +1 -0
  207. package/dist/mcp/tools/performance.js +26 -0
  208. package/dist/mcp/tools/performance.js.map +1 -0
  209. package/dist/mcp/tools/runtime.d.ts +4 -0
  210. package/dist/mcp/tools/runtime.d.ts.map +1 -0
  211. package/dist/mcp/tools/runtime.js +128 -0
  212. package/dist/mcp/tools/runtime.js.map +1 -0
  213. package/dist/mcp/tools/snapshot.d.ts +4 -0
  214. package/dist/mcp/tools/snapshot.d.ts.map +1 -0
  215. package/dist/mcp/tools/snapshot.js +42 -0
  216. package/dist/mcp/tools/snapshot.js.map +1 -0
  217. package/dist/mcp/tools/styles.d.ts +4 -0
  218. package/dist/mcp/tools/styles.d.ts.map +1 -0
  219. package/dist/mcp/tools/styles.js +27 -0
  220. package/dist/mcp/tools/styles.js.map +1 -0
  221. package/dist/util/config.d.ts.map +1 -1
  222. package/dist/util/config.js +3 -2
  223. package/dist/util/config.js.map +1 -1
  224. package/package.json +7 -3
  225. package/src/browser/browser-manager.ts +457 -0
  226. package/src/browser/chrome-launcher.ts +271 -0
  227. package/src/browser/connection.ts +84 -0
  228. package/src/browser/events.ts +121 -0
  229. package/src/browser/index.ts +20 -0
  230. package/src/browser/inspect/element-ref.ts +68 -0
  231. package/src/browser/inspect/snapshot.ts +348 -0
  232. package/src/browser/inspect/styles.ts +258 -0
  233. package/src/browser/inspect/types.ts +37 -0
  234. package/src/browser/interaction/dialogs.ts +54 -0
  235. package/src/browser/interaction/downloads.ts +116 -0
  236. package/src/browser/interaction/events.ts +79 -0
  237. package/src/browser/interaction/ref-resolver.ts +53 -0
  238. package/src/browser/interaction/uploads.ts +60 -0
  239. package/src/browser/network/network-buffer.ts +133 -0
  240. package/src/browser/network/network-monitor.ts +107 -0
  241. package/src/browser/network/redaction.ts +42 -0
  242. package/src/browser/network/types.ts +47 -0
  243. package/src/browser/performance/analyze.ts +298 -0
  244. package/src/browser/performance/trace.ts +60 -0
  245. package/src/browser/performance/types.ts +34 -0
  246. package/src/browser/security/origins.ts +102 -0
  247. package/src/browser/tabs.ts +110 -0
  248. package/src/browser/types.ts +31 -0
  249. package/src/browser.ts +22 -798
  250. package/src/commands/check.ts +59 -0
  251. package/src/commands/click.ts +18 -4
  252. package/src/commands/connect.ts +40 -0
  253. package/src/commands/dialog.ts +79 -0
  254. package/src/commands/downloads.ts +111 -0
  255. package/src/commands/fill.ts +34 -0
  256. package/src/commands/find.ts +52 -0
  257. package/src/commands/hover.ts +42 -0
  258. package/src/commands/navigate.ts +20 -0
  259. package/src/commands/network.ts +141 -0
  260. package/src/commands/performance.ts +51 -0
  261. package/src/commands/press_key.ts +134 -0
  262. package/src/commands/scroll.ts +79 -0
  263. package/src/commands/select.ts +78 -0
  264. package/src/commands/snapshot.ts +18 -0
  265. package/src/commands/styles.ts +24 -0
  266. package/src/commands/type.ts +12 -2
  267. package/src/commands/upload.ts +36 -0
  268. package/src/mcp/console-buffer.ts +10 -0
  269. package/src/mcp/server.ts +74 -2
  270. package/src/mcp/tools/browser.ts +113 -7
  271. package/src/mcp/tools/connect.ts +62 -0
  272. package/src/mcp/tools/files.ts +120 -0
  273. package/src/mcp/tools/interaction.ts +232 -0
  274. package/src/mcp/tools/names.ts +19 -1
  275. package/src/mcp/tools/network.ts +86 -0
  276. package/src/mcp/tools/performance.ts +40 -0
  277. package/src/mcp/tools/runtime.ts +164 -0
  278. package/src/mcp/tools/snapshot.ts +64 -0
  279. package/src/mcp/tools/styles.ts +41 -0
  280. package/src/util/config.ts +3 -2
@@ -0,0 +1,298 @@
1
+ import type { Client } from 'chrome-remote-interface';
2
+ import type {
3
+ PerformanceSummary,
4
+ NavigationTiming,
5
+ LongTask,
6
+ SlowRequest,
7
+ LargeResource
8
+ } from './types.js';
9
+
10
+ const LONG_TASK_THRESHOLD_MS = 50;
11
+ const SLOW_REQUEST_THRESHOLD_MS = 1000;
12
+ const LARGE_RESOURCE_THRESHOLD_BYTES = 500 * 1024;
13
+ const MAX_ENTRIES = 100;
14
+
15
+ interface TraceEvent {
16
+ cat?: string;
17
+ name?: string;
18
+ ph?: string;
19
+ dur?: number;
20
+ ts?: number;
21
+ args?: Record<string, unknown>;
22
+ }
23
+
24
+ function findNavigationStart(events: TraceEvent[]): number {
25
+ for (const event of events) {
26
+ if (event.name === 'navigationStart' && event.ts) {
27
+ return event.ts;
28
+ }
29
+ }
30
+ // Fallback: earliest timestamp in the trace
31
+ let earliest = Infinity;
32
+ for (const event of events) {
33
+ if (event.ts && event.ts < earliest) {
34
+ earliest = event.ts;
35
+ }
36
+ }
37
+ return earliest === Infinity ? 0 : earliest;
38
+ }
39
+
40
+ function extractNavigationTiming(events: TraceEvent[]): NavigationTiming {
41
+ const timing: NavigationTiming = {};
42
+ const baseTs = findNavigationStart(events);
43
+
44
+ for (const event of events) {
45
+ if (event.name === 'domContentLoadedEventEnd' && event.ts !== undefined) {
46
+ timing.domContentLoaded = (event.ts - baseTs) / 1000;
47
+ }
48
+ if (event.name === 'loadEventEnd' && event.ts !== undefined) {
49
+ timing.load = (event.ts - baseTs) / 1000;
50
+ }
51
+ if (event.name === 'firstPaint' && event.ts !== undefined) {
52
+ timing.firstPaint = (event.ts - baseTs) / 1000;
53
+ }
54
+ if (event.name === 'firstContentfulPaint' && event.ts !== undefined) {
55
+ timing.firstContentfulPaint = (event.ts - baseTs) / 1000;
56
+ }
57
+ }
58
+
59
+ return timing;
60
+ }
61
+
62
+ function extractLongTasks(events: TraceEvent[]): LongTask[] {
63
+ const tasks: LongTask[] = [];
64
+
65
+ for (const event of events) {
66
+ if (
67
+ event.name === 'RunTask' &&
68
+ event.ph === 'X' &&
69
+ event.dur &&
70
+ event.dur / 1000 > LONG_TASK_THRESHOLD_MS
71
+ ) {
72
+ tasks.push({
73
+ name: event.name,
74
+ duration: Math.round(event.dur / 1000),
75
+ startTime: event.ts ? Math.round(event.ts / 1000) : 0
76
+ });
77
+ }
78
+ }
79
+
80
+ return tasks.sort((a, b) => b.duration - a.duration).slice(0, MAX_ENTRIES);
81
+ }
82
+
83
+ function extractSlowRequests(events: TraceEvent[]): SlowRequest[] {
84
+ const requests = new Map<
85
+ string,
86
+ { url: string; start: number; end?: number; method?: string }
87
+ >();
88
+
89
+ for (const event of events) {
90
+ if (event.name === 'ResourceSendRequest' && event.args) {
91
+ const data = event.args.data as
92
+ | { requestId?: string; url?: string; requestMethod?: string }
93
+ | undefined;
94
+ if (data?.requestId && data.url) {
95
+ requests.set(data.requestId, {
96
+ url: data.url,
97
+ start: event.ts ?? 0,
98
+ method: data.requestMethod
99
+ });
100
+ }
101
+ }
102
+ if (event.name === 'ResourceFinish' && event.args) {
103
+ const data = event.args.data as { requestId?: string } | undefined;
104
+ if (data?.requestId) {
105
+ const req = requests.get(data.requestId);
106
+ if (req) {
107
+ req.end = event.ts;
108
+ }
109
+ }
110
+ }
111
+ }
112
+
113
+ const slow: SlowRequest[] = [];
114
+ for (const req of requests.values()) {
115
+ if (req.end) {
116
+ const duration = Math.round((req.end - req.start) / 1000);
117
+ if (duration > SLOW_REQUEST_THRESHOLD_MS) {
118
+ slow.push({ url: req.url, duration, method: req.method });
119
+ }
120
+ }
121
+ }
122
+
123
+ return slow.sort((a, b) => b.duration - a.duration).slice(0, MAX_ENTRIES);
124
+ }
125
+
126
+ function extractLargeResources(events: TraceEvent[]): LargeResource[] {
127
+ // Build a requestId → URL map from ResourceSendRequest events
128
+ const urlMap = new Map<string, string>();
129
+ for (const event of events) {
130
+ if (event.name === 'ResourceSendRequest' && event.args) {
131
+ const data = event.args.data as
132
+ { requestId?: string; url?: string } | undefined;
133
+ if (data?.requestId && data.url) {
134
+ urlMap.set(data.requestId, data.url);
135
+ }
136
+ }
137
+ }
138
+
139
+ const resources: LargeResource[] = [];
140
+ for (const event of events) {
141
+ if (event.name === 'ResourceFinish' && event.args) {
142
+ const data = event.args.data as
143
+ { decodedBodyLength?: number; requestId?: string } | undefined;
144
+ if (
145
+ data?.decodedBodyLength &&
146
+ data.decodedBodyLength > LARGE_RESOURCE_THRESHOLD_BYTES
147
+ ) {
148
+ resources.push({
149
+ url: (data.requestId && urlMap.get(data.requestId)) || '',
150
+ size: data.decodedBodyLength
151
+ });
152
+ }
153
+ }
154
+ }
155
+
156
+ return resources.sort((a, b) => b.size - a.size).slice(0, MAX_ENTRIES);
157
+ }
158
+
159
+ function extractJsExecutionTime(events: TraceEvent[]): number {
160
+ let totalUs = 0;
161
+ for (const event of events) {
162
+ if (event.cat?.includes('v8') && event.ph === 'X' && event.dur) {
163
+ totalUs += event.dur;
164
+ }
165
+ }
166
+ return Math.round(totalUs / 1000);
167
+ }
168
+
169
+ export async function analyzePerformance(
170
+ client: Client,
171
+ traceEvents: unknown[]
172
+ ): Promise<PerformanceSummary> {
173
+ // Get current performance metrics from CDP
174
+ const { metrics: rawMetrics } = await client.Performance.getMetrics();
175
+ const metrics: Record<string, number> = {};
176
+ for (const m of rawMetrics) {
177
+ metrics[m.name] = m.value;
178
+ }
179
+
180
+ const events = traceEvents as TraceEvent[];
181
+
182
+ return {
183
+ navigationTiming: extractNavigationTiming(events),
184
+ metrics,
185
+ longTasks: extractLongTasks(events),
186
+ slowRequests: extractSlowRequests(events),
187
+ largeResources: extractLargeResources(events),
188
+ totalJsExecutionMs: extractJsExecutionTime(events),
189
+ traceEventCount: events.length
190
+ };
191
+ }
192
+
193
+ export function formatPerformanceSummary(summary: PerformanceSummary): string {
194
+ const lines: string[] = ['Performance Summary:\n'];
195
+
196
+ // Navigation timing — use explicit undefined checks so 0ms values are shown
197
+ const nt = summary.navigationTiming;
198
+ lines.push('Navigation Timing:');
199
+ if (nt.domContentLoaded !== undefined)
200
+ lines.push(` DOMContentLoaded: ${Math.round(nt.domContentLoaded)}ms`);
201
+ if (nt.load !== undefined) lines.push(` Load: ${Math.round(nt.load)}ms`);
202
+ if (nt.firstPaint !== undefined)
203
+ lines.push(` First Paint: ${Math.round(nt.firstPaint)}ms`);
204
+ if (nt.firstContentfulPaint !== undefined)
205
+ lines.push(
206
+ ` First Contentful Paint: ${Math.round(nt.firstContentfulPaint)}ms`
207
+ );
208
+ if (
209
+ nt.domContentLoaded === undefined &&
210
+ nt.load === undefined &&
211
+ nt.firstPaint === undefined &&
212
+ nt.firstContentfulPaint === undefined
213
+ ) {
214
+ lines.push(' (no timing data captured)');
215
+ }
216
+
217
+ // Key metrics
218
+ const keyMetrics = [
219
+ 'JSHeapUsedSize',
220
+ 'JSHeapTotalSize',
221
+ 'Documents',
222
+ 'Frames',
223
+ 'LayoutCount',
224
+ 'RecalcStyleCount',
225
+ 'ScriptDuration',
226
+ 'TaskDuration'
227
+ ];
228
+ const hasMetrics = keyMetrics.some((k) => summary.metrics[k] !== undefined);
229
+ if (hasMetrics) {
230
+ lines.push('');
231
+ lines.push('Key Metrics:');
232
+ for (const k of keyMetrics) {
233
+ if (summary.metrics[k] !== undefined) {
234
+ let val = summary.metrics[k];
235
+ let unit = '';
236
+ if (k.includes('Size')) {
237
+ val = Math.round(val / 1024);
238
+ unit = ' KB';
239
+ } else if (k.includes('Duration')) {
240
+ val = Math.round(val * 1000);
241
+ unit = ' ms';
242
+ }
243
+ lines.push(` ${k}: ${val}${unit}`);
244
+ }
245
+ }
246
+ }
247
+
248
+ // JS execution time
249
+ if (summary.totalJsExecutionMs) {
250
+ lines.push('');
251
+ lines.push(`Total JS Execution: ${summary.totalJsExecutionMs}ms`);
252
+ }
253
+
254
+ // Long tasks
255
+ if (summary.longTasks.length > 0) {
256
+ lines.push('');
257
+ lines.push(
258
+ `Long Tasks (>${LONG_TASK_THRESHOLD_MS}ms): ${summary.longTasks.length}`
259
+ );
260
+ for (const t of summary.longTasks.slice(0, 10)) {
261
+ lines.push(` ${t.name}: ${t.duration}ms`);
262
+ }
263
+ if (summary.longTasks.length > 10) {
264
+ lines.push(` ... and ${summary.longTasks.length - 10} more`);
265
+ }
266
+ }
267
+
268
+ // Slow requests
269
+ if (summary.slowRequests.length > 0) {
270
+ lines.push('');
271
+ lines.push(
272
+ `Slow Requests (>${SLOW_REQUEST_THRESHOLD_MS}ms): ${summary.slowRequests.length}`
273
+ );
274
+ for (const r of summary.slowRequests.slice(0, 10)) {
275
+ lines.push(` ${r.method ?? 'GET'} ${r.url}: ${r.duration}ms`);
276
+ }
277
+ }
278
+
279
+ // Large resources
280
+ if (summary.largeResources.length > 0) {
281
+ lines.push('');
282
+ lines.push(
283
+ `Large Resources (>${Math.round(LARGE_RESOURCE_THRESHOLD_BYTES / 1024)}KB): ${summary.largeResources.length}`
284
+ );
285
+ for (const r of summary.largeResources.slice(0, 10)) {
286
+ lines.push(
287
+ ` ${Math.round(r.size / 1024)}KB ${r.url || '(unknown url)'}`
288
+ );
289
+ }
290
+ }
291
+
292
+ if (summary.traceEventCount) {
293
+ lines.push('');
294
+ lines.push(`Trace events processed: ${summary.traceEventCount}`);
295
+ }
296
+
297
+ return lines.join('\n');
298
+ }
@@ -0,0 +1,60 @@
1
+ import type { Client } from 'chrome-remote-interface';
2
+
3
+ const MAX_TRACE_EVENTS = 100000;
4
+
5
+ export interface TraceState {
6
+ tracing: boolean;
7
+ events: unknown[];
8
+ listenerClient: WeakRef<Client> | null;
9
+ }
10
+
11
+ export function createTraceState(): TraceState {
12
+ return { tracing: false, events: [], listenerClient: null };
13
+ }
14
+
15
+ export async function startTrace(
16
+ client: Client,
17
+ state: TraceState
18
+ ): Promise<void> {
19
+ if (state.tracing) {
20
+ throw new Error('Tracing is already active. Stop it first.');
21
+ }
22
+
23
+ state.events = [];
24
+ state.tracing = true;
25
+
26
+ // Only attach listener if not already attached to this client
27
+ if (state.listenerClient?.deref() !== client) {
28
+ client.Tracing.dataCollected((params) => {
29
+ if (state.events.length < MAX_TRACE_EVENTS) {
30
+ state.events.push(...params.value);
31
+ }
32
+ });
33
+ state.listenerClient = new WeakRef(client);
34
+ }
35
+
36
+ await client.Tracing.start({
37
+ categories:
38
+ 'devtools.timeline,loading,v8,v8.execute,blink.user_timing,navigation',
39
+ transferMode: 'ReportEvents'
40
+ });
41
+ }
42
+
43
+ export async function stopTrace(
44
+ client: Client,
45
+ state: TraceState
46
+ ): Promise<unknown[]> {
47
+ if (!state.tracing) {
48
+ throw new Error('No active trace. Call start first.');
49
+ }
50
+
51
+ await new Promise<void>((resolve) => {
52
+ client.Tracing.tracingComplete(() => {
53
+ resolve();
54
+ });
55
+ void client.Tracing.end();
56
+ });
57
+
58
+ state.tracing = false;
59
+ return state.events;
60
+ }
@@ -0,0 +1,34 @@
1
+ export interface NavigationTiming {
2
+ domContentLoaded?: number;
3
+ load?: number;
4
+ firstPaint?: number;
5
+ firstContentfulPaint?: number;
6
+ }
7
+
8
+ export interface LongTask {
9
+ name: string;
10
+ duration: number;
11
+ startTime: number;
12
+ }
13
+
14
+ export interface SlowRequest {
15
+ url: string;
16
+ duration: number;
17
+ method?: string;
18
+ }
19
+
20
+ export interface LargeResource {
21
+ url: string;
22
+ size: number;
23
+ type?: string;
24
+ }
25
+
26
+ export interface PerformanceSummary {
27
+ navigationTiming: NavigationTiming;
28
+ metrics: Record<string, number>;
29
+ longTasks: LongTask[];
30
+ slowRequests: SlowRequest[];
31
+ largeResources: LargeResource[];
32
+ totalJsExecutionMs?: number;
33
+ traceEventCount?: number;
34
+ }
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Origin allow/block list evaluation.
3
+ *
4
+ * Rules:
5
+ * - Block wins over allow.
6
+ * - Both empty = everything allowed.
7
+ * - Supports wildcard patterns: *.example.com matches sub.example.com
8
+ * - Patterns with a scheme (https://...) match the full origin.
9
+ * - Patterns without a scheme match the host (+port) only, any scheme.
10
+ */
11
+
12
+ function getOriginList(envVar: string): string[] {
13
+ const env = process.env[envVar];
14
+ if (!env) return [];
15
+ return env
16
+ .split(',')
17
+ .map((p) => p.trim())
18
+ .filter((p) => p.length > 0);
19
+ }
20
+
21
+ function patternToRegex(pattern: string): RegExp {
22
+ // Escape regex special chars except *
23
+ const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
24
+ // Replace * with regex wildcard
25
+ const regexStr = escaped.replace(/\*/g, '.*');
26
+ return new RegExp(`^${regexStr}$`, 'i');
27
+ }
28
+
29
+ function hasScheme(pattern: string): boolean {
30
+ return /^[a-z][a-z0-9+.-]*:\/\//i.test(pattern);
31
+ }
32
+
33
+ function matchesPattern(
34
+ origin: string,
35
+ host: string,
36
+ pattern: string
37
+ ): boolean {
38
+ if (hasScheme(pattern)) {
39
+ // Pattern includes scheme — match against full origin
40
+ return patternToRegex(pattern).test(origin);
41
+ }
42
+ // Scheme-less pattern — match against host (+port) only
43
+ return patternToRegex(pattern).test(host);
44
+ }
45
+
46
+ function matchesAny(origin: string, host: string, patterns: string[]): boolean {
47
+ return patterns.some((p) => matchesPattern(origin, host, p));
48
+ }
49
+
50
+ export function extractOrigin(url: string): string {
51
+ try {
52
+ const parsed = new URL(url);
53
+ return parsed.origin;
54
+ } catch {
55
+ return url;
56
+ }
57
+ }
58
+
59
+ export function extractHost(url: string): string {
60
+ try {
61
+ const parsed = new URL(url);
62
+ return parsed.host;
63
+ } catch {
64
+ return url;
65
+ }
66
+ }
67
+
68
+ export interface OriginCheckResult {
69
+ allowed: boolean;
70
+ reason?: string;
71
+ }
72
+
73
+ export function checkOrigin(url: string): OriginCheckResult {
74
+ const blocked = getOriginList('PILOT_BLOCKED_ORIGINS');
75
+ const allowed = getOriginList('PILOT_ALLOWED_ORIGINS');
76
+
77
+ // Both empty = everything allowed
78
+ if (blocked.length === 0 && allowed.length === 0) {
79
+ return { allowed: true };
80
+ }
81
+
82
+ const origin = extractOrigin(url);
83
+ const host = extractHost(url);
84
+
85
+ // Block wins over allow
86
+ if (blocked.length > 0 && matchesAny(origin, host, blocked)) {
87
+ return {
88
+ allowed: false,
89
+ reason: `Origin "${origin}" is blocked by PILOT_BLOCKED_ORIGINS`
90
+ };
91
+ }
92
+
93
+ // If allow list exists, origin must match it
94
+ if (allowed.length > 0 && !matchesAny(origin, host, allowed)) {
95
+ return {
96
+ allowed: false,
97
+ reason: `Origin "${origin}" is not in PILOT_ALLOWED_ORIGINS`
98
+ };
99
+ }
100
+
101
+ return { allowed: true };
102
+ }
@@ -0,0 +1,110 @@
1
+ import CDP from 'chrome-remote-interface';
2
+ import type { Client } from 'chrome-remote-interface';
3
+ import { getLogger } from '../util/logger.js';
4
+ import type { TabInfo } from './types.js';
5
+ import type { ConnectionInfo } from './connection.js';
6
+ import { enableDomains } from './connection.js';
7
+ import { attachEventListeners } from './events.js';
8
+ import type { ViewportConfig } from '../viewport.js';
9
+
10
+ export async function listTabs(
11
+ connInfo: ConnectionInfo,
12
+ activeTargetId: string | null
13
+ ): Promise<TabInfo[]> {
14
+ const targets = await CDP.List({ host: connInfo.host, port: connInfo.port });
15
+ return targets
16
+ .filter((t) => t.type === 'page')
17
+ .map((t) => ({
18
+ targetId: t.id,
19
+ url: t.url,
20
+ title: t.title,
21
+ active: t.id === activeTargetId
22
+ }));
23
+ }
24
+
25
+ export async function createTab(
26
+ connInfo: ConnectionInfo,
27
+ url?: string
28
+ ): Promise<TabInfo> {
29
+ const encodedUrl = url ? encodeURI(url) : undefined;
30
+ const target = await CDP.New({
31
+ host: connInfo.host,
32
+ port: connInfo.port,
33
+ url: encodedUrl
34
+ });
35
+ return {
36
+ targetId: target.id,
37
+ url: target.url,
38
+ title: target.title,
39
+ active: false
40
+ };
41
+ }
42
+
43
+ export async function closeTab(
44
+ connInfo: ConnectionInfo,
45
+ targetId: string,
46
+ activeTargetId: string | null
47
+ ): Promise<void> {
48
+ if (targetId === activeTargetId) {
49
+ throw new Error(
50
+ 'Cannot close the active tab. Switch to another tab first.'
51
+ );
52
+ }
53
+ await CDP.Close({ host: connInfo.host, port: connInfo.port, id: targetId });
54
+ }
55
+
56
+ export interface SwitchTabResult {
57
+ client: Client;
58
+ targetId: string;
59
+ url: string;
60
+ }
61
+
62
+ export async function switchTab(
63
+ connInfo: ConnectionInfo,
64
+ targetId: string,
65
+ currentTargetId: string | null,
66
+ currentClient: Client | null,
67
+ viewportConfig: ViewportConfig | null,
68
+ eventListenerOpts: Parameters<typeof attachEventListeners>[1]
69
+ ): Promise<SwitchTabResult> {
70
+ if (targetId === currentTargetId && currentClient) {
71
+ return {
72
+ client: currentClient,
73
+ targetId,
74
+ url: ''
75
+ };
76
+ }
77
+
78
+ // Verify the target exists
79
+ const targets = await CDP.List({
80
+ host: connInfo.host,
81
+ port: connInfo.port
82
+ });
83
+ const target = targets.find((t) => t.id === targetId && t.type === 'page');
84
+ if (!target) {
85
+ throw new Error(`No page tab found with targetId: ${targetId}`);
86
+ }
87
+
88
+ // Close old client connection (but not the tab)
89
+ if (currentClient) {
90
+ await currentClient.close();
91
+ }
92
+
93
+ // Connect to the new target
94
+ const client = await CDP({
95
+ host: connInfo.host,
96
+ port: connInfo.port,
97
+ target: targetId
98
+ });
99
+
100
+ await enableDomains(client, viewportConfig);
101
+ attachEventListeners(client, eventListenerOpts);
102
+
103
+ // Activate the tab in the browser UI
104
+ await client.Target.activateTarget({ targetId });
105
+
106
+ const logger = getLogger();
107
+ logger.info({ targetId, url: target.url }, 'Switched to tab');
108
+
109
+ return { client, targetId, url: target.url };
110
+ }
@@ -0,0 +1,31 @@
1
+ import type { Client } from 'chrome-remote-interface';
2
+
3
+ export type BrowserEventCallback = (event: string, data: unknown) => void;
4
+
5
+ export interface TabInfo {
6
+ targetId: string;
7
+ url: string;
8
+ title: string;
9
+ active: boolean;
10
+ }
11
+
12
+ export interface BrowserState {
13
+ client: Client | null;
14
+ targetId: string | null;
15
+ url: string | null;
16
+ connected: boolean;
17
+ }
18
+
19
+ export interface LaunchOptions {
20
+ headless?: boolean;
21
+ chromePath?: string;
22
+ profileName?: string;
23
+ viewport?: string;
24
+ viewportWidth?: number;
25
+ viewportHeight?: number;
26
+ deviceScaleFactor?: number;
27
+ responsive?: boolean;
28
+ }
29
+
30
+ export type SandboxDecision =
31
+ { disable: false } | { disable: true; reason: 'env_override' | 'root_user' };