@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
package/dist/browser.js CHANGED
@@ -1,654 +1,4 @@
1
- import CDP from 'chrome-remote-interface';
2
- import { spawn } from 'child_process';
3
- import { existsSync, lstatSync, mkdirSync, readlinkSync, unlinkSync } from 'fs';
4
- import * as net from 'net';
5
- import { homedir, hostname } from 'os';
6
- import { join } from 'path';
7
- import { getLogger } from './util/logger.js';
8
- import { resolveViewport, applyViewport } from './viewport.js';
9
- export function sandboxDecision() {
10
- const envOverride = process.env.PILOT_CHROME_NO_SANDBOX;
11
- if (envOverride !== undefined) {
12
- const truthy = envOverride === 'true' || envOverride === '1';
13
- return truthy
14
- ? { disable: true, reason: 'env_override' }
15
- : { disable: false };
16
- }
17
- // --no-sandbox is relevant on Linux and (in theory) Windows.
18
- // macOS uses a different sandboxing model and Chrome ignores the flag.
19
- if (process.platform !== 'linux' && process.platform !== 'win32') {
20
- return { disable: false };
21
- }
22
- // Auto-apply only when running as root. Chrome's user-namespace sandbox
23
- // typically fails as root in containers, and root is the most reliable
24
- // signal that the sandbox cannot work. Non-root processes (including
25
- // non-root in containers) usually have a working sandbox — set
26
- // PILOT_CHROME_NO_SANDBOX=true to opt in if a specific environment
27
- // genuinely requires it.
28
- if (process.getuid?.() === 0) {
29
- return { disable: true, reason: 'root_user' };
30
- }
31
- return { disable: false };
32
- }
33
- export function shouldDisableSandbox() {
34
- return sandboxDecision().disable;
35
- }
36
- export const PROFILE_NAME_RE = /^[a-zA-Z0-9][a-zA-Z0-9_-]*$/;
37
- export function resolveUserDataDir(profileName) {
38
- if (!PROFILE_NAME_RE.test(profileName)) {
39
- throw new Error(`Invalid profile name "${profileName}". Use only letters, digits, hyphens, and underscores.`);
40
- }
41
- const xdgData = process.env.XDG_DATA_HOME || join(homedir(), '.local', 'share');
42
- return join(xdgData, 'pilot', profileName);
43
- }
44
- function fileOrSymlinkExists(filePath) {
45
- try {
46
- lstatSync(filePath);
47
- return true;
48
- }
49
- catch (err) {
50
- const code = err?.code;
51
- if (code === 'ENOENT' || code === 'ENOTDIR') {
52
- return false;
53
- }
54
- // Permission errors (EACCES) or other unexpected failures — treat as
55
- // "exists" so callers don't silently ignore an inaccessible lock file.
56
- return true;
57
- }
58
- }
59
- /**
60
- * Extract the PID from a Chrome SingletonLock symlink target.
61
- * On Linux, Chrome writes the symlink target as "hostname-pid".
62
- * Returns the PID if extraction succeeds, or null otherwise.
63
- */
64
- export function extractLockPid(lockPath) {
65
- try {
66
- const stat = lstatSync(lockPath);
67
- if (!stat.isSymbolicLink())
68
- return null;
69
- const target = readlinkSync(lockPath);
70
- // Format: "hostname-pid" — find the last hyphen to split
71
- const lastDash = target.lastIndexOf('-');
72
- if (lastDash === -1)
73
- return null;
74
- const lockHost = target.slice(0, lastDash);
75
- const pid = parseInt(target.slice(lastDash + 1), 10);
76
- if (isNaN(pid) || pid <= 0)
77
- return null;
78
- return { pid, lockHost };
79
- }
80
- catch {
81
- return null;
82
- }
83
- }
84
- /**
85
- * Check whether a process with the given PID is still running.
86
- * Returns true if the process exists (even if we lack permission to signal it).
87
- * Returns false only when the OS confirms the process does not exist (ESRCH).
88
- */
89
- export function isProcessAlive(pid) {
90
- if (pid <= 0)
91
- return false;
92
- try {
93
- process.kill(pid, 0);
94
- return true;
95
- }
96
- catch (err) {
97
- const code = err?.code;
98
- // ESRCH = no such process — the process is genuinely dead.
99
- // EPERM = process exists but we lack permission to signal it — treat as alive.
100
- return code !== 'ESRCH';
101
- }
102
- }
103
- /**
104
- * Remove stale lock files from a Chrome user data directory.
105
- * Only ignores ENOENT (file already gone); throws on other errors
106
- * (EACCES, EPERM, etc.) so callers can fall back to treating the
107
- * profile as locked.
108
- */
109
- function removeStaleLocks(userDataDir) {
110
- for (const lockName of ['SingletonLock', 'lockfile']) {
111
- try {
112
- unlinkSync(join(userDataDir, lockName));
113
- }
114
- catch (err) {
115
- const code = err?.code;
116
- if (code !== 'ENOENT')
117
- throw err;
118
- }
119
- }
120
- const logger = getLogger();
121
- logger.warn({ userDataDir }, 'Cleaned up stale Chrome lock files from a previous crash');
122
- }
123
- export function isProfileLocked(userDataDir) {
124
- const singletonPath = join(userDataDir, 'SingletonLock');
125
- const lockfilePath = join(userDataDir, 'lockfile');
126
- const hasSingleton = fileOrSymlinkExists(singletonPath);
127
- const hasLockfile = existsSync(lockfilePath);
128
- if (!hasSingleton && !hasLockfile)
129
- return false;
130
- // Try to extract PID from SingletonLock symlink (Linux)
131
- const lockInfo = hasSingleton ? extractLockPid(singletonPath) : null;
132
- if (!lockInfo) {
133
- // Cannot extract PID (macOS regular file, missing symlink, etc.)
134
- // Fall back to conservative behavior: treat as locked.
135
- return true;
136
- }
137
- // If the lock was created on a different host, treat as locked
138
- // (e.g. shared filesystem across machines).
139
- const currentHost = hostname();
140
- if (lockInfo.lockHost !== currentHost) {
141
- return true;
142
- }
143
- // Check if the process that created the lock is still running
144
- if (isProcessAlive(lockInfo.pid)) {
145
- return true;
146
- }
147
- // Process is dead — stale lock from a crash. Clean up and allow reuse.
148
- // If cleanup fails (e.g. permission error), treat as locked to be safe.
149
- try {
150
- removeStaleLocks(userDataDir);
151
- return false;
152
- }
153
- catch {
154
- return true;
155
- }
156
- }
157
- export class BrowserManager {
158
- config;
159
- client = null;
160
- targetId = null;
161
- currentUrl = null;
162
- retryTimer = null;
163
- retryMs;
164
- destroyed = false;
165
- intentionalClose = false;
166
- eventCallback = null;
167
- pendingNetworkRequests = new Set();
168
- networkIdleTimer = null;
169
- networkIdleResolvers = [];
170
- chromeProcess = null;
171
- launchedCdpPort = null;
172
- viewportConfig = null;
173
- launchedUserDataDir = null;
174
- constructor(config) {
175
- this.config = config;
176
- this.retryMs = config.cdpRetryMs;
177
- }
178
- setEventCallback(cb) {
179
- this.eventCallback = cb;
180
- }
181
- emit(event, data) {
182
- this.eventCallback?.(event, data);
183
- }
184
- getState() {
185
- return {
186
- client: this.client,
187
- targetId: this.targetId,
188
- url: this.currentUrl,
189
- connected: this.client !== null
190
- };
191
- }
192
- getClient() {
193
- return this.client;
194
- }
195
- isConnected() {
196
- return this.client !== null;
197
- }
198
- isLaunched() {
199
- return this.chromeProcess !== null;
200
- }
201
- getViewportConfig() {
202
- return this.viewportConfig;
203
- }
204
- setViewportConfig(config) {
205
- this.viewportConfig = config;
206
- }
207
- static findChromeExecutable(override) {
208
- if (override)
209
- return override;
210
- const env = process.env.PILOT_CHROME_PATH;
211
- if (env)
212
- return env;
213
- const platform = process.platform;
214
- const candidates = platform === 'darwin'
215
- ? [
216
- '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
217
- '/Applications/Chromium.app/Contents/MacOS/Chromium'
218
- ]
219
- : platform === 'linux'
220
- ? [
221
- '/usr/bin/google-chrome-stable',
222
- '/usr/bin/google-chrome',
223
- '/usr/bin/chromium-browser',
224
- '/usr/bin/chromium',
225
- '/snap/bin/chromium'
226
- ]
227
- : platform === 'win32'
228
- ? [
229
- 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
230
- 'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe'
231
- ]
232
- : [];
233
- for (const p of candidates) {
234
- if (existsSync(p))
235
- return p;
236
- }
237
- throw new Error(`Chrome not found on ${platform}. Install Google Chrome or set PILOT_CHROME_PATH.`);
238
- }
239
- static async findFreePort() {
240
- return new Promise((resolve, reject) => {
241
- const server = net.createServer();
242
- server.listen(0, '127.0.0.1', () => {
243
- const { port } = server.address();
244
- server.close(() => resolve(port));
245
- });
246
- server.on('error', reject);
247
- });
248
- }
249
- async launch(opts = {}) {
250
- if (this.chromeProcess) {
251
- throw new Error('Chrome is already launched');
252
- }
253
- if (this.destroyed) {
254
- throw new Error('BrowserManager has been destroyed; create a new instance');
255
- }
256
- const logger = getLogger();
257
- const port = await BrowserManager.findFreePort();
258
- const chromePath = BrowserManager.findChromeExecutable(opts.chromePath);
259
- const profileName = opts.profileName ?? this.config.profileName;
260
- const userDataDir = resolveUserDataDir(profileName);
261
- if (isProfileLocked(userDataDir)) {
262
- throw new Error(`Profile "${profileName}" is already in use by another browser instance.`);
263
- }
264
- mkdirSync(userDataDir, { recursive: true });
265
- const viewportPreset = opts.viewport ?? this.config.viewport;
266
- this.viewportConfig = resolveViewport({
267
- preset: viewportPreset,
268
- width: opts.viewportWidth,
269
- height: opts.viewportHeight,
270
- deviceScaleFactor: opts.deviceScaleFactor,
271
- responsive: opts.responsive ?? this.config.responsive
272
- });
273
- const args = [
274
- `--remote-debugging-port=${port}`,
275
- `--user-data-dir=${userDataDir}`,
276
- `--window-size=${this.viewportConfig.width},${this.viewportConfig.height}`,
277
- '--no-first-run',
278
- '--no-default-browser-check',
279
- '--disable-default-apps',
280
- '--disable-extensions'
281
- ];
282
- const headless = opts.headless ?? this.config.headless;
283
- if (headless) {
284
- args.push('--headless', '--disable-gpu');
285
- }
286
- if (process.platform === 'linux') {
287
- args.push('--disable-dev-shm-usage', '--disable-software-rasterizer', '--disable-breakpad', '--password-store=basic', '--use-mock-keychain');
288
- }
289
- const sandbox = sandboxDecision();
290
- if (sandbox.disable) {
291
- args.push('--no-sandbox');
292
- logger.warn({ reason: sandbox.reason }, '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.');
293
- }
294
- logger.info({
295
- chromePath,
296
- port,
297
- headless,
298
- viewport: viewportPreset,
299
- sandbox: sandbox.disable ? 'disabled' : 'enabled'
300
- }, 'Launching Chrome');
301
- this.chromeProcess = spawn(chromePath, args, {
302
- stdio: ['ignore', 'ignore', 'pipe'],
303
- detached: false
304
- });
305
- this.launchedCdpPort = port;
306
- this.launchedUserDataDir = userDataDir;
307
- const stderrChunks = [];
308
- this.chromeProcess.stderr?.on('data', (data) => {
309
- stderrChunks.push(data.toString());
310
- });
311
- this.chromeProcess.on('exit', (code, signal) => {
312
- const stderr = stderrChunks.join('').trim();
313
- const stderrFiltered = stderr
314
- .split('\n')
315
- .filter((l) => !l.includes('dbus/bus.cc'))
316
- .join('\n')
317
- .trim();
318
- logger.warn({
319
- code,
320
- signal,
321
- ...(stderrFiltered && { stderr: stderrFiltered.slice(0, 1000) }),
322
- ...(stderr && !stderrFiltered && { stderrRaw: stderr.slice(0, 500) })
323
- }, 'Chrome process exited');
324
- this.chromeProcess = null;
325
- this.launchedCdpPort = null;
326
- this.cleanupUserDataDir();
327
- });
328
- await this.waitForChromeReady(port);
329
- await this.connect();
330
- return { headless, viewport: viewportPreset };
331
- }
332
- async waitForChromeReady(port, timeoutMs = 10000) {
333
- const deadline = Date.now() + timeoutMs;
334
- while (Date.now() < deadline) {
335
- try {
336
- await CDP.List({ host: '127.0.0.1', port });
337
- return;
338
- }
339
- catch {
340
- await new Promise((r) => setTimeout(r, 200));
341
- }
342
- }
343
- throw new Error(`Chrome did not become ready on port ${port} within ${timeoutMs}ms`);
344
- }
345
- async connect() {
346
- if (this.destroyed)
347
- return;
348
- const logger = getLogger();
349
- try {
350
- const cdpHost = this.launchedCdpPort ? '127.0.0.1' : this.config.cdpHost;
351
- const cdpPort = this.launchedCdpPort ?? this.config.cdpPort;
352
- const targets = await CDP.List({ host: cdpHost, port: cdpPort });
353
- const pageTarget = targets.find((t) => t.type === 'page');
354
- if (!pageTarget) {
355
- throw new Error('No page target found in Chrome');
356
- }
357
- const client = await CDP({
358
- host: cdpHost,
359
- port: cdpPort,
360
- target: pageTarget.id
361
- });
362
- this.client = client;
363
- this.targetId = pageTarget.id;
364
- this.currentUrl = pageTarget.url;
365
- this.retryMs = this.config.cdpRetryMs;
366
- await this.enableDomains(client);
367
- this.attachEventListeners(client);
368
- logger.info({ targetId: this.targetId, url: this.currentUrl }, 'Connected to Chrome');
369
- this.emit('browser_connected', {
370
- targetId: this.targetId,
371
- url: this.currentUrl
372
- });
373
- }
374
- catch (err) {
375
- const logger = getLogger();
376
- logger.warn({ err, retryMs: this.retryMs }, 'Failed to connect to Chrome, retrying...');
377
- this.scheduleReconnect();
378
- }
379
- }
380
- async enableDomains(client) {
381
- await Promise.all([
382
- client.Network.enable({}),
383
- client.Console.enable(),
384
- client.Page.enable(),
385
- client.Runtime.enable()
386
- ]);
387
- if (this.viewportConfig) {
388
- await applyViewport(client, this.viewportConfig);
389
- }
390
- }
391
- attachEventListeners(client) {
392
- client.on('disconnect', () => {
393
- if (this.intentionalClose) {
394
- this.intentionalClose = false;
395
- return;
396
- }
397
- const logger = getLogger();
398
- logger.warn('Chrome disconnected');
399
- this.client = null;
400
- this.targetId = null;
401
- this.emit('browser_disconnected', { reason: 'chrome_disconnected' });
402
- this.scheduleReconnect();
403
- });
404
- // Console events
405
- // CDP uses "warning" for console.warn; normalize to "warn" to match ConsoleLevel.
406
- const normLevel = (raw) => raw === 'warning' ? 'warn' : raw;
407
- client.Runtime.consoleAPICalled((params) => {
408
- const text = params.args
409
- .map((a) => a.value !== undefined ? String(a.value) : (a.description ?? ''))
410
- .join(' ');
411
- this.emit('console_message', {
412
- level: normLevel(params.type),
413
- text,
414
- url: params.stackTrace?.callFrames?.[0]?.url ?? '',
415
- lineNumber: params.stackTrace?.callFrames?.[0]?.lineNumber ?? 0,
416
- timestamp: Date.now()
417
- });
418
- });
419
- client.Console.messageAdded((params) => {
420
- this.emit('console_message', {
421
- level: normLevel(params.message.level),
422
- text: params.message.text,
423
- url: params.message.url ?? '',
424
- lineNumber: params.message.line ?? 0,
425
- timestamp: Date.now()
426
- });
427
- });
428
- // Network events
429
- client.Network.requestWillBeSent((params) => {
430
- this.pendingNetworkRequests.add(params.requestId);
431
- this.emit('network_request', {
432
- requestId: params.requestId,
433
- url: params.request.url,
434
- method: params.request.method,
435
- timestamp: Date.now()
436
- });
437
- });
438
- client.Network.responseReceived((params) => {
439
- this.pendingNetworkRequests.delete(params.requestId);
440
- this.checkNetworkIdle();
441
- this.emit('network_response', {
442
- requestId: params.requestId,
443
- url: params.response.url,
444
- status: params.response.status,
445
- mimeType: params.response.mimeType,
446
- fromCache: params.response.fromDiskCache ?? false,
447
- timestamp: Date.now()
448
- });
449
- });
450
- client.Network.loadingFailed((params) => {
451
- this.pendingNetworkRequests.delete(params.requestId);
452
- this.checkNetworkIdle();
453
- this.emit('network_failed', {
454
- requestId: params.requestId,
455
- url: '',
456
- errorText: params.errorText,
457
- timestamp: Date.now()
458
- });
459
- });
460
- client.Network.loadingFinished((params) => {
461
- this.pendingNetworkRequests.delete(params.requestId);
462
- this.checkNetworkIdle();
463
- });
464
- // Page navigation
465
- client.Page.frameNavigated((params) => {
466
- if (params.frame.parentId === undefined) {
467
- this.currentUrl = params.frame.url;
468
- this.emit('page_navigated', {
469
- url: params.frame.url,
470
- timestamp: Date.now()
471
- });
472
- }
473
- });
474
- }
475
- checkNetworkIdle() {
476
- if (this.pendingNetworkRequests.size === 0) {
477
- if (this.networkIdleTimer)
478
- clearTimeout(this.networkIdleTimer);
479
- this.networkIdleTimer = setTimeout(() => {
480
- const resolvers = this.networkIdleResolvers.splice(0);
481
- resolvers.forEach((r) => r());
482
- }, 500);
483
- this.networkIdleTimer.unref();
484
- }
485
- }
486
- waitForNetworkIdle(timeoutMs = 10000) {
487
- return new Promise((resolve, reject) => {
488
- if (this.pendingNetworkRequests.size === 0) {
489
- resolve();
490
- return;
491
- }
492
- const timer = setTimeout(() => {
493
- const idx = this.networkIdleResolvers.indexOf(resolve);
494
- if (idx !== -1)
495
- this.networkIdleResolvers.splice(idx, 1);
496
- reject(new Error('timeout waiting for network idle'));
497
- }, timeoutMs);
498
- this.networkIdleResolvers.push(() => {
499
- clearTimeout(timer);
500
- resolve();
501
- });
502
- });
503
- }
504
- getCdpConnectionInfo() {
505
- return {
506
- host: this.launchedCdpPort ? '127.0.0.1' : this.config.cdpHost,
507
- port: this.launchedCdpPort ?? this.config.cdpPort
508
- };
509
- }
510
- async listTabs() {
511
- const { host, port } = this.getCdpConnectionInfo();
512
- const targets = await CDP.List({ host, port });
513
- return targets
514
- .filter((t) => t.type === 'page')
515
- .map((t) => ({
516
- targetId: t.id,
517
- url: t.url,
518
- title: t.title,
519
- active: t.id === this.targetId
520
- }));
521
- }
522
- async createTab(url) {
523
- const { host, port } = this.getCdpConnectionInfo();
524
- // chrome-remote-interface appends the url to the HTTP request path
525
- // without encoding, so we must encode it ourselves to avoid
526
- // ERR_UNESCAPED_CHARACTERS on URLs with special characters.
527
- const encodedUrl = url ? encodeURI(url) : undefined;
528
- const target = await CDP.New({ host, port, url: encodedUrl });
529
- return {
530
- targetId: target.id,
531
- url: target.url,
532
- title: target.title,
533
- active: false
534
- };
535
- }
536
- async closeTab(targetId) {
537
- if (targetId === this.targetId) {
538
- throw new Error('Cannot close the active tab. Switch to another tab first.');
539
- }
540
- const { host, port } = this.getCdpConnectionInfo();
541
- await CDP.Close({ host, port, id: targetId });
542
- }
543
- async switchTab(targetId) {
544
- if (targetId === this.targetId)
545
- return;
546
- const { host, port } = this.getCdpConnectionInfo();
547
- // Verify the target exists
548
- const targets = await CDP.List({ host, port });
549
- const target = targets.find((t) => t.id === targetId && t.type === 'page');
550
- if (!target) {
551
- throw new Error(`No page tab found with targetId: ${targetId}`);
552
- }
553
- // Close old client connection (but not the tab)
554
- if (this.client) {
555
- this.intentionalClose = true;
556
- await this.client.close();
557
- this.client = null;
558
- }
559
- // Clear any pending reconnect from a prior disconnect
560
- if (this.retryTimer) {
561
- clearTimeout(this.retryTimer);
562
- this.retryTimer = null;
563
- }
564
- // Connect to the new target
565
- const client = await CDP({ host, port, target: targetId });
566
- this.client = client;
567
- this.targetId = targetId;
568
- this.currentUrl = target.url;
569
- this.retryMs = this.config.cdpRetryMs;
570
- await this.enableDomains(client);
571
- this.attachEventListeners(client);
572
- // Activate the tab in the browser UI
573
- await client.Target.activateTarget({ targetId });
574
- const logger = getLogger();
575
- logger.info({ targetId, url: target.url }, 'Switched to tab');
576
- }
577
- scheduleReconnect() {
578
- if (this.destroyed)
579
- return;
580
- if (this.retryTimer)
581
- return;
582
- this.retryTimer = setTimeout(() => {
583
- this.retryTimer = null;
584
- this.retryMs = Math.min(this.retryMs * 2, this.config.cdpMaxRetryMs);
585
- void this.connect();
586
- }, this.retryMs);
587
- this.retryTimer.unref();
588
- }
589
- cleanupUserDataDir() {
590
- if (this.launchedUserDataDir) {
591
- // Remove stale lock files left behind after Chrome exits so the
592
- // profile can be reused without a false "already in use" error.
593
- for (const lockName of ['SingletonLock', 'lockfile']) {
594
- try {
595
- unlinkSync(join(this.launchedUserDataDir, lockName));
596
- }
597
- catch {
598
- // lock file may not exist — that's fine
599
- }
600
- }
601
- }
602
- this.launchedUserDataDir = null;
603
- }
604
- async destroy() {
605
- this.destroyed = true;
606
- if (this.retryTimer) {
607
- clearTimeout(this.retryTimer);
608
- this.retryTimer = null;
609
- }
610
- if (this.networkIdleTimer) {
611
- clearTimeout(this.networkIdleTimer);
612
- }
613
- if (this.client) {
614
- await this.client.close();
615
- this.client = null;
616
- }
617
- if (this.chromeProcess) {
618
- const proc = this.chromeProcess;
619
- this.chromeProcess = null;
620
- this.launchedCdpPort = null;
621
- try {
622
- proc.kill();
623
- }
624
- catch {
625
- // process may have already exited
626
- }
627
- // Wait for Chrome to fully exit before removing lock files so a
628
- // concurrent launch cannot race against a still-running process.
629
- // `proc.killed` only confirms a signal was delivered, not that the
630
- // process is gone — only `exitCode !== null` or the `exit` event do.
631
- await new Promise((resolve) => {
632
- if (proc.exitCode !== null) {
633
- resolve();
634
- return;
635
- }
636
- const timer = setTimeout(() => {
637
- try {
638
- proc.kill('SIGKILL');
639
- }
640
- catch {
641
- // process may have already exited
642
- }
643
- resolve();
644
- }, 5000);
645
- proc.once('exit', () => {
646
- clearTimeout(timer);
647
- resolve();
648
- });
649
- });
650
- this.cleanupUserDataDir();
651
- }
652
- }
653
- }
1
+ // Backward-compatible re-export from the refactored browser module.
2
+ // All public types, classes, and functions are now defined in src/browser/.
3
+ export { BrowserManager, PROFILE_NAME_RE, sandboxDecision, shouldDisableSandbox, resolveUserDataDir, extractLockPid, isProcessAlive, isProfileLocked, findChromeExecutable, findFreePort } from './browser/index.js';
654
4
  //# sourceMappingURL=browser.js.map