@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 +6 -3
- package/dist/cli/setupCookies.d.ts +47 -1
- package/dist/cli/setupCookies.js +215 -31
- package/dist/runtime/playwrightRuntime.d.ts +4 -0
- package/dist/runtime/playwrightRuntime.js +15 -3
- package/dist/utils/browserProfileImport.d.ts +49 -0
- package/dist/utils/browserProfileImport.js +159 -0
- package/dist/utils/ytdlp.d.ts +4 -0
- package/dist/utils/ytdlp.js +10 -4
- 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,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` —
|
|
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 {};
|
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.
|
|
@@ -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
|
-
|
|
29
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
104
|
-
|
|
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
|
-
|
|
258
|
+
return null;
|
|
108
259
|
}
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
113
|
-
" If it fails, run:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
179
|
-
|
|
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
|
-
|
|
183
|
-
|
|
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
|
-
|
|
192
|
-
|
|
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
|
|
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 {
|
|
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: "
|
|
39
|
+
message: "yt-mcp runtime install",
|
|
29
40
|
};
|
|
30
41
|
}
|
|
31
42
|
export async function installPlaywrightRuntime() {
|
|
32
|
-
|
|
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
|
+
}
|
package/dist/utils/ytdlp.d.ts
CHANGED
|
@@ -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;
|
package/dist/utils/ytdlp.js
CHANGED
|
@@ -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 =
|
|
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
|
});
|