@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.
- package/AGENTS.md +11 -0
- package/CHANGELOG.md +45 -0
- package/README.md +105 -11
- package/dist/browser/browser-manager.d.ts +7 -1
- package/dist/browser/browser-manager.d.ts.map +1 -1
- package/dist/browser/browser-manager.js +196 -40
- package/dist/browser/browser-manager.js.map +1 -1
- package/dist/browser/chrome-launcher.d.ts +13 -2
- package/dist/browser/chrome-launcher.d.ts.map +1 -1
- package/dist/browser/chrome-launcher.js +33 -4
- package/dist/browser/chrome-launcher.js.map +1 -1
- package/dist/browser/driver/capabilities.d.ts +22 -0
- package/dist/browser/driver/capabilities.d.ts.map +1 -0
- package/dist/browser/driver/capabilities.js +70 -0
- package/dist/browser/driver/capabilities.js.map +1 -0
- package/dist/browser/driver/cdp-driver.d.ts +77 -0
- package/dist/browser/driver/cdp-driver.d.ts.map +1 -0
- package/dist/browser/driver/cdp-driver.js +190 -0
- package/dist/browser/driver/cdp-driver.js.map +1 -0
- package/dist/browser/driver/factory.d.ts +10 -0
- package/dist/browser/driver/factory.d.ts.map +1 -0
- package/dist/browser/driver/factory.js +23 -0
- package/dist/browser/driver/factory.js.map +1 -0
- package/dist/browser/driver/index.d.ts +6 -0
- package/dist/browser/driver/index.d.ts.map +1 -0
- package/dist/browser/driver/index.js +5 -0
- package/dist/browser/driver/index.js.map +1 -0
- package/dist/browser/driver/types.d.ts +106 -0
- package/dist/browser/driver/types.d.ts.map +1 -0
- package/dist/browser/driver/types.js +25 -0
- package/dist/browser/driver/types.js.map +1 -0
- package/dist/browser/sessions.d.ts +49 -0
- package/dist/browser/sessions.d.ts.map +1 -0
- package/dist/browser/sessions.js +249 -0
- package/dist/browser/sessions.js.map +1 -0
- package/dist/browser/types.d.ts +13 -0
- package/dist/browser/types.d.ts.map +1 -1
- package/dist/cli/skill-template.d.ts +1 -1
- package/dist/cli/skill-template.d.ts.map +1 -1
- package/dist/cli/skill-template.js +1 -1
- package/dist/cli/skill-template.js.map +1 -1
- package/dist/commands/connect.d.ts +2 -2
- package/dist/commands/connect.d.ts.map +1 -1
- package/dist/commands/connect.js +2 -2
- package/dist/commands/connect.js.map +1 -1
- package/dist/mcp/server.d.ts +9 -15
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +32 -74
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/tools/browser.d.ts +2 -3
- package/dist/mcp/tools/browser.d.ts.map +1 -1
- package/dist/mcp/tools/browser.js +85 -70
- package/dist/mcp/tools/browser.js.map +1 -1
- package/dist/mcp/tools/connect.d.ts +2 -3
- package/dist/mcp/tools/connect.d.ts.map +1 -1
- package/dist/mcp/tools/connect.js +9 -9
- package/dist/mcp/tools/connect.js.map +1 -1
- package/dist/mcp/tools/driver-context.d.ts +14 -0
- package/dist/mcp/tools/driver-context.d.ts.map +1 -0
- package/dist/mcp/tools/driver-context.js +43 -0
- package/dist/mcp/tools/driver-context.js.map +1 -0
- package/dist/mcp/tools/files.d.ts.map +1 -1
- package/dist/mcp/tools/files.js +5 -15
- package/dist/mcp/tools/files.js.map +1 -1
- package/dist/mcp/tools/interaction.d.ts.map +1 -1
- package/dist/mcp/tools/interaction.js +7 -26
- package/dist/mcp/tools/interaction.js.map +1 -1
- package/dist/mcp/tools/names.d.ts +1 -1
- package/dist/mcp/tools/names.d.ts.map +1 -1
- package/dist/mcp/tools/names.js +2 -0
- package/dist/mcp/tools/names.js.map +1 -1
- package/dist/mcp/tools/network.d.ts.map +1 -1
- package/dist/mcp/tools/network.js +3 -11
- package/dist/mcp/tools/network.js.map +1 -1
- package/dist/mcp/tools/performance.d.ts.map +1 -1
- package/dist/mcp/tools/performance.js +2 -12
- package/dist/mcp/tools/performance.js.map +1 -1
- package/dist/mcp/tools/snapshot.d.ts.map +1 -1
- package/dist/mcp/tools/snapshot.js +3 -14
- package/dist/mcp/tools/snapshot.js.map +1 -1
- package/dist/mcp/tools/styles.d.ts.map +1 -1
- package/dist/mcp/tools/styles.js +2 -11
- package/dist/mcp/tools/styles.js.map +1 -1
- package/package.json +1 -1
- package/skills/pilot/SKILL.md +9 -0
- package/src/browser/browser-manager.ts +248 -41
- package/src/browser/chrome-launcher.ts +40 -4
- package/src/browser/driver/capabilities.ts +88 -0
- package/src/browser/driver/cdp-driver.ts +260 -0
- package/src/browser/driver/factory.ts +33 -0
- package/src/browser/driver/index.ts +22 -0
- package/src/browser/driver/types.ts +156 -0
- package/src/browser/sessions.ts +299 -0
- package/src/browser/types.ts +14 -0
- package/src/cli/skill-template.ts +1 -1
- package/src/commands/connect.ts +3 -3
- package/src/mcp/server.ts +48 -91
- package/src/mcp/tools/browser.ts +105 -87
- package/src/mcp/tools/connect.ts +10 -11
- package/src/mcp/tools/driver-context.ts +56 -0
- package/src/mcp/tools/files.ts +5 -16
- package/src/mcp/tools/interaction.ts +7 -27
- package/src/mcp/tools/names.ts +2 -0
- package/src/mcp/tools/network.ts +2 -12
- package/src/mcp/tools/performance.ts +2 -13
- package/src/mcp/tools/snapshot.ts +3 -15
- 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
|
+
}
|
package/src/browser/types.ts
CHANGED
|
@@ -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';
|
package/src/commands/connect.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
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
|
-
|
|
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
|
|
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 {
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
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.
|
|
134
|
-
await context.
|
|
135
|
-
context.
|
|
90
|
+
if (context.driver) {
|
|
91
|
+
await context.driver.destroy();
|
|
92
|
+
context.driver = null;
|
|
136
93
|
}
|
|
137
94
|
}
|
|
138
95
|
};
|