@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,271 @@
1
+ import { spawn, type ChildProcess } from 'child_process';
2
+ import { existsSync, lstatSync, readlinkSync, unlinkSync } from 'fs';
3
+ import * as net from 'net';
4
+ import { homedir, hostname } from 'os';
5
+ import { join } from 'path';
6
+ import CDP from 'chrome-remote-interface';
7
+ import { getLogger } from '../util/logger.js';
8
+ import type { SandboxDecision } from './types.js';
9
+
10
+ export const PROFILE_NAME_RE = /^[a-zA-Z0-9][a-zA-Z0-9_-]*$/;
11
+
12
+ export function sandboxDecision(): SandboxDecision {
13
+ const envOverride = process.env.PILOT_CHROME_NO_SANDBOX;
14
+ if (envOverride !== undefined) {
15
+ const truthy = envOverride === 'true' || envOverride === '1';
16
+ return truthy
17
+ ? { disable: true, reason: 'env_override' }
18
+ : { disable: false };
19
+ }
20
+ if (process.platform !== 'linux' && process.platform !== 'win32') {
21
+ return { disable: false };
22
+ }
23
+ if (process.getuid?.() === 0) {
24
+ return { disable: true, reason: 'root_user' };
25
+ }
26
+ return { disable: false };
27
+ }
28
+
29
+ export function shouldDisableSandbox(): boolean {
30
+ return sandboxDecision().disable;
31
+ }
32
+
33
+ export function resolveUserDataDir(profileName: string): string {
34
+ if (!PROFILE_NAME_RE.test(profileName)) {
35
+ throw new Error(
36
+ `Invalid profile name "${profileName}". Use only letters, digits, hyphens, and underscores.`
37
+ );
38
+ }
39
+ const xdgData =
40
+ process.env.XDG_DATA_HOME || join(homedir(), '.local', 'share');
41
+ return join(xdgData, 'pilot', profileName);
42
+ }
43
+
44
+ function fileOrSymlinkExists(filePath: string): boolean {
45
+ try {
46
+ lstatSync(filePath);
47
+ return true;
48
+ } catch (err: unknown) {
49
+ const code = (err as NodeJS.ErrnoException)?.code;
50
+ if (code === 'ENOENT' || code === 'ENOTDIR') {
51
+ return false;
52
+ }
53
+ return true;
54
+ }
55
+ }
56
+
57
+ export function extractLockPid(
58
+ lockPath: string
59
+ ): { pid: number; lockHost: string } | null {
60
+ try {
61
+ const stat = lstatSync(lockPath);
62
+ if (!stat.isSymbolicLink()) return null;
63
+ const target = readlinkSync(lockPath);
64
+ const lastDash = target.lastIndexOf('-');
65
+ if (lastDash === -1) return null;
66
+ const lockHost = target.slice(0, lastDash);
67
+ const pid = parseInt(target.slice(lastDash + 1), 10);
68
+ if (isNaN(pid) || pid <= 0) return null;
69
+ return { pid, lockHost };
70
+ } catch {
71
+ return null;
72
+ }
73
+ }
74
+
75
+ export function isProcessAlive(pid: number): boolean {
76
+ if (pid <= 0) return false;
77
+ try {
78
+ process.kill(pid, 0);
79
+ return true;
80
+ } catch (err: unknown) {
81
+ const code = (err as NodeJS.ErrnoException)?.code;
82
+ return code !== 'ESRCH';
83
+ }
84
+ }
85
+
86
+ function removeStaleLocks(userDataDir: string): void {
87
+ for (const lockName of ['SingletonLock', 'lockfile']) {
88
+ try {
89
+ unlinkSync(join(userDataDir, lockName));
90
+ } catch (err: unknown) {
91
+ const code = (err as NodeJS.ErrnoException)?.code;
92
+ if (code !== 'ENOENT') throw err;
93
+ }
94
+ }
95
+ const logger = getLogger();
96
+ logger.warn(
97
+ { userDataDir },
98
+ 'Cleaned up stale Chrome lock files from a previous crash'
99
+ );
100
+ }
101
+
102
+ export function isProfileLocked(userDataDir: string): boolean {
103
+ const singletonPath = join(userDataDir, 'SingletonLock');
104
+ const lockfilePath = join(userDataDir, 'lockfile');
105
+
106
+ const hasSingleton = fileOrSymlinkExists(singletonPath);
107
+ const hasLockfile = existsSync(lockfilePath);
108
+
109
+ if (!hasSingleton && !hasLockfile) return false;
110
+
111
+ const lockInfo = hasSingleton ? extractLockPid(singletonPath) : null;
112
+
113
+ if (!lockInfo) {
114
+ return true;
115
+ }
116
+
117
+ const currentHost = hostname();
118
+ if (lockInfo.lockHost !== currentHost) {
119
+ return true;
120
+ }
121
+
122
+ if (isProcessAlive(lockInfo.pid)) {
123
+ return true;
124
+ }
125
+
126
+ try {
127
+ removeStaleLocks(userDataDir);
128
+ return false;
129
+ } catch {
130
+ return true;
131
+ }
132
+ }
133
+
134
+ export function findChromeExecutable(override?: string): string {
135
+ if (override) return override;
136
+ const env = process.env.PILOT_CHROME_PATH;
137
+ if (env) return env;
138
+
139
+ const platform = process.platform;
140
+ const candidates: string[] =
141
+ platform === 'darwin'
142
+ ? [
143
+ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
144
+ '/Applications/Chromium.app/Contents/MacOS/Chromium'
145
+ ]
146
+ : platform === 'linux'
147
+ ? [
148
+ '/usr/bin/google-chrome-stable',
149
+ '/usr/bin/google-chrome',
150
+ '/usr/bin/chromium-browser',
151
+ '/usr/bin/chromium',
152
+ '/snap/bin/chromium'
153
+ ]
154
+ : platform === 'win32'
155
+ ? [
156
+ 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
157
+ 'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe'
158
+ ]
159
+ : [];
160
+
161
+ for (const p of candidates) {
162
+ if (existsSync(p)) return p;
163
+ }
164
+ throw new Error(
165
+ `Chrome not found on ${platform}. Install Google Chrome or set PILOT_CHROME_PATH.`
166
+ );
167
+ }
168
+
169
+ export async function findFreePort(): Promise<number> {
170
+ return new Promise((resolve, reject) => {
171
+ const server = net.createServer();
172
+ server.listen(0, '127.0.0.1', () => {
173
+ const { port } = server.address() as net.AddressInfo;
174
+ server.close(() => resolve(port));
175
+ });
176
+ server.on('error', reject);
177
+ });
178
+ }
179
+
180
+ export async function waitForChromeReady(
181
+ port: number,
182
+ timeoutMs = 10000
183
+ ): Promise<void> {
184
+ const deadline = Date.now() + timeoutMs;
185
+ while (Date.now() < deadline) {
186
+ try {
187
+ await CDP.List({ host: '127.0.0.1', port });
188
+ return;
189
+ } catch {
190
+ await new Promise((r) => setTimeout(r, 200));
191
+ }
192
+ }
193
+ throw new Error(
194
+ `Chrome did not become ready on port ${port} within ${timeoutMs}ms`
195
+ );
196
+ }
197
+
198
+ export interface ChromeLaunchResult {
199
+ process: ChildProcess;
200
+ port: number;
201
+ userDataDir: string;
202
+ }
203
+
204
+ export function launchChrome(opts: {
205
+ chromePath: string;
206
+ port: number;
207
+ userDataDir: string;
208
+ headless: boolean;
209
+ windowWidth: number;
210
+ windowHeight: number;
211
+ }): ChildProcess {
212
+ const logger = getLogger();
213
+
214
+ const args = [
215
+ `--remote-debugging-port=${opts.port}`,
216
+ `--user-data-dir=${opts.userDataDir}`,
217
+ `--window-size=${opts.windowWidth},${opts.windowHeight}`,
218
+ '--no-first-run',
219
+ '--no-default-browser-check',
220
+ '--disable-default-apps',
221
+ '--disable-extensions'
222
+ ];
223
+
224
+ if (opts.headless) {
225
+ args.push('--headless', '--disable-gpu');
226
+ }
227
+
228
+ if (process.platform === 'linux') {
229
+ args.push(
230
+ '--disable-dev-shm-usage',
231
+ '--disable-software-rasterizer',
232
+ '--disable-breakpad',
233
+ '--password-store=basic',
234
+ '--use-mock-keychain'
235
+ );
236
+ }
237
+
238
+ const sandbox = sandboxDecision();
239
+ if (sandbox.disable) {
240
+ args.push('--no-sandbox');
241
+ logger.warn(
242
+ { reason: sandbox.reason },
243
+ 'Chrome will run with --no-sandbox. The renderer sandbox is disabled; any page the agent visits runs with the same privileges as this process. Set PILOT_CHROME_NO_SANDBOX=false to override, or run as a non-root user to keep the sandbox enabled.'
244
+ );
245
+ }
246
+
247
+ logger.info(
248
+ {
249
+ chromePath: opts.chromePath,
250
+ port: opts.port,
251
+ headless: opts.headless,
252
+ sandbox: sandbox.disable ? 'disabled' : 'enabled'
253
+ },
254
+ 'Launching Chrome'
255
+ );
256
+
257
+ return spawn(opts.chromePath, args, {
258
+ stdio: ['ignore', 'ignore', 'pipe'],
259
+ detached: false
260
+ });
261
+ }
262
+
263
+ export function cleanupLockFiles(userDataDir: string): void {
264
+ for (const lockName of ['SingletonLock', 'lockfile']) {
265
+ try {
266
+ unlinkSync(join(userDataDir, lockName));
267
+ } catch {
268
+ // lock file may not exist — that's fine
269
+ }
270
+ }
271
+ }
@@ -0,0 +1,84 @@
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 { Config } from '../util/config.js';
5
+ import { type ViewportConfig, applyViewport } from '../viewport.js';
6
+
7
+ export interface ConnectionInfo {
8
+ host: string;
9
+ port: number;
10
+ }
11
+
12
+ export async function connectToChrome(opts: {
13
+ host: string;
14
+ port: number;
15
+ }): Promise<{ client: Client; targetId: string; url: string }> {
16
+ const targets = await CDP.List({ host: opts.host, port: opts.port });
17
+
18
+ const pageTarget = targets.find((t) => t.type === 'page');
19
+ if (!pageTarget) {
20
+ throw new Error('No page target found in Chrome');
21
+ }
22
+
23
+ const client = await CDP({
24
+ host: opts.host,
25
+ port: opts.port,
26
+ target: pageTarget.id
27
+ });
28
+
29
+ return {
30
+ client,
31
+ targetId: pageTarget.id,
32
+ url: pageTarget.url
33
+ };
34
+ }
35
+
36
+ export async function enableDomains(
37
+ client: Client,
38
+ viewportConfig: ViewportConfig | null
39
+ ): Promise<void> {
40
+ await Promise.all([
41
+ client.Network.enable({}),
42
+ client.Console.enable(),
43
+ client.Page.enable(),
44
+ client.Runtime.enable()
45
+ ]);
46
+
47
+ if (viewportConfig) {
48
+ await applyViewport(client, viewportConfig);
49
+ }
50
+ }
51
+
52
+ export function getConnectionInfo(
53
+ config: Config,
54
+ launchedCdpPort: number | null
55
+ ): ConnectionInfo {
56
+ return {
57
+ host: launchedCdpPort ? '127.0.0.1' : config.cdpHost,
58
+ port: launchedCdpPort ?? config.cdpPort
59
+ };
60
+ }
61
+
62
+ export function scheduleReconnect(
63
+ config: Config,
64
+ state: {
65
+ destroyed: boolean;
66
+ retryTimer: ReturnType<typeof setTimeout> | null;
67
+ retryMs: number;
68
+ },
69
+ reconnectFn: () => void
70
+ ): ReturnType<typeof setTimeout> | null {
71
+ if (state.destroyed) return null;
72
+ if (state.retryTimer) return state.retryTimer;
73
+
74
+ const logger = getLogger();
75
+ logger.debug({ retryMs: state.retryMs }, 'Scheduling reconnect');
76
+
77
+ const timer = setTimeout(() => {
78
+ state.retryTimer = null;
79
+ state.retryMs = Math.min(state.retryMs * 2, config.cdpMaxRetryMs);
80
+ reconnectFn();
81
+ }, state.retryMs);
82
+ timer.unref();
83
+ return timer;
84
+ }
@@ -0,0 +1,121 @@
1
+ import type { Client } from 'chrome-remote-interface';
2
+
3
+ /**
4
+ * Normalize CDP console level names to match ConsoleLevel.
5
+ * CDP uses "warning" for console.warn; normalize to "warn".
6
+ */
7
+ function normLevel(raw: string): string {
8
+ return raw === 'warning' ? 'warn' : raw;
9
+ }
10
+
11
+ export function attachEventListeners(
12
+ client: Client,
13
+ opts: {
14
+ emit: (event: string, data: unknown) => void;
15
+ onDisconnect: () => void;
16
+ pendingNetworkRequests: Set<string>;
17
+ checkNetworkIdle: () => void;
18
+ onFrameNavigated: (url: string) => void;
19
+ }
20
+ ): void {
21
+ client.on('disconnect', () => {
22
+ opts.onDisconnect();
23
+ });
24
+
25
+ // Console events
26
+ client.Runtime.consoleAPICalled((params) => {
27
+ const text = params.args
28
+ .map((a) =>
29
+ a.value !== undefined ? String(a.value) : (a.description ?? '')
30
+ )
31
+ .join(' ');
32
+ opts.emit('console_message', {
33
+ level: normLevel(params.type),
34
+ text,
35
+ url: params.stackTrace?.callFrames?.[0]?.url ?? '',
36
+ lineNumber: params.stackTrace?.callFrames?.[0]?.lineNumber ?? 0,
37
+ timestamp: Date.now()
38
+ });
39
+ });
40
+
41
+ client.Console.messageAdded((params) => {
42
+ opts.emit('console_message', {
43
+ level: normLevel(params.message.level),
44
+ text: params.message.text,
45
+ url: params.message.url ?? '',
46
+ lineNumber: params.message.line ?? 0,
47
+ timestamp: Date.now()
48
+ });
49
+ });
50
+
51
+ // Runtime exceptions
52
+ client.Runtime.exceptionThrown((params) => {
53
+ const ex = params.exceptionDetails;
54
+ const message = ex.exception?.description ?? ex.text ?? 'Unknown exception';
55
+ const frames = ex.stackTrace?.callFrames?.map((f) => ({
56
+ url: f.url,
57
+ functionName: f.functionName,
58
+ lineNumber: f.lineNumber,
59
+ columnNumber: f.columnNumber
60
+ }));
61
+
62
+ const topFrame = frames?.[0];
63
+ opts.emit('console_message', {
64
+ level: 'error',
65
+ text: message,
66
+ url: ex.url ?? topFrame?.url ?? '',
67
+ lineNumber: ex.lineNumber ?? topFrame?.lineNumber ?? 0,
68
+ columnNumber: ex.columnNumber ?? topFrame?.columnNumber,
69
+ timestamp: Date.now(),
70
+ stackFrames: frames,
71
+ isException: true
72
+ });
73
+ });
74
+
75
+ // Network events
76
+ client.Network.requestWillBeSent((params) => {
77
+ opts.pendingNetworkRequests.add(params.requestId);
78
+ opts.emit('network_request', {
79
+ requestId: params.requestId,
80
+ url: params.request.url,
81
+ method: params.request.method,
82
+ timestamp: Date.now()
83
+ });
84
+ });
85
+
86
+ client.Network.responseReceived((params) => {
87
+ opts.pendingNetworkRequests.delete(params.requestId);
88
+ opts.checkNetworkIdle();
89
+ opts.emit('network_response', {
90
+ requestId: params.requestId,
91
+ url: params.response.url,
92
+ status: params.response.status,
93
+ mimeType: params.response.mimeType,
94
+ fromCache: params.response.fromDiskCache ?? false,
95
+ timestamp: Date.now()
96
+ });
97
+ });
98
+
99
+ client.Network.loadingFailed((params) => {
100
+ opts.pendingNetworkRequests.delete(params.requestId);
101
+ opts.checkNetworkIdle();
102
+ opts.emit('network_failed', {
103
+ requestId: params.requestId,
104
+ url: '',
105
+ errorText: params.errorText,
106
+ timestamp: Date.now()
107
+ });
108
+ });
109
+
110
+ client.Network.loadingFinished((params) => {
111
+ opts.pendingNetworkRequests.delete(params.requestId);
112
+ opts.checkNetworkIdle();
113
+ });
114
+
115
+ // Page navigation
116
+ client.Page.frameNavigated((params) => {
117
+ if (params.frame.parentId === undefined) {
118
+ opts.onFrameNavigated(params.frame.url);
119
+ }
120
+ });
121
+ }
@@ -0,0 +1,20 @@
1
+ // Re-export all public types and classes from the browser module
2
+ export { BrowserManager } from './browser-manager.js';
3
+ export type {
4
+ BrowserEventCallback,
5
+ TabInfo,
6
+ BrowserState,
7
+ LaunchOptions,
8
+ SandboxDecision
9
+ } from './types.js';
10
+ export {
11
+ PROFILE_NAME_RE,
12
+ sandboxDecision,
13
+ shouldDisableSandbox,
14
+ resolveUserDataDir,
15
+ extractLockPid,
16
+ isProcessAlive,
17
+ isProfileLocked,
18
+ findChromeExecutable,
19
+ findFreePort
20
+ } from './chrome-launcher.js';
@@ -0,0 +1,68 @@
1
+ import type { StaleElementError } from './types.js';
2
+
3
+ interface RefEntry {
4
+ backendNodeId: number;
5
+ }
6
+
7
+ export class ElementRefMap {
8
+ private generation = 0;
9
+ private refs = new Map<string, RefEntry>();
10
+ private counter = 0;
11
+
12
+ getGeneration(): number {
13
+ return this.generation;
14
+ }
15
+
16
+ /**
17
+ * Clear refs for a new snapshot within the same document.
18
+ * Does not increment generation.
19
+ */
20
+ resetRefs(): void {
21
+ this.refs.clear();
22
+ this.counter = 0;
23
+ }
24
+
25
+ /**
26
+ * Invalidate all refs due to navigation or DOM replacement.
27
+ * Increments generation so stale refs are detected.
28
+ */
29
+ invalidate(): void {
30
+ this.refs.clear();
31
+ this.counter = 0;
32
+ this.generation++;
33
+ }
34
+
35
+ nextRef(): string {
36
+ this.counter++;
37
+ return `e${this.counter}`;
38
+ }
39
+
40
+ set(ref: string, backendNodeId: number): void {
41
+ this.refs.set(ref, { backendNodeId });
42
+ }
43
+
44
+ resolve(ref: string): number {
45
+ const entry = this.refs.get(ref);
46
+ if (!entry) {
47
+ throw this.staleError(ref);
48
+ }
49
+ return entry.backendNodeId;
50
+ }
51
+
52
+ has(ref: string): boolean {
53
+ return this.refs.has(ref);
54
+ }
55
+
56
+ size(): number {
57
+ return this.refs.size;
58
+ }
59
+
60
+ private staleError(ref: string): StaleElementError & Error {
61
+ const err = new Error(
62
+ `Element ref "${ref}" is stale. The page has navigated or the DOM has been replaced. Take a new snapshot to get fresh refs.`
63
+ ) as StaleElementError & Error;
64
+ err.code = 'STALE_ELEMENT_REFERENCE';
65
+ err.ref = ref;
66
+ return err;
67
+ }
68
+ }