@mkterswingman/5mghost-yonder 0.0.29 → 0.0.31

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/dist/cli/index.js CHANGED
File without changes
@@ -251,7 +251,9 @@ export async function readImportedBrowserCookies(candidate, chromium, deps) {
251
251
  "--headless=new",
252
252
  "--no-first-run",
253
253
  "--no-default-browser-check",
254
- "https://www.youtube.com",
254
+ // Why: about:blank avoids network requests that trigger Google token rotation,
255
+ // which would invalidate the user's real Chrome YouTube session.
256
+ "about:blank",
255
257
  ], {
256
258
  detached: true,
257
259
  stdio: "ignore",
@@ -261,14 +261,25 @@ function assertManagerInput(manager, jobId) {
261
261
  throw new Error("jobManager and jobId must be provided together.");
262
262
  }
263
263
  }
264
+ const VIDEO_FORMAT_BY_QUALITY = {
265
+ "2160p60": "bv*[height<=2160][fps<=60]+ba/b[height<=2160]/b",
266
+ "1440p60": "bv*[height<=1440][fps<=60]+ba/b[height<=1440]/b",
267
+ "1080p60": "bv*[height<=1080][fps<=60]+ba/b[height<=1080]/b",
268
+ "1080p": "bv*[height<=1080]+ba/b[height<=1080]/b",
269
+ "720p60": "bv*[height<=720][fps<=60]+ba/b[height<=720]/b",
270
+ "480p": "bv*[height<=480]+ba/b[height<=480]/b",
271
+ "360p": "bv*[height<=360]+ba/b[height<=360]/b",
272
+ "240p": "bv*[height<=240]+ba/b[height<=240]/b",
273
+ max: "bv*+ba/b",
274
+ };
264
275
  function normalizeVideoQuality(videoQuality) {
265
- return videoQuality === "max" ? "max" : "1080p";
276
+ if (videoQuality && Object.hasOwn(VIDEO_FORMAT_BY_QUALITY, videoQuality)) {
277
+ return videoQuality;
278
+ }
279
+ return "1080p";
266
280
  }
267
281
  function resolveVideoFormat(videoQuality) {
268
- if (videoQuality === "max") {
269
- return "bv*+ba/b";
270
- }
271
- return "bv*[height<=1080]+ba/b[height<=1080]/b";
282
+ return VIDEO_FORMAT_BY_QUALITY[videoQuality] ?? VIDEO_FORMAT_BY_QUALITY["1080p"];
272
283
  }
273
284
  function needsVideo(mode) {
274
285
  return mode === "video" || mode === "both";
@@ -5,7 +5,17 @@ import { buildMediaOutputPaths } from "../utils/mediaPaths.js";
5
5
  import { resolveVideoInput } from "../utils/videoInput.js";
6
6
  const AUTH_REQUIRED_MSG = "❌ 未认证。请先登录:\n• OAuth: npx @mkterswingman/5mghost-yonder setup\n• PAT: 设置环境变量 YT_MCP_TOKEN 或在 https://mkterswingman.com/pat/login 生成 token";
7
7
  const DOWNLOAD_MODES = ["video", "subtitles", "both"];
8
- const VIDEO_QUALITIES = ["1080p", "max"];
8
+ const VIDEO_QUALITIES = [
9
+ "2160p60",
10
+ "1440p60",
11
+ "1080p60",
12
+ "1080p",
13
+ "720p60",
14
+ "480p",
15
+ "360p",
16
+ "240p",
17
+ "max",
18
+ ];
9
19
  const SUBTITLE_FORMATS = ["vtt", "srt", "ttml", "srv3", "csv"];
10
20
  const MAX_DOWNLOAD_VIDEOS = 5;
11
21
  class DownloadToolError extends Error {
@@ -1,18 +1,20 @@
1
1
  /**
2
- * Headless cookie auto-refresh.
2
+ * Cookie auto-refresh with three-tier fallback.
3
3
  *
4
- * Uses Playwright to open YouTube in a headless browser with the existing
5
- * browser-profile. If Google session is still valid, YouTube cookies are
6
- * extracted and saved automatically — no user interaction needed.
4
+ * Tier 1: Headless Playwright refresh using existing browser-profile.
5
+ * If Google session is still valid, cookies are extracted automatically.
7
6
  *
8
- * If Google session has expired too, returns false so the caller can
9
- * prompt the user for interactive login.
7
+ * Tier 2: Re-import from system Chrome/Edge (headless, about:blank).
8
+ * Copies the real browser profile to a temp dir and reads cookies via CDP
9
+ * without making any network requests (avoids Google token rotation).
10
+ *
11
+ * Tier 3: Open a headed browser for manual user login.
12
+ * Blocks the MCP tool call for up to ~2 minutes while the user logs in.
13
+ * Creates browser-profile so Tier 1 works next time.
10
14
  */
11
15
  /**
12
- * Attempt to refresh YouTube cookies headlessly using existing browser profile.
16
+ * Attempt to refresh YouTube cookies through a three-tier fallback chain.
13
17
  *
14
- * @returns true if cookies were refreshed, false if Google session expired
15
- * (needs interactive login).
16
- * @throws if Playwright is not installed or browser can't launch.
18
+ * @returns true if cookies were refreshed, false if all tiers failed.
17
19
  */
18
20
  export declare function tryHeadlessRefresh(): Promise<boolean>;
@@ -1,38 +1,55 @@
1
1
  /**
2
- * Headless cookie auto-refresh.
2
+ * Cookie auto-refresh with three-tier fallback.
3
3
  *
4
- * Uses Playwright to open YouTube in a headless browser with the existing
5
- * browser-profile. If Google session is still valid, YouTube cookies are
6
- * extracted and saved automatically — no user interaction needed.
4
+ * Tier 1: Headless Playwright refresh using existing browser-profile.
5
+ * If Google session is still valid, cookies are extracted automatically.
7
6
  *
8
- * If Google session has expired too, returns false so the caller can
9
- * prompt the user for interactive login.
7
+ * Tier 2: Re-import from system Chrome/Edge (headless, about:blank).
8
+ * Copies the real browser profile to a temp dir and reads cookies via CDP
9
+ * without making any network requests (avoids Google token rotation).
10
+ *
11
+ * Tier 3: Open a headed browser for manual user login.
12
+ * Blocks the MCP tool call for up to ~2 minutes while the user logs in.
13
+ * Creates browser-profile so Tier 1 works next time.
10
14
  */
11
15
  import { existsSync } from "node:fs";
12
16
  import { PATHS, ensureConfigDir } from "./config.js";
13
17
  import { detectBrowserChannel, hasYouTubeSession, saveCookiesToDisk, } from "../cli/setupCookies.js";
18
+ import { hasSIDCookies } from "./cookies.js";
14
19
  const HEADLESS_TIMEOUT_MS = 30_000;
15
20
  /**
16
- * Attempt to refresh YouTube cookies headlessly using existing browser profile.
21
+ * Attempt to refresh YouTube cookies through a three-tier fallback chain.
17
22
  *
18
- * @returns true if cookies were refreshed, false if Google session expired
19
- * (needs interactive login).
20
- * @throws if Playwright is not installed or browser can't launch.
23
+ * @returns true if cookies were refreshed, false if all tiers failed.
21
24
  */
22
25
  export async function tryHeadlessRefresh() {
23
- // No browser profile = never logged in = can't refresh
24
- if (!existsSync(PATHS.browserProfile)) {
25
- return false;
26
- }
27
26
  ensureConfigDir();
28
- // Dynamic import to keep serve startup fast
27
+ // Tier 1: Playwright persistent context (requires browser-profile from prior manual login)
28
+ if (existsSync(PATHS.browserProfile)) {
29
+ const tier1 = await tryPlaywrightRefresh();
30
+ if (tier1)
31
+ return true;
32
+ }
33
+ // Tier 2: Re-import from system browser (headless, no network requests)
34
+ const tier2 = await tryReimportFromSystemBrowser();
35
+ if (tier2)
36
+ return true;
37
+ // Tier 3: Open headed browser for manual login
38
+ const tier3 = await tryHeadedManualLogin();
39
+ return tier3;
40
+ }
41
+ /**
42
+ * Tier 1: Use existing browser-profile with Playwright to refresh cookies.
43
+ * This is the original tryHeadlessRefresh logic.
44
+ */
45
+ async function tryPlaywrightRefresh() {
29
46
  let chromium;
30
47
  try {
31
48
  const pw = await import("playwright");
32
49
  chromium = pw.chromium;
33
50
  }
34
51
  catch {
35
- return false; // Playwright not installed — can't refresh
52
+ return false;
36
53
  }
37
54
  const channel = await detectBrowserChannel(chromium);
38
55
  let context;
@@ -44,7 +61,7 @@ export async function tryHeadlessRefresh() {
44
61
  });
45
62
  }
46
63
  catch {
47
- return false; // Browser launch failed
64
+ return false;
48
65
  }
49
66
  try {
50
67
  const page = context.pages()[0] ?? (await context.newPage());
@@ -52,11 +69,9 @@ export async function tryHeadlessRefresh() {
52
69
  await page.waitForLoadState("domcontentloaded");
53
70
  const cookies = await context.cookies("https://www.youtube.com");
54
71
  if (!hasYouTubeSession(cookies)) {
55
- // Google session expired — can't auto-refresh
56
72
  await context.close().catch(() => { });
57
73
  return false;
58
74
  }
59
- // Wait a moment for all cookies to settle
60
75
  await new Promise((r) => setTimeout(r, 1500));
61
76
  const finalCookies = await context.cookies("https://www.youtube.com");
62
77
  saveCookiesToDisk(finalCookies);
@@ -68,3 +83,53 @@ export async function tryHeadlessRefresh() {
68
83
  return false;
69
84
  }
70
85
  }
86
+ /**
87
+ * Tier 2: Re-import cookies from the user's system Chrome/Edge.
88
+ * Uses headless Chrome with about:blank to avoid triggering Google token rotation.
89
+ */
90
+ async function tryReimportFromSystemBrowser() {
91
+ try {
92
+ const pw = await import("playwright");
93
+ const { tryImportBrowserCookies, saveCookiesAndClose, readImportedBrowserCookies, } = await import("../cli/setupCookies.js");
94
+ const { findImportableBrowserProfileCandidates, prepareImportedBrowserWorkspace, cleanupImportedBrowserWorkspace, } = await import("./browserProfileImport.js");
95
+ const deps = {
96
+ ensureConfigDir,
97
+ loadChromium: async () => pw.chromium,
98
+ detectBrowserChannel,
99
+ hasYouTubeSession,
100
+ saveCookiesAndClose,
101
+ // Why: waitForLogin is unused in import path but required by SetupCookiesDeps type
102
+ waitForLogin: async () => null,
103
+ findImportableBrowserProfileCandidates,
104
+ prepareImportedBrowserWorkspace,
105
+ cleanupImportedBrowserWorkspace,
106
+ readImportedBrowserCookies,
107
+ tryImportBrowserCookies,
108
+ // Why: runManualCookieSetup is unused in import path but required by type
109
+ runManualCookieSetup: async () => { },
110
+ // Why: silent logging during auto-refresh — user doesn't see MCP server stdout
111
+ log: () => { },
112
+ };
113
+ return await tryImportBrowserCookies(pw.chromium, deps);
114
+ }
115
+ catch {
116
+ return false;
117
+ }
118
+ }
119
+ /**
120
+ * Tier 3: Open a headed browser for the user to manually log in.
121
+ * Blocks the MCP tool call for up to ~2 minutes.
122
+ * Creates browser-profile so Tier 1 works for future refreshes.
123
+ */
124
+ async function tryHeadedManualLogin() {
125
+ try {
126
+ // Why: runSetupCookies with { headed: true } skips import and goes straight
127
+ // to manual Playwright login, which creates browser-profile for future Tier 1 use.
128
+ const { runSetupCookies } = await import("../cli/setupCookies.js");
129
+ await runSetupCookies({}, { headed: true });
130
+ return hasSIDCookies(PATHS.cookiesTxt);
131
+ }
132
+ catch {
133
+ return false;
134
+ }
135
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mkterswingman/5mghost-yonder",
3
- "version": "0.0.29",
3
+ "version": "0.0.31",
4
4
  "description": "Internal MCP client with local data tools and remote API proxy",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: use-yt-mcp
3
3
  preamble-tier: 3
4
- version: 1.2.0
4
+ version: 1.2.1
5
5
  description: |
6
6
  Use when the user wants YouTube analysis through yt-mcp: channel performance,
7
7
  video stats, subtitles, comments, trending, or batch YouTube data work.
@@ -54,6 +54,8 @@ All tools are prefixed `mcp__yt-mcp__` in actual calls.
54
54
  - If the user provides a fuzzy channel name (for example only a brand/person name), call `search_channels` first and present 3-5 candidates before doing channel analysis.
55
55
  - After `search_channels` returns candidates, explicitly ask the user which channel they mean; do not auto-select candidate #1.
56
56
  - If the user already provides a channel URL, `@handle`, or explicit `channel_id`, skip candidate search and go directly to `get_channel_stats` / `list_channel_uploads`.
57
+ - For local media download requests where the user says to download/save a video but does not explicitly choose mode, call `start_download_job` with `mode: "both"` so subtitles are downloaded by default.
58
+ - If the user explicitly says no subtitles / video-only (for example "不要字幕", "只下视频"), then call `start_download_job` with `mode: "video"`.
57
59
  - `get_video_stats` accepts up to 1000 items. If the response returns `mode: "file"`, then inline `preview_items` is only a preview sample from a larger dataset and must not be used as the full basis of analysis. Tell the user the complete result is in `output_file` or `download_url`.
58
60
  - For large video/comment analyses where completeness matters, prefer async jobs: `start_video_stats_job` -> `poll_video_stats_job`, or `start_comments_job` -> `poll_comments_job`.
59
61
  - When using async jobs, poll until status is `completed`, and tell the user if the result is still partial or in progress.