@everydaydevopsio/pilot 0.5.2 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (306) hide show
  1. package/.mcp.json +9 -0
  2. package/AGENTS.md +9 -0
  3. package/CHANGELOG.md +64 -0
  4. package/CLAUDE.md +7 -7
  5. package/README.md +282 -69
  6. package/dist/browser/browser-manager.d.ts +60 -0
  7. package/dist/browser/browser-manager.d.ts.map +1 -0
  8. package/dist/browser/browser-manager.js +355 -0
  9. package/dist/browser/browser-manager.js.map +1 -0
  10. package/dist/browser/chrome-launcher.d.ts +31 -0
  11. package/dist/browser/chrome-launcher.d.ts.map +1 -0
  12. package/dist/browser/chrome-launcher.js +225 -0
  13. package/dist/browser/chrome-launcher.js.map +1 -0
  14. package/dist/browser/connection.d.ts +23 -0
  15. package/dist/browser/connection.d.ts.map +1 -0
  16. package/dist/browser/connection.js +53 -0
  17. package/dist/browser/connection.js.map +1 -0
  18. package/dist/browser/events.d.ts +9 -0
  19. package/dist/browser/events.d.ts.map +1 -0
  20. package/dist/browser/events.js +99 -0
  21. package/dist/browser/events.js.map +1 -0
  22. package/dist/browser/index.d.ts +4 -0
  23. package/dist/browser/index.d.ts.map +1 -0
  24. package/dist/browser/index.js +4 -0
  25. package/dist/browser/index.js.map +1 -0
  26. package/dist/browser/inspect/element-ref.d.ts +23 -0
  27. package/dist/browser/inspect/element-ref.d.ts.map +1 -0
  28. package/dist/browser/inspect/element-ref.js +52 -0
  29. package/dist/browser/inspect/element-ref.js.map +1 -0
  30. package/dist/browser/inspect/snapshot.d.ts +11 -0
  31. package/dist/browser/inspect/snapshot.d.ts.map +1 -0
  32. package/dist/browser/inspect/snapshot.js +282 -0
  33. package/dist/browser/inspect/snapshot.js.map +1 -0
  34. package/dist/browser/inspect/styles.d.ts +45 -0
  35. package/dist/browser/inspect/styles.d.ts.map +1 -0
  36. package/dist/browser/inspect/styles.js +190 -0
  37. package/dist/browser/inspect/styles.js.map +1 -0
  38. package/dist/browser/inspect/types.d.ts +35 -0
  39. package/dist/browser/inspect/types.d.ts.map +1 -0
  40. package/dist/browser/inspect/types.js +2 -0
  41. package/dist/browser/inspect/types.js.map +1 -0
  42. package/dist/browser/interaction/dialogs.d.ts +18 -0
  43. package/dist/browser/interaction/dialogs.d.ts.map +1 -0
  44. package/dist/browser/interaction/dialogs.js +35 -0
  45. package/dist/browser/interaction/dialogs.js.map +1 -0
  46. package/dist/browser/interaction/downloads.d.ts +23 -0
  47. package/dist/browser/interaction/downloads.d.ts.map +1 -0
  48. package/dist/browser/interaction/downloads.js +87 -0
  49. package/dist/browser/interaction/downloads.js.map +1 -0
  50. package/dist/browser/interaction/events.d.ts +17 -0
  51. package/dist/browser/interaction/events.d.ts.map +1 -0
  52. package/dist/browser/interaction/events.js +66 -0
  53. package/dist/browser/interaction/events.js.map +1 -0
  54. package/dist/browser/interaction/ref-resolver.d.ts +18 -0
  55. package/dist/browser/interaction/ref-resolver.d.ts.map +1 -0
  56. package/dist/browser/interaction/ref-resolver.js +32 -0
  57. package/dist/browser/interaction/ref-resolver.js.map +1 -0
  58. package/dist/browser/interaction/uploads.d.ts +4 -0
  59. package/dist/browser/interaction/uploads.d.ts.map +1 -0
  60. package/dist/browser/interaction/uploads.js +43 -0
  61. package/dist/browser/interaction/uploads.js.map +1 -0
  62. package/dist/browser/network/network-buffer.d.ts +19 -0
  63. package/dist/browser/network/network-buffer.d.ts.map +1 -0
  64. package/dist/browser/network/network-buffer.js +118 -0
  65. package/dist/browser/network/network-buffer.js.map +1 -0
  66. package/dist/browser/network/network-monitor.d.ts +8 -0
  67. package/dist/browser/network/network-monitor.d.ts.map +1 -0
  68. package/dist/browser/network/network-monitor.js +85 -0
  69. package/dist/browser/network/network-monitor.js.map +1 -0
  70. package/dist/browser/network/redaction.d.ts +3 -0
  71. package/dist/browser/network/redaction.d.ts.map +1 -0
  72. package/dist/browser/network/redaction.js +36 -0
  73. package/dist/browser/network/redaction.js.map +1 -0
  74. package/dist/browser/network/types.d.ts +45 -0
  75. package/dist/browser/network/types.d.ts.map +1 -0
  76. package/dist/browser/network/types.js +2 -0
  77. package/dist/browser/network/types.js.map +1 -0
  78. package/dist/browser/performance/analyze.d.ts +5 -0
  79. package/dist/browser/performance/analyze.d.ts.map +1 -0
  80. package/dist/browser/performance/analyze.js +230 -0
  81. package/dist/browser/performance/analyze.js.map +1 -0
  82. package/dist/browser/performance/trace.d.ts +10 -0
  83. package/dist/browser/performance/trace.d.ts.map +1 -0
  84. package/dist/browser/performance/trace.js +38 -0
  85. package/dist/browser/performance/trace.js.map +1 -0
  86. package/dist/browser/performance/types.d.ts +31 -0
  87. package/dist/browser/performance/types.d.ts.map +1 -0
  88. package/dist/browser/performance/types.js +2 -0
  89. package/dist/browser/performance/types.js.map +1 -0
  90. package/dist/browser/preflight.d.ts +20 -0
  91. package/dist/browser/preflight.d.ts.map +1 -0
  92. package/dist/browser/preflight.js +105 -0
  93. package/dist/browser/preflight.js.map +1 -0
  94. package/dist/browser/security/origins.d.ts +18 -0
  95. package/dist/browser/security/origins.d.ts.map +1 -0
  96. package/dist/browser/security/origins.js +84 -0
  97. package/dist/browser/security/origins.js.map +1 -0
  98. package/dist/browser/tabs.d.ts +15 -0
  99. package/dist/browser/tabs.d.ts.map +1 -0
  100. package/dist/browser/tabs.js +71 -0
  101. package/dist/browser/tabs.js.map +1 -0
  102. package/dist/browser/types.d.ts +32 -0
  103. package/dist/browser/types.d.ts.map +1 -0
  104. package/dist/browser/types.js +2 -0
  105. package/dist/browser/types.js.map +1 -0
  106. package/dist/browser.d.ts +2 -99
  107. package/dist/browser.d.ts.map +1 -1
  108. package/dist/browser.js +3 -653
  109. package/dist/browser.js.map +1 -1
  110. package/dist/cli/init.d.ts +3 -0
  111. package/dist/cli/init.d.ts.map +1 -1
  112. package/dist/cli/init.js +33 -48
  113. package/dist/cli/init.js.map +1 -1
  114. package/dist/cli/linux-check.d.ts +2 -0
  115. package/dist/cli/linux-check.d.ts.map +1 -0
  116. package/dist/cli/linux-check.js +20 -0
  117. package/dist/cli/linux-check.js.map +1 -0
  118. package/dist/cli/skill-template.d.ts +1 -1
  119. package/dist/cli/skill-template.d.ts.map +1 -1
  120. package/dist/cli/skill-template.js +3 -113
  121. package/dist/cli/skill-template.js.map +1 -1
  122. package/dist/commands/check.d.ts +17 -0
  123. package/dist/commands/check.d.ts.map +1 -0
  124. package/dist/commands/check.js +38 -0
  125. package/dist/commands/check.js.map +1 -0
  126. package/dist/commands/click.d.ts +3 -1
  127. package/dist/commands/click.d.ts.map +1 -1
  128. package/dist/commands/click.js +16 -4
  129. package/dist/commands/click.js.map +1 -1
  130. package/dist/commands/connect.d.ts +14 -0
  131. package/dist/commands/connect.d.ts.map +1 -0
  132. package/dist/commands/connect.js +19 -0
  133. package/dist/commands/connect.js.map +1 -0
  134. package/dist/commands/dialog.d.ts +19 -0
  135. package/dist/commands/dialog.d.ts.map +1 -0
  136. package/dist/commands/dialog.js +51 -0
  137. package/dist/commands/dialog.js.map +1 -0
  138. package/dist/commands/downloads.d.ts +30 -0
  139. package/dist/commands/downloads.d.ts.map +1 -0
  140. package/dist/commands/downloads.js +66 -0
  141. package/dist/commands/downloads.js.map +1 -0
  142. package/dist/commands/fill.d.ts +16 -0
  143. package/dist/commands/fill.d.ts.map +1 -0
  144. package/dist/commands/fill.js +15 -0
  145. package/dist/commands/fill.js.map +1 -0
  146. package/dist/commands/find.d.ts +14 -0
  147. package/dist/commands/find.d.ts.map +1 -0
  148. package/dist/commands/find.js +26 -0
  149. package/dist/commands/find.js.map +1 -0
  150. package/dist/commands/hover.d.ts +13 -0
  151. package/dist/commands/hover.d.ts.map +1 -0
  152. package/dist/commands/hover.js +24 -0
  153. package/dist/commands/hover.js.map +1 -0
  154. package/dist/commands/navigate.d.ts.map +1 -1
  155. package/dist/commands/navigate.js +13 -0
  156. package/dist/commands/navigate.js.map +1 -1
  157. package/dist/commands/network.d.ts +33 -0
  158. package/dist/commands/network.d.ts.map +1 -0
  159. package/dist/commands/network.js +84 -0
  160. package/dist/commands/network.js.map +1 -0
  161. package/dist/commands/performance.d.ts +12 -0
  162. package/dist/commands/performance.d.ts.map +1 -0
  163. package/dist/commands/performance.js +26 -0
  164. package/dist/commands/performance.js.map +1 -0
  165. package/dist/commands/press_key.d.ts +10 -0
  166. package/dist/commands/press_key.d.ts.map +1 -0
  167. package/dist/commands/press_key.js +103 -0
  168. package/dist/commands/press_key.js.map +1 -0
  169. package/dist/commands/scroll.d.ts +21 -0
  170. package/dist/commands/scroll.d.ts.map +1 -0
  171. package/dist/commands/scroll.js +54 -0
  172. package/dist/commands/scroll.js.map +1 -0
  173. package/dist/commands/select.d.ts +18 -0
  174. package/dist/commands/select.d.ts.map +1 -0
  175. package/dist/commands/select.js +53 -0
  176. package/dist/commands/select.js.map +1 -0
  177. package/dist/commands/snapshot.d.ts +9 -0
  178. package/dist/commands/snapshot.d.ts.map +1 -0
  179. package/dist/commands/snapshot.js +7 -0
  180. package/dist/commands/snapshot.js.map +1 -0
  181. package/dist/commands/styles.d.ts +13 -0
  182. package/dist/commands/styles.d.ts.map +1 -0
  183. package/dist/commands/styles.js +7 -0
  184. package/dist/commands/styles.js.map +1 -0
  185. package/dist/commands/type.d.ts +3 -1
  186. package/dist/commands/type.d.ts.map +1 -1
  187. package/dist/commands/type.js +11 -2
  188. package/dist/commands/type.js.map +1 -1
  189. package/dist/commands/upload.d.ts +12 -0
  190. package/dist/commands/upload.d.ts.map +1 -0
  191. package/dist/commands/upload.js +13 -0
  192. package/dist/commands/upload.js.map +1 -0
  193. package/dist/mcp/console-buffer.d.ts +9 -0
  194. package/dist/mcp/console-buffer.d.ts.map +1 -1
  195. package/dist/mcp/console-buffer.js.map +1 -1
  196. package/dist/mcp/index.js +20 -2
  197. package/dist/mcp/index.js.map +1 -1
  198. package/dist/mcp/server.d.ts +10 -0
  199. package/dist/mcp/server.d.ts.map +1 -1
  200. package/dist/mcp/server.js +63 -2
  201. package/dist/mcp/server.js.map +1 -1
  202. package/dist/mcp/tools/browser.d.ts.map +1 -1
  203. package/dist/mcp/tools/browser.js +109 -11
  204. package/dist/mcp/tools/browser.js.map +1 -1
  205. package/dist/mcp/tools/connect.d.ts +5 -0
  206. package/dist/mcp/tools/connect.d.ts.map +1 -0
  207. package/dist/mcp/tools/connect.js +48 -0
  208. package/dist/mcp/tools/connect.js.map +1 -0
  209. package/dist/mcp/tools/files.d.ts +4 -0
  210. package/dist/mcp/tools/files.d.ts.map +1 -0
  211. package/dist/mcp/tools/files.js +85 -0
  212. package/dist/mcp/tools/files.js.map +1 -0
  213. package/dist/mcp/tools/interaction.d.ts +4 -0
  214. package/dist/mcp/tools/interaction.d.ts.map +1 -0
  215. package/dist/mcp/tools/interaction.js +176 -0
  216. package/dist/mcp/tools/interaction.js.map +1 -0
  217. package/dist/mcp/tools/names.d.ts +1 -1
  218. package/dist/mcp/tools/names.d.ts.map +1 -1
  219. package/dist/mcp/tools/names.js +19 -1
  220. package/dist/mcp/tools/names.js.map +1 -1
  221. package/dist/mcp/tools/network.d.ts +4 -0
  222. package/dist/mcp/tools/network.d.ts.map +1 -0
  223. package/dist/mcp/tools/network.js +68 -0
  224. package/dist/mcp/tools/network.js.map +1 -0
  225. package/dist/mcp/tools/performance.d.ts +4 -0
  226. package/dist/mcp/tools/performance.d.ts.map +1 -0
  227. package/dist/mcp/tools/performance.js +26 -0
  228. package/dist/mcp/tools/performance.js.map +1 -0
  229. package/dist/mcp/tools/runtime.d.ts +4 -0
  230. package/dist/mcp/tools/runtime.d.ts.map +1 -0
  231. package/dist/mcp/tools/runtime.js +128 -0
  232. package/dist/mcp/tools/runtime.js.map +1 -0
  233. package/dist/mcp/tools/snapshot.d.ts +4 -0
  234. package/dist/mcp/tools/snapshot.d.ts.map +1 -0
  235. package/dist/mcp/tools/snapshot.js +42 -0
  236. package/dist/mcp/tools/snapshot.js.map +1 -0
  237. package/dist/mcp/tools/styles.d.ts +4 -0
  238. package/dist/mcp/tools/styles.d.ts.map +1 -0
  239. package/dist/mcp/tools/styles.js +27 -0
  240. package/dist/mcp/tools/styles.js.map +1 -0
  241. package/dist/util/config.d.ts.map +1 -1
  242. package/dist/util/config.js +3 -2
  243. package/dist/util/config.js.map +1 -1
  244. package/package.json +5 -4
  245. package/skills/pilot/SKILL.md +48 -0
  246. package/src/browser/browser-manager.ts +465 -0
  247. package/src/browser/chrome-launcher.ts +282 -0
  248. package/src/browser/connection.ts +84 -0
  249. package/src/browser/events.ts +121 -0
  250. package/src/browser/index.ts +20 -0
  251. package/src/browser/inspect/element-ref.ts +68 -0
  252. package/src/browser/inspect/snapshot.ts +348 -0
  253. package/src/browser/inspect/styles.ts +258 -0
  254. package/src/browser/inspect/types.ts +37 -0
  255. package/src/browser/interaction/dialogs.ts +54 -0
  256. package/src/browser/interaction/downloads.ts +116 -0
  257. package/src/browser/interaction/events.ts +79 -0
  258. package/src/browser/interaction/ref-resolver.ts +53 -0
  259. package/src/browser/interaction/uploads.ts +60 -0
  260. package/src/browser/network/network-buffer.ts +133 -0
  261. package/src/browser/network/network-monitor.ts +107 -0
  262. package/src/browser/network/redaction.ts +42 -0
  263. package/src/browser/network/types.ts +47 -0
  264. package/src/browser/performance/analyze.ts +298 -0
  265. package/src/browser/performance/trace.ts +60 -0
  266. package/src/browser/performance/types.ts +34 -0
  267. package/src/browser/preflight.ts +144 -0
  268. package/src/browser/security/origins.ts +102 -0
  269. package/src/browser/tabs.ts +110 -0
  270. package/src/browser/types.ts +32 -0
  271. package/src/browser.ts +22 -798
  272. package/src/cli/init.ts +40 -49
  273. package/src/cli/linux-check.ts +23 -0
  274. package/src/cli/skill-template.ts +4 -113
  275. package/src/commands/check.ts +59 -0
  276. package/src/commands/click.ts +18 -4
  277. package/src/commands/connect.ts +40 -0
  278. package/src/commands/dialog.ts +79 -0
  279. package/src/commands/downloads.ts +111 -0
  280. package/src/commands/fill.ts +34 -0
  281. package/src/commands/find.ts +52 -0
  282. package/src/commands/hover.ts +42 -0
  283. package/src/commands/navigate.ts +20 -0
  284. package/src/commands/network.ts +141 -0
  285. package/src/commands/performance.ts +51 -0
  286. package/src/commands/press_key.ts +134 -0
  287. package/src/commands/scroll.ts +79 -0
  288. package/src/commands/select.ts +78 -0
  289. package/src/commands/snapshot.ts +18 -0
  290. package/src/commands/styles.ts +24 -0
  291. package/src/commands/type.ts +12 -2
  292. package/src/commands/upload.ts +36 -0
  293. package/src/mcp/console-buffer.ts +10 -0
  294. package/src/mcp/index.ts +24 -2
  295. package/src/mcp/server.ts +74 -2
  296. package/src/mcp/tools/browser.ts +125 -9
  297. package/src/mcp/tools/connect.ts +62 -0
  298. package/src/mcp/tools/files.ts +120 -0
  299. package/src/mcp/tools/interaction.ts +232 -0
  300. package/src/mcp/tools/names.ts +19 -1
  301. package/src/mcp/tools/network.ts +86 -0
  302. package/src/mcp/tools/performance.ts +40 -0
  303. package/src/mcp/tools/runtime.ts +164 -0
  304. package/src/mcp/tools/snapshot.ts +64 -0
  305. package/src/mcp/tools/styles.ts +41 -0
  306. 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,144 @@
1
+ import { constants } from 'fs';
2
+ import { access, mkdir } from 'fs/promises';
3
+ import { request } from 'https';
4
+ import {
5
+ findChromeExecutable,
6
+ findFreePort,
7
+ resolveUserDataDir
8
+ } from './chrome-launcher.js';
9
+
10
+ export interface LinuxCheckResult {
11
+ name: string;
12
+ ok: boolean;
13
+ detail: string;
14
+ }
15
+
16
+ export interface LinuxCheckOptions {
17
+ headless: boolean;
18
+ chromePath?: string;
19
+ profileName: string;
20
+ checkNetwork?: boolean;
21
+ }
22
+
23
+ export function checkDisplay(headless: boolean): LinuxCheckResult {
24
+ if (headless) {
25
+ return {
26
+ name: 'display',
27
+ ok: true,
28
+ detail: 'not required in headless mode'
29
+ };
30
+ }
31
+ const display = process.env.DISPLAY || process.env.WAYLAND_DISPLAY;
32
+ return display
33
+ ? { name: 'display', ok: true, detail: display }
34
+ : {
35
+ name: 'display',
36
+ ok: false,
37
+ detail:
38
+ 'headless:false requires DISPLAY or WAYLAND_DISPLAY; use a desktop session, xvfb-run, or headless:true'
39
+ };
40
+ }
41
+
42
+ export async function checkChrome(
43
+ chromePath?: string
44
+ ): Promise<LinuxCheckResult> {
45
+ try {
46
+ const resolved = findChromeExecutable(chromePath);
47
+ await access(resolved, constants.X_OK);
48
+ return { name: 'chrome', ok: true, detail: resolved };
49
+ } catch (error) {
50
+ return { name: 'chrome', ok: false, detail: String(error) };
51
+ }
52
+ }
53
+
54
+ export async function checkProfile(
55
+ profileName: string
56
+ ): Promise<LinuxCheckResult> {
57
+ let profileDir: string;
58
+ try {
59
+ profileDir = resolveUserDataDir(profileName);
60
+ } catch (error) {
61
+ return { name: 'profile', ok: false, detail: String(error) };
62
+ }
63
+
64
+ try {
65
+ await mkdir(profileDir, { recursive: true });
66
+ await access(profileDir, constants.R_OK | constants.W_OK | constants.X_OK);
67
+ return { name: 'profile', ok: true, detail: profileDir };
68
+ } catch (error) {
69
+ return {
70
+ name: 'profile',
71
+ ok: false,
72
+ detail: `${profileDir} is not writable: ${String(error)}; set XDG_DATA_HOME to a writable directory`
73
+ };
74
+ }
75
+ }
76
+
77
+ export async function checkLoopback(): Promise<LinuxCheckResult> {
78
+ try {
79
+ const port = await findFreePort();
80
+ return { name: 'loopback', ok: true, detail: `127.0.0.1:${port}` };
81
+ } catch (error) {
82
+ return {
83
+ name: 'loopback',
84
+ ok: false,
85
+ detail: `${String(error)}; Pilot requires permission to bind an ephemeral CDP port on 127.0.0.1`
86
+ };
87
+ }
88
+ }
89
+
90
+ export async function checkOutboundHttps(
91
+ timeoutMs = 5000,
92
+ requestFn: typeof request = request
93
+ ): Promise<LinuxCheckResult> {
94
+ return new Promise((resolve) => {
95
+ const req = requestFn(
96
+ 'https://clients3.google.com/generate_204',
97
+ { method: 'HEAD', timeout: timeoutMs },
98
+ (response) => {
99
+ response.resume();
100
+ resolve({
101
+ name: 'network',
102
+ ok: response.statusCode === 204,
103
+ detail: `HTTPS status ${response.statusCode ?? 'unknown'} (expected 204)`
104
+ });
105
+ }
106
+ );
107
+ req.on('timeout', () =>
108
+ req.destroy(new Error(`timeout after ${timeoutMs}ms`))
109
+ );
110
+ req.on('error', (error) =>
111
+ resolve({ name: 'network', ok: false, detail: String(error) })
112
+ );
113
+ req.end();
114
+ });
115
+ }
116
+
117
+ export async function runLinuxChecks(
118
+ options: LinuxCheckOptions
119
+ ): Promise<LinuxCheckResult[]> {
120
+ if (process.platform !== 'linux') return [];
121
+ const checks = await Promise.all([
122
+ checkChrome(options.chromePath),
123
+ Promise.resolve(checkDisplay(options.headless)),
124
+ checkProfile(options.profileName),
125
+ checkLoopback()
126
+ ]);
127
+ if (options.checkNetwork !== false) checks.push(await checkOutboundHttps());
128
+ return checks;
129
+ }
130
+
131
+ export async function assertLinuxBrowserPrerequisites(
132
+ options: LinuxCheckOptions
133
+ ): Promise<void> {
134
+ if (process.platform !== 'linux') return;
135
+ const checks = await runLinuxChecks({ ...options, checkNetwork: false });
136
+ const failures = checks.filter((check) => !check.ok);
137
+ if (failures.length > 0) {
138
+ throw new Error(
139
+ `Linux browser preflight failed:\n${failures
140
+ .map((failure) => `- ${failure.name}: ${failure.detail}`)
141
+ .join('\n')}\nRun "pilot check" for complete diagnostics.`
142
+ );
143
+ }
144
+ }
@@ -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
+ }