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