@mcpware/chrome-pilot 0.1.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/README.md +110 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +454 -0
- package/dist/profiles.d.ts +53 -0
- package/dist/profiles.js +176 -0
- package/package.json +41 -0
- package/src/index.ts +514 -0
- package/src/profiles.ts +224 -0
- package/tsconfig.json +15 -0
package/src/profiles.ts
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { chromium, type BrowserContext, type Page } from "playwright-core";
|
|
2
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { homedir } from "os";
|
|
5
|
+
|
|
6
|
+
const PROFILES_DIR = join(homedir(), ".chrome-pilot", "profiles");
|
|
7
|
+
const CONFIG_PATH = join(homedir(), ".chrome-pilot", "config.json");
|
|
8
|
+
|
|
9
|
+
export interface Profile {
|
|
10
|
+
name: string;
|
|
11
|
+
path: string;
|
|
12
|
+
lastUsed?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface ActiveSession {
|
|
16
|
+
context: BrowserContext;
|
|
17
|
+
pages: Page[];
|
|
18
|
+
cdpPort: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Track active sessions per profile
|
|
22
|
+
const activeSessions = new Map<string, ActiveSession>();
|
|
23
|
+
|
|
24
|
+
// Next available CDP port
|
|
25
|
+
let nextPort = 9300;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Find Chrome executable on the system
|
|
29
|
+
*/
|
|
30
|
+
export function findChrome(): string {
|
|
31
|
+
const candidates = [
|
|
32
|
+
// Linux
|
|
33
|
+
"/opt/google/chrome/chrome",
|
|
34
|
+
"/usr/bin/google-chrome",
|
|
35
|
+
"/usr/bin/google-chrome-stable",
|
|
36
|
+
"/usr/bin/chromium-browser",
|
|
37
|
+
"/usr/bin/chromium",
|
|
38
|
+
// macOS
|
|
39
|
+
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
40
|
+
// WSL / Windows
|
|
41
|
+
"/mnt/c/Program Files/Google/Chrome/Application/chrome.exe",
|
|
42
|
+
"/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe",
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
for (const path of candidates) {
|
|
46
|
+
if (existsSync(path)) return path;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
throw new Error(
|
|
50
|
+
"Chrome not found. Install Google Chrome or set CHROME_PATH environment variable."
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Ensure profiles directory exists
|
|
56
|
+
*/
|
|
57
|
+
function ensureProfilesDir(): void {
|
|
58
|
+
if (!existsSync(PROFILES_DIR)) {
|
|
59
|
+
mkdirSync(PROFILES_DIR, { recursive: true });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* List all available profiles
|
|
65
|
+
*/
|
|
66
|
+
export function listProfiles(): Profile[] {
|
|
67
|
+
ensureProfilesDir();
|
|
68
|
+
const dirs = readdirSync(PROFILES_DIR, { withFileTypes: true })
|
|
69
|
+
.filter((d) => d.isDirectory())
|
|
70
|
+
.map((d) => {
|
|
71
|
+
const profilePath = join(PROFILES_DIR, d.name);
|
|
72
|
+
const prefsPath = join(profilePath, "Default", "Preferences");
|
|
73
|
+
let lastUsed: string | undefined;
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const prefs = JSON.parse(readFileSync(prefsPath, "utf-8"));
|
|
77
|
+
const accountInfo = prefs?.account_info?.[0];
|
|
78
|
+
if (accountInfo?.email) {
|
|
79
|
+
lastUsed = accountInfo.email;
|
|
80
|
+
}
|
|
81
|
+
} catch {
|
|
82
|
+
// No prefs yet — fresh profile
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
name: d.name,
|
|
87
|
+
path: profilePath,
|
|
88
|
+
lastUsed,
|
|
89
|
+
};
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
return dirs;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Create a new profile
|
|
97
|
+
*/
|
|
98
|
+
export function createProfile(name: string): Profile {
|
|
99
|
+
ensureProfilesDir();
|
|
100
|
+
const profilePath = join(PROFILES_DIR, name);
|
|
101
|
+
|
|
102
|
+
if (existsSync(profilePath)) {
|
|
103
|
+
throw new Error(`Profile "${name}" already exists`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
mkdirSync(profilePath, { recursive: true });
|
|
107
|
+
return { name, path: profilePath };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Delete a profile
|
|
112
|
+
*/
|
|
113
|
+
export function deleteProfile(name: string): void {
|
|
114
|
+
const profilePath = join(PROFILES_DIR, name);
|
|
115
|
+
if (!existsSync(profilePath)) {
|
|
116
|
+
throw new Error(`Profile "${name}" does not exist`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Close session if active
|
|
120
|
+
const session = activeSessions.get(name);
|
|
121
|
+
if (session) {
|
|
122
|
+
session.context.close().catch(() => {});
|
|
123
|
+
activeSessions.delete(name);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
rmSync(profilePath, { recursive: true, force: true });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Launch Chrome with a profile — the core function
|
|
131
|
+
*
|
|
132
|
+
* Uses Playwright's launch_persistent_context with the real Chrome binary.
|
|
133
|
+
* First launch: user sees Chrome sign-in page → signs into Google → Chrome Sync pulls data.
|
|
134
|
+
* Subsequent launches: all data persists (cookies, passwords, bookmarks, extensions).
|
|
135
|
+
*/
|
|
136
|
+
export async function launchProfile(
|
|
137
|
+
name: string,
|
|
138
|
+
options?: { url?: string; headless?: boolean }
|
|
139
|
+
): Promise<{ context: BrowserContext; page: Page; port: number }> {
|
|
140
|
+
// Check if already active
|
|
141
|
+
const existing = activeSessions.get(name);
|
|
142
|
+
if (existing) {
|
|
143
|
+
return {
|
|
144
|
+
context: existing.context,
|
|
145
|
+
page: existing.pages[0] || (await existing.context.newPage()),
|
|
146
|
+
port: existing.cdpPort,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
ensureProfilesDir();
|
|
151
|
+
const profilePath = join(PROFILES_DIR, name);
|
|
152
|
+
|
|
153
|
+
// Auto-create profile if it doesn't exist
|
|
154
|
+
if (!existsSync(profilePath)) {
|
|
155
|
+
mkdirSync(profilePath, { recursive: true });
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const chromePath = process.env.CHROME_PATH || findChrome();
|
|
159
|
+
const port = nextPort++;
|
|
160
|
+
|
|
161
|
+
const context = await chromium.launchPersistentContext(profilePath, {
|
|
162
|
+
executablePath: chromePath,
|
|
163
|
+
headless: options?.headless ?? false,
|
|
164
|
+
args: [
|
|
165
|
+
"--disable-blink-features=AutomationControlled",
|
|
166
|
+
`--remote-debugging-port=${port}`,
|
|
167
|
+
],
|
|
168
|
+
viewport: null, // Use default window size
|
|
169
|
+
ignoreDefaultArgs: ["--enable-automation"],
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
const page =
|
|
173
|
+
context.pages()[0] || (await context.newPage());
|
|
174
|
+
|
|
175
|
+
// Navigate to requested URL or Chrome welcome page
|
|
176
|
+
if (options?.url) {
|
|
177
|
+
await page.goto(options.url, { waitUntil: "domcontentloaded" });
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Track session
|
|
181
|
+
activeSessions.set(name, {
|
|
182
|
+
context,
|
|
183
|
+
pages: context.pages(),
|
|
184
|
+
cdpPort: port,
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
return { context, page, port };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Get active sessions
|
|
192
|
+
*/
|
|
193
|
+
export function getActiveSessions(): Array<{
|
|
194
|
+
name: string;
|
|
195
|
+
port: number;
|
|
196
|
+
pageCount: number;
|
|
197
|
+
}> {
|
|
198
|
+
return Array.from(activeSessions.entries()).map(([name, session]) => ({
|
|
199
|
+
name,
|
|
200
|
+
port: session.cdpPort,
|
|
201
|
+
pageCount: session.context.pages().length,
|
|
202
|
+
}));
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Get a page from an active session
|
|
207
|
+
*/
|
|
208
|
+
export function getSessionPage(name: string): Page | null {
|
|
209
|
+
const session = activeSessions.get(name);
|
|
210
|
+
if (!session) return null;
|
|
211
|
+
return session.context.pages()[0] || null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Close a profile session
|
|
216
|
+
*/
|
|
217
|
+
export async function closeProfile(name: string): Promise<void> {
|
|
218
|
+
const session = activeSessions.get(name);
|
|
219
|
+
if (!session) {
|
|
220
|
+
throw new Error(`No active session for profile "${name}"`);
|
|
221
|
+
}
|
|
222
|
+
await session.context.close();
|
|
223
|
+
activeSessions.delete(name);
|
|
224
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"outDir": "dist",
|
|
7
|
+
"rootDir": "src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"declaration": true,
|
|
12
|
+
"resolveJsonModule": true
|
|
13
|
+
},
|
|
14
|
+
"include": ["src/**/*"]
|
|
15
|
+
}
|