@mkterswingman/5mghost-yonder 0.0.5 → 0.0.7

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,12 @@ 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
59
+
60
+ Runtime notes:
61
+
62
+ - Playwright browser installs are managed through `yt-mcp runtime install`, not a raw `npx playwright install ...` call
63
+ - `yt-dlp` is invoked with `--js-runtimes node`, so no extra Deno install is required
@@ -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,51 @@ 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
+ interface CdpCookie {
56
+ name: string;
57
+ value: string;
58
+ domain: string;
59
+ path: string;
60
+ secure?: boolean;
61
+ httpOnly?: boolean;
62
+ expires?: number;
63
+ }
64
+ interface CdpCookieClient {
65
+ send(method: "Network.getAllCookies"): Promise<{
66
+ cookies?: CdpCookie[];
67
+ }>;
68
+ }
69
+ export declare function readCdpCookiesUntilSession(client: CdpCookieClient, deps: Pick<SetupCookiesDeps, "hasYouTubeSession">, options?: {
70
+ attempts?: number;
71
+ delayMs?: number;
72
+ }): Promise<PlaywrightCookie[] | null>;
73
+ export declare function tryImportBrowserCookies(chromium: SetupCookiesChromium, deps: SetupCookiesDeps): Promise<boolean>;
74
+ export declare function readImportedBrowserCookies(candidate: BrowserProfileCandidate, chromium: SetupCookiesChromium, deps: SetupCookiesDeps): Promise<PlaywrightCookie[] | null>;
75
+ export declare function runManualCookieSetup(chromium: SetupCookiesChromium, deps: SetupCookiesDeps): Promise<void>;
31
76
  /**
32
77
  * Interactive cookie setup — opens a visible browser for user to log in.
33
78
  */
34
- export declare function runSetupCookies(): Promise<void>;
79
+ export declare function runSetupCookies(overrides?: Partial<SetupCookiesDeps>): Promise<void>;
80
+ 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.
@@ -25,8 +28,17 @@ export const CHANNEL_LABELS = {
25
28
  };
26
29
  /** Check if YouTube SID cookies are present — the real signal of a logged-in session. */
27
30
  export function hasYouTubeSession(cookies) {
28
- return cookies.some((c) => (c.name === "SID" || c.name === "HSID" || c.name === "SSID") &&
29
- c.domain.includes("youtube.com"));
31
+ const hasCookieTriplet = (domainFragment) => {
32
+ const names = new Set(cookies
33
+ .filter((cookie) => cookie.domain.includes(domainFragment))
34
+ .map((cookie) => cookie.name));
35
+ return names.has("SID") && names.has("HSID") && names.has("SSID");
36
+ };
37
+ if (hasCookieTriplet("youtube.com")) {
38
+ return true;
39
+ }
40
+ const hasYouTubeLoginInfo = cookies.some((cookie) => cookie.name === "LOGIN_INFO" && cookie.domain.includes("youtube.com"));
41
+ return hasYouTubeLoginInfo && hasCookieTriplet("google.");
30
42
  }
31
43
  /**
32
44
  * Save cookies to Netscape format file and close the browser context.
@@ -92,25 +104,177 @@ async function waitForLogin(context, isClosed, timeoutMs, pollIntervalMs = 2000)
92
104
  }
93
105
  return null;
94
106
  }
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;
107
+ async function loadPlaywrightChromium() {
108
+ const pw = await import("playwright");
109
+ return pw.chromium;
110
+ }
111
+ function buildSetupCookiesDeps(overrides = {}) {
112
+ return {
113
+ ensureConfigDir,
114
+ loadChromium: loadPlaywrightChromium,
115
+ detectBrowserChannel,
116
+ hasYouTubeSession,
117
+ saveCookiesAndClose,
118
+ waitForLogin,
119
+ findImportableBrowserProfileCandidates,
120
+ prepareImportedBrowserWorkspace,
121
+ cleanupImportedBrowserWorkspace,
122
+ readImportedBrowserCookies,
123
+ tryImportBrowserCookies,
124
+ runManualCookieSetup,
125
+ log: console.log,
126
+ ...overrides,
127
+ };
128
+ }
129
+ async function allocateDebugPort() {
130
+ const server = createServer();
131
+ await new Promise((resolve, reject) => {
132
+ server.once("error", reject);
133
+ server.listen(0, "127.0.0.1", () => resolve());
134
+ });
135
+ const address = server.address();
136
+ const port = typeof address === "object" && address ? address.port : 0;
137
+ await new Promise((resolve, reject) => {
138
+ server.close((err) => err ? reject(err) : resolve());
139
+ });
140
+ return port;
141
+ }
142
+ async function waitForCdpVersion(port, timeoutMs = 15_000) {
143
+ const startedAt = Date.now();
144
+ while (Date.now() - startedAt < timeoutMs) {
145
+ try {
146
+ const response = await fetch(`http://127.0.0.1:${port}/json/version`);
147
+ if (response.ok) {
148
+ return await response.json();
149
+ }
150
+ }
151
+ catch {
152
+ // Browser not ready yet.
153
+ }
154
+ await new Promise((resolve) => setTimeout(resolve, 250));
155
+ }
156
+ return null;
157
+ }
158
+ export async function readCdpCookiesUntilSession(client, deps, options = {}) {
159
+ const attempts = options.attempts ?? 5;
160
+ const delayMs = options.delayMs ?? 1_000;
161
+ for (let attempt = 0; attempt < attempts; attempt += 1) {
162
+ const result = await client.send("Network.getAllCookies");
163
+ const cookies = (result.cookies ?? []).map((cookie) => ({
164
+ name: cookie.name,
165
+ value: cookie.value,
166
+ domain: cookie.domain,
167
+ path: cookie.path,
168
+ secure: cookie.secure ?? false,
169
+ httpOnly: cookie.httpOnly ?? false,
170
+ expires: cookie.expires ?? -1,
171
+ }));
172
+ if (deps.hasYouTubeSession(cookies)) {
173
+ return cookies;
174
+ }
175
+ if (attempt < attempts - 1) {
176
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
177
+ }
178
+ }
179
+ return null;
180
+ }
181
+ async function terminateImportedBrowser(processHandle) {
182
+ if (processHandle.exitCode !== null || processHandle.signalCode !== null) {
183
+ return;
184
+ }
185
+ processHandle.kill("SIGTERM");
186
+ await Promise.race([
187
+ new Promise((resolve) => processHandle.once("exit", () => resolve())),
188
+ new Promise((resolve) => setTimeout(resolve, 2_000)),
189
+ ]);
190
+ if (processHandle.exitCode === null && processHandle.signalCode === null) {
191
+ processHandle.kill("SIGKILL");
192
+ }
193
+ }
194
+ export async function tryImportBrowserCookies(chromium, deps) {
195
+ const candidates = deps.findImportableBrowserProfileCandidates();
196
+ if (candidates.length === 0) {
197
+ return false;
198
+ }
199
+ for (let index = 0; index < candidates.length; index += 1) {
200
+ const candidate = candidates[index];
201
+ const isLastCandidate = index === candidates.length - 1;
202
+ deps.log(`Trying to import existing YouTube login from ${candidate.label} (${candidate.profileLabel})...`);
203
+ const importedCookies = await deps.readImportedBrowserCookies(candidate, chromium, deps);
204
+ if (importedCookies) {
205
+ await deps.saveCookiesAndClose({ close: async () => { } }, importedCookies, true);
206
+ deps.log(`✅ Imported YouTube session from ${candidate.label} (${candidate.profileLabel})\n`);
207
+ return true;
208
+ }
209
+ if (!isLastCandidate) {
210
+ const nextCandidate = candidates[index + 1];
211
+ deps.log(`${candidate.label} (${candidate.profileLabel}) import unavailable, trying ${nextCandidate.label} (${nextCandidate.profileLabel})...`);
212
+ }
213
+ }
214
+ deps.log("Browser profile import failed, falling back to manual login...\n");
215
+ return false;
216
+ }
217
+ export async function readImportedBrowserCookies(candidate, chromium, deps) {
218
+ let workspace = null;
219
+ let browser = null;
220
+ let browserProcess = null;
102
221
  try {
103
- const pw = await import("playwright");
104
- chromium = pw.chromium;
222
+ workspace = deps.prepareImportedBrowserWorkspace(candidate);
223
+ const port = await allocateDebugPort();
224
+ browserProcess = spawn(workspace.executablePath, [
225
+ `--user-data-dir=${workspace.workspaceDir}`,
226
+ `--profile-directory=${workspace.profileName}`,
227
+ `--remote-debugging-port=${port}`,
228
+ "--headless=new",
229
+ "--no-first-run",
230
+ "--no-default-browser-check",
231
+ "https://www.youtube.com",
232
+ ], {
233
+ detached: true,
234
+ stdio: "ignore",
235
+ });
236
+ browserProcess.unref();
237
+ const version = await waitForCdpVersion(port);
238
+ if (!version?.webSocketDebuggerUrl) {
239
+ return null;
240
+ }
241
+ browser = await chromium.connectOverCDP(`http://127.0.0.1:${port}`);
242
+ const context = browser.contexts()[0];
243
+ if (!context) {
244
+ return null;
245
+ }
246
+ const page = context.pages()[0] ?? await context.newPage();
247
+ const client = await context.newCDPSession(page);
248
+ const cookies = await readCdpCookiesUntilSession(client, deps, {
249
+ attempts: 6,
250
+ delayMs: 1_000,
251
+ });
252
+ if (!cookies) {
253
+ return null;
254
+ }
255
+ return cookies;
105
256
  }
106
257
  catch {
107
- throw new Error("Playwright runtime is not installed.\nRun: yt-mcp runtime install");
258
+ return null;
108
259
  }
109
- const channel = await detectBrowserChannel(chromium);
110
- console.log(`Using browser: ${CHANNEL_LABELS[channel] ?? channel}`);
260
+ finally {
261
+ if (browser) {
262
+ await browser.close().catch(() => { });
263
+ }
264
+ if (browserProcess) {
265
+ await terminateImportedBrowser(browserProcess);
266
+ }
267
+ if (workspace) {
268
+ deps.cleanupImportedBrowserWorkspace(workspace);
269
+ }
270
+ }
271
+ }
272
+ export async function runManualCookieSetup(chromium, deps) {
273
+ const channel = await deps.detectBrowserChannel(chromium);
274
+ deps.log(`Using browser: ${CHANNEL_LABELS[channel] ?? channel}`);
111
275
  if (channel === "chromium") {
112
- console.log("⚠️ No system Chrome or Edge found. Using bundled Chromium.\n" +
113
- " If it fails, run: npx playwright install chromium\n");
276
+ deps.log("⚠️ No system Chrome or Edge found. Using bundled Chromium.\n" +
277
+ " If it fails, run: yt-mcp runtime install\n");
114
278
  }
115
279
  let context;
116
280
  try {
@@ -141,15 +305,15 @@ export async function runSetupCookies() {
141
305
  }
142
306
  catch {
143
307
  if (browserClosed) {
144
- console.log("\n⚠️ Browser was closed. Setup cancelled.\n");
308
+ deps.log("\n⚠️ Browser was closed. Setup cancelled.\n");
145
309
  return;
146
310
  }
147
311
  throw new Error("Failed to navigate to YouTube");
148
312
  }
149
313
  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);
314
+ if (deps.hasYouTubeSession(existingCookies)) {
315
+ deps.log("✅ Already logged in to YouTube!\n");
316
+ await deps.saveCookiesAndClose(context, existingCookies);
153
317
  return;
154
318
  }
155
319
  try {
@@ -159,28 +323,28 @@ export async function runSetupCookies() {
159
323
  'ytd-button-renderer a:has-text("Sign in")').first();
160
324
  await signInBtn.waitFor({ state: "visible", timeout: 5000 });
161
325
  await signInBtn.click();
162
- console.log("🔑 Opened sign-in page. Please log in to your Google account.\n");
326
+ deps.log("🔑 Opened sign-in page. Please log in to your Google account.\n");
163
327
  }
164
328
  catch {
165
329
  try {
166
330
  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");
331
+ deps.log("🔑 Please log in to your Google account in the browser window.\n");
168
332
  }
169
333
  catch {
170
334
  if (browserClosed) {
171
- console.log("\n⚠️ Browser was closed. Setup cancelled.\n");
335
+ deps.log("\n⚠️ Browser was closed. Setup cancelled.\n");
172
336
  return;
173
337
  }
174
338
  throw new Error("Failed to navigate to Google login");
175
339
  }
176
340
  }
177
341
  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);
342
+ deps.log("⏳ Waiting for login (up to 2 minutes)...");
343
+ deps.log(" Login will be detected automatically once you sign in.\n");
344
+ const finalCookies = await deps.waitForLogin(context, () => browserClosed, LOGIN_TIMEOUT_MS);
181
345
  if (browserClosed) {
182
- console.log("\n⚠️ Browser was closed before login completed.");
183
- console.log(" Run again: npx @mkterswingman/5mghost-yonder setup-cookies\n");
346
+ deps.log("\n⚠️ Browser was closed before login completed.");
347
+ deps.log(" Run again: npx @mkterswingman/5mghost-yonder setup-cookies\n");
184
348
  return;
185
349
  }
186
350
  if (!finalCookies) {
@@ -188,9 +352,29 @@ export async function runSetupCookies() {
188
352
  await context.close();
189
353
  }
190
354
  catch { /* already closed */ }
191
- console.log("\n⏰ Login timed out (2 minutes).");
192
- console.log(" Run again: npx @mkterswingman/5mghost-yonder setup-cookies\n");
355
+ deps.log("\n⏰ Login timed out (2 minutes).");
356
+ deps.log(" Run again: npx @mkterswingman/5mghost-yonder setup-cookies\n");
357
+ return;
358
+ }
359
+ await deps.saveCookiesAndClose(context, finalCookies);
360
+ }
361
+ /**
362
+ * Interactive cookie setup — opens a visible browser for user to log in.
363
+ */
364
+ export async function runSetupCookies(overrides = {}) {
365
+ const deps = buildSetupCookiesDeps(overrides);
366
+ deps.log("\n🍪 YouTube Cookie Setup\n");
367
+ deps.ensureConfigDir();
368
+ let chromium;
369
+ try {
370
+ chromium = await deps.loadChromium();
371
+ }
372
+ catch {
373
+ throw new Error("Playwright runtime is not installed.\nRun: yt-mcp runtime install");
374
+ }
375
+ const imported = await deps.tryImportBrowserCookies(chromium, deps);
376
+ if (imported) {
193
377
  return;
194
378
  }
195
- await saveCookiesAndClose(context, finalCookies);
379
+ await deps.runManualCookieSetup(chromium, deps);
196
380
  }
@@ -1,4 +1,8 @@
1
1
  import type { RuntimeComponentState } from "./manifest.js";
2
+ export declare function getPlaywrightInstallInvocation(): {
3
+ command: string;
4
+ args: string[];
5
+ };
2
6
  export declare function checkPlaywrightRuntime(): Promise<RuntimeComponentState & {
3
7
  name: "playwright";
4
8
  message?: string;
@@ -1,5 +1,16 @@
1
- import { execSync } from "node:child_process";
1
+ import { execFileSync } from "node:child_process";
2
2
  import { existsSync } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+ import { createRequire } from "node:module";
5
+ const require = createRequire(import.meta.url);
6
+ export function getPlaywrightInstallInvocation() {
7
+ const packageJsonPath = require.resolve("playwright/package.json");
8
+ const cliPath = join(dirname(packageJsonPath), "cli.js");
9
+ return {
10
+ command: process.execPath,
11
+ args: [cliPath, "install", "chromium"],
12
+ };
13
+ }
3
14
  export async function checkPlaywrightRuntime() {
4
15
  try {
5
16
  const { chromium } = await import("playwright");
@@ -25,11 +36,12 @@ export async function checkPlaywrightRuntime() {
25
36
  source: "runtime",
26
37
  installed_at: null,
27
38
  binary_path: null,
28
- message: "npx playwright install chromium",
39
+ message: "yt-mcp runtime install",
29
40
  };
30
41
  }
31
42
  export async function installPlaywrightRuntime() {
32
- execSync("npx playwright install chromium", { stdio: "inherit" });
43
+ const invocation = getPlaywrightInstallInvocation();
44
+ execFileSync(invocation.command, invocation.args, { stdio: "inherit" });
33
45
  return checkPlaywrightRuntime();
34
46
  }
35
47
  export async function updatePlaywrightRuntime() {
@@ -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
+ }
@@ -12,6 +12,10 @@ export interface YtDlpStderrLineSplitter {
12
12
  type SpawnSyncFn = typeof spawnSync;
13
13
  export declare function createYtDlpStderrLineSplitter(onLine: (line: string) => void): YtDlpStderrLineSplitter;
14
14
  export declare function hasYtDlp(runSpawnSync?: SpawnSyncFn): boolean;
15
+ export declare function buildYtDlpArgs(args: string[], options?: {
16
+ cookiesPath?: string;
17
+ cookiesExist?: boolean;
18
+ }): string[];
15
19
  export declare function runYtDlp(args: string[], timeoutMs?: number, onStderrLine?: (line: string) => void): Promise<YtDlpResult>;
16
20
  export declare function runYtDlpJson<T>(args: string[], timeoutMs?: number): Promise<{
17
21
  ok: true;
@@ -57,13 +57,19 @@ export function hasYtDlp(runSpawnSync = spawnSync) {
57
57
  });
58
58
  return result.status === 0 && result.error == null;
59
59
  }
60
+ export function buildYtDlpArgs(args, options = {}) {
61
+ const cookiesPath = options.cookiesPath ?? PATHS.cookiesTxt;
62
+ const cookiesExist = options.cookiesExist ?? existsSync(cookiesPath);
63
+ const finalArgs = ["--force-ipv4", "--no-warnings", "--js-runtimes", "node", ...args];
64
+ if (cookiesExist) {
65
+ finalArgs.push("--cookies", cookiesPath);
66
+ }
67
+ return finalArgs;
68
+ }
60
69
  export function runYtDlp(args, timeoutMs = 45_000, onStderrLine) {
61
70
  return new Promise((resolve, reject) => {
62
71
  const start = Date.now();
63
- const finalArgs = ["--force-ipv4", "--no-warnings", ...args];
64
- if (existsSync(PATHS.cookiesTxt)) {
65
- finalArgs.push("--cookies", PATHS.cookiesTxt);
66
- }
72
+ const finalArgs = buildYtDlpArgs(args);
67
73
  const proc = spawn(getYtDlpPath(), finalArgs, {
68
74
  stdio: ["ignore", "pipe", "pipe"],
69
75
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mkterswingman/5mghost-yonder",
3
- "version": "0.0.5",
3
+ "version": "0.0.7",
4
4
  "description": "Internal MCP client with local data tools and remote API proxy",
5
5
  "type": "module",
6
6
  "bin": {