@mkterswingman/5mghost-yonder 0.0.11 → 0.0.13

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.
@@ -38,6 +38,7 @@ interface DownloadOneItemDeps {
38
38
  videoQuality: string;
39
39
  onProgress: (update: DownloadOneItemProgressUpdate) => void;
40
40
  }) => Promise<string>;
41
+ refreshCookies: () => Promise<boolean>;
41
42
  downloadSubtitles: (input: {
42
43
  videoId: string;
43
44
  sourceUrl: string;
@@ -2,9 +2,11 @@ import { mkdir, rm, stat, writeFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
3
  import { downloadSubtitlesForLanguages, toReadableSubtitleJobError } from "../tools/subtitles.js";
4
4
  import { loadConfig } from "../utils/config.js";
5
+ import { tryHeadlessRefresh } from "../utils/cookieRefresh.js";
5
6
  import { hasFfmpeg, isRuntimeMissingMessage } from "../utils/ffmpeg.js";
6
7
  import { formatBytes } from "../utils/formatters.js";
7
8
  import { hasYtDlp, runYtDlp, runYtDlpJson } from "../utils/ytdlp.js";
9
+ import { appendDiagnosticLog, classifyYtDlpFailure } from "../utils/ytdlpFailures.js";
8
10
  import { parseYtDlpProgressLine } from "../utils/ytdlpProgress.js";
9
11
  export async function downloadOneItem(input) {
10
12
  const deps = resolveDeps(input.deps);
@@ -50,12 +52,28 @@ export async function downloadOneItem(input) {
50
52
  if (needsVideo(input.mode)) {
51
53
  currentStep = "download_video";
52
54
  manager?.updateItemProgress(jobId, input.item.video_id, currentStep, 0);
53
- result.video_file = await deps.downloadVideo({
54
- sourceUrl: input.item.source_url,
55
- outputFile: videoFile,
56
- videoQuality,
57
- onProgress,
58
- });
55
+ try {
56
+ result.video_file = await deps.downloadVideo({
57
+ sourceUrl: input.item.source_url,
58
+ outputFile: videoFile,
59
+ videoQuality,
60
+ onProgress,
61
+ });
62
+ }
63
+ catch (error) {
64
+ const failureKind = classifyVideoDownloadError(error);
65
+ if (failureKind != null && failureKind !== "RATE_LIMITED" && await deps.refreshCookies()) {
66
+ result.video_file = await deps.downloadVideo({
67
+ sourceUrl: input.item.source_url,
68
+ outputFile: videoFile,
69
+ videoQuality,
70
+ onProgress,
71
+ });
72
+ }
73
+ else {
74
+ throw error;
75
+ }
76
+ }
59
77
  result.final_file_size = formatBytes((await deps.stat(result.video_file)).size);
60
78
  }
61
79
  if (needsSubtitles(input.mode)) {
@@ -103,6 +121,7 @@ function resolveDeps(overrides) {
103
121
  hasFfmpeg,
104
122
  fetchMetadata: fetchMetadataWithYtDlp,
105
123
  downloadVideo: downloadVideoWithYtDlp,
124
+ refreshCookies: tryHeadlessRefresh,
106
125
  downloadSubtitles: downloadSubtitlesWithYtDlp,
107
126
  mkdir: async (path, options) => {
108
127
  await mkdir(path, options);
@@ -166,7 +185,17 @@ async function downloadVideoWithYtDlp(input) {
166
185
  }
167
186
  });
168
187
  if (result.exitCode !== 0) {
169
- throw new Error(result.stderr.slice(0, 500) || `yt-dlp exited with ${result.exitCode}`);
188
+ const failureKind = classifyYtDlpFailure(result.stderr);
189
+ if (failureKind === "RATE_LIMITED") {
190
+ throw new Error(appendDiagnosticLog("The current YouTube session has been rate-limited. Wait and retry later.", result.failureLogPath));
191
+ }
192
+ if (failureKind === "SIGN_IN_REQUIRED") {
193
+ throw new Error(appendDiagnosticLog("YouTube requested an additional sign-in confirmation for this video.", result.failureLogPath));
194
+ }
195
+ if (failureKind === "COOKIES_INVALID" || failureKind === "COOKIES_EXPIRED") {
196
+ throw new Error(appendDiagnosticLog("YouTube rejected the current cookies for video download.", result.failureLogPath));
197
+ }
198
+ throw new Error(appendDiagnosticLog(result.stderr.slice(0, 500) || `yt-dlp exited with ${result.exitCode}`, result.failureLogPath));
170
199
  }
171
200
  return resolveYtDlpOutputPath(input.outputFile, result.stdout);
172
201
  }
@@ -259,6 +288,10 @@ function sortSubtitleFiles(files) {
259
288
  .sort(([left], [right]) => left.localeCompare(right))
260
289
  .map(([format, paths]) => [format, [...paths].sort()]));
261
290
  }
291
+ function classifyVideoDownloadError(error) {
292
+ const message = error instanceof Error ? error.message : String(error);
293
+ return classifyYtDlpFailure(message);
294
+ }
262
295
  function toErrorMessage(error) {
263
296
  return toReadableSubtitleJobError(error);
264
297
  }
@@ -5,12 +5,16 @@ import { PATHS } from "../utils/config.js";
5
5
  import { runYtDlp } from "../utils/ytdlp.js";
6
6
  import { hasSIDCookies, areCookiesExpired } from "../utils/cookies.js";
7
7
  import { tryHeadlessRefresh } from "../utils/cookieRefresh.js";
8
+ import { appendDiagnosticLog, classifyYtDlpFailure } from "../utils/ytdlpFailures.js";
8
9
  import { resolveVideoInput, normalizeVideoInputs } from "../utils/videoInput.js";
9
10
  const AUTH_REQUIRED_MSG = "❌ 未认证。请先登录:\n• OAuth: npx @mkterswingman/5mghost-yonder setup\n• PAT: 设置环境变量 YT_MCP_TOKEN 或在 https://mkterswingman.com/pat/login 生成 token";
10
11
  const COOKIE_SETUP_COMMAND = "yt-mcp setup-cookies";
11
12
  const COOKIE_MISSING_MESSAGE = `No valid YouTube cookies found.\nPlease run in your terminal: ${COOKIE_SETUP_COMMAND}`;
12
13
  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}`;
14
+ const COOKIE_INVALID_MESSAGE = `YouTube rejected the current cookies and auto-refresh failed.\nPlease run in your terminal: ${COOKIE_SETUP_COMMAND}`;
15
+ const SIGN_IN_REQUIRED_MESSAGE = `YouTube requested an additional sign-in confirmation and auto-refresh failed.\nPlease run in your terminal: ${COOKIE_SETUP_COMMAND}`;
16
+ const RATE_LIMITED_MESSAGE = "The current YouTube session has been rate-limited. Wait and retry later.";
17
+ const COOKIE_JOB_MESSAGE = `YouTube cookies are missing, expired, or invalid.\nPlease run in your terminal: ${COOKIE_SETUP_COMMAND}`;
14
18
  function toolOk(payload) {
15
19
  return {
16
20
  structuredContent: payload,
@@ -55,12 +59,29 @@ function isCookieFailureText(error) {
55
59
  normalized.includes("login") ||
56
60
  normalized.includes("not a bot"));
57
61
  }
62
+ function classifyYtDlpCookieFailure(stderr) {
63
+ const kind = classifyYtDlpFailure(stderr);
64
+ switch (kind) {
65
+ case "COOKIES_EXPIRED":
66
+ return { code: kind, message: COOKIE_EXPIRED_MESSAGE };
67
+ case "COOKIES_INVALID":
68
+ return { code: kind, message: COOKIE_INVALID_MESSAGE };
69
+ case "SIGN_IN_REQUIRED":
70
+ return { code: kind, message: SIGN_IN_REQUIRED_MESSAGE };
71
+ case "RATE_LIMITED":
72
+ return { code: kind, message: RATE_LIMITED_MESSAGE };
73
+ default:
74
+ return null;
75
+ }
76
+ }
58
77
  export function toReadableSubtitleJobError(error) {
59
78
  const message = error instanceof Error ? error.message : String(error);
60
79
  return isCookieFailureText(message) ? COOKIE_JOB_MESSAGE : message;
61
80
  }
62
81
  async function ensureSubtitleCookiesReady() {
63
- if (hasSIDCookies(PATHS.cookiesTxt) && !areCookiesExpired(PATHS.cookiesTxt)) {
82
+ const hasSession = hasSIDCookies(PATHS.cookiesTxt);
83
+ const expired = areCookiesExpired(PATHS.cookiesTxt);
84
+ if (hasSession && !expired) {
64
85
  return { ok: true };
65
86
  }
66
87
  const refreshed = await tryRefreshSubtitleCookies();
@@ -69,9 +90,9 @@ async function ensureSubtitleCookiesReady() {
69
90
  }
70
91
  return {
71
92
  ok: false,
72
- code: "COOKIES_MISSING",
73
- toolMessage: COOKIE_MISSING_MESSAGE,
74
- jobMessage: COOKIE_JOB_MESSAGE,
93
+ code: hasSession && expired ? "COOKIES_EXPIRED" : "COOKIES_MISSING",
94
+ toolMessage: hasSession && expired ? COOKIE_EXPIRED_MESSAGE : COOKIE_MISSING_MESSAGE,
95
+ jobMessage: appendDiagnosticLog(COOKIE_JOB_MESSAGE, undefined),
75
96
  };
76
97
  }
77
98
  /**
@@ -244,16 +265,20 @@ async function downloadSubtitle(videoId, lang, format, options = {}) {
244
265
  outTemplate,
245
266
  options.sourceUrl ?? `https://www.youtube.com/watch?v=${videoId}`,
246
267
  ]);
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" };
268
+ const cookieFailure = result.exitCode !== 0 ? classifyYtDlpCookieFailure(result.stderr) : null;
269
+ if (cookieFailure) {
270
+ return {
271
+ ok: false,
272
+ cookieFailureCode: cookieFailure.code,
273
+ diagnosticLogPath: result.failureLogPath,
274
+ error: appendDiagnosticLog(cookieFailure.message, result.failureLogPath),
275
+ };
252
276
  }
253
277
  if (result.exitCode !== 0) {
254
278
  return {
255
279
  ok: false,
256
- error: result.stderr.slice(0, 500) || `yt-dlp exited with ${result.exitCode}`,
280
+ diagnosticLogPath: result.failureLogPath,
281
+ error: appendDiagnosticLog(result.stderr.slice(0, 500) || `yt-dlp exited with ${result.exitCode}`, result.failureLogPath),
257
282
  };
258
283
  }
259
284
  // Find the output file - yt-dlp appends lang and format extension.
@@ -335,7 +360,7 @@ export async function downloadSubtitlesForLanguages(input) {
335
360
  outputStem: `${lang}`,
336
361
  targetFile,
337
362
  });
338
- if (result.cookiesExpired) {
363
+ if (result.cookieFailureCode && result.cookieFailureCode !== "RATE_LIMITED") {
339
364
  const refreshed = await tryRefreshSubtitleCookies();
340
365
  if (refreshed) {
341
366
  result = await downloadSubtitle(input.videoId, lang, format, {
@@ -419,7 +444,7 @@ export function registerSubtitleTools(server, config, tokenManager) {
419
444
  }
420
445
  const lang = langs[i];
421
446
  const dl = await downloadSubtitle(videoId, lang, fmt);
422
- if (dl.cookiesExpired) {
447
+ if (dl.cookieFailureCode) {
423
448
  const refreshed = await tryRefreshSubtitleCookies();
424
449
  if (refreshed) {
425
450
  // Retry the download with fresh cookies
@@ -435,7 +460,14 @@ export function registerSubtitleTools(server, config, tokenManager) {
435
460
  }
436
461
  // Retry also failed with expired cookies — give up
437
462
  }
438
- return toolErr("COOKIES_EXPIRED", COOKIE_EXPIRED_MESSAGE);
463
+ return toolErr(dl.cookieFailureCode, dl.error ??
464
+ (dl.cookieFailureCode === "COOKIES_EXPIRED"
465
+ ? COOKIE_EXPIRED_MESSAGE
466
+ : dl.cookieFailureCode === "SIGN_IN_REQUIRED"
467
+ ? SIGN_IN_REQUIRED_MESSAGE
468
+ : dl.cookieFailureCode === "RATE_LIMITED"
469
+ ? RATE_LIMITED_MESSAGE
470
+ : 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,10 @@ export function registerSubtitleTools(server, config, tokenManager) {
522
554
  lastError = dl.error;
523
555
  }
524
556
  }
525
- if (cookiesExpired) {
557
+ if (cookieFailureCode === "RATE_LIMITED") {
558
+ return toolErr("RATE_LIMITED", RATE_LIMITED_MESSAGE);
559
+ }
560
+ if (cookieFailureCode) {
526
561
  const refreshed = await tryRefreshSubtitleCookies();
527
562
  if (refreshed) {
528
563
  // Retry this video's subtitles
@@ -535,11 +570,15 @@ export function registerSubtitleTools(server, config, tokenManager) {
535
570
  }
536
571
  }
537
572
  if (retrySuccess) {
538
- cookiesExpired = false;
573
+ cookieFailureCode = null;
539
574
  }
540
575
  }
541
- if (cookiesExpired) {
542
- return toolErr("COOKIES_EXPIRED", COOKIE_EXPIRED_MESSAGE);
576
+ if (cookieFailureCode) {
577
+ return toolErr(cookieFailureCode, cookieFailureCode === "COOKIES_EXPIRED"
578
+ ? COOKIE_EXPIRED_MESSAGE
579
+ : cookieFailureCode === "SIGN_IN_REQUIRED"
580
+ ? SIGN_IN_REQUIRED_MESSAGE
581
+ : COOKIE_INVALID_MESSAGE);
543
582
  }
544
583
  }
545
584
  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
  });
@@ -0,0 +1,3 @@
1
+ export type YtDlpFailureKind = "COOKIES_EXPIRED" | "COOKIES_INVALID" | "SIGN_IN_REQUIRED" | "RATE_LIMITED";
2
+ export declare function classifyYtDlpFailure(stderr: string): YtDlpFailureKind | null;
3
+ export declare function appendDiagnosticLog(message: string, logPath?: string): string;
@@ -0,0 +1,27 @@
1
+ export function classifyYtDlpFailure(stderr) {
2
+ const normalized = stderr.toLowerCase();
3
+ if (normalized.includes("cookies are no longer valid") ||
4
+ normalized.includes("cookie has expired") ||
5
+ normalized.includes("cookies have expired") ||
6
+ normalized.includes("session expired")) {
7
+ return "COOKIES_EXPIRED";
8
+ }
9
+ if (normalized.includes("rate-limited by youtube") ||
10
+ normalized.includes("current session has been rate-limited")) {
11
+ return "RATE_LIMITED";
12
+ }
13
+ if (normalized.includes("sign in to confirm you're not a bot") ||
14
+ normalized.includes("sign in to confirm you’re not a bot")) {
15
+ return "SIGN_IN_REQUIRED";
16
+ }
17
+ if (normalized.includes("sign in") ||
18
+ normalized.includes("login") ||
19
+ normalized.includes("cookies") ||
20
+ normalized.includes("not a bot")) {
21
+ return "COOKIES_INVALID";
22
+ }
23
+ return null;
24
+ }
25
+ export function appendDiagnosticLog(message, logPath) {
26
+ return logPath ? `${message}\nDiagnostic log: ${logPath}` : message;
27
+ }
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.13",
4
4
  "description": "Internal MCP client with local data tools and remote API proxy",
5
5
  "type": "module",
6
6
  "bin": {