@mkterswingman/5mghost-yonder 0.0.4 → 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 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` — refresh YouTube 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,8 @@
1
+ export declare function canOpenBrowserForEnv(input: {
2
+ platform: NodeJS.Platform;
3
+ env: NodeJS.ProcessEnv;
4
+ stdinIsTTY: boolean;
5
+ }): boolean;
1
6
  export declare function getNoBrowserSessionNotice(): string;
2
7
  export declare function getNoBrowserPatHint(authUrl: string): string[];
3
8
  export declare function getCookieSetupDeferredHint(): string[];
package/dist/cli/setup.js CHANGED
@@ -40,27 +40,28 @@ function tryRegisterMcp(cmd, label) {
40
40
  return false;
41
41
  }
42
42
  }
43
- /**
44
- * Detect if we can open a browser (local machine with display).
45
- * Cloud environments (SSH, Docker, cloud IDE) typically can't.
46
- */
47
- function canOpenBrowser() {
48
- // Explicit override
49
- if (process.env.YT_MCP_NO_BROWSER === "1")
50
- return false;
51
- // Check for display (Linux/macOS GUI)
52
- if (process.platform === "linux" && !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY) {
43
+ export function canOpenBrowserForEnv(input) {
44
+ if (input.env.YT_MCP_NO_BROWSER === "1") {
53
45
  return false;
54
46
  }
55
- // Check if running in common cloud/container environments
56
- if (process.env.CODESPACES || process.env.GITPOD_WORKSPACE_ID || process.env.CLOUD_SHELL) {
47
+ if (input.env.CODESPACES || input.env.GITPOD_WORKSPACE_ID || input.env.CLOUD_SHELL) {
57
48
  return false;
58
49
  }
59
- // Check if we have a TTY (interactive terminal) — headless environments often don't
60
- if (!process.stdin.isTTY) {
61
- return false;
50
+ if (input.platform === "linux") {
51
+ return Boolean(input.env.DISPLAY || input.env.WAYLAND_DISPLAY);
62
52
  }
63
- return true;
53
+ // Why: desktop Windows/macOS sessions can launch browsers even when the host app pipes stdio.
54
+ if (input.platform === "win32" || input.platform === "darwin") {
55
+ return true;
56
+ }
57
+ return input.stdinIsTTY;
58
+ }
59
+ function canOpenBrowser() {
60
+ return canOpenBrowserForEnv({
61
+ platform: process.platform,
62
+ env: process.env,
63
+ stdinIsTTY: Boolean(process.stdin.isTTY),
64
+ });
64
65
  }
65
66
  function openUrl(url) {
66
67
  try {
@@ -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 {};
@@ -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
- * Interactive cookie setup — opens a visible browser for user to log in.
97
- */
98
- export async function runSetupCookies() {
99
- console.log("\n🍪 YouTube Cookie Setup\n");
100
- ensureConfigDir();
101
- let chromium;
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
- const pw = await import("playwright");
104
- chromium = pw.chromium;
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
- throw new Error("Playwright runtime is not installed.\nRun: yt-mcp runtime install");
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
- const channel = await detectBrowserChannel(chromium);
110
- console.log(`Using browser: ${CHANNEL_LABELS[channel] ?? channel}`);
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
- console.log("⚠️ No system Chrome or Edge found. Using bundled Chromium.\n" +
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
- console.log("\n⚠️ Browser was closed. Setup cancelled.\n");
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
- console.log("✅ Already logged in to YouTube!\n");
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
- console.log("🔑 Opened sign-in page. Please log in to your Google account.\n");
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
- console.log("🔑 Please log in to your Google account in the browser window.\n");
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
- console.log("\n⚠️ Browser was closed. Setup cancelled.\n");
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
- console.log("⏳ Waiting for login (up to 2 minutes)...");
179
- console.log(" Login will be detected automatically once you sign in.\n");
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
- console.log("\n⚠️ Browser was closed before login completed.");
183
- console.log(" Run again: npx @mkterswingman/5mghost-yonder setup-cookies\n");
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
- console.log("\n⏰ Login timed out (2 minutes).");
192
- console.log(" Run again: npx @mkterswingman/5mghost-yonder setup-cookies\n");
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 saveCookiesAndClose(context, finalCookies);
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mkterswingman/5mghost-yonder",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
4
4
  "description": "Internal MCP client with local data tools and remote API proxy",
5
5
  "type": "module",
6
6
  "bin": {