@mkterswingman/5mghost-yonder 0.0.2 → 0.0.4

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 (67) hide show
  1. package/README.md +44 -3
  2. package/dist/auth/sharedAuth.d.ts +10 -0
  3. package/dist/auth/sharedAuth.js +24 -0
  4. package/dist/auth/tokenManager.d.ts +10 -1
  5. package/dist/auth/tokenManager.js +14 -22
  6. package/dist/cli/check.js +6 -3
  7. package/dist/cli/index.d.ts +15 -1
  8. package/dist/cli/index.js +74 -31
  9. package/dist/cli/runtime.d.ts +9 -0
  10. package/dist/cli/runtime.js +35 -0
  11. package/dist/cli/serve.js +3 -1
  12. package/dist/cli/setup.d.ts +3 -0
  13. package/dist/cli/setup.js +84 -68
  14. package/dist/cli/setupCookies.js +2 -2
  15. package/dist/cli/smoke.d.ts +27 -0
  16. package/dist/cli/smoke.js +108 -0
  17. package/dist/cli/uninstall.d.ts +1 -0
  18. package/dist/cli/uninstall.js +67 -0
  19. package/dist/download/downloader.d.ts +64 -0
  20. package/dist/download/downloader.js +264 -0
  21. package/dist/download/jobManager.d.ts +21 -0
  22. package/dist/download/jobManager.js +198 -0
  23. package/dist/download/types.d.ts +43 -0
  24. package/dist/download/types.js +1 -0
  25. package/dist/runtime/ffmpegRuntime.d.ts +13 -0
  26. package/dist/runtime/ffmpegRuntime.js +51 -0
  27. package/dist/runtime/installers.d.ts +12 -0
  28. package/dist/runtime/installers.js +45 -0
  29. package/dist/runtime/manifest.d.ts +18 -0
  30. package/dist/runtime/manifest.js +43 -0
  31. package/dist/runtime/playwrightRuntime.d.ts +13 -0
  32. package/dist/runtime/playwrightRuntime.js +37 -0
  33. package/dist/runtime/systemDeps.d.ts +3 -0
  34. package/dist/runtime/systemDeps.js +30 -0
  35. package/dist/runtime/ytdlpRuntime.d.ts +14 -0
  36. package/dist/runtime/ytdlpRuntime.js +58 -0
  37. package/dist/server.d.ts +3 -1
  38. package/dist/server.js +4 -1
  39. package/dist/tools/downloads.d.ts +11 -0
  40. package/dist/tools/downloads.js +220 -0
  41. package/dist/tools/subtitles.d.ts +25 -0
  42. package/dist/tools/subtitles.js +135 -47
  43. package/dist/utils/config.d.ts +28 -0
  44. package/dist/utils/config.js +40 -11
  45. package/dist/utils/ffmpeg.d.ts +5 -0
  46. package/dist/utils/ffmpeg.js +16 -0
  47. package/dist/utils/ffmpegPath.d.ts +8 -0
  48. package/dist/utils/ffmpegPath.js +21 -0
  49. package/dist/utils/formatters.d.ts +4 -0
  50. package/dist/utils/formatters.js +42 -0
  51. package/dist/utils/mediaPaths.d.ts +7 -0
  52. package/dist/utils/mediaPaths.js +10 -0
  53. package/dist/utils/openClaw.d.ts +17 -0
  54. package/dist/utils/openClaw.js +79 -0
  55. package/dist/utils/videoInput.js +3 -0
  56. package/dist/utils/videoMetadata.d.ts +11 -0
  57. package/dist/utils/videoMetadata.js +1 -0
  58. package/dist/utils/ytdlp.d.ts +17 -1
  59. package/dist/utils/ytdlp.js +89 -2
  60. package/dist/utils/ytdlpPath.d.ts +9 -2
  61. package/dist/utils/ytdlpPath.js +19 -20
  62. package/dist/utils/ytdlpProgress.d.ts +13 -0
  63. package/dist/utils/ytdlpProgress.js +77 -0
  64. package/package.json +5 -3
  65. package/scripts/download-ytdlp.mjs +1 -1
  66. package/scripts/install.ps1 +9 -0
  67. package/scripts/install.sh +15 -0
package/dist/cli/setup.js CHANGED
@@ -1,18 +1,16 @@
1
- import { existsSync } from "node:fs";
2
- import { execSync } from "node:child_process";
1
+ import { execFileSync, execSync } from "node:child_process";
3
2
  import { createInterface } from "node:readline";
4
- import { join, dirname } from "node:path";
5
- import { fileURLToPath } from "node:url";
6
3
  import { loadConfig, saveConfig, PATHS, ensureConfigDir } from "../utils/config.js";
7
4
  import { TokenManager } from "../auth/tokenManager.js";
8
5
  import { runOAuthFlow } from "../auth/oauthFlow.js";
9
6
  import { hasSIDCookies } from "../utils/cookies.js";
10
- import { getYtDlpPath, getYtDlpVersion } from "../utils/ytdlpPath.js";
11
7
  import { buildLauncherCommand, writeLauncherFile } from "../utils/launcher.js";
8
+ import { checkAll } from "../runtime/installers.js";
9
+ import { getOpenClawConfigPath, isOpenClawInstallLikelyInstalled, writeOpenClawConfig, } from "../utils/openClaw.js";
12
10
  import { MCP_REGISTER_TIMEOUT_MS, classifyRegistrationFailure, } from "../utils/mcpRegistration.js";
13
11
  function detectCli(name) {
14
12
  try {
15
- execSync(`${name} --version`, { stdio: "pipe" });
13
+ execFileSync(name, ["--version"], { stdio: "pipe" });
16
14
  return true;
17
15
  }
18
16
  catch {
@@ -21,7 +19,7 @@ function detectCli(name) {
21
19
  }
22
20
  function tryRegisterMcp(cmd, label) {
23
21
  try {
24
- execSync(cmd, { stdio: "pipe", timeout: MCP_REGISTER_TIMEOUT_MS });
22
+ execFileSync(cmd.file, cmd.args, { stdio: "pipe", timeout: MCP_REGISTER_TIMEOUT_MS });
25
23
  console.log(` ✅ MCP registered in ${label}`);
26
24
  return true;
27
25
  }
@@ -42,15 +40,6 @@ function tryRegisterMcp(cmd, label) {
42
40
  return false;
43
41
  }
44
42
  }
45
- function quoteForShell(value) {
46
- return `'${value.replace(/'/g, `'\\''`)}'`;
47
- }
48
- function quoteForCmd(value) {
49
- return `"${value.replace(/"/g, '""')}"`;
50
- }
51
- function quoteArg(value) {
52
- return process.platform === "win32" ? quoteForCmd(value) : quoteForShell(value);
53
- }
54
43
  /**
55
44
  * Detect if we can open a browser (local machine with display).
56
45
  * Cloud environments (SSH, Docker, cloud IDE) typically can't.
@@ -96,47 +85,44 @@ function prompt(question) {
96
85
  });
97
86
  });
98
87
  }
88
+ export function getNoBrowserSessionNotice() {
89
+ return " ℹ️ 当前安装会话无法自动拉起浏览器 — 使用 PAT 模式\n";
90
+ }
91
+ export function getNoBrowserPatHint(authUrl) {
92
+ return [
93
+ " 🔗 请在当前这台运行安装器的桌面环境浏览器中打开此链接获取 PAT token:",
94
+ ` ${authUrl}/pat/login`,
95
+ "",
96
+ " ⚠️ 如果你在云桌面上运行,请在云桌面的浏览器中完成登录,不要在本地电脑打开。"
97
+ ];
98
+ }
99
+ export function getCookieSetupDeferredHint() {
100
+ return [
101
+ " ⏭️ Skipped (current session can't open a browser automatically — subtitle verification deferred)",
102
+ " 💡 在当前这台机器或云桌面的交互终端里运行:npx @mkterswingman/5mghost-yonder setup-cookies"
103
+ ];
104
+ }
99
105
  export async function runSetup() {
100
106
  console.log("\n🚀 yt-mcp setup\n");
101
107
  ensureConfigDir();
102
108
  const hasBrowser = canOpenBrowser();
103
109
  if (!hasBrowser) {
104
- console.log(" ℹ️ Cloud/headless environment detected — using PAT mode\n");
105
- }
106
- // ── Step 1: Check yt-dlp ──
107
- console.log("Step 1/5: Checking subtitle engine...");
108
- const ytdlpPath = getYtDlpPath();
109
- const bundledExists = existsSync(ytdlpPath) && ytdlpPath !== "yt-dlp";
110
- const ytdlpInfo = getYtDlpVersion();
111
- if (ytdlpInfo) {
112
- console.log(` ✅ Subtitle engine ready`);
110
+ console.log(getNoBrowserSessionNotice());
113
111
  }
114
- else if (bundledExists) {
115
- // Binary exists but execFileSync("--version") timed out.
116
- // Cause: macOS Gatekeeper network verification on first run (~15-20s).
117
- console.log(" ✅ yt-dlp ready");
118
- }
119
- else {
120
- // Binary truly missing — download it
121
- process.stdout.write(" ⏳ Preparing subtitle engine...");
122
- try {
123
- execSync("node scripts/download-ytdlp.mjs", {
124
- cwd: join(dirname(fileURLToPath(import.meta.url)), "..", ".."),
125
- stdio: "pipe",
126
- env: { ...process.env, YT_MCP_QUIET: "1" },
127
- });
128
- const retryInfo = getYtDlpVersion();
129
- if (retryInfo) {
130
- console.log(`\r ✅ Subtitle engine ready `);
131
- }
132
- else {
133
- console.log(`\r ✅ Subtitle engine ready `);
134
- }
112
+ // ── Step 1: Runtime check ──
113
+ console.log("Step 1/5: Checking required runtimes...");
114
+ const runtimeSummary = await checkAll();
115
+ for (const component of runtimeSummary.components) {
116
+ if (component.status === "installed") {
117
+ console.log(` ✅ ${component.name}: ${component.source}${component.version ? ` (${component.version})` : ""}`);
135
118
  }
136
- catch {
137
- console.log(`\r ⚠️ Subtitle engine setup failedsubtitles will be unavailable`);
119
+ else {
120
+ console.log(` ⚠️ ${component.name}: missing${component.message ? `${component.message}` : ""}`);
138
121
  }
139
122
  }
123
+ if (runtimeSummary.components.some((component) => component.status !== "installed")) {
124
+ console.log(" 💡 Install missing runtimes with: yt-mcp runtime install");
125
+ }
140
126
  // ── Step 2: Config ──
141
127
  console.log("Step 2/5: Initializing config...");
142
128
  const config = loadConfig();
@@ -144,6 +130,7 @@ export async function runSetup() {
144
130
  writeLauncherFile();
145
131
  console.log(` ✅ Config: ${PATHS.configJson}`);
146
132
  console.log(` ✅ Launcher: ${PATHS.launcherJs}`);
133
+ console.log(` ✅ Shared auth: ${PATHS.sharedAuthJson}`);
147
134
  // ── Step 3: Authentication ──
148
135
  console.log("Step 3/5: Authentication...");
149
136
  const tokenManager = new TokenManager(config.auth_url);
@@ -151,7 +138,7 @@ export async function runSetup() {
151
138
  if (pat) {
152
139
  // Explicit PAT provided via env var
153
140
  await tokenManager.savePAT(pat);
154
- console.log(" ✅ PAT saved from YT_MCP_TOKEN");
141
+ console.log(` ✅ PAT saved from YT_MCP_TOKEN to shared auth`);
155
142
  }
156
143
  else if (hasBrowser) {
157
144
  // Local or cloud desktop — let user choose auth method
@@ -172,7 +159,7 @@ export async function runSetup() {
172
159
  const patInput = await prompt(" 粘贴你的 PAT token (pat_xxx): ");
173
160
  if (patInput) {
174
161
  await tokenManager.savePAT(patInput);
175
- console.log(" ✅ PAT saved");
162
+ console.log(" ✅ PAT saved to shared auth");
176
163
  }
177
164
  else {
178
165
  console.log(" ⚠️ 未输入 token,稍后可通过环境变量配置:");
@@ -189,6 +176,7 @@ export async function runSetup() {
189
176
  const tokens = await runOAuthFlow(config.auth_url);
190
177
  await tokenManager.saveTokens(tokens.accessToken, tokens.refreshToken, tokens.expiresIn, tokens.clientId);
191
178
  console.log(" ✅ OAuth login successful");
179
+ console.log(" ℹ️ Other first-party local MCPs on this machine can reuse this login.");
192
180
  }
193
181
  catch (err) {
194
182
  // OAuth failed — auto fallback to PAT
@@ -202,7 +190,7 @@ export async function runSetup() {
202
190
  const patInput = await prompt(" 粘贴你的 PAT token (pat_xxx), 或直接回车跳过: ");
203
191
  if (patInput) {
204
192
  await tokenManager.savePAT(patInput);
205
- console.log(" ✅ PAT saved");
193
+ console.log(" ✅ PAT saved to shared auth");
206
194
  }
207
195
  else {
208
196
  console.log(" ⚠️ 稍后可通过环境变量配置:");
@@ -213,14 +201,13 @@ export async function runSetup() {
213
201
  }
214
202
  else {
215
203
  // Cloud/headless — can't open browser, PAT only
216
- const patUrl = `${config.auth_url}/pat/login`;
217
- console.log(` 🔗 请在本地电脑浏览器中打开此链接获取 PAT token:`);
218
- console.log(` ${patUrl}`);
219
- console.log("");
204
+ for (const line of getNoBrowserPatHint(config.auth_url)) {
205
+ console.log(line);
206
+ }
220
207
  const patInput = await prompt(" 粘贴你的 PAT token (pat_xxx), 或直接回车跳过: ");
221
208
  if (patInput) {
222
209
  await tokenManager.savePAT(patInput);
223
- console.log(" ✅ PAT saved");
210
+ console.log(" ✅ PAT saved to shared auth");
224
211
  }
225
212
  else {
226
213
  console.log(" ⚠️ 稍后可通过环境变量配置:");
@@ -230,8 +217,9 @@ export async function runSetup() {
230
217
  // ── Step 4: YouTube Cookies ──
231
218
  console.log("Step 4/5: YouTube cookies...");
232
219
  if (!hasBrowser) {
233
- console.log(" ⏭️ Skipped (no browser subtitle features unavailable in cloud)");
234
- console.log(" 💡 Run on your local machine: npx @mkterswingman/5mghost-yonder setup-cookies");
220
+ for (const line of getCookieSetupDeferredHint()) {
221
+ console.log(line);
222
+ }
235
223
  }
236
224
  else if (hasSIDCookies(PATHS.cookiesTxt)) {
237
225
  console.log(" ✅ Cookies already present");
@@ -253,35 +241,32 @@ export async function runSetup() {
253
241
  // ── Step 5: MCP Registration ──
254
242
  console.log("Step 5/5: Registering MCP in AI clients...");
255
243
  const launcherCommand = buildLauncherCommand();
256
- const mcpArgs = [launcherCommand.command, ...launcherCommand.args].map(quoteArg).join(" ");
257
244
  let registered = false;
258
245
  const cliCandidates = [
259
246
  // Claude Code: {bin} mcp add yt-mcp -- npx ... serve
260
247
  { bin: "claude-internal", label: "Claude Code (internal)",
261
- cmd: (b, a) => `${b} mcp add -s user yt-mcp -- ${a}` },
248
+ cmd: (b, launcher) => ({ file: b, args: ["mcp", "add", "-s", "user", "yt-mcp", "--", launcher.command, ...launcher.args] }) },
262
249
  { bin: "claude", label: "Claude Code",
263
- cmd: (b, a) => `${b} mcp add -s user yt-mcp -- ${a}` },
250
+ cmd: (b, launcher) => ({ file: b, args: ["mcp", "add", "-s", "user", "yt-mcp", "--", launcher.command, ...launcher.args] }) },
264
251
  // Codex (public): {bin} mcp add yt-mcp -- npx ... serve
265
252
  { bin: "codex", label: "Codex CLI / Codex App",
266
- cmd: (b, a) => `${b} mcp add yt-mcp -- ${a}` },
253
+ cmd: (b, launcher) => ({ file: b, args: ["mcp", "add", "yt-mcp", "--", launcher.command, ...launcher.args] }) },
267
254
  // Codex-internal doesn't support mcp add — needs manual config
268
255
  { bin: "codex-internal", label: "Codex CLI (internal)",
269
256
  cmd: () => null },
270
257
  // Gemini: {bin} mcp add -s user yt-mcp node <launcher> serve (no --)
271
258
  { bin: "gemini-internal", label: "Gemini CLI (internal)",
272
- cmd: (b, a) => `${b} mcp add -s user yt-mcp ${a}` },
259
+ cmd: (b, launcher) => ({ file: b, args: ["mcp", "add", "-s", "user", "yt-mcp", launcher.command, ...launcher.args] }) },
273
260
  { bin: "gemini", label: "Gemini CLI",
274
- cmd: (b, a) => `${b} mcp add -s user yt-mcp ${a}` },
261
+ cmd: (b, launcher) => ({ file: b, args: ["mcp", "add", "-s", "user", "yt-mcp", launcher.command, ...launcher.args] }) },
275
262
  // Others: assume Claude-style syntax
276
263
  { bin: "opencode", label: "OpenCode",
277
- cmd: (b, a) => `${b} mcp add yt-mcp -- ${a}` },
278
- { bin: "openclaw", label: "OpenClaw",
279
- cmd: (b, a) => `${b} mcp add yt-mcp -- ${a}` },
264
+ cmd: (b, launcher) => ({ file: b, args: ["mcp", "add", "yt-mcp", "--", launcher.command, ...launcher.args] }) },
280
265
  ];
281
266
  for (const { bin, label, cmd } of cliCandidates) {
282
267
  if (!detectCli(bin))
283
268
  continue;
284
- const command = cmd(bin, mcpArgs);
269
+ const command = cmd(bin, launcherCommand);
285
270
  if (!command) {
286
271
  // CLI detected but doesn't support auto-registration
287
272
  console.log(` ⚠️ ${label} detected but requires manual MCP config.`);
@@ -291,6 +276,17 @@ export async function runSetup() {
291
276
  registered = true;
292
277
  }
293
278
  }
279
+ if (isOpenClawInstallLikelyInstalled(detectCli)) {
280
+ try {
281
+ const status = writeOpenClawConfig("yt-mcp", launcherCommand);
282
+ const suffix = status === "created" ? "created" : "updated";
283
+ console.log(` ✅ MCP registered in OpenClaw (${suffix} mcporter.json)`);
284
+ registered = true;
285
+ }
286
+ catch (err) {
287
+ console.log(` ⚠️ OpenClaw auto-register failed: ${err instanceof Error ? err.message : String(err)}`);
288
+ }
289
+ }
294
290
  if (!registered) {
295
291
  console.log(" ℹ️ No supported CLI found. Add manually to your AI client:");
296
292
  }
@@ -307,6 +303,26 @@ export async function runSetup() {
307
303
  }
308
304
  }
309
305
  `);
306
+ console.log(` OpenClaw mcporter config: ${getOpenClawConfigPath()}`);
307
+ console.log(" OpenClaw manual config:");
308
+ console.log(`
309
+ {
310
+ "servers": {
311
+ "yt-mcp": {
312
+ "transport": "stdio",
313
+ "command": "node",
314
+ "args": [${JSON.stringify(PATHS.launcherJs)}, "serve"]
315
+ }
316
+ }
317
+ }
318
+ `);
319
+ console.log(` OpenClaw uses ${PATHS.sharedAuthJson} for PAT/JWT, so env.YT_MCP_TOKEN is optional after setup.`);
320
+ console.log(" Media downloads:");
321
+ console.log(" - `start_download_job` / `poll_download_job` are job-based local tools");
322
+ console.log(" - Batch limit: 5 YouTube videos per job");
323
+ console.log(" - Output path: ~/Downloads/yt-mcp/YYYY-MM-DD_<video_id>");
324
+ console.log(" - `ffmpeg` is required for video download modes");
325
+ console.log("");
310
326
  console.log("✅ Setup complete!");
311
327
  if (hasBrowser) {
312
328
  console.log(' Open your AI client and try: "搜索 Python 教程"');
@@ -104,7 +104,7 @@ export async function runSetupCookies() {
104
104
  chromium = pw.chromium;
105
105
  }
106
106
  catch {
107
- throw new Error("Playwright is not installed.\nRun: npm install playwright");
107
+ throw new Error("Playwright runtime is not installed.\nRun: yt-mcp runtime install");
108
108
  }
109
109
  const channel = await detectBrowserChannel(chromium);
110
110
  console.log(`Using browser: ${CHANNEL_LABELS[channel] ?? channel}`);
@@ -123,7 +123,7 @@ export async function runSetupCookies() {
123
123
  catch (err) {
124
124
  const msg = err instanceof Error ? err.message : String(err);
125
125
  if (channel === "chromium" && msg.includes("Executable doesn't exist")) {
126
- throw new Error("Chromium browser not found.\nRun: npx playwright install chromium");
126
+ throw new Error("Chromium browser runtime not found.\nRun: yt-mcp runtime install");
127
127
  }
128
128
  throw err;
129
129
  }
@@ -0,0 +1,27 @@
1
+ export interface SmokeOptions {
2
+ subtitles: boolean;
3
+ }
4
+ export interface SmokeSummary {
5
+ remote: {
6
+ video_id: string;
7
+ title: string;
8
+ };
9
+ subtitles?: {
10
+ valid: true;
11
+ };
12
+ }
13
+ interface SmokeClient {
14
+ listTools(): Promise<{
15
+ tools: Array<{
16
+ name: string;
17
+ }>;
18
+ }>;
19
+ callTool(params: {
20
+ name: string;
21
+ arguments: Record<string, unknown>;
22
+ }): Promise<Record<string, unknown>>;
23
+ }
24
+ export declare function parseSmokeArgs(argv: string[]): SmokeOptions;
25
+ export declare function runSmokeChecks(client: SmokeClient, options: SmokeOptions): Promise<SmokeSummary>;
26
+ export declare function runSmoke(argv: string[]): Promise<void>;
27
+ export {};
@@ -0,0 +1,108 @@
1
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
3
+ function toSmokeToolResult(value) {
4
+ return value;
5
+ }
6
+ export function parseSmokeArgs(argv) {
7
+ return {
8
+ subtitles: argv.includes("--subtitles"),
9
+ };
10
+ }
11
+ function extractPayload(result) {
12
+ if (result.structuredContent && typeof result.structuredContent === "object") {
13
+ return result.structuredContent;
14
+ }
15
+ const text = result.content?.find((item) => item.type === "text" && typeof item.text === "string")?.text;
16
+ if (!text) {
17
+ throw new Error("Tool returned no readable payload");
18
+ }
19
+ try {
20
+ return JSON.parse(text);
21
+ }
22
+ catch {
23
+ throw new Error(`Tool returned non-JSON text: ${text.slice(0, 200)}`);
24
+ }
25
+ }
26
+ function requireTool(tools, name) {
27
+ if (!tools.some((tool) => tool.name === name)) {
28
+ throw new Error(`Required tool not registered: ${name}`);
29
+ }
30
+ }
31
+ export async function runSmokeChecks(client, options) {
32
+ const listed = await client.listTools();
33
+ requireTool(listed.tools, "search_videos");
34
+ if (options.subtitles) {
35
+ requireTool(listed.tools, "validate_cookies");
36
+ }
37
+ const remoteResult = await client.callTool({
38
+ name: "search_videos",
39
+ arguments: { query: "OpenAI", max_results: 1 },
40
+ });
41
+ const remotePayload = extractPayload(remoteResult);
42
+ const remoteItems = Array.isArray(remotePayload.items) ? remotePayload.items : [];
43
+ const firstItem = remoteItems[0];
44
+ if (!firstItem || typeof firstItem !== "object") {
45
+ throw new Error("Remote smoke returned no videos");
46
+ }
47
+ const videoId = typeof firstItem.video_id === "string" ? firstItem.video_id : null;
48
+ const title = typeof firstItem.title === "string" ? firstItem.title : null;
49
+ if (!videoId || !title) {
50
+ throw new Error("Remote smoke returned malformed video data");
51
+ }
52
+ const summary = {
53
+ remote: {
54
+ video_id: videoId,
55
+ title,
56
+ },
57
+ };
58
+ if (options.subtitles) {
59
+ const subtitleResult = await client.callTool({
60
+ name: "validate_cookies",
61
+ arguments: {},
62
+ });
63
+ const subtitlePayload = extractPayload(subtitleResult);
64
+ if (subtitlePayload.valid !== true) {
65
+ const error = typeof subtitlePayload.error === "string"
66
+ ? subtitlePayload.error
67
+ : "Subtitle validation failed";
68
+ throw new Error(error);
69
+ }
70
+ summary.subtitles = { valid: true };
71
+ }
72
+ return summary;
73
+ }
74
+ export async function runSmoke(argv) {
75
+ const options = parseSmokeArgs(argv);
76
+ const stderrChunks = [];
77
+ const transport = new StdioClientTransport({
78
+ command: process.execPath,
79
+ args: [process.argv[1], "serve"],
80
+ stderr: "pipe",
81
+ env: Object.fromEntries(Object.entries(process.env).filter((entry) => typeof entry[1] === "string")),
82
+ });
83
+ if (transport.stderr) {
84
+ transport.stderr.on("data", (chunk) => {
85
+ stderrChunks.push(String(chunk));
86
+ });
87
+ }
88
+ const client = new Client({ name: "yt-mcp-smoke", version: "1.0.0" }, { capabilities: {} });
89
+ try {
90
+ await client.connect(transport);
91
+ const summary = await runSmokeChecks({
92
+ listTools: () => client.listTools(),
93
+ callTool: async (params) => toSmokeToolResult(await client.callTool(params)),
94
+ }, options);
95
+ console.log(`✅ MCP smoke passed: ${summary.remote.video_id} — ${summary.remote.title}`);
96
+ if (summary.subtitles?.valid) {
97
+ console.log("✅ Subtitle smoke passed via validate_cookies");
98
+ }
99
+ }
100
+ catch (error) {
101
+ const message = error instanceof Error ? error.message : String(error);
102
+ const stderr = stderrChunks.join("").trim();
103
+ throw new Error(stderr ? `${message}\n${stderr}` : message);
104
+ }
105
+ finally {
106
+ await transport.close().catch(() => undefined);
107
+ }
108
+ }
@@ -0,0 +1 @@
1
+ export declare function runUninstall(): Promise<void>;
@@ -0,0 +1,67 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { existsSync, rmSync } from "node:fs";
3
+ import { PATHS } from "../utils/config.js";
4
+ import { getOpenClawConfigPath, removeOpenClawConfig } from "../utils/openClaw.js";
5
+ function detectCli(name) {
6
+ try {
7
+ execFileSync(name, ["--version"], { stdio: "pipe" });
8
+ return true;
9
+ }
10
+ catch {
11
+ return false;
12
+ }
13
+ }
14
+ function tryRemoveMcp(command, label) {
15
+ try {
16
+ execFileSync(command.file, command.args, { stdio: "pipe" });
17
+ console.log(` ✅ Removed MCP registration from ${label}`);
18
+ return "removed";
19
+ }
20
+ catch (err) {
21
+ const details = err && typeof err === "object" && "stderr" in err
22
+ ? String(err.stderr ?? "")
23
+ : "";
24
+ const lower = details.toLowerCase();
25
+ if (lower.includes("not found") || lower.includes("no server") || lower.includes("unknown")) {
26
+ console.log(` ℹ️ ${label} did not have yt-mcp registered`);
27
+ return "missing";
28
+ }
29
+ console.log(` ⚠️ Failed to remove MCP registration from ${label}`);
30
+ return "failed";
31
+ }
32
+ }
33
+ export async function runUninstall() {
34
+ console.log("\n🧹 yt-mcp uninstall\n");
35
+ console.log("Removing MCP client registrations...");
36
+ const cliCandidates = [
37
+ { bin: "claude-internal", label: "Claude Code (internal)", command: { file: "claude-internal", args: ["mcp", "remove", "-s", "user", "yt-mcp"] } },
38
+ { bin: "claude", label: "Claude Code", command: { file: "claude", args: ["mcp", "remove", "-s", "user", "yt-mcp"] } },
39
+ { bin: "codex", label: "Codex CLI / Codex App", command: { file: "codex", args: ["mcp", "remove", "yt-mcp"] } },
40
+ { bin: "gemini-internal", label: "Gemini CLI (internal)", command: { file: "gemini-internal", args: ["mcp", "remove", "-s", "user", "yt-mcp"] } },
41
+ { bin: "gemini", label: "Gemini CLI", command: { file: "gemini", args: ["mcp", "remove", "-s", "user", "yt-mcp"] } },
42
+ { bin: "opencode", label: "OpenCode", command: { file: "opencode", args: ["mcp", "remove", "yt-mcp"] } },
43
+ ];
44
+ for (const candidate of cliCandidates) {
45
+ if (!detectCli(candidate.bin) || !candidate.command)
46
+ continue;
47
+ tryRemoveMcp(candidate.command, candidate.label);
48
+ }
49
+ const openClawStatus = removeOpenClawConfig("yt-mcp");
50
+ if (openClawStatus === "removed") {
51
+ console.log(` ✅ Removed MCP registration from OpenClaw (${getOpenClawConfigPath()})`);
52
+ }
53
+ else {
54
+ console.log(" ℹ️ OpenClaw did not have yt-mcp registered");
55
+ }
56
+ if (existsSync(PATHS.configDir)) {
57
+ // Why: ~/.yt-mcp contains launcher, token cache, cookies, and npm cache owned by this package.
58
+ rmSync(PATHS.configDir, { recursive: true, force: true });
59
+ console.log(` ✅ Removed local yt-mcp config: ${PATHS.configDir}`);
60
+ }
61
+ else {
62
+ console.log(` ℹ️ Local yt-mcp config already absent: ${PATHS.configDir}`);
63
+ }
64
+ console.log(` ℹ️ Preserved downloaded media: ${PATHS.subtitlesDir}`);
65
+ console.log(" ℹ️ If you installed globally, remove the package with: npm uninstall -g @mkterswingman/5mghost-yonder");
66
+ console.log("");
67
+ }
@@ -0,0 +1,64 @@
1
+ import type { DownloadJobManager } from "./jobManager.js";
2
+ import type { DownloadJobItemSnapshot, DownloadMode } from "./types.js";
3
+ import type { PreflightVideoMetadata } from "../utils/videoMetadata.js";
4
+ type DownloadingStep = "download_video" | "download_subtitles" | "merge";
5
+ export interface DownloadOneItemProgressUpdate {
6
+ step: DownloadingStep;
7
+ progressPercent: number | null;
8
+ }
9
+ export interface DownloadOneItemResult {
10
+ video_file: string | null;
11
+ metadata_file: string;
12
+ final_file_size: string | null;
13
+ subtitle_files: Record<string, string[]>;
14
+ metadata: PreflightVideoMetadata;
15
+ }
16
+ export interface DownloadOneItemInput {
17
+ item: Pick<DownloadJobItemSnapshot, "video_id" | "source_url" | "title" | "output_dir">;
18
+ mode: DownloadMode;
19
+ videoQuality?: string | null;
20
+ subtitleFormats: string[];
21
+ subtitleLanguages?: string[];
22
+ defaultSubtitleLanguages?: string[];
23
+ jobManager?: Pick<DownloadJobManager, "startItem" | "updateItemProgress" | "completeItem" | "failItem">;
24
+ jobId?: string;
25
+ deps?: Partial<DownloadOneItemDeps>;
26
+ }
27
+ interface DownloadOneItemDeps {
28
+ hasYtDlp: () => boolean;
29
+ hasFfmpeg: () => boolean;
30
+ fetchMetadata: (input: {
31
+ sourceUrl: string;
32
+ videoId: string;
33
+ title: string;
34
+ }) => Promise<PreflightVideoMetadata>;
35
+ downloadVideo: (input: {
36
+ sourceUrl: string;
37
+ outputFile: string;
38
+ videoQuality: string;
39
+ onProgress: (update: DownloadOneItemProgressUpdate) => void;
40
+ }) => Promise<string>;
41
+ downloadSubtitles: (input: {
42
+ videoId: string;
43
+ sourceUrl: string;
44
+ subtitlesDir: string;
45
+ formats: string[];
46
+ languages: string[];
47
+ skipMissingLanguages: boolean;
48
+ onProgress: (update: DownloadOneItemProgressUpdate) => void;
49
+ }) => Promise<Record<string, string[]>>;
50
+ mkdir: (path: string, options: {
51
+ recursive: true;
52
+ }) => Promise<void>;
53
+ rm: (path: string, options: {
54
+ recursive: true;
55
+ force: true;
56
+ }) => Promise<void>;
57
+ stat: (path: string) => Promise<{
58
+ size: number;
59
+ }>;
60
+ writeMetadata: (path: string, contents: string) => Promise<void>;
61
+ }
62
+ export declare function downloadOneItem(input: DownloadOneItemInput): Promise<DownloadOneItemResult>;
63
+ export declare function resolveYtDlpOutputPath(expectedPath: string, stdout: string): string;
64
+ export {};