@mkterswingman/5mghost-yonder 0.0.27 → 0.0.29

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.
Files changed (93) hide show
  1. package/dist/auth/oauthFlow.d.ts +9 -0
  2. package/dist/auth/oauthFlow.js +151 -0
  3. package/dist/auth/sharedAuth.d.ts +10 -0
  4. package/dist/auth/sharedAuth.js +31 -0
  5. package/dist/auth/tokenManager.d.ts +18 -0
  6. package/dist/auth/tokenManager.js +92 -0
  7. package/dist/cli/check.d.ts +4 -0
  8. package/dist/cli/check.js +90 -0
  9. package/dist/cli/index.d.ts +18 -0
  10. package/dist/cli/index.js +166 -0
  11. package/dist/cli/installSkills.d.ts +1 -0
  12. package/dist/cli/installSkills.js +39 -0
  13. package/dist/cli/runtime.d.ts +9 -0
  14. package/dist/cli/runtime.js +35 -0
  15. package/dist/cli/serve.d.ts +1 -0
  16. package/dist/cli/serve.js +26 -0
  17. package/dist/cli/setup.d.ts +33 -0
  18. package/dist/cli/setup.js +450 -0
  19. package/dist/cli/setupCookies.d.ts +88 -0
  20. package/dist/cli/setupCookies.js +440 -0
  21. package/dist/cli/smoke.d.ts +27 -0
  22. package/dist/cli/smoke.js +108 -0
  23. package/dist/cli/uninstall.d.ts +16 -0
  24. package/dist/cli/uninstall.js +99 -0
  25. package/dist/download/downloader.d.ts +67 -0
  26. package/dist/download/downloader.js +309 -0
  27. package/dist/download/jobManager.d.ts +22 -0
  28. package/dist/download/jobManager.js +211 -0
  29. package/dist/download/types.d.ts +44 -0
  30. package/dist/download/types.js +1 -0
  31. package/dist/runtime/ffmpegRuntime.d.ts +13 -0
  32. package/dist/runtime/ffmpegRuntime.js +51 -0
  33. package/dist/runtime/installers.d.ts +12 -0
  34. package/dist/runtime/installers.js +45 -0
  35. package/dist/runtime/manifest.d.ts +18 -0
  36. package/dist/runtime/manifest.js +43 -0
  37. package/dist/runtime/playwrightRuntime.d.ts +17 -0
  38. package/dist/runtime/playwrightRuntime.js +49 -0
  39. package/dist/runtime/systemDeps.d.ts +3 -0
  40. package/dist/runtime/systemDeps.js +30 -0
  41. package/dist/runtime/ytdlpRuntime.d.ts +14 -0
  42. package/dist/runtime/ytdlpRuntime.js +58 -0
  43. package/dist/server.d.ts +23 -0
  44. package/dist/server.js +81 -0
  45. package/dist/tools/downloads.d.ts +11 -0
  46. package/dist/tools/downloads.js +220 -0
  47. package/dist/tools/remote.d.ts +4 -0
  48. package/dist/tools/remote.js +239 -0
  49. package/dist/tools/subtitles.d.ts +29 -0
  50. package/dist/tools/subtitles.js +713 -0
  51. package/dist/utils/browserLaunch.d.ts +5 -0
  52. package/dist/utils/browserLaunch.js +22 -0
  53. package/dist/utils/browserProfileImport.d.ts +49 -0
  54. package/dist/utils/browserProfileImport.js +163 -0
  55. package/dist/utils/codexInternal.d.ts +9 -0
  56. package/dist/utils/codexInternal.js +60 -0
  57. package/dist/utils/config.d.ts +53 -0
  58. package/dist/utils/config.js +77 -0
  59. package/dist/utils/cookieRefresh.d.ts +18 -0
  60. package/dist/utils/cookieRefresh.js +70 -0
  61. package/dist/utils/cookies.d.ts +18 -0
  62. package/dist/utils/cookies.js +100 -0
  63. package/dist/utils/ffmpeg.d.ts +5 -0
  64. package/dist/utils/ffmpeg.js +16 -0
  65. package/dist/utils/ffmpegPath.d.ts +8 -0
  66. package/dist/utils/ffmpegPath.js +21 -0
  67. package/dist/utils/formatters.d.ts +4 -0
  68. package/dist/utils/formatters.js +42 -0
  69. package/dist/utils/launcher.d.ts +12 -0
  70. package/dist/utils/launcher.js +90 -0
  71. package/dist/utils/mcpRegistration.d.ts +7 -0
  72. package/dist/utils/mcpRegistration.js +23 -0
  73. package/dist/utils/mediaPaths.d.ts +7 -0
  74. package/dist/utils/mediaPaths.js +10 -0
  75. package/dist/utils/openClaw.d.ts +18 -0
  76. package/dist/utils/openClaw.js +82 -0
  77. package/dist/utils/skills.d.ts +16 -0
  78. package/dist/utils/skills.js +56 -0
  79. package/dist/utils/videoInput.d.ts +5 -0
  80. package/dist/utils/videoInput.js +58 -0
  81. package/dist/utils/videoMetadata.d.ts +11 -0
  82. package/dist/utils/videoMetadata.js +1 -0
  83. package/dist/utils/ytdlp.d.ts +28 -0
  84. package/dist/utils/ytdlp.js +205 -0
  85. package/dist/utils/ytdlpFailures.d.ts +3 -0
  86. package/dist/utils/ytdlpFailures.js +27 -0
  87. package/dist/utils/ytdlpPath.d.ts +33 -0
  88. package/dist/utils/ytdlpPath.js +77 -0
  89. package/dist/utils/ytdlpProgress.d.ts +13 -0
  90. package/dist/utils/ytdlpProgress.js +77 -0
  91. package/dist/utils/ytdlpScheduler.d.ts +25 -0
  92. package/dist/utils/ytdlpScheduler.js +78 -0
  93. package/package.json +2 -1
@@ -0,0 +1,35 @@
1
+ import { checkAll, installAll, updateAll } from "../runtime/installers.js";
2
+ export function formatRuntimeSummary(summary) {
3
+ const lines = [`Runtime ${summary.action} completed`];
4
+ for (const component of summary.components) {
5
+ const base = `${component.name}: ${component.status} (${component.source}`;
6
+ if (component.version) {
7
+ lines.push(`${base}, ${component.version})`);
8
+ }
9
+ else if (component.message) {
10
+ lines.push(`${base}) — ${component.message}`);
11
+ }
12
+ else {
13
+ lines.push(`${base})`);
14
+ }
15
+ }
16
+ return lines.join("\n");
17
+ }
18
+ export async function runRuntimeCommand(action, deps = {
19
+ installAll,
20
+ updateAll,
21
+ checkAll,
22
+ }) {
23
+ const summary = action === "install"
24
+ ? await deps.installAll()
25
+ : action === "update"
26
+ ? await deps.updateAll()
27
+ : await deps.checkAll();
28
+ return formatRuntimeSummary(summary);
29
+ }
30
+ export async function runRuntimeCli(action = "check") {
31
+ const normalized = action === "install" || action === "update" || action === "check"
32
+ ? action
33
+ : "check";
34
+ console.log(await runRuntimeCommand(normalized));
35
+ }
@@ -0,0 +1 @@
1
+ export declare function runServe(): Promise<void>;
@@ -0,0 +1,26 @@
1
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
2
+ import { loadConfig } from "../utils/config.js";
3
+ import { TokenManager } from "../auth/tokenManager.js";
4
+ import { DownloadJobManager } from "../download/jobManager.js";
5
+ import { createServer } from "../server.js";
6
+ export async function runServe() {
7
+ const config = loadConfig();
8
+ const tokenManager = new TokenManager(config.auth_url);
9
+ const downloadJobManager = new DownloadJobManager();
10
+ // PAT mode via env var (don't persist — just keep in memory for this session)
11
+ const pat = process.env.YT_MCP_TOKEN;
12
+ if (pat) {
13
+ await tokenManager.savePAT(pat);
14
+ }
15
+ const server = await createServer(config, tokenManager, downloadJobManager);
16
+ const transport = new StdioServerTransport();
17
+ await server.connect(transport);
18
+ process.on("SIGINT", async () => {
19
+ await server.close();
20
+ process.exit(0);
21
+ });
22
+ process.on("SIGTERM", async () => {
23
+ await server.close();
24
+ process.exit(0);
25
+ });
26
+ }
@@ -0,0 +1,33 @@
1
+ interface CliCommand {
2
+ file: string;
3
+ args: string[];
4
+ }
5
+ interface SetupCliCandidate {
6
+ bin: string;
7
+ label: string;
8
+ command: CliCommand;
9
+ }
10
+ export declare function buildSetupCliCandidates(launcherCommand: CliCommand): SetupCliCandidate[];
11
+ export declare function canOpenBrowserForEnv(input: {
12
+ platform: NodeJS.Platform;
13
+ env: NodeJS.ProcessEnv;
14
+ stdinIsTTY: boolean;
15
+ }): boolean;
16
+ type PromptQuestionFn = (question: string, signal: AbortSignal) => Promise<string>;
17
+ export interface PromptWithDefaultOptions {
18
+ defaultValue: string;
19
+ defaultLabel?: string;
20
+ timeoutMs?: number;
21
+ stdinIsTTY?: boolean;
22
+ log?: (message: string) => void;
23
+ questionFn?: PromptQuestionFn;
24
+ }
25
+ export declare const INSTALLER_OAUTH_TIMEOUT_MS = 180000;
26
+ export declare function getOAuthRecoveryHint(authUrl: string): string[];
27
+ export declare function getCookieSetupRecoveryHint(): string[];
28
+ export declare function promptWithDefault(question: string, options: PromptWithDefaultOptions): Promise<string>;
29
+ export declare function getNoBrowserSessionNotice(): string;
30
+ export declare function getNoBrowserPatHint(authUrl: string): string[];
31
+ export declare function getCookieSetupDeferredHint(): string[];
32
+ export declare function runSetup(): Promise<void>;
33
+ export {};
@@ -0,0 +1,450 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import { createInterface } from "node:readline/promises";
4
+ import { loadConfig, saveConfig, PATHS, ensureConfigDir } from "../utils/config.js";
5
+ import { TokenManager } from "../auth/tokenManager.js";
6
+ import { runOAuthFlow } from "../auth/oauthFlow.js";
7
+ import { hasSIDCookies } from "../utils/cookies.js";
8
+ import { buildLauncherCommand, writeLauncherFile } from "../utils/launcher.js";
9
+ import { buildBrowserOpenCommand } from "../utils/browserLaunch.js";
10
+ import { checkAll } from "../runtime/installers.js";
11
+ import { runInstallSkills } from "./installSkills.js";
12
+ import { getOpenClawConfigPath, isOpenClawInstallLikelyInstalled, writeOpenClawConfig, } from "../utils/openClaw.js";
13
+ import { getCodexInternalConfigPath, writeCodexInternalConfig, } from "../utils/codexInternal.js";
14
+ import { MCP_REGISTER_TIMEOUT_MS, classifyRegistrationFailure, } from "../utils/mcpRegistration.js";
15
+ function detectCli(name) {
16
+ try {
17
+ execFileSync(name, ["--version"], { stdio: "pipe" });
18
+ return true;
19
+ }
20
+ catch {
21
+ return false;
22
+ }
23
+ }
24
+ function tryRegisterMcp(cmd, label) {
25
+ try {
26
+ execFileSync(cmd.file, cmd.args, { stdio: "pipe", timeout: MCP_REGISTER_TIMEOUT_MS });
27
+ console.log(` ✅ MCP registered in ${label}`);
28
+ return true;
29
+ }
30
+ catch (err) {
31
+ const failure = classifyRegistrationFailure(err);
32
+ if (failure.kind === "already_exists") {
33
+ console.log(` ✅ MCP already registered in ${label}`);
34
+ return true;
35
+ }
36
+ if (failure.kind === "authentication") {
37
+ console.log(` ⚠️ ${label} is waiting for authentication — skipped auto-register.`);
38
+ return false;
39
+ }
40
+ if (failure.kind === "timeout") {
41
+ console.log(` ⚠️ ${label} registration timed out after ${MCP_REGISTER_TIMEOUT_MS}ms — skipped auto-register.`);
42
+ return false;
43
+ }
44
+ const summary = failure.output.replace(/\s+/g, " ").trim().slice(0, 240);
45
+ if (summary) {
46
+ console.log(` ⚠️ ${label} auto-register failed: ${summary}`);
47
+ }
48
+ else {
49
+ console.log(` ⚠️ ${label} auto-register failed with no diagnostic output.`);
50
+ }
51
+ return false;
52
+ }
53
+ }
54
+ export function buildSetupCliCandidates(launcherCommand) {
55
+ return [
56
+ { bin: "claude-internal", label: "Claude Code (internal)", command: { file: "claude-internal", args: ["mcp", "add", "-s", "user", "yt-mcp", "--", launcherCommand.file, ...launcherCommand.args] } },
57
+ { bin: "claude", label: "Claude Code", command: { file: "claude", args: ["mcp", "add", "-s", "user", "yt-mcp", "--", launcherCommand.file, ...launcherCommand.args] } },
58
+ { bin: "codex", label: "Codex CLI / Codex App", command: { file: "codex", args: ["mcp", "add", "yt-mcp", "--", launcherCommand.file, ...launcherCommand.args] } },
59
+ { bin: "gemini-internal", label: "Gemini CLI (internal)", command: { file: "gemini-internal", args: ["mcp", "add", "-s", "user", "yt-mcp", launcherCommand.file, ...launcherCommand.args] } },
60
+ { bin: "gemini", label: "Gemini CLI", command: { file: "gemini", args: ["mcp", "add", "-s", "user", "yt-mcp", launcherCommand.file, ...launcherCommand.args] } },
61
+ { bin: "opencode", label: "OpenCode", command: { file: "opencode", args: ["mcp", "add", "yt-mcp", "--", launcherCommand.file, ...launcherCommand.args] } },
62
+ ];
63
+ }
64
+ export function canOpenBrowserForEnv(input) {
65
+ if (input.env.YT_MCP_NO_BROWSER === "1") {
66
+ return false;
67
+ }
68
+ if (input.env.CODESPACES || input.env.GITPOD_WORKSPACE_ID || input.env.CLOUD_SHELL) {
69
+ return false;
70
+ }
71
+ if (input.platform === "linux") {
72
+ return Boolean(input.env.DISPLAY || input.env.WAYLAND_DISPLAY);
73
+ }
74
+ // Why: desktop Windows/macOS sessions can launch browsers even when the host app pipes stdio.
75
+ if (input.platform === "win32" || input.platform === "darwin") {
76
+ return true;
77
+ }
78
+ return input.stdinIsTTY;
79
+ }
80
+ function canOpenBrowser() {
81
+ return canOpenBrowserForEnv({
82
+ platform: process.platform,
83
+ env: process.env,
84
+ stdinIsTTY: Boolean(process.stdin.isTTY),
85
+ });
86
+ }
87
+ function openUrl(url) {
88
+ try {
89
+ const command = buildBrowserOpenCommand(url);
90
+ execFileSync(command.file, command.args, { stdio: "ignore" });
91
+ }
92
+ catch {
93
+ // Can't open — user will see the URL in console
94
+ }
95
+ }
96
+ export const INSTALLER_OAUTH_TIMEOUT_MS = 180_000;
97
+ export function getOAuthRecoveryHint(authUrl) {
98
+ return [
99
+ " 如果浏览器没有出现,或你之后想改用 PAT,可直接继续:",
100
+ ` PAT 登录页:${authUrl}/pat/login`,
101
+ " macOS / Linux: YT_MCP_TOKEN='pat_xxx' yt-mcp setup",
102
+ " Windows: $env:YT_MCP_TOKEN='pat_xxx'; yt-mcp setup",
103
+ ];
104
+ }
105
+ export function getCookieSetupRecoveryHint() {
106
+ return [
107
+ " 如果浏览器没有出现,或这次没完成 YouTube 登录,可稍后继续:",
108
+ " 重新执行:yt-mcp setup-cookies",
109
+ " 验证字幕:yt-mcp smoke --subtitles",
110
+ ];
111
+ }
112
+ function createPromptQuestionFn() {
113
+ return async (question, signal) => {
114
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
115
+ try {
116
+ return (await rl.question(question, { signal })).trim();
117
+ }
118
+ finally {
119
+ rl.close();
120
+ }
121
+ };
122
+ }
123
+ export async function promptWithDefault(question, options) {
124
+ const stdinIsTTY = options.stdinIsTTY ?? Boolean(process.stdin.isTTY);
125
+ const defaultLabel = options.defaultLabel ?? options.defaultValue;
126
+ const log = options.log ?? console.log;
127
+ if (!stdinIsTTY) {
128
+ log(` ℹ️ No interactive terminal detected; defaulting to ${defaultLabel}.`);
129
+ return options.defaultValue;
130
+ }
131
+ const questionFn = options.questionFn ?? createPromptQuestionFn();
132
+ const timeoutMs = options.timeoutMs ?? 15_000;
133
+ const controller = new AbortController();
134
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
135
+ try {
136
+ const answer = await questionFn(question, controller.signal);
137
+ return answer || options.defaultValue;
138
+ }
139
+ catch (err) {
140
+ if (err instanceof Error && err.name === "AbortError") {
141
+ log(` ℹ️ No input received within ${Math.round(timeoutMs / 1000)}s; defaulting to ${defaultLabel}.`);
142
+ return options.defaultValue;
143
+ }
144
+ throw err;
145
+ }
146
+ finally {
147
+ clearTimeout(timer);
148
+ }
149
+ }
150
+ function getRequestedAuthModeFromEnv(env) {
151
+ const value = env.YT_MCP_AUTH_MODE?.trim().toLowerCase();
152
+ if (value === "oauth" || value === "1") {
153
+ return "oauth";
154
+ }
155
+ if (value === "pat" || value === "2") {
156
+ return "pat";
157
+ }
158
+ return null;
159
+ }
160
+ export function getNoBrowserSessionNotice() {
161
+ return " ℹ️ 当前安装会话无法自动拉起浏览器 — 使用 PAT 模式\n";
162
+ }
163
+ export function getNoBrowserPatHint(authUrl) {
164
+ return [
165
+ " 🔗 请在当前这台运行安装器的桌面环境浏览器中打开此链接获取 PAT token:",
166
+ ` ${authUrl}/pat/login`,
167
+ "",
168
+ " ⚠️ 如果你在云桌面上运行,请在云桌面的浏览器中完成登录,不要在本地电脑打开。"
169
+ ];
170
+ }
171
+ export function getCookieSetupDeferredHint() {
172
+ return [
173
+ " ⏭️ Skipped (current session can't open a browser automatically — subtitle verification deferred)",
174
+ " 💡 在当前这台机器或云桌面的交互终端里运行:npx @mkterswingman/5mghost-yonder setup-cookies"
175
+ ];
176
+ }
177
+ export async function runSetup() {
178
+ const installerMode = process.env.YT_MCP_INSTALLER_MODE === "1";
179
+ const skipCookieStep = installerMode || process.env.YT_MCP_SETUP_SKIP_COOKIES === "1";
180
+ const quietSummary = installerMode || process.env.YT_MCP_SETUP_QUIET_SUMMARY === "1";
181
+ console.log("\n🚀 yt-mcp setup\n");
182
+ ensureConfigDir();
183
+ const hasBrowser = canOpenBrowser();
184
+ if (!hasBrowser) {
185
+ console.log(getNoBrowserSessionNotice());
186
+ }
187
+ // ── Step 1: Runtime check ──
188
+ console.log("Step 1/5: Checking required runtimes...");
189
+ const runtimeSummary = await checkAll();
190
+ for (const component of runtimeSummary.components) {
191
+ if (component.status === "installed") {
192
+ console.log(` ✅ ${component.name}: ${component.source}${component.version ? ` (${component.version})` : ""}`);
193
+ }
194
+ else {
195
+ console.log(` ⚠️ ${component.name}: missing${component.message ? ` — ${component.message}` : ""}`);
196
+ }
197
+ }
198
+ if (runtimeSummary.components.some((component) => component.status !== "installed")) {
199
+ console.log(" 💡 Install missing runtimes with: yt-mcp runtime install");
200
+ }
201
+ // ── Step 2: Config ──
202
+ console.log("Step 2/5: Initializing config...");
203
+ const config = loadConfig();
204
+ saveConfig(config);
205
+ writeLauncherFile();
206
+ console.log(` ✅ Config: ${PATHS.configJson}`);
207
+ console.log(` ✅ Launcher: ${PATHS.launcherJs}`);
208
+ console.log(` ✅ Shared auth: ${PATHS.sharedAuthJson}`);
209
+ // ── Step 3: Authentication ──
210
+ console.log("Step 3/5: Authentication...");
211
+ const tokenManager = new TokenManager(config.auth_url);
212
+ const pat = process.env.YT_MCP_TOKEN;
213
+ if (pat) {
214
+ // Explicit PAT provided via env var
215
+ await tokenManager.savePAT(pat);
216
+ console.log(` ✅ PAT saved from YT_MCP_TOKEN to shared auth`);
217
+ }
218
+ else if (hasBrowser) {
219
+ // Local or cloud desktop — let user choose auth method
220
+ console.log(" 请选择登录方式:");
221
+ console.log(" [1] OAuth 自动登录(推荐 — 自动弹出浏览器完成登录)");
222
+ console.log(" [2] PAT Token 登录(手动 — 在浏览器中生成 token 后粘贴)");
223
+ console.log("");
224
+ const forcedAuthMode = getRequestedAuthModeFromEnv(process.env);
225
+ const usePAT = forcedAuthMode
226
+ ? forcedAuthMode === "pat"
227
+ : (await promptWithDefault(" 请输入 1 或 2 (默认 1): ", {
228
+ defaultValue: "1",
229
+ defaultLabel: "1",
230
+ })) === "2";
231
+ if (usePAT) {
232
+ // User chose PAT
233
+ const patUrl = `${config.auth_url}/pat/login`;
234
+ console.log("");
235
+ console.log(` 🔗 Opening PAT login page: ${patUrl}`);
236
+ openUrl(patUrl);
237
+ console.log("");
238
+ console.log(" 请在浏览器中登录并生成 PAT token。");
239
+ const patInput = process.stdin.isTTY
240
+ ? await promptWithDefault(" 粘贴你的 PAT token (pat_xxx): ", {
241
+ defaultValue: "",
242
+ defaultLabel: "skip",
243
+ })
244
+ : "";
245
+ if (patInput) {
246
+ await tokenManager.savePAT(patInput);
247
+ console.log(" ✅ PAT saved to shared auth");
248
+ }
249
+ else {
250
+ console.log(" ⚠️ 未输入 token,稍后可通过环境变量配置:");
251
+ console.log(' "env": { "YT_MCP_TOKEN": "pat_xxx" }');
252
+ }
253
+ }
254
+ else {
255
+ // User chose OAuth (default)
256
+ const oauthTimeoutMs = installerMode ? INSTALLER_OAUTH_TIMEOUT_MS : 5 * 60 * 1000;
257
+ console.log("");
258
+ console.log(" 🌐 Opening browser for OAuth login...");
259
+ console.log(" ⚠️ 如果你在云桌面上运行,请在云桌面的浏览器中完成登录!");
260
+ console.log(" 在本地电脑打开链接将无法完成回调。");
261
+ for (const line of getOAuthRecoveryHint(config.auth_url)) {
262
+ console.log(line);
263
+ }
264
+ console.log(` OAuth 等待上限:${Math.round(oauthTimeoutMs / 1000)}s`);
265
+ try {
266
+ const tokens = await runOAuthFlow(config.auth_url, { timeoutMs: oauthTimeoutMs });
267
+ await tokenManager.saveTokens(tokens.accessToken, tokens.refreshToken, tokens.expiresIn, tokens.clientId);
268
+ console.log(" ✅ OAuth login successful");
269
+ console.log(" ℹ️ Other first-party local MCPs on this machine can reuse this login.");
270
+ }
271
+ catch (err) {
272
+ // OAuth failed — auto fallback to PAT
273
+ console.log(` ⚠️ OAuth failed: ${err instanceof Error ? err.message : String(err)}`);
274
+ for (const line of getOAuthRecoveryHint(config.auth_url)) {
275
+ console.log(line);
276
+ }
277
+ if (process.stdin.isTTY) {
278
+ console.log("");
279
+ console.log(" 📋 Falling back to PAT login...");
280
+ const patUrl = `${config.auth_url}/pat/login`;
281
+ console.log(` 🔗 Opening PAT login page: ${patUrl}`);
282
+ openUrl(patUrl);
283
+ console.log("");
284
+ }
285
+ else {
286
+ console.log(" ℹ️ 当前安装会话无交互输入,请拿到 PAT 后直接带 YT_MCP_TOKEN 重跑。");
287
+ }
288
+ const patInput = process.stdin.isTTY
289
+ ? await promptWithDefault(" 粘贴你的 PAT token (pat_xxx), 或直接回车跳过: ", {
290
+ defaultValue: "",
291
+ defaultLabel: "skip",
292
+ })
293
+ : "";
294
+ if (patInput) {
295
+ await tokenManager.savePAT(patInput);
296
+ console.log(" ✅ PAT saved to shared auth");
297
+ }
298
+ else {
299
+ console.log(" ⚠️ 稍后可通过环境变量配置:");
300
+ console.log(' "env": { "YT_MCP_TOKEN": "pat_xxx" }');
301
+ }
302
+ }
303
+ }
304
+ }
305
+ else {
306
+ // Cloud/headless — can't open browser, PAT only
307
+ for (const line of getNoBrowserPatHint(config.auth_url)) {
308
+ console.log(line);
309
+ }
310
+ const patInput = process.stdin.isTTY
311
+ ? await promptWithDefault(" 粘贴你的 PAT token (pat_xxx), 或直接回车跳过: ", {
312
+ defaultValue: "",
313
+ defaultLabel: "skip",
314
+ })
315
+ : "";
316
+ if (patInput) {
317
+ await tokenManager.savePAT(patInput);
318
+ console.log(" ✅ PAT saved to shared auth");
319
+ }
320
+ else {
321
+ console.log(" ⚠️ 稍后可通过环境变量配置:");
322
+ console.log(' "env": { "YT_MCP_TOKEN": "pat_xxx" }');
323
+ }
324
+ }
325
+ const hasSharedAuth = Boolean(pat) || existsSync(PATHS.sharedAuthJson);
326
+ // ── Step 4: YouTube Cookies ──
327
+ console.log("Step 4/5: YouTube cookies...");
328
+ if (!hasSharedAuth) {
329
+ console.log(" ℹ️ Deferred because authentication is not complete yet");
330
+ }
331
+ else if (skipCookieStep) {
332
+ console.log(" ℹ️ Deferred to installer cookie flow");
333
+ }
334
+ else if (!hasBrowser) {
335
+ for (const line of getCookieSetupDeferredHint()) {
336
+ console.log(line);
337
+ }
338
+ }
339
+ else if (hasSIDCookies(PATHS.cookiesTxt)) {
340
+ console.log(" ✅ Cookies already present");
341
+ }
342
+ else {
343
+ console.log(" 🌐 Opening browser for YouTube login...");
344
+ console.log(" ⚠️ 如果你在云桌面上运行,请在云桌面的浏览器窗口中登录 YouTube!");
345
+ console.log(" (Please log in to YouTube in the browser window)");
346
+ try {
347
+ const { runSetupCookies } = await import("./setupCookies.js");
348
+ await runSetupCookies();
349
+ console.log(" ✅ YouTube cookies saved");
350
+ }
351
+ catch (err) {
352
+ console.log(` ⚠️ Cookie setup skipped: ${err instanceof Error ? err.message : String(err)}`);
353
+ console.log(" 💡 Run later: npx @mkterswingman/5mghost-yonder setup-cookies");
354
+ }
355
+ }
356
+ // ── Step 5: MCP Registration ──
357
+ console.log("Step 5/5: Registering MCP in AI clients...");
358
+ const launcherCommand = buildLauncherCommand();
359
+ let registered = false;
360
+ let skillsInstalled = false;
361
+ for (const { bin, label, command } of buildSetupCliCandidates({
362
+ file: launcherCommand.command,
363
+ args: launcherCommand.args,
364
+ })) {
365
+ if (!detectCli(bin))
366
+ continue;
367
+ if (tryRegisterMcp(command, label)) {
368
+ registered = true;
369
+ }
370
+ }
371
+ if (isOpenClawInstallLikelyInstalled(detectCli)) {
372
+ try {
373
+ const status = writeOpenClawConfig("yt-mcp", launcherCommand);
374
+ const suffix = status === "created" ? "created" : "updated";
375
+ console.log(` ✅ MCP registered in OpenClaw (${suffix} mcporter.json)`);
376
+ registered = true;
377
+ }
378
+ catch (err) {
379
+ console.log(` ⚠️ OpenClaw auto-register failed: ${err instanceof Error ? err.message : String(err)}`);
380
+ }
381
+ }
382
+ if (detectCli("codex-internal")) {
383
+ try {
384
+ const status = writeCodexInternalConfig("yt-mcp", launcherCommand);
385
+ const suffix = status === "created" ? "created" : "updated";
386
+ console.log(` ✅ MCP registered in Codex CLI (internal) (${suffix} ${getCodexInternalConfigPath()})`);
387
+ registered = true;
388
+ }
389
+ catch (err) {
390
+ console.log(` ⚠️ Codex CLI (internal) auto-register failed: ${err instanceof Error ? err.message : String(err)}`);
391
+ }
392
+ }
393
+ try {
394
+ process.env.YT_MCP_INSTALL_OPENCLAW_SKILL = isOpenClawInstallLikelyInstalled(detectCli) ? "1" : "0";
395
+ await runInstallSkills();
396
+ skillsInstalled = true;
397
+ }
398
+ catch (err) {
399
+ console.log(` ⚠️ Bundled skill install failed: ${err instanceof Error ? err.message : String(err)}`);
400
+ }
401
+ if (!registered) {
402
+ console.log(" ℹ️ No supported CLI found. Add manually to your AI client:");
403
+ }
404
+ // Always print manual config
405
+ console.log("");
406
+ console.log(" MCP config (for manual setup / other clients):");
407
+ console.log(`
408
+ {
409
+ "mcpServers": {
410
+ "yt-mcp": {
411
+ "command": "node",
412
+ "args": [${JSON.stringify(PATHS.launcherJs)}, "serve"]
413
+ }
414
+ }
415
+ }
416
+ `);
417
+ console.log(` OpenClaw mcporter config: ${getOpenClawConfigPath()}`);
418
+ console.log(" OpenClaw manual config:");
419
+ console.log(`
420
+ {
421
+ "servers": {
422
+ "yt-mcp": {
423
+ "transport": "stdio",
424
+ "command": "node",
425
+ "args": [${JSON.stringify(PATHS.launcherJs)}, "serve"]
426
+ }
427
+ }
428
+ }
429
+ `);
430
+ console.log(` OpenClaw uses ${PATHS.sharedAuthJson} for PAT/JWT, so env.YT_MCP_TOKEN is optional after setup.`);
431
+ if (skillsInstalled) {
432
+ console.log(" ✅ Installed bundled yt-mcp analysis skill for detected AI clients");
433
+ }
434
+ console.log(" Media downloads:");
435
+ console.log(" - `start_download_job` / `poll_download_job` are job-based local tools");
436
+ console.log(" - Batch limit: 5 YouTube videos per job");
437
+ console.log(" - Output path: ~/Downloads/yt-mcp/YYYY-MM-DD_<video_id>");
438
+ console.log(" - `ffmpeg` is required for video download modes");
439
+ console.log("");
440
+ if (!quietSummary) {
441
+ console.log("✅ Setup complete!");
442
+ if (hasBrowser) {
443
+ console.log(' Open your AI client and try: "搜索 Python 教程"');
444
+ }
445
+ else {
446
+ console.log(" Set YT_MCP_TOKEN in your MCP env config, then restart your AI client.");
447
+ }
448
+ console.log("");
449
+ }
450
+ }
@@ -0,0 +1,88 @@
1
+ import { ensureConfigDir } from "../utils/config.js";
2
+ import { cleanupImportedBrowserWorkspace, findImportableBrowserProfileCandidates, prepareImportedBrowserWorkspace, type BrowserProfileCandidate } from "../utils/browserProfileImport.js";
3
+ export type PlaywrightCookie = {
4
+ name: string;
5
+ value: string;
6
+ domain: string;
7
+ path: string;
8
+ secure: boolean;
9
+ httpOnly: boolean;
10
+ expires: number;
11
+ };
12
+ /**
13
+ * Detect which browser channel is available on the system.
14
+ * Prefers Chrome → Edge → falls back to bundled Chromium.
15
+ */
16
+ export declare function detectBrowserChannel(chromium: typeof import("playwright").chromium): Promise<string>;
17
+ export declare const CHANNEL_LABELS: Record<string, string>;
18
+ /** Check if YouTube SID cookies are present — the real signal of a logged-in session. */
19
+ export declare function hasYouTubeSession(cookies: Array<{
20
+ name: string;
21
+ domain: string;
22
+ }>): boolean;
23
+ /**
24
+ * Save cookies to Netscape format file and close the browser context.
25
+ */
26
+ export declare function saveCookiesAndClose(context: {
27
+ close(): Promise<void>;
28
+ }, rawCookies: PlaywrightCookie[], silent?: boolean): Promise<void>;
29
+ /**
30
+ * Save cookies to disk WITHOUT closing the context (caller manages lifecycle).
31
+ */
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
+ export interface SetupCookiesOptions {
55
+ importOnly?: boolean;
56
+ headed?: boolean;
57
+ }
58
+ export declare class BrowserProfileLockedError extends Error {
59
+ constructor(message?: string);
60
+ }
61
+ type SetupCookiesChromium = Awaited<ReturnType<SetupCookiesDeps["loadChromium"]>>;
62
+ interface CdpCookie {
63
+ name: string;
64
+ value: string;
65
+ domain: string;
66
+ path: string;
67
+ secure?: boolean;
68
+ httpOnly?: boolean;
69
+ expires?: number;
70
+ }
71
+ interface CdpCookieClient {
72
+ send(method: "Network.getAllCookies"): Promise<{
73
+ cookies?: CdpCookie[];
74
+ }>;
75
+ }
76
+ export declare function readCdpCookiesUntilSession(client: CdpCookieClient, deps: Pick<SetupCookiesDeps, "hasYouTubeSession">, options?: {
77
+ attempts?: number;
78
+ delayMs?: number;
79
+ }): Promise<PlaywrightCookie[] | null>;
80
+ export declare function tryImportBrowserCookies(chromium: SetupCookiesChromium, deps: SetupCookiesDeps): Promise<boolean>;
81
+ export declare function readImportedBrowserCookies(candidate: BrowserProfileCandidate, chromium: SetupCookiesChromium, deps: SetupCookiesDeps): Promise<PlaywrightCookie[] | null>;
82
+ export declare function runManualCookieSetup(chromium: SetupCookiesChromium, deps: SetupCookiesDeps): Promise<void>;
83
+ /**
84
+ * Interactive cookie setup — opens a visible browser for user to log in.
85
+ */
86
+ export declare function parseSetupCookiesArgs(argv: string[]): SetupCookiesOptions;
87
+ export declare function runSetupCookies(overrides?: Partial<SetupCookiesDeps>, options?: SetupCookiesOptions): Promise<void>;
88
+ export {};