@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.
@@ -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
+ }