@everydaydevopsio/pilot 0.6.2 → 0.7.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 (107) hide show
  1. package/AGENTS.md +11 -0
  2. package/CHANGELOG.md +45 -0
  3. package/README.md +105 -11
  4. package/dist/browser/browser-manager.d.ts +7 -1
  5. package/dist/browser/browser-manager.d.ts.map +1 -1
  6. package/dist/browser/browser-manager.js +196 -40
  7. package/dist/browser/browser-manager.js.map +1 -1
  8. package/dist/browser/chrome-launcher.d.ts +13 -2
  9. package/dist/browser/chrome-launcher.d.ts.map +1 -1
  10. package/dist/browser/chrome-launcher.js +33 -4
  11. package/dist/browser/chrome-launcher.js.map +1 -1
  12. package/dist/browser/driver/capabilities.d.ts +22 -0
  13. package/dist/browser/driver/capabilities.d.ts.map +1 -0
  14. package/dist/browser/driver/capabilities.js +70 -0
  15. package/dist/browser/driver/capabilities.js.map +1 -0
  16. package/dist/browser/driver/cdp-driver.d.ts +77 -0
  17. package/dist/browser/driver/cdp-driver.d.ts.map +1 -0
  18. package/dist/browser/driver/cdp-driver.js +190 -0
  19. package/dist/browser/driver/cdp-driver.js.map +1 -0
  20. package/dist/browser/driver/factory.d.ts +10 -0
  21. package/dist/browser/driver/factory.d.ts.map +1 -0
  22. package/dist/browser/driver/factory.js +23 -0
  23. package/dist/browser/driver/factory.js.map +1 -0
  24. package/dist/browser/driver/index.d.ts +6 -0
  25. package/dist/browser/driver/index.d.ts.map +1 -0
  26. package/dist/browser/driver/index.js +5 -0
  27. package/dist/browser/driver/index.js.map +1 -0
  28. package/dist/browser/driver/types.d.ts +106 -0
  29. package/dist/browser/driver/types.d.ts.map +1 -0
  30. package/dist/browser/driver/types.js +25 -0
  31. package/dist/browser/driver/types.js.map +1 -0
  32. package/dist/browser/sessions.d.ts +49 -0
  33. package/dist/browser/sessions.d.ts.map +1 -0
  34. package/dist/browser/sessions.js +249 -0
  35. package/dist/browser/sessions.js.map +1 -0
  36. package/dist/browser/types.d.ts +13 -0
  37. package/dist/browser/types.d.ts.map +1 -1
  38. package/dist/cli/skill-template.d.ts +1 -1
  39. package/dist/cli/skill-template.d.ts.map +1 -1
  40. package/dist/cli/skill-template.js +1 -1
  41. package/dist/cli/skill-template.js.map +1 -1
  42. package/dist/commands/connect.d.ts +2 -2
  43. package/dist/commands/connect.d.ts.map +1 -1
  44. package/dist/commands/connect.js +2 -2
  45. package/dist/commands/connect.js.map +1 -1
  46. package/dist/mcp/server.d.ts +9 -15
  47. package/dist/mcp/server.d.ts.map +1 -1
  48. package/dist/mcp/server.js +32 -74
  49. package/dist/mcp/server.js.map +1 -1
  50. package/dist/mcp/tools/browser.d.ts +2 -3
  51. package/dist/mcp/tools/browser.d.ts.map +1 -1
  52. package/dist/mcp/tools/browser.js +85 -70
  53. package/dist/mcp/tools/browser.js.map +1 -1
  54. package/dist/mcp/tools/connect.d.ts +2 -3
  55. package/dist/mcp/tools/connect.d.ts.map +1 -1
  56. package/dist/mcp/tools/connect.js +9 -9
  57. package/dist/mcp/tools/connect.js.map +1 -1
  58. package/dist/mcp/tools/driver-context.d.ts +14 -0
  59. package/dist/mcp/tools/driver-context.d.ts.map +1 -0
  60. package/dist/mcp/tools/driver-context.js +43 -0
  61. package/dist/mcp/tools/driver-context.js.map +1 -0
  62. package/dist/mcp/tools/files.d.ts.map +1 -1
  63. package/dist/mcp/tools/files.js +5 -15
  64. package/dist/mcp/tools/files.js.map +1 -1
  65. package/dist/mcp/tools/interaction.d.ts.map +1 -1
  66. package/dist/mcp/tools/interaction.js +7 -26
  67. package/dist/mcp/tools/interaction.js.map +1 -1
  68. package/dist/mcp/tools/names.d.ts +1 -1
  69. package/dist/mcp/tools/names.d.ts.map +1 -1
  70. package/dist/mcp/tools/names.js +2 -0
  71. package/dist/mcp/tools/names.js.map +1 -1
  72. package/dist/mcp/tools/network.d.ts.map +1 -1
  73. package/dist/mcp/tools/network.js +3 -11
  74. package/dist/mcp/tools/network.js.map +1 -1
  75. package/dist/mcp/tools/performance.d.ts.map +1 -1
  76. package/dist/mcp/tools/performance.js +2 -12
  77. package/dist/mcp/tools/performance.js.map +1 -1
  78. package/dist/mcp/tools/snapshot.d.ts.map +1 -1
  79. package/dist/mcp/tools/snapshot.js +3 -14
  80. package/dist/mcp/tools/snapshot.js.map +1 -1
  81. package/dist/mcp/tools/styles.d.ts.map +1 -1
  82. package/dist/mcp/tools/styles.js +2 -11
  83. package/dist/mcp/tools/styles.js.map +1 -1
  84. package/package.json +1 -1
  85. package/skills/pilot/SKILL.md +9 -0
  86. package/src/browser/browser-manager.ts +248 -41
  87. package/src/browser/chrome-launcher.ts +40 -4
  88. package/src/browser/driver/capabilities.ts +88 -0
  89. package/src/browser/driver/cdp-driver.ts +260 -0
  90. package/src/browser/driver/factory.ts +33 -0
  91. package/src/browser/driver/index.ts +22 -0
  92. package/src/browser/driver/types.ts +156 -0
  93. package/src/browser/sessions.ts +299 -0
  94. package/src/browser/types.ts +14 -0
  95. package/src/cli/skill-template.ts +1 -1
  96. package/src/commands/connect.ts +3 -3
  97. package/src/mcp/server.ts +48 -91
  98. package/src/mcp/tools/browser.ts +105 -87
  99. package/src/mcp/tools/connect.ts +10 -11
  100. package/src/mcp/tools/driver-context.ts +56 -0
  101. package/src/mcp/tools/files.ts +5 -16
  102. package/src/mcp/tools/interaction.ts +7 -27
  103. package/src/mcp/tools/names.ts +2 -0
  104. package/src/mcp/tools/network.ts +2 -12
  105. package/src/mcp/tools/performance.ts +2 -13
  106. package/src/mcp/tools/snapshot.ts +3 -15
  107. package/src/mcp/tools/styles.ts +2 -12
@@ -0,0 +1,299 @@
1
+ import { execFileSync } from 'child_process';
2
+ import { existsSync, lstatSync, readdirSync, statSync } from 'fs';
3
+ import { hostname } from 'os';
4
+ import { basename, dirname, join } from 'path';
5
+ import {
6
+ PROFILE_NAME_RE,
7
+ cleanupLockFiles,
8
+ isProcessAlive,
9
+ profileLockOwner,
10
+ resolveUserDataDir
11
+ } from './chrome-launcher.js';
12
+
13
+ export type ProfileLockState =
14
+ /** No lock files: the profile is free. */
15
+ | 'unlocked'
16
+ /** Locked by a live process on this host. */
17
+ | 'locked'
18
+ /** Locked by a process on this host that is no longer running. */
19
+ | 'stale'
20
+ /** Locked from another host; liveness cannot be checked here. */
21
+ | 'locked_other_host'
22
+ /** Lock files exist but name no owner Pilot can read. */
23
+ | 'locked_unknown';
24
+
25
+ export interface ProfileStatus {
26
+ profile: string;
27
+ userDataDir: string;
28
+ state: ProfileLockState;
29
+ pid?: number;
30
+ host?: string;
31
+ /** True when the lock owner's command line shows Chrome using this Pilot profile. */
32
+ pilotChrome?: boolean;
33
+ }
34
+
35
+ export function pilotDataDir(): string {
36
+ // resolveUserDataDir validates the name; any valid name gives the parent.
37
+ return dirname(resolveUserDataDir('profile1'));
38
+ }
39
+
40
+ /**
41
+ * Report the lock state of every Pilot profile. Read-only: unlike
42
+ * isProfileLocked, it never removes stale locks.
43
+ */
44
+ export function listProfiles(): ProfileStatus[] {
45
+ const dataDir = pilotDataDir();
46
+ let names: string[];
47
+ try {
48
+ names = readdirSync(dataDir);
49
+ } catch {
50
+ return [];
51
+ }
52
+
53
+ return names
54
+ .filter((name) => PROFILE_NAME_RE.test(name))
55
+ .filter((name) => {
56
+ try {
57
+ return statSync(join(dataDir, name)).isDirectory();
58
+ } catch {
59
+ return false;
60
+ }
61
+ })
62
+ .sort()
63
+ .map((profile) => profileStatus(profile, join(dataDir, profile)));
64
+ }
65
+
66
+ function profileStatus(profile: string, userDataDir: string): ProfileStatus {
67
+ const owner = profileLockOwner(userDataDir);
68
+ const base = { profile, userDataDir };
69
+
70
+ if (!owner) {
71
+ const hasLock =
72
+ lockPresent(join(userDataDir, 'SingletonLock')) ||
73
+ existsSync(join(userDataDir, 'lockfile'));
74
+ return { ...base, state: hasLock ? 'locked_unknown' : 'unlocked' };
75
+ }
76
+ if (owner.lockHost !== hostname()) {
77
+ return {
78
+ ...base,
79
+ state: 'locked_other_host',
80
+ pid: owner.pid,
81
+ host: owner.lockHost
82
+ };
83
+ }
84
+ if (!isProcessAlive(owner.pid)) {
85
+ return { ...base, state: 'stale', pid: owner.pid, host: owner.lockHost };
86
+ }
87
+ return {
88
+ ...base,
89
+ state: 'locked',
90
+ pid: owner.pid,
91
+ host: owner.lockHost,
92
+ pilotChrome: isPilotChrome(owner.pid, userDataDir)
93
+ };
94
+ }
95
+
96
+ function lockPresent(path: string): boolean {
97
+ try {
98
+ lstatSync(path);
99
+ return true;
100
+ } catch {
101
+ return false;
102
+ }
103
+ }
104
+
105
+ function ps(pid: number, field: 'args' | 'comm'): string | null {
106
+ if (process.platform === 'win32') return null;
107
+ try {
108
+ const out = execFileSync('ps', ['-o', `${field}=`, '-p', String(pid)], {
109
+ encoding: 'utf8',
110
+ stdio: ['ignore', 'pipe', 'ignore']
111
+ }).trim();
112
+ return out || null;
113
+ } catch {
114
+ return null;
115
+ }
116
+ }
117
+
118
+ /** The full command line of a process, or null when it cannot be read. */
119
+ export function readProcessCommand(pid: number): string | null {
120
+ return ps(pid, 'args');
121
+ }
122
+
123
+ // Chrome's main executable: "chrome" or "chromium-browser" on Linux (comm is
124
+ // truncated to 15 characters), "Google Chrome" or "Chromium" on macOS.
125
+ const CHROME_EXECUTABLE_RE = /chrom(e|ium)/i;
126
+
127
+ /**
128
+ * Whether pid is a Chrome or Chromium browser started with exactly this Pilot
129
+ * profile directory. A pid read from a lock can be reused by an unrelated
130
+ * process, so stop-all only signals processes that pass this check.
131
+ */
132
+ export function isPilotChrome(pid: number, userDataDir: string): boolean {
133
+ const executable = ps(pid, 'comm');
134
+ if (!executable || !CHROME_EXECUTABLE_RE.test(basename(executable))) {
135
+ return false;
136
+ }
137
+ const command = readProcessCommand(pid);
138
+ if (!command) return false;
139
+ const flag = ` --user-data-dir=${userDataDir}`;
140
+ let at = command.indexOf(flag);
141
+ while (at !== -1) {
142
+ const next = command.charAt(at + flag.length);
143
+ if (next === '' || next === ' ') return true;
144
+ at = command.indexOf(flag, at + 1);
145
+ }
146
+ return false;
147
+ }
148
+
149
+ export interface StopResult {
150
+ profile: string;
151
+ pid?: number;
152
+ outcome: 'stopped' | 'cleaned' | 'failed' | 'skipped';
153
+ detail: string;
154
+ }
155
+
156
+ async function waitForProcessExit(pid: number, ms: number): Promise<boolean> {
157
+ const deadline = Date.now() + ms;
158
+ while (Date.now() < deadline) {
159
+ if (!isProcessAlive(pid)) return true;
160
+ await new Promise((r) => setTimeout(r, 100));
161
+ }
162
+ return !isProcessAlive(pid);
163
+ }
164
+
165
+ type TerminateResult =
166
+ | { outcome: 'stopped' }
167
+ | { outcome: 'not_pilot' }
168
+ | { outcome: 'failed'; detail: string };
169
+
170
+ /**
171
+ * SIGTERM, then SIGKILL. The pid's identity is re-checked right before each
172
+ * signal: a process seen earlier can exit and its pid be reused while
173
+ * other browsers are being stopped.
174
+ */
175
+ async function terminate(
176
+ pid: number,
177
+ userDataDir: string
178
+ ): Promise<TerminateResult> {
179
+ const signal = (sig: NodeJS.Signals): 'sent' | 'gone' | { error: string } => {
180
+ if (!isPilotChrome(pid, userDataDir)) return 'gone';
181
+ try {
182
+ process.kill(pid, sig);
183
+ return 'sent';
184
+ } catch (err) {
185
+ const code = (err as NodeJS.ErrnoException).code;
186
+ if (code === 'ESRCH') return 'gone';
187
+ return { error: `could not send ${sig} to pid ${pid} (${code})` };
188
+ }
189
+ };
190
+
191
+ const term = signal('SIGTERM');
192
+ if (term === 'gone') {
193
+ return isProcessAlive(pid)
194
+ ? { outcome: 'not_pilot' }
195
+ : { outcome: 'stopped' };
196
+ }
197
+ if (typeof term === 'object')
198
+ return { outcome: 'failed', detail: term.error };
199
+ if (await waitForProcessExit(pid, 5000)) return { outcome: 'stopped' };
200
+
201
+ const kill = signal('SIGKILL');
202
+ if (kill === 'gone') {
203
+ // Only a confirmed exit counts as stopped. A live pid that no longer
204
+ // verifies (ps failed, or the process changed) is left alone.
205
+ return isProcessAlive(pid)
206
+ ? {
207
+ outcome: 'failed',
208
+ detail: `pid ${pid} did not exit after SIGTERM and could no longer be verified as Chrome using ${userDataDir}; not killed, lock kept`
209
+ }
210
+ : { outcome: 'stopped' };
211
+ }
212
+ if (typeof kill === 'object')
213
+ return { outcome: 'failed', detail: kill.error };
214
+ return (await waitForProcessExit(pid, 2000))
215
+ ? { outcome: 'stopped' }
216
+ : { outcome: 'failed', detail: `pid ${pid} did not exit after SIGKILL` };
217
+ }
218
+
219
+ /**
220
+ * Stop every Chrome on this host that holds a Pilot profile lock. Processes
221
+ * are only signalled after their command line confirms they use that
222
+ * profile's directory; nothing else is touched.
223
+ */
224
+ export async function stopPilotBrowsers(): Promise<StopResult[]> {
225
+ const results: StopResult[] = [];
226
+ for (const status of listProfiles()) {
227
+ const { profile, pid } = status;
228
+ switch (status.state) {
229
+ case 'unlocked':
230
+ continue;
231
+ case 'stale': {
232
+ const removed = cleanupLockFiles(status.userDataDir, pid!);
233
+ results.push(
234
+ removed
235
+ ? {
236
+ profile,
237
+ pid,
238
+ outcome: 'cleaned',
239
+ detail: `removed stale lock left by pid ${pid}, which is no longer running`
240
+ }
241
+ : {
242
+ profile,
243
+ pid,
244
+ outcome: 'failed',
245
+ detail: `stale lock left by pid ${pid} could not be removed (the lock changed owner or is not writable)`
246
+ }
247
+ );
248
+ continue;
249
+ }
250
+ case 'locked_other_host':
251
+ results.push({
252
+ profile,
253
+ pid,
254
+ outcome: 'skipped',
255
+ detail: `locked by pid ${pid} on host "${status.host}"; Pilot can only stop browsers on this host`
256
+ });
257
+ continue;
258
+ case 'locked_unknown':
259
+ results.push({
260
+ profile,
261
+ outcome: 'skipped',
262
+ detail: 'lock files name no owner; left in place'
263
+ });
264
+ continue;
265
+ case 'locked':
266
+ results.push(await stopLockedProfile(status));
267
+ }
268
+ }
269
+ return results;
270
+ }
271
+
272
+ /** Stop the Chrome holding a profile lock, if it is verifiably Pilot's. */
273
+ export async function stopLockedProfile(
274
+ status: ProfileStatus
275
+ ): Promise<StopResult> {
276
+ const { profile, pid, userDataDir } = status;
277
+ const notPilot: StopResult = {
278
+ profile,
279
+ pid,
280
+ outcome: 'skipped',
281
+ detail: `pid ${pid} holds the lock but its command line does not show Chrome using ${userDataDir}; not stopped`
282
+ };
283
+ if (pid === undefined || !status.pilotChrome) return notPilot;
284
+
285
+ const result = await terminate(pid, userDataDir);
286
+ if (result.outcome === 'not_pilot') return notPilot;
287
+ if (result.outcome === 'failed') {
288
+ return { profile, pid, outcome: 'failed', detail: result.detail };
289
+ }
290
+ const removed = cleanupLockFiles(userDataDir, pid);
291
+ return {
292
+ profile,
293
+ pid,
294
+ outcome: 'stopped',
295
+ detail: removed
296
+ ? `stopped Chrome pid ${pid}`
297
+ : `stopped Chrome pid ${pid}, but its lock could not be removed`
298
+ };
299
+ }
@@ -1,4 +1,5 @@
1
1
  import type { Client } from 'chrome-remote-interface';
2
+ import type { BrowserKind } from './driver/types.js';
2
3
 
3
4
  export type BrowserEventCallback = (event: string, data: unknown) => void;
4
5
 
@@ -28,5 +29,18 @@ export interface LaunchOptions {
28
29
  startupTimeoutMs?: number;
29
30
  }
30
31
 
32
+ export interface SessionInfo {
33
+ /** Browser the session drives; set by the driver. */
34
+ browser?: BrowserKind;
35
+ /** launched: Chrome started by this session; external: browser_connect. */
36
+ mode: 'launched' | 'external' | 'none';
37
+ connected: boolean;
38
+ profile?: string;
39
+ userDataDir?: string;
40
+ pid?: number;
41
+ host?: string;
42
+ cdpPort?: number;
43
+ }
44
+
31
45
  export type SandboxDecision =
32
46
  { disable: false } | { disable: true; reason: 'env_override' | 'root_user' };
@@ -1,4 +1,4 @@
1
1
  // Generated by scripts/build-skill.mjs from skills/pilot/SKILL.md.
2
2
  // Do not edit this file directly.
3
3
  export const SKILL_CONTENT =
4
- '---\nname: pilot\ndescription: Control and debug web pages with the Pilot MCP browser tools. Use when the user asks to open, inspect, test, or troubleshoot a site in Chrome with Pilot.\n---\n\n# Pilot browser workflow\n\nUse the `pilot` MCP tools to operate Chrome through the Chrome DevTools Protocol.\n\n## Start and inspect\n\n1. Call `browser_start` before any other Pilot browser tool. Use visible mode unless the user requests headless operation or the environment has no graphical display.\n2. Navigate with `browser_navigate`, then confirm the result with `browser_page_info` or `browser_snapshot`.\n3. Use refs returned by `browser_snapshot` or `browser_find` for interaction. Take a fresh snapshot after navigation or substantial DOM changes because refs can become stale.\n4. For debugging, clear errors before reproducing the problem, then inspect `browser_errors`, `browser_console`, and `browser_network`. Use performance tracing only when the user asks about load or runtime performance.\n5. Call `browser_stop` when the browser is no longer needed.\n\nDo not enter credentials, submit consequential forms, upload files, or make other externally visible changes unless the user authorized that action.\n\n## Troubleshooting\n\n### Visible Chrome on Linux\n\nRun `pilot check` when `browser_start` fails. Visible Chrome requires `DISPLAY` or `WAYLAND_DISPLAY` in the Pilot MCP server process. Headless operation does not.\n\nIf desktop applications open but Pilot reports that the display is missing, inspect the graphical session with `printenv DISPLAY`. Forward the returned value through the MCP server configuration and restart the agent so Pilot is relaunched. For Codex:\n\n```toml\n[mcp_servers.pilot]\ncommand = "npx"\nargs = ["-y", "@everydaydevopsio/pilot"]\nenv = { DISPLAY = ":1" }\n```\n\nReplace `:1` with the actual value. If the X server requires a cookie, forward `XAUTHORITY` in the same map. For SSH, use trusted X11 forwarding (`ssh -Y`) when policy permits, or use a remote desktop session and its display value. Use `PILOT_HEADLESS=true` on systems without a desktop. In CI, use headless mode or run Pilot under Xvfb.\n\n### Other Linux preflight failures\n\n- If the profile directory is not writable, set `XDG_DATA_HOME` to a writable directory.\n- If loopback binding fails with `EPERM`, the host or container must allow binding on `127.0.0.1`; disabling Chrome\'s renderer sandbox does not fix it.\n- If Chrome is not detected, set `PILOT_CHROME_PATH` to its executable.\n- Avoid `PILOT_CHROME_NO_SANDBOX=true` unless the runtime genuinely cannot support Chrome\'s sandbox and the user accepts the security tradeoff.\n\n### Connection and page failures\n\n- Confirm the active page with `browser_page_info` and list tabs if Pilot controls the wrong tab.\n- Inspect failed document, script, XHR, and fetch requests with `browser_network`.\n- If element refs stop resolving after a page update, take another snapshot and retry with the new refs.\n';
4
+ '---\nname: pilot\ndescription: Control and debug web pages with the Pilot MCP browser tools. Use when the user asks to open, inspect, test, or troubleshoot a site in Chrome with Pilot.\n---\n\n# Pilot browser workflow\n\nUse the `pilot` MCP tools to operate Chrome through the Chrome DevTools Protocol.\n\n## Start and inspect\n\n1. Call `browser_start` before any other Pilot browser tool. Use visible mode unless the user requests headless operation or the environment has no graphical display.\n2. Navigate with `browser_navigate`, then confirm the result with `browser_page_info` or `browser_snapshot`.\n3. Use refs returned by `browser_snapshot` or `browser_find` for interaction. Take a fresh snapshot after navigation or substantial DOM changes because refs can become stale.\n4. For debugging, clear errors before reproducing the problem, then inspect `browser_errors`, `browser_console`, and `browser_network`. Use performance tracing only when the user asks about load or runtime performance.\n5. Call `browser_stop` when the browser is no longer needed.\n\nDo not enter credentials, submit consequential forms, upload files, or make other externally visible changes unless the user authorized that action.\n\n## Troubleshooting\n\n### Visible Chrome on Linux\n\nRun `pilot check` when `browser_start` fails. Visible Chrome requires `DISPLAY` or `WAYLAND_DISPLAY` in the Pilot MCP server process. Headless operation does not.\n\nIf desktop applications open but Pilot reports that the display is missing, inspect the graphical session with `printenv DISPLAY`. Forward the returned value through the MCP server configuration and restart the agent so Pilot is relaunched. For Codex:\n\n```toml\n[mcp_servers.pilot]\ncommand = "npx"\nargs = ["-y", "@everydaydevopsio/pilot"]\nenv = { DISPLAY = ":1" }\n```\n\nReplace `:1` with the actual value. If the X server requires a cookie, forward `XAUTHORITY` in the same map. For SSH, use trusted X11 forwarding (`ssh -Y`) when policy permits, or use a remote desktop session and its display value. Use `PILOT_HEADLESS=true` on systems without a desktop. In CI, use headless mode or run Pilot under Xvfb.\n\n### Startup failures\n\nRead the `Launch:`, `Cleanup:`, and `Next:` lines in the `browser_start` error before retrying.\n\n- A failed `browser_start` stops the Chrome it launched and frees the profile unless `Cleanup:` names a pid that did not stop. Do not switch profiles just because a start failed.\n- On a timeout, retry once with a larger `startupTimeoutMs` (for example `180000`). First runs and software-rendered Linux desktops can be slow.\n- If Chrome exited in visible mode, check the display (above) or retry with `headless: true`.\n- If the profile is already in use, the error names the owning Chrome pid and host. `browser_stop` cannot stop a browser that this session did not launch. Call `browser_status` to see which profiles are free and who holds the others. Use a different `profileName`, or, only after the user agrees, call `browser_stop_all`. It also stops browsers that other agents may be using.\n\n### Other Linux preflight failures\n\n- If the profile directory is not writable, set `XDG_DATA_HOME` to a writable directory.\n- If loopback binding fails with `EPERM`, the host or container must allow binding on `127.0.0.1`; disabling Chrome\'s renderer sandbox does not fix it.\n- If Chrome is not detected, set `PILOT_CHROME_PATH` to its executable.\n- Avoid `PILOT_CHROME_NO_SANDBOX=true` unless the runtime genuinely cannot support Chrome\'s sandbox and the user accepts the security tradeoff.\n\n### Connection and page failures\n\n- Confirm the active page with `browser_page_info` and list tabs if Pilot controls the wrong tab.\n- Inspect failed document, script, XHR, and fetch requests with `browser_network`.\n- If element refs stop resolving after a page update, take another snapshot and retry with the new refs.\n';
@@ -1,4 +1,4 @@
1
- import type { BrowserManager } from '../browser/browser-manager.js';
1
+ import type { BrowserDriver } from '../browser/driver/types.js';
2
2
  import type { TabInfo } from '../browser/types.js';
3
3
 
4
4
  export interface ConnectParams {
@@ -13,13 +13,13 @@ export interface ConnectResult {
13
13
  }
14
14
 
15
15
  export async function executeConnect(
16
- manager: BrowserManager,
16
+ driver: Pick<BrowserDriver, 'connectExisting'>,
17
17
  params: ConnectParams
18
18
  ): Promise<ConnectResult> {
19
19
  const host = params.host ?? '127.0.0.1';
20
20
  const port = params.port ?? 9222;
21
21
 
22
- const { tabs } = await manager.connectExisting({ host, port });
22
+ const { tabs } = await driver.connectExisting({ host, port });
23
23
 
24
24
  return { host, port, tabs };
25
25
  }
package/src/mcp/server.ts CHANGED
@@ -1,18 +1,12 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
- import { BrowserManager } from '../browser.js';
2
+ import { createDriver } from '../browser/driver/factory.js';
3
+ import type { BrowserDriver, DriverSession } from '../browser/driver/types.js';
3
4
  import { ElementRefMap } from '../browser/inspect/element-ref.js';
4
- import { resetCssState } from '../browser/inspect/styles.js';
5
5
  import { DialogQueue } from '../browser/interaction/dialogs.js';
6
- import { attachDialogHandler } from '../browser/interaction/dialogs.js';
7
- import {
8
- DownloadTracker,
9
- enableDownloads,
10
- attachDownloadHandler
11
- } from '../browser/interaction/downloads.js';
6
+ import { DownloadTracker } from '../browser/interaction/downloads.js';
12
7
  import { NetworkBuffer } from '../browser/network/network-buffer.js';
13
- import { attachNetworkMonitor } from '../browser/network/network-monitor.js';
14
8
  import { loadConfig } from '../util/config.js';
15
- import { ConsoleBuffer, type ConsoleMessage } from './console-buffer.js';
9
+ import { ConsoleBuffer } from './console-buffer.js';
16
10
  import { registerBrowserTools } from './tools/browser.js';
17
11
  import { registerErrorTools } from './tools/errors.js';
18
12
  import { registerSnapshotTools } from './tools/snapshot.js';
@@ -24,6 +18,7 @@ import { registerFilesTools } from './tools/files.js';
24
18
  import { createTraceState } from '../browser/performance/trace.js';
25
19
  import { registerPerformanceTools } from './tools/performance.js';
26
20
  import { registerConnectTools } from './tools/connect.js';
21
+ import { withCapabilityGate } from './tools/driver-context.js';
27
22
 
28
23
  export interface McpConfig {
29
24
  bufferSize: number;
@@ -31,17 +26,35 @@ export interface McpConfig {
31
26
  cdpHost?: string;
32
27
  }
33
28
 
34
- export interface BrowserContext {
35
- manager: BrowserManager | null;
36
- consoleBuffer: ConsoleBuffer;
37
- elementRefMap: ElementRefMap;
38
- networkBuffer: NetworkBuffer;
39
- dialogQueue: DialogQueue;
40
- downloadTracker: DownloadTracker;
41
- traceState: ReturnType<typeof createTraceState>;
29
+ export interface BrowserContext extends DriverSession {
30
+ driver: BrowserDriver | null;
42
31
  baseConfig: McpConfig;
43
32
  }
44
33
 
34
+ export type DriverFactory = () => BrowserDriver;
35
+
36
+ /**
37
+ * Register every Pilot tool behind the capability gate, so a tool the active
38
+ * driver cannot support fails before its handler runs.
39
+ */
40
+ export function registerAllTools(
41
+ server: McpServer,
42
+ context: BrowserContext,
43
+ makeDriver: DriverFactory
44
+ ): void {
45
+ const gated = withCapabilityGate(server, () => context.driver);
46
+ registerBrowserTools(gated, context, makeDriver);
47
+ registerErrorTools(gated, context.consoleBuffer);
48
+ registerSnapshotTools(gated, context);
49
+ registerInteractionTools(gated, context);
50
+ registerNetworkTools(gated, context);
51
+ registerRuntimeTools(gated, context.consoleBuffer);
52
+ registerStylesTools(gated, context);
53
+ registerFilesTools(gated, context);
54
+ registerPerformanceTools(gated, context);
55
+ registerConnectTools(gated, context, makeDriver);
56
+ }
57
+
45
58
  export async function createMcpServer(config: McpConfig): Promise<{
46
59
  server: McpServer;
47
60
  cleanup: () => Promise<void>;
@@ -51,88 +64,32 @@ export async function createMcpServer(config: McpConfig): Promise<{
51
64
  version: '0.1.0'
52
65
  });
53
66
 
54
- const consoleBuffer = new ConsoleBuffer(config.bufferSize);
55
- const elementRefMap = new ElementRefMap();
56
- const networkBuffer = new NetworkBuffer();
57
- const dialogQueue = new DialogQueue();
58
- const downloadTracker = new DownloadTracker();
59
- const traceState = createTraceState();
60
-
61
67
  const context: BrowserContext = {
62
- manager: null,
63
- consoleBuffer,
64
- elementRefMap,
65
- networkBuffer,
66
- dialogQueue,
67
- downloadTracker,
68
- traceState,
68
+ driver: null,
69
+ consoleBuffer: new ConsoleBuffer(config.bufferSize),
70
+ elementRefMap: new ElementRefMap(),
71
+ networkBuffer: new NetworkBuffer(),
72
+ dialogQueue: new DialogQueue(),
73
+ downloadTracker: new DownloadTracker(),
74
+ traceState: createTraceState(),
69
75
  baseConfig: config
70
76
  };
71
77
 
72
- function attachEventHandlers(manager: BrowserManager): void {
73
- let monitoredClient: unknown = null;
74
-
75
- function ensureMonitors(): void {
76
- const client = manager.getClient();
77
- if (client && client !== monitoredClient) {
78
- attachNetworkMonitor(client, networkBuffer);
79
- attachDialogHandler(client, dialogQueue);
80
- attachDownloadHandler(client, downloadTracker);
81
- void enableDownloads(client).catch(() => {
82
- // Download behavior setup may fail in some environments
83
- });
84
- monitoredClient = client;
85
- }
86
- }
87
-
88
- manager.setEventCallback((event, data) => {
89
- if (event === 'console_message') {
90
- consoleBuffer.push(data as ConsoleMessage);
91
- }
92
- if (event === 'page_navigated') {
93
- elementRefMap.invalidate();
94
- }
95
- if (event === 'browser_connected') {
96
- ensureMonitors();
97
- }
98
- if (event === 'browser_disconnected') {
99
- monitoredClient = null;
100
- resetCssState();
101
- dialogQueue.clear();
102
- downloadTracker.clear();
103
- }
104
- // Reattach after tab switch (new client, no connect event)
105
- ensureMonitors();
106
- });
107
- }
108
-
109
- function makeBrowserManager(): BrowserManager {
110
- const browserConfig = loadConfig({
111
- cdpPort: config.cdpPort,
112
- cdpHost: config.cdpHost
113
- });
114
- const manager = new BrowserManager(browserConfig);
115
- attachEventHandlers(manager);
116
- return manager;
117
- }
78
+ const makeDriver: DriverFactory = () =>
79
+ createDriver(
80
+ 'chrome',
81
+ loadConfig({ cdpPort: config.cdpPort, cdpHost: config.cdpHost }),
82
+ context
83
+ );
118
84
 
119
- registerBrowserTools(server, context, makeBrowserManager);
120
- registerErrorTools(server, consoleBuffer);
121
- registerSnapshotTools(server, context);
122
- registerInteractionTools(server, context);
123
- registerNetworkTools(server, context);
124
- registerRuntimeTools(server, consoleBuffer);
125
- registerStylesTools(server, context);
126
- registerFilesTools(server, context);
127
- registerPerformanceTools(server, context);
128
- registerConnectTools(server, context, makeBrowserManager);
85
+ registerAllTools(server, context, makeDriver);
129
86
 
130
87
  return {
131
88
  server,
132
89
  cleanup: async () => {
133
- if (context.manager) {
134
- await context.manager.destroy();
135
- context.manager = null;
90
+ if (context.driver) {
91
+ await context.driver.destroy();
92
+ context.driver = null;
136
93
  }
137
94
  }
138
95
  };