@mkterswingman/5mghost-yonder 0.0.5 → 0.0.6
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 +1 -3
- package/dist/cli/setupCookies.d.ts +29 -1
- package/dist/cli/setupCookies.js +186 -28
- package/dist/utils/browserProfileImport.d.ts +49 -0
- package/dist/utils/browserProfileImport.js +159 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,8 +12,6 @@ curl -fsSL https://mkterswingman.com/install/yt-mcp.sh | bash
|
|
|
12
12
|
powershell -ExecutionPolicy Bypass -Command "irm https://mkterswingman.com/install/yt-mcp.ps1 | iex"
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
```
|
|
16
|
-
|
|
17
15
|
The bootstrap installer:
|
|
18
16
|
|
|
19
17
|
- installs the npm package
|
|
@@ -54,7 +52,7 @@ Media download runtime expectations:
|
|
|
54
52
|
- `smoke` — run the installer smoke checks (`search_videos`, plus `validate_cookies` with `--subtitles`)
|
|
55
53
|
- `runtime` — install, update, or check required runtimes
|
|
56
54
|
- `check` — inspect shared auth, runtime status, and YouTube cookies
|
|
57
|
-
- `setup-cookies` —
|
|
55
|
+
- `setup-cookies` — first try importing an existing YouTube login from the most recently active local Chrome/Edge profile on macOS or Windows with a headless real-browser CDP probe, then fall back to manual browser login
|
|
58
56
|
- `uninstall` — remove MCP registrations and local `~/.yt-mcp` config
|
|
59
57
|
- `update` — update the main package and required runtimes
|
|
60
58
|
- `version` — print the installed version
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { ensureConfigDir } from "../utils/config.js";
|
|
2
|
+
import { cleanupImportedBrowserWorkspace, findImportableBrowserProfileCandidates, prepareImportedBrowserWorkspace, type BrowserProfileCandidate } from "../utils/browserProfileImport.js";
|
|
1
3
|
export type PlaywrightCookie = {
|
|
2
4
|
name: string;
|
|
3
5
|
value: string;
|
|
@@ -28,7 +30,33 @@ export declare function saveCookiesAndClose(context: {
|
|
|
28
30
|
* Save cookies to disk WITHOUT closing the context (caller manages lifecycle).
|
|
29
31
|
*/
|
|
30
32
|
export declare function saveCookiesToDisk(rawCookies: PlaywrightCookie[]): void;
|
|
33
|
+
/**
|
|
34
|
+
* Poll cookies at intervals until YouTube session cookies appear or timeout.
|
|
35
|
+
* Returns the full cookie list on success, or null on timeout / browser closed.
|
|
36
|
+
*/
|
|
37
|
+
declare function waitForLogin(context: Awaited<ReturnType<typeof import("playwright").chromium.launchPersistentContext>>, isClosed: () => boolean, timeoutMs: number, pollIntervalMs?: number): Promise<PlaywrightCookie[] | null>;
|
|
38
|
+
declare function loadPlaywrightChromium(): Promise<typeof import("playwright").chromium>;
|
|
39
|
+
export interface SetupCookiesDeps {
|
|
40
|
+
ensureConfigDir: typeof ensureConfigDir;
|
|
41
|
+
loadChromium: typeof loadPlaywrightChromium;
|
|
42
|
+
detectBrowserChannel: typeof detectBrowserChannel;
|
|
43
|
+
hasYouTubeSession: typeof hasYouTubeSession;
|
|
44
|
+
saveCookiesAndClose: typeof saveCookiesAndClose;
|
|
45
|
+
waitForLogin: typeof waitForLogin;
|
|
46
|
+
findImportableBrowserProfileCandidates: typeof findImportableBrowserProfileCandidates;
|
|
47
|
+
prepareImportedBrowserWorkspace: typeof prepareImportedBrowserWorkspace;
|
|
48
|
+
cleanupImportedBrowserWorkspace: typeof cleanupImportedBrowserWorkspace;
|
|
49
|
+
readImportedBrowserCookies: typeof readImportedBrowserCookies;
|
|
50
|
+
tryImportBrowserCookies: typeof tryImportBrowserCookies;
|
|
51
|
+
runManualCookieSetup: typeof runManualCookieSetup;
|
|
52
|
+
log: (message: string) => void;
|
|
53
|
+
}
|
|
54
|
+
type SetupCookiesChromium = Awaited<ReturnType<SetupCookiesDeps["loadChromium"]>>;
|
|
55
|
+
export declare function tryImportBrowserCookies(chromium: SetupCookiesChromium, deps: SetupCookiesDeps): Promise<boolean>;
|
|
56
|
+
export declare function readImportedBrowserCookies(candidate: BrowserProfileCandidate, chromium: SetupCookiesChromium, deps: SetupCookiesDeps): Promise<PlaywrightCookie[] | null>;
|
|
57
|
+
export declare function runManualCookieSetup(chromium: SetupCookiesChromium, deps: SetupCookiesDeps): Promise<void>;
|
|
31
58
|
/**
|
|
32
59
|
* Interactive cookie setup — opens a visible browser for user to log in.
|
|
33
60
|
*/
|
|
34
|
-
export declare function runSetupCookies(): Promise<void>;
|
|
61
|
+
export declare function runSetupCookies(overrides?: Partial<SetupCookiesDeps>): Promise<void>;
|
|
62
|
+
export {};
|
package/dist/cli/setupCookies.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
1
2
|
import { writeFileSync } from "node:fs";
|
|
3
|
+
import { createServer } from "node:net";
|
|
2
4
|
import { PATHS, ensureConfigDir } from "../utils/config.js";
|
|
3
5
|
import { cookiesToNetscape } from "../utils/cookies.js";
|
|
6
|
+
import { cleanupImportedBrowserWorkspace, findImportableBrowserProfileCandidates, prepareImportedBrowserWorkspace, } from "../utils/browserProfileImport.js";
|
|
4
7
|
/**
|
|
5
8
|
* Detect which browser channel is available on the system.
|
|
6
9
|
* Prefers Chrome → Edge → falls back to bundled Chromium.
|
|
@@ -92,24 +95,159 @@ async function waitForLogin(context, isClosed, timeoutMs, pollIntervalMs = 2000)
|
|
|
92
95
|
}
|
|
93
96
|
return null;
|
|
94
97
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
98
|
+
async function loadPlaywrightChromium() {
|
|
99
|
+
const pw = await import("playwright");
|
|
100
|
+
return pw.chromium;
|
|
101
|
+
}
|
|
102
|
+
function buildSetupCookiesDeps(overrides = {}) {
|
|
103
|
+
return {
|
|
104
|
+
ensureConfigDir,
|
|
105
|
+
loadChromium: loadPlaywrightChromium,
|
|
106
|
+
detectBrowserChannel,
|
|
107
|
+
hasYouTubeSession,
|
|
108
|
+
saveCookiesAndClose,
|
|
109
|
+
waitForLogin,
|
|
110
|
+
findImportableBrowserProfileCandidates,
|
|
111
|
+
prepareImportedBrowserWorkspace,
|
|
112
|
+
cleanupImportedBrowserWorkspace,
|
|
113
|
+
readImportedBrowserCookies,
|
|
114
|
+
tryImportBrowserCookies,
|
|
115
|
+
runManualCookieSetup,
|
|
116
|
+
log: console.log,
|
|
117
|
+
...overrides,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
async function allocateDebugPort() {
|
|
121
|
+
const server = createServer();
|
|
122
|
+
await new Promise((resolve, reject) => {
|
|
123
|
+
server.once("error", reject);
|
|
124
|
+
server.listen(0, "127.0.0.1", () => resolve());
|
|
125
|
+
});
|
|
126
|
+
const address = server.address();
|
|
127
|
+
const port = typeof address === "object" && address ? address.port : 0;
|
|
128
|
+
await new Promise((resolve, reject) => {
|
|
129
|
+
server.close((err) => err ? reject(err) : resolve());
|
|
130
|
+
});
|
|
131
|
+
return port;
|
|
132
|
+
}
|
|
133
|
+
async function waitForCdpVersion(port, timeoutMs = 15_000) {
|
|
134
|
+
const startedAt = Date.now();
|
|
135
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
136
|
+
try {
|
|
137
|
+
const response = await fetch(`http://127.0.0.1:${port}/json/version`);
|
|
138
|
+
if (response.ok) {
|
|
139
|
+
return await response.json();
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
// Browser not ready yet.
|
|
144
|
+
}
|
|
145
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
146
|
+
}
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
async function terminateImportedBrowser(processHandle) {
|
|
150
|
+
if (processHandle.exitCode !== null || processHandle.signalCode !== null) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
processHandle.kill("SIGTERM");
|
|
154
|
+
await Promise.race([
|
|
155
|
+
new Promise((resolve) => processHandle.once("exit", () => resolve())),
|
|
156
|
+
new Promise((resolve) => setTimeout(resolve, 2_000)),
|
|
157
|
+
]);
|
|
158
|
+
if (processHandle.exitCode === null && processHandle.signalCode === null) {
|
|
159
|
+
processHandle.kill("SIGKILL");
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
export async function tryImportBrowserCookies(chromium, deps) {
|
|
163
|
+
const candidates = deps.findImportableBrowserProfileCandidates();
|
|
164
|
+
if (candidates.length === 0) {
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
for (let index = 0; index < candidates.length; index += 1) {
|
|
168
|
+
const candidate = candidates[index];
|
|
169
|
+
const isLastCandidate = index === candidates.length - 1;
|
|
170
|
+
deps.log(`Trying to import existing YouTube login from ${candidate.label} (${candidate.profileLabel})...`);
|
|
171
|
+
const importedCookies = await deps.readImportedBrowserCookies(candidate, chromium, deps);
|
|
172
|
+
if (importedCookies) {
|
|
173
|
+
await deps.saveCookiesAndClose({ close: async () => { } }, importedCookies, true);
|
|
174
|
+
deps.log(`✅ Imported YouTube session from ${candidate.label} (${candidate.profileLabel})\n`);
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
if (!isLastCandidate) {
|
|
178
|
+
const nextCandidate = candidates[index + 1];
|
|
179
|
+
deps.log(`${candidate.label} (${candidate.profileLabel}) import unavailable, trying ${nextCandidate.label} (${nextCandidate.profileLabel})...`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
deps.log("Browser profile import failed, falling back to manual login...\n");
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
export async function readImportedBrowserCookies(candidate, chromium, deps) {
|
|
186
|
+
let workspace = null;
|
|
187
|
+
let browser = null;
|
|
188
|
+
let browserProcess = null;
|
|
102
189
|
try {
|
|
103
|
-
|
|
104
|
-
|
|
190
|
+
workspace = deps.prepareImportedBrowserWorkspace(candidate);
|
|
191
|
+
const port = await allocateDebugPort();
|
|
192
|
+
browserProcess = spawn(workspace.executablePath, [
|
|
193
|
+
`--user-data-dir=${workspace.workspaceDir}`,
|
|
194
|
+
`--profile-directory=${workspace.profileName}`,
|
|
195
|
+
`--remote-debugging-port=${port}`,
|
|
196
|
+
"--headless=new",
|
|
197
|
+
"--no-first-run",
|
|
198
|
+
"--no-default-browser-check",
|
|
199
|
+
"https://www.youtube.com",
|
|
200
|
+
], {
|
|
201
|
+
detached: true,
|
|
202
|
+
stdio: "ignore",
|
|
203
|
+
});
|
|
204
|
+
browserProcess.unref();
|
|
205
|
+
const version = await waitForCdpVersion(port);
|
|
206
|
+
if (!version?.webSocketDebuggerUrl) {
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
browser = await chromium.connectOverCDP(`http://127.0.0.1:${port}`);
|
|
210
|
+
const context = browser.contexts()[0];
|
|
211
|
+
if (!context) {
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
const page = context.pages()[0] ?? await context.newPage();
|
|
215
|
+
const client = await context.newCDPSession(page);
|
|
216
|
+
const result = await client.send("Network.getAllCookies");
|
|
217
|
+
const cookies = (result.cookies ?? []).map((cookie) => ({
|
|
218
|
+
name: cookie.name,
|
|
219
|
+
value: cookie.value,
|
|
220
|
+
domain: cookie.domain,
|
|
221
|
+
path: cookie.path,
|
|
222
|
+
secure: cookie.secure ?? false,
|
|
223
|
+
httpOnly: cookie.httpOnly ?? false,
|
|
224
|
+
expires: cookie.expires ?? -1,
|
|
225
|
+
}));
|
|
226
|
+
if (!deps.hasYouTubeSession(cookies)) {
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
return cookies;
|
|
105
230
|
}
|
|
106
231
|
catch {
|
|
107
|
-
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
finally {
|
|
235
|
+
if (browser) {
|
|
236
|
+
await browser.close().catch(() => { });
|
|
237
|
+
}
|
|
238
|
+
if (browserProcess) {
|
|
239
|
+
await terminateImportedBrowser(browserProcess);
|
|
240
|
+
}
|
|
241
|
+
if (workspace) {
|
|
242
|
+
deps.cleanupImportedBrowserWorkspace(workspace);
|
|
243
|
+
}
|
|
108
244
|
}
|
|
109
|
-
|
|
110
|
-
|
|
245
|
+
}
|
|
246
|
+
export async function runManualCookieSetup(chromium, deps) {
|
|
247
|
+
const channel = await deps.detectBrowserChannel(chromium);
|
|
248
|
+
deps.log(`Using browser: ${CHANNEL_LABELS[channel] ?? channel}`);
|
|
111
249
|
if (channel === "chromium") {
|
|
112
|
-
|
|
250
|
+
deps.log("⚠️ No system Chrome or Edge found. Using bundled Chromium.\n" +
|
|
113
251
|
" If it fails, run: npx playwright install chromium\n");
|
|
114
252
|
}
|
|
115
253
|
let context;
|
|
@@ -141,15 +279,15 @@ export async function runSetupCookies() {
|
|
|
141
279
|
}
|
|
142
280
|
catch {
|
|
143
281
|
if (browserClosed) {
|
|
144
|
-
|
|
282
|
+
deps.log("\n⚠️ Browser was closed. Setup cancelled.\n");
|
|
145
283
|
return;
|
|
146
284
|
}
|
|
147
285
|
throw new Error("Failed to navigate to YouTube");
|
|
148
286
|
}
|
|
149
287
|
const existingCookies = await context.cookies("https://www.youtube.com");
|
|
150
|
-
if (hasYouTubeSession(existingCookies)) {
|
|
151
|
-
|
|
152
|
-
await saveCookiesAndClose(context, existingCookies);
|
|
288
|
+
if (deps.hasYouTubeSession(existingCookies)) {
|
|
289
|
+
deps.log("✅ Already logged in to YouTube!\n");
|
|
290
|
+
await deps.saveCookiesAndClose(context, existingCookies);
|
|
153
291
|
return;
|
|
154
292
|
}
|
|
155
293
|
try {
|
|
@@ -159,28 +297,28 @@ export async function runSetupCookies() {
|
|
|
159
297
|
'ytd-button-renderer a:has-text("Sign in")').first();
|
|
160
298
|
await signInBtn.waitFor({ state: "visible", timeout: 5000 });
|
|
161
299
|
await signInBtn.click();
|
|
162
|
-
|
|
300
|
+
deps.log("🔑 Opened sign-in page. Please log in to your Google account.\n");
|
|
163
301
|
}
|
|
164
302
|
catch {
|
|
165
303
|
try {
|
|
166
304
|
await page.goto("https://accounts.google.com/ServiceLogin?continue=https://www.youtube.com");
|
|
167
|
-
|
|
305
|
+
deps.log("🔑 Please log in to your Google account in the browser window.\n");
|
|
168
306
|
}
|
|
169
307
|
catch {
|
|
170
308
|
if (browserClosed) {
|
|
171
|
-
|
|
309
|
+
deps.log("\n⚠️ Browser was closed. Setup cancelled.\n");
|
|
172
310
|
return;
|
|
173
311
|
}
|
|
174
312
|
throw new Error("Failed to navigate to Google login");
|
|
175
313
|
}
|
|
176
314
|
}
|
|
177
315
|
const LOGIN_TIMEOUT_MS = 2 * 60 * 1000;
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
const finalCookies = await waitForLogin(context, () => browserClosed, LOGIN_TIMEOUT_MS);
|
|
316
|
+
deps.log("⏳ Waiting for login (up to 2 minutes)...");
|
|
317
|
+
deps.log(" Login will be detected automatically once you sign in.\n");
|
|
318
|
+
const finalCookies = await deps.waitForLogin(context, () => browserClosed, LOGIN_TIMEOUT_MS);
|
|
181
319
|
if (browserClosed) {
|
|
182
|
-
|
|
183
|
-
|
|
320
|
+
deps.log("\n⚠️ Browser was closed before login completed.");
|
|
321
|
+
deps.log(" Run again: npx @mkterswingman/5mghost-yonder setup-cookies\n");
|
|
184
322
|
return;
|
|
185
323
|
}
|
|
186
324
|
if (!finalCookies) {
|
|
@@ -188,9 +326,29 @@ export async function runSetupCookies() {
|
|
|
188
326
|
await context.close();
|
|
189
327
|
}
|
|
190
328
|
catch { /* already closed */ }
|
|
191
|
-
|
|
192
|
-
|
|
329
|
+
deps.log("\n⏰ Login timed out (2 minutes).");
|
|
330
|
+
deps.log(" Run again: npx @mkterswingman/5mghost-yonder setup-cookies\n");
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
await deps.saveCookiesAndClose(context, finalCookies);
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Interactive cookie setup — opens a visible browser for user to log in.
|
|
337
|
+
*/
|
|
338
|
+
export async function runSetupCookies(overrides = {}) {
|
|
339
|
+
const deps = buildSetupCookiesDeps(overrides);
|
|
340
|
+
deps.log("\n🍪 YouTube Cookie Setup\n");
|
|
341
|
+
deps.ensureConfigDir();
|
|
342
|
+
let chromium;
|
|
343
|
+
try {
|
|
344
|
+
chromium = await deps.loadChromium();
|
|
345
|
+
}
|
|
346
|
+
catch {
|
|
347
|
+
throw new Error("Playwright runtime is not installed.\nRun: yt-mcp runtime install");
|
|
348
|
+
}
|
|
349
|
+
const imported = await deps.tryImportBrowserCookies(chromium, deps);
|
|
350
|
+
if (imported) {
|
|
193
351
|
return;
|
|
194
352
|
}
|
|
195
|
-
await
|
|
353
|
+
await deps.runManualCookieSetup(chromium, deps);
|
|
196
354
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export type SupportedBrowser = "chrome" | "msedge";
|
|
2
|
+
export interface BrowserInstallationCandidate {
|
|
3
|
+
browser: SupportedBrowser;
|
|
4
|
+
label: string;
|
|
5
|
+
rootDir: string;
|
|
6
|
+
localStatePath: string;
|
|
7
|
+
executablePathCandidates: string[];
|
|
8
|
+
}
|
|
9
|
+
export interface BrowserProfileCandidate extends BrowserInstallationCandidate {
|
|
10
|
+
profileName: string;
|
|
11
|
+
profileLabel: string;
|
|
12
|
+
profileDir: string;
|
|
13
|
+
executablePath: string;
|
|
14
|
+
}
|
|
15
|
+
export interface ImportedBrowserWorkspace {
|
|
16
|
+
browser: SupportedBrowser;
|
|
17
|
+
label: string;
|
|
18
|
+
profileName: string;
|
|
19
|
+
profileLabel: string;
|
|
20
|
+
workspaceDir: string;
|
|
21
|
+
localStatePath: string;
|
|
22
|
+
profileDir: string;
|
|
23
|
+
executablePath: string;
|
|
24
|
+
}
|
|
25
|
+
interface LocalStateProfileMeta {
|
|
26
|
+
name?: string;
|
|
27
|
+
user_name?: string;
|
|
28
|
+
gaia_name?: string;
|
|
29
|
+
active_time?: number;
|
|
30
|
+
}
|
|
31
|
+
export declare function listSupportedBrowserInstallations(input?: {
|
|
32
|
+
platform?: NodeJS.Platform;
|
|
33
|
+
env?: NodeJS.ProcessEnv;
|
|
34
|
+
homeDir?: string;
|
|
35
|
+
}): BrowserInstallationCandidate[];
|
|
36
|
+
export declare function parseBrowserProfilesFromLocalState(localStateText: string): {
|
|
37
|
+
orderedProfileNames: string[];
|
|
38
|
+
infoCache: Record<string, LocalStateProfileMeta>;
|
|
39
|
+
};
|
|
40
|
+
export declare function findImportableBrowserProfileCandidates(input?: {
|
|
41
|
+
platform?: NodeJS.Platform;
|
|
42
|
+
env?: NodeJS.ProcessEnv;
|
|
43
|
+
homeDir?: string;
|
|
44
|
+
exists?: (path: string) => boolean;
|
|
45
|
+
readFile?: (path: string) => string;
|
|
46
|
+
}): BrowserProfileCandidate[];
|
|
47
|
+
export declare function prepareImportedBrowserWorkspace(candidate: BrowserProfileCandidate, tempRootDir?: string): ImportedBrowserWorkspace;
|
|
48
|
+
export declare function cleanupImportedBrowserWorkspace(workspace: ImportedBrowserWorkspace): void;
|
|
49
|
+
export {};
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { cpSync, existsSync, mkdtempSync, readFileSync, rmSync, } from "node:fs";
|
|
2
|
+
import { homedir, tmpdir } from "node:os";
|
|
3
|
+
import { basename, join } from "node:path";
|
|
4
|
+
const CACHE_DIR_NAMES = new Set([
|
|
5
|
+
"Cache",
|
|
6
|
+
"Code Cache",
|
|
7
|
+
"GPUCache",
|
|
8
|
+
"GrShaderCache",
|
|
9
|
+
"ShaderCache",
|
|
10
|
+
"IndexedDB",
|
|
11
|
+
"Storage",
|
|
12
|
+
"WebStorage",
|
|
13
|
+
"Service Worker",
|
|
14
|
+
"blob_storage",
|
|
15
|
+
"File System",
|
|
16
|
+
"SharedStorage",
|
|
17
|
+
"DawnGraphiteCache",
|
|
18
|
+
"GraphiteDawnCache",
|
|
19
|
+
"segmentation_platform",
|
|
20
|
+
"commerce_subscription_db",
|
|
21
|
+
]);
|
|
22
|
+
export function listSupportedBrowserInstallations(input) {
|
|
23
|
+
const platform = input?.platform ?? process.platform;
|
|
24
|
+
const env = input?.env ?? process.env;
|
|
25
|
+
const homeDir = input?.homeDir ?? homedir();
|
|
26
|
+
if (platform === "win32") {
|
|
27
|
+
const localAppData = env.LOCALAPPDATA?.trim();
|
|
28
|
+
const programFiles = env.ProgramFiles?.trim() ?? "C:/Program Files";
|
|
29
|
+
const programFilesX86 = env["ProgramFiles(x86)"]?.trim() ?? "C:/Program Files (x86)";
|
|
30
|
+
if (!localAppData) {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
return [
|
|
34
|
+
buildInstallation("chrome", "Google Chrome", join(localAppData, "Google", "Chrome", "User Data"), [
|
|
35
|
+
join(programFiles, "Google", "Chrome", "Application", "chrome.exe"),
|
|
36
|
+
join(programFilesX86, "Google", "Chrome", "Application", "chrome.exe"),
|
|
37
|
+
]),
|
|
38
|
+
buildInstallation("msedge", "Microsoft Edge", join(localAppData, "Microsoft", "Edge", "User Data"), [
|
|
39
|
+
join(programFiles, "Microsoft", "Edge", "Application", "msedge.exe"),
|
|
40
|
+
join(programFilesX86, "Microsoft", "Edge", "Application", "msedge.exe"),
|
|
41
|
+
]),
|
|
42
|
+
];
|
|
43
|
+
}
|
|
44
|
+
if (platform === "darwin") {
|
|
45
|
+
return [
|
|
46
|
+
buildInstallation("chrome", "Google Chrome", join(homeDir, "Library", "Application Support", "Google", "Chrome"), ["/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"]),
|
|
47
|
+
buildInstallation("msedge", "Microsoft Edge", join(homeDir, "Library", "Application Support", "Microsoft Edge"), ["/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge"]),
|
|
48
|
+
];
|
|
49
|
+
}
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
function buildInstallation(browser, label, rootDir, executablePathCandidates) {
|
|
53
|
+
return {
|
|
54
|
+
browser,
|
|
55
|
+
label,
|
|
56
|
+
rootDir,
|
|
57
|
+
localStatePath: join(rootDir, "Local State"),
|
|
58
|
+
executablePathCandidates,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
export function parseBrowserProfilesFromLocalState(localStateText) {
|
|
62
|
+
const parsed = JSON.parse(localStateText);
|
|
63
|
+
const profile = parsed.profile ?? {};
|
|
64
|
+
const infoCache = profile.info_cache ?? {};
|
|
65
|
+
const orderedNames = [];
|
|
66
|
+
const pushUnique = (name) => {
|
|
67
|
+
if (!name || orderedNames.includes(name)) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
orderedNames.push(name);
|
|
71
|
+
};
|
|
72
|
+
for (const name of profile.last_active_profiles ?? []) {
|
|
73
|
+
pushUnique(name);
|
|
74
|
+
}
|
|
75
|
+
pushUnique(profile.last_used);
|
|
76
|
+
for (const name of profile.profiles_order ?? []) {
|
|
77
|
+
pushUnique(name);
|
|
78
|
+
}
|
|
79
|
+
const remaining = Object.keys(infoCache)
|
|
80
|
+
.filter((name) => !orderedNames.includes(name))
|
|
81
|
+
.sort((left, right) => ((infoCache[right]?.active_time ?? 0) - (infoCache[left]?.active_time ?? 0)));
|
|
82
|
+
for (const name of remaining) {
|
|
83
|
+
pushUnique(name);
|
|
84
|
+
}
|
|
85
|
+
if (orderedNames.length === 0 && infoCache.Default) {
|
|
86
|
+
orderedNames.push("Default");
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
orderedProfileNames: orderedNames,
|
|
90
|
+
infoCache,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function buildProfileLabel(profileName, meta) {
|
|
94
|
+
return meta?.name?.trim() || meta?.user_name?.trim() || meta?.gaia_name?.trim() || profileName;
|
|
95
|
+
}
|
|
96
|
+
export function findImportableBrowserProfileCandidates(input) {
|
|
97
|
+
const exists = input?.exists ?? existsSync;
|
|
98
|
+
const readFile = input?.readFile ?? ((path) => readFileSync(path, "utf8"));
|
|
99
|
+
const candidates = [];
|
|
100
|
+
for (const installation of listSupportedBrowserInstallations(input)) {
|
|
101
|
+
if (!exists(installation.rootDir) || !exists(installation.localStatePath)) {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
const executablePath = installation.executablePathCandidates.find((path) => exists(path));
|
|
105
|
+
if (!executablePath) {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
let parsedState;
|
|
109
|
+
try {
|
|
110
|
+
parsedState = parseBrowserProfilesFromLocalState(readFile(installation.localStatePath));
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
for (const profileName of parsedState.orderedProfileNames) {
|
|
116
|
+
const profileDir = join(installation.rootDir, profileName);
|
|
117
|
+
if (!exists(profileDir)) {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
candidates.push({
|
|
121
|
+
...installation,
|
|
122
|
+
executablePath,
|
|
123
|
+
profileName,
|
|
124
|
+
profileLabel: buildProfileLabel(profileName, parsedState.infoCache[profileName]),
|
|
125
|
+
profileDir,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return candidates;
|
|
130
|
+
}
|
|
131
|
+
function shouldCopyProfilePath(sourcePath) {
|
|
132
|
+
return !sourcePath
|
|
133
|
+
.split(/[\\/]+/)
|
|
134
|
+
.filter(Boolean)
|
|
135
|
+
.some((segment) => CACHE_DIR_NAMES.has(segment));
|
|
136
|
+
}
|
|
137
|
+
export function prepareImportedBrowserWorkspace(candidate, tempRootDir = tmpdir()) {
|
|
138
|
+
const workspaceDir = mkdtempSync(join(tempRootDir, `yt-mcp-${candidate.browser}-profile-`));
|
|
139
|
+
const localStatePath = join(workspaceDir, "Local State");
|
|
140
|
+
const profileDir = join(workspaceDir, candidate.profileName);
|
|
141
|
+
cpSync(candidate.localStatePath, localStatePath);
|
|
142
|
+
cpSync(candidate.profileDir, profileDir, {
|
|
143
|
+
recursive: true,
|
|
144
|
+
filter: (sourcePath) => shouldCopyProfilePath(sourcePath.replace(candidate.profileDir, basename(candidate.profileDir))),
|
|
145
|
+
});
|
|
146
|
+
return {
|
|
147
|
+
browser: candidate.browser,
|
|
148
|
+
label: candidate.label,
|
|
149
|
+
profileName: candidate.profileName,
|
|
150
|
+
profileLabel: candidate.profileLabel,
|
|
151
|
+
workspaceDir,
|
|
152
|
+
localStatePath,
|
|
153
|
+
profileDir,
|
|
154
|
+
executablePath: candidate.executablePath,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
export function cleanupImportedBrowserWorkspace(workspace) {
|
|
158
|
+
rmSync(workspace.workspaceDir, { recursive: true, force: true });
|
|
159
|
+
}
|