@mkterswingman/5mghost-yonder 0.0.11 → 0.0.12

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.
@@ -10,7 +10,8 @@ const AUTH_REQUIRED_MSG = "❌ 未认证。请先登录:\n• OAuth: npx @mkte
10
10
  const COOKIE_SETUP_COMMAND = "yt-mcp setup-cookies";
11
11
  const COOKIE_MISSING_MESSAGE = `No valid YouTube cookies found.\nPlease run in your terminal: ${COOKIE_SETUP_COMMAND}`;
12
12
  const COOKIE_EXPIRED_MESSAGE = `YouTube cookies have expired and auto-refresh failed.\nPlease run in your terminal: ${COOKIE_SETUP_COMMAND}`;
13
- const COOKIE_JOB_MESSAGE = `YouTube cookies are missing or expired.\nPlease run in your terminal: ${COOKIE_SETUP_COMMAND}`;
13
+ const COOKIE_INVALID_MESSAGE = `YouTube rejected the current cookies and auto-refresh failed.\nPlease run in your terminal: ${COOKIE_SETUP_COMMAND}`;
14
+ const COOKIE_JOB_MESSAGE = `YouTube cookies are missing, expired, or invalid.\nPlease run in your terminal: ${COOKIE_SETUP_COMMAND}`;
14
15
  function toolOk(payload) {
15
16
  return {
16
17
  structuredContent: payload,
@@ -55,12 +56,39 @@ function isCookieFailureText(error) {
55
56
  normalized.includes("login") ||
56
57
  normalized.includes("not a bot"));
57
58
  }
59
+ function appendDiagnosticLog(message, logPath) {
60
+ return logPath ? `${message}\nDiagnostic log: ${logPath}` : message;
61
+ }
62
+ function classifyYtDlpCookieFailure(stderr) {
63
+ const normalized = stderr.toLowerCase();
64
+ if (normalized.includes("cookies are no longer valid") ||
65
+ normalized.includes("cookie has expired") ||
66
+ normalized.includes("cookies have expired") ||
67
+ normalized.includes("session expired")) {
68
+ return {
69
+ code: "COOKIES_EXPIRED",
70
+ message: COOKIE_EXPIRED_MESSAGE,
71
+ };
72
+ }
73
+ if (normalized.includes("sign in") ||
74
+ normalized.includes("login") ||
75
+ normalized.includes("cookies") ||
76
+ normalized.includes("not a bot")) {
77
+ return {
78
+ code: "COOKIES_INVALID",
79
+ message: COOKIE_INVALID_MESSAGE,
80
+ };
81
+ }
82
+ return null;
83
+ }
58
84
  export function toReadableSubtitleJobError(error) {
59
85
  const message = error instanceof Error ? error.message : String(error);
60
86
  return isCookieFailureText(message) ? COOKIE_JOB_MESSAGE : message;
61
87
  }
62
88
  async function ensureSubtitleCookiesReady() {
63
- if (hasSIDCookies(PATHS.cookiesTxt) && !areCookiesExpired(PATHS.cookiesTxt)) {
89
+ const hasSession = hasSIDCookies(PATHS.cookiesTxt);
90
+ const expired = areCookiesExpired(PATHS.cookiesTxt);
91
+ if (hasSession && !expired) {
64
92
  return { ok: true };
65
93
  }
66
94
  const refreshed = await tryRefreshSubtitleCookies();
@@ -69,9 +97,9 @@ async function ensureSubtitleCookiesReady() {
69
97
  }
70
98
  return {
71
99
  ok: false,
72
- code: "COOKIES_MISSING",
73
- toolMessage: COOKIE_MISSING_MESSAGE,
74
- jobMessage: COOKIE_JOB_MESSAGE,
100
+ code: hasSession && expired ? "COOKIES_EXPIRED" : "COOKIES_MISSING",
101
+ toolMessage: hasSession && expired ? COOKIE_EXPIRED_MESSAGE : COOKIE_MISSING_MESSAGE,
102
+ jobMessage: appendDiagnosticLog(COOKIE_JOB_MESSAGE, undefined),
75
103
  };
76
104
  }
77
105
  /**
@@ -244,16 +272,20 @@ async function downloadSubtitle(videoId, lang, format, options = {}) {
244
272
  outTemplate,
245
273
  options.sourceUrl ?? `https://www.youtube.com/watch?v=${videoId}`,
246
274
  ]);
247
- if (result.exitCode !== 0 &&
248
- (result.stderr.includes("Sign in") ||
249
- result.stderr.includes("cookies") ||
250
- result.stderr.includes("login"))) {
251
- return { ok: false, cookiesExpired: true, error: "COOKIES_EXPIRED" };
275
+ const cookieFailure = result.exitCode !== 0 ? classifyYtDlpCookieFailure(result.stderr) : null;
276
+ if (cookieFailure) {
277
+ return {
278
+ ok: false,
279
+ cookieFailureCode: cookieFailure.code,
280
+ diagnosticLogPath: result.failureLogPath,
281
+ error: appendDiagnosticLog(cookieFailure.message, result.failureLogPath),
282
+ };
252
283
  }
253
284
  if (result.exitCode !== 0) {
254
285
  return {
255
286
  ok: false,
256
- error: result.stderr.slice(0, 500) || `yt-dlp exited with ${result.exitCode}`,
287
+ diagnosticLogPath: result.failureLogPath,
288
+ error: appendDiagnosticLog(result.stderr.slice(0, 500) || `yt-dlp exited with ${result.exitCode}`, result.failureLogPath),
257
289
  };
258
290
  }
259
291
  // Find the output file - yt-dlp appends lang and format extension.
@@ -335,7 +367,7 @@ export async function downloadSubtitlesForLanguages(input) {
335
367
  outputStem: `${lang}`,
336
368
  targetFile,
337
369
  });
338
- if (result.cookiesExpired) {
370
+ if (result.cookieFailureCode) {
339
371
  const refreshed = await tryRefreshSubtitleCookies();
340
372
  if (refreshed) {
341
373
  result = await downloadSubtitle(input.videoId, lang, format, {
@@ -419,7 +451,7 @@ export function registerSubtitleTools(server, config, tokenManager) {
419
451
  }
420
452
  const lang = langs[i];
421
453
  const dl = await downloadSubtitle(videoId, lang, fmt);
422
- if (dl.cookiesExpired) {
454
+ if (dl.cookieFailureCode) {
423
455
  const refreshed = await tryRefreshSubtitleCookies();
424
456
  if (refreshed) {
425
457
  // Retry the download with fresh cookies
@@ -435,7 +467,7 @@ export function registerSubtitleTools(server, config, tokenManager) {
435
467
  }
436
468
  // Retry also failed with expired cookies — give up
437
469
  }
438
- return toolErr("COOKIES_EXPIRED", COOKIE_EXPIRED_MESSAGE);
470
+ return toolErr(dl.cookieFailureCode, dl.error ?? (dl.cookieFailureCode === "COOKIES_EXPIRED" ? COOKIE_EXPIRED_MESSAGE : COOKIE_INVALID_MESSAGE));
439
471
  }
440
472
  if (!dl.ok) {
441
473
  // When using default languages, silently skip unavailable ones
@@ -505,14 +537,14 @@ export function registerSubtitleTools(server, config, tokenManager) {
505
537
  const videoId = resolvedIds[v];
506
538
  const langFound = [];
507
539
  let lastError;
508
- let cookiesExpired = false;
540
+ let cookieFailureCode = null;
509
541
  for (let l = 0; l < langs.length; l++) {
510
542
  if (l > 0) {
511
543
  await randomSleep(2000, 5000);
512
544
  }
513
545
  const dl = await downloadSubtitle(videoId, langs[l], fmt);
514
- if (dl.cookiesExpired) {
515
- cookiesExpired = true;
546
+ if (dl.cookieFailureCode) {
547
+ cookieFailureCode = dl.cookieFailureCode;
516
548
  break;
517
549
  }
518
550
  if (dl.ok) {
@@ -522,7 +554,7 @@ export function registerSubtitleTools(server, config, tokenManager) {
522
554
  lastError = dl.error;
523
555
  }
524
556
  }
525
- if (cookiesExpired) {
557
+ if (cookieFailureCode) {
526
558
  const refreshed = await tryRefreshSubtitleCookies();
527
559
  if (refreshed) {
528
560
  // Retry this video's subtitles
@@ -535,11 +567,11 @@ export function registerSubtitleTools(server, config, tokenManager) {
535
567
  }
536
568
  }
537
569
  if (retrySuccess) {
538
- cookiesExpired = false;
570
+ cookieFailureCode = null;
539
571
  }
540
572
  }
541
- if (cookiesExpired) {
542
- return toolErr("COOKIES_EXPIRED", COOKIE_EXPIRED_MESSAGE);
573
+ if (cookieFailureCode) {
574
+ return toolErr(cookieFailureCode, cookieFailureCode === "COOKIES_EXPIRED" ? COOKIE_EXPIRED_MESSAGE : COOKIE_INVALID_MESSAGE);
543
575
  }
544
576
  }
545
577
  if (langFound.length > 0) {
@@ -6,6 +6,7 @@ export declare function buildPaths(homeDir?: string): {
6
6
  configDir: string;
7
7
  authJson: string;
8
8
  cookiesTxt: string;
9
+ logsDir: string;
9
10
  browserProfile: string;
10
11
  launcherJs: string;
11
12
  npmCacheDir: string;
@@ -25,6 +26,7 @@ export declare const PATHS: {
25
26
  configDir: string;
26
27
  authJson: string;
27
28
  cookiesTxt: string;
29
+ logsDir: string;
28
30
  browserProfile: string;
29
31
  launcherJs: string;
30
32
  npmCacheDir: string;
@@ -20,6 +20,7 @@ export function buildPaths(homeDir = homedir()) {
20
20
  configDir,
21
21
  authJson: join(configDir, "auth.json"),
22
22
  cookiesTxt: join(configDir, "cookies.txt"),
23
+ logsDir: join(configDir, "logs"),
23
24
  browserProfile: join(configDir, "browser-profile"),
24
25
  launcherJs: join(configDir, "launcher.mjs"),
25
26
  npmCacheDir: join(configDir, "npm-cache"),
@@ -15,12 +15,39 @@ export function cookiesToNetscape(cookies) {
15
15
  }
16
16
  return lines.join("\n") + "\n";
17
17
  }
18
+ function parseCookieRows(content) {
19
+ const rows = [];
20
+ for (const line of content.split("\n")) {
21
+ if (line.startsWith("#") || !line.trim())
22
+ continue;
23
+ const fields = line.split("\t");
24
+ if (fields.length < 7)
25
+ continue;
26
+ rows.push({
27
+ domain: fields[0],
28
+ expires: Number(fields[4]),
29
+ name: fields[5],
30
+ });
31
+ }
32
+ return rows;
33
+ }
34
+ function hasCookieTriplet(rows, domainFragment) {
35
+ const names = new Set(rows
36
+ .filter((row) => row.domain.includes(domainFragment))
37
+ .map((row) => row.name));
38
+ return names.has("SID") && names.has("HSID") && names.has("SSID");
39
+ }
18
40
  export function hasSIDCookies(cookiesPath) {
19
41
  if (!existsSync(cookiesPath))
20
42
  return false;
21
43
  try {
22
44
  const content = readFileSync(cookiesPath, "utf8");
23
- return content.includes("SID") && content.includes(".youtube.com");
45
+ const rows = parseCookieRows(content);
46
+ if (hasCookieTriplet(rows, "youtube.com")) {
47
+ return true;
48
+ }
49
+ const hasYouTubeLoginInfo = rows.some((row) => row.name === "LOGIN_INFO" && row.domain.includes("youtube.com"));
50
+ return hasYouTubeLoginInfo && hasCookieTriplet(rows, "google.");
24
51
  }
25
52
  catch {
26
53
  return false;
@@ -48,19 +75,14 @@ export function areCookiesExpired(cookiesPath) {
48
75
  try {
49
76
  const content = readFileSync(cookiesPath, "utf8");
50
77
  const nowSec = Date.now() / 1000;
78
+ const rows = parseCookieRows(content);
51
79
  let foundAnySession = false;
52
80
  let foundAnyValid = false;
53
- for (const line of content.split("\n")) {
54
- if (line.startsWith("#") || !line.trim())
55
- continue;
56
- const fields = line.split("\t");
57
- if (fields.length < 7)
58
- continue;
59
- const name = fields[5];
60
- if (!SESSION_COOKIE_PATTERNS.includes(name))
81
+ for (const row of rows) {
82
+ if (!SESSION_COOKIE_PATTERNS.includes(row.name))
61
83
  continue;
62
84
  foundAnySession = true;
63
- const expiry = Number(fields[4]);
85
+ const expiry = row.expires;
64
86
  // expiry 0 = session cookie (no expiration) — treat as valid
65
87
  if (expiry === 0 || expiry >= nowSec) {
66
88
  foundAnyValid = true;
@@ -4,6 +4,7 @@ export interface YtDlpResult {
4
4
  stdout: string;
5
5
  stderr: string;
6
6
  durationMs: number;
7
+ failureLogPath?: string;
7
8
  }
8
9
  export interface YtDlpStderrLineSplitter {
9
10
  push(chunk: Buffer | string): void;
@@ -1,8 +1,25 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { spawnSync } from "node:child_process";
3
- import { existsSync } from "node:fs";
3
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
4
+ import { join } from "node:path";
4
5
  import { PATHS } from "./config.js";
5
6
  import { getYtDlpPath } from "./ytdlpPath.js";
7
+ function writeYtDlpFailureLog(payload) {
8
+ try {
9
+ mkdirSync(PATHS.logsDir, { recursive: true });
10
+ const stamp = new Date().toISOString().replace(/[:.]/g, "-");
11
+ const filePath = join(PATHS.logsDir, `yt-dlp-failure-${stamp}.json`);
12
+ writeFileSync(filePath, JSON.stringify({
13
+ timestamp: new Date().toISOString(),
14
+ binary: getYtDlpPath(),
15
+ ...payload,
16
+ }, null, 2), "utf8");
17
+ return filePath;
18
+ }
19
+ catch {
20
+ return undefined;
21
+ }
22
+ }
6
23
  export function createYtDlpStderrLineSplitter(onLine) {
7
24
  let buffer = "";
8
25
  let pendingCarriageReturn = false;
@@ -92,11 +109,22 @@ export function runYtDlp(args, timeoutMs = 45_000, onStderrLine) {
92
109
  settled = true;
93
110
  proc.kill("SIGKILL");
94
111
  flushStderrLineBuffer();
112
+ const stdout = Buffer.concat(stdoutChunks).toString("utf8");
113
+ const stderr = `yt-dlp timed out after ${timeoutMs}ms`;
95
114
  resolve({
96
115
  exitCode: -1,
97
- stdout: Buffer.concat(stdoutChunks).toString("utf8"),
98
- stderr: `yt-dlp timed out after ${timeoutMs}ms`,
116
+ stdout,
117
+ stderr,
99
118
  durationMs: Date.now() - start,
119
+ failureLogPath: writeYtDlpFailureLog({
120
+ args: finalArgs,
121
+ exitCode: -1,
122
+ stdout,
123
+ stderr,
124
+ durationMs: Date.now() - start,
125
+ timeoutMs,
126
+ trigger: "timeout",
127
+ }),
100
128
  });
101
129
  }
102
130
  }, timeoutMs);
@@ -105,11 +133,25 @@ export function runYtDlp(args, timeoutMs = 45_000, onStderrLine) {
105
133
  if (!settled) {
106
134
  settled = true;
107
135
  flushStderrLineBuffer();
136
+ const stdout = Buffer.concat(stdoutChunks).toString("utf8");
137
+ const stderr = Buffer.concat(stderrChunks).toString("utf8");
138
+ const exitCode = code ?? 1;
108
139
  resolve({
109
- exitCode: code ?? 1,
110
- stdout: Buffer.concat(stdoutChunks).toString("utf8"),
111
- stderr: Buffer.concat(stderrChunks).toString("utf8"),
140
+ exitCode,
141
+ stdout,
142
+ stderr,
112
143
  durationMs: Date.now() - start,
144
+ failureLogPath: exitCode === 0
145
+ ? undefined
146
+ : writeYtDlpFailureLog({
147
+ args: finalArgs,
148
+ exitCode,
149
+ stdout,
150
+ stderr,
151
+ durationMs: Date.now() - start,
152
+ timeoutMs,
153
+ trigger: "exit",
154
+ }),
113
155
  });
114
156
  }
115
157
  });
@@ -117,7 +159,20 @@ export function runYtDlp(args, timeoutMs = 45_000, onStderrLine) {
117
159
  clearTimeout(timer);
118
160
  if (!settled) {
119
161
  settled = true;
120
- reject(err);
162
+ const stdout = Buffer.concat(stdoutChunks).toString("utf8");
163
+ const stderr = Buffer.concat(stderrChunks).toString("utf8");
164
+ reject(Object.assign(err, {
165
+ failureLogPath: writeYtDlpFailureLog({
166
+ args: finalArgs,
167
+ exitCode: 1,
168
+ stdout,
169
+ stderr,
170
+ durationMs: Date.now() - start,
171
+ timeoutMs,
172
+ trigger: "spawn_error",
173
+ spawnError: err instanceof Error ? err.message : String(err),
174
+ }),
175
+ }));
121
176
  }
122
177
  });
123
178
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mkterswingman/5mghost-yonder",
3
- "version": "0.0.11",
3
+ "version": "0.0.12",
4
4
  "description": "Internal MCP client with local data tools and remote API proxy",
5
5
  "type": "module",
6
6
  "bin": {