@mkterswingman/5mghost-yonder 0.0.2 → 0.0.3

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 (66) 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.js +60 -61
  13. package/dist/cli/setupCookies.js +2 -2
  14. package/dist/cli/smoke.d.ts +27 -0
  15. package/dist/cli/smoke.js +108 -0
  16. package/dist/cli/uninstall.d.ts +1 -0
  17. package/dist/cli/uninstall.js +67 -0
  18. package/dist/download/downloader.d.ts +64 -0
  19. package/dist/download/downloader.js +264 -0
  20. package/dist/download/jobManager.d.ts +21 -0
  21. package/dist/download/jobManager.js +198 -0
  22. package/dist/download/types.d.ts +43 -0
  23. package/dist/download/types.js +1 -0
  24. package/dist/runtime/ffmpegRuntime.d.ts +13 -0
  25. package/dist/runtime/ffmpegRuntime.js +51 -0
  26. package/dist/runtime/installers.d.ts +12 -0
  27. package/dist/runtime/installers.js +45 -0
  28. package/dist/runtime/manifest.d.ts +18 -0
  29. package/dist/runtime/manifest.js +43 -0
  30. package/dist/runtime/playwrightRuntime.d.ts +13 -0
  31. package/dist/runtime/playwrightRuntime.js +37 -0
  32. package/dist/runtime/systemDeps.d.ts +3 -0
  33. package/dist/runtime/systemDeps.js +30 -0
  34. package/dist/runtime/ytdlpRuntime.d.ts +14 -0
  35. package/dist/runtime/ytdlpRuntime.js +58 -0
  36. package/dist/server.d.ts +3 -1
  37. package/dist/server.js +4 -1
  38. package/dist/tools/downloads.d.ts +11 -0
  39. package/dist/tools/downloads.js +220 -0
  40. package/dist/tools/subtitles.d.ts +25 -0
  41. package/dist/tools/subtitles.js +135 -47
  42. package/dist/utils/config.d.ts +28 -0
  43. package/dist/utils/config.js +40 -11
  44. package/dist/utils/ffmpeg.d.ts +5 -0
  45. package/dist/utils/ffmpeg.js +16 -0
  46. package/dist/utils/ffmpegPath.d.ts +8 -0
  47. package/dist/utils/ffmpegPath.js +21 -0
  48. package/dist/utils/formatters.d.ts +4 -0
  49. package/dist/utils/formatters.js +42 -0
  50. package/dist/utils/mediaPaths.d.ts +7 -0
  51. package/dist/utils/mediaPaths.js +10 -0
  52. package/dist/utils/openClaw.d.ts +17 -0
  53. package/dist/utils/openClaw.js +79 -0
  54. package/dist/utils/videoInput.js +3 -0
  55. package/dist/utils/videoMetadata.d.ts +11 -0
  56. package/dist/utils/videoMetadata.js +1 -0
  57. package/dist/utils/ytdlp.d.ts +17 -1
  58. package/dist/utils/ytdlp.js +89 -2
  59. package/dist/utils/ytdlpPath.d.ts +9 -2
  60. package/dist/utils/ytdlpPath.js +19 -20
  61. package/dist/utils/ytdlpProgress.d.ts +13 -0
  62. package/dist/utils/ytdlpProgress.js +77 -0
  63. package/package.json +5 -3
  64. package/scripts/download-ytdlp.mjs +1 -1
  65. package/scripts/install.ps1 +9 -0
  66. 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.
@@ -103,40 +92,20 @@ export async function runSetup() {
103
92
  if (!hasBrowser) {
104
93
  console.log(" ℹ️ Cloud/headless environment detected — using PAT mode\n");
105
94
  }
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`);
113
- }
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
- }
95
+ // ── Step 1: Runtime check ──
96
+ console.log("Step 1/5: Checking required runtimes...");
97
+ const runtimeSummary = await checkAll();
98
+ for (const component of runtimeSummary.components) {
99
+ if (component.status === "installed") {
100
+ console.log(` ✅ ${component.name}: ${component.source}${component.version ? ` (${component.version})` : ""}`);
135
101
  }
136
- catch {
137
- console.log(`\r ⚠️ Subtitle engine setup failedsubtitles will be unavailable`);
102
+ else {
103
+ console.log(` ⚠️ ${component.name}: missing${component.message ? `${component.message}` : ""}`);
138
104
  }
139
105
  }
106
+ if (runtimeSummary.components.some((component) => component.status !== "installed")) {
107
+ console.log(" 💡 Install missing runtimes with: yt-mcp runtime install");
108
+ }
140
109
  // ── Step 2: Config ──
141
110
  console.log("Step 2/5: Initializing config...");
142
111
  const config = loadConfig();
@@ -144,6 +113,7 @@ export async function runSetup() {
144
113
  writeLauncherFile();
145
114
  console.log(` ✅ Config: ${PATHS.configJson}`);
146
115
  console.log(` ✅ Launcher: ${PATHS.launcherJs}`);
116
+ console.log(` ✅ Shared auth: ${PATHS.sharedAuthJson}`);
147
117
  // ── Step 3: Authentication ──
148
118
  console.log("Step 3/5: Authentication...");
149
119
  const tokenManager = new TokenManager(config.auth_url);
@@ -151,7 +121,7 @@ export async function runSetup() {
151
121
  if (pat) {
152
122
  // Explicit PAT provided via env var
153
123
  await tokenManager.savePAT(pat);
154
- console.log(" ✅ PAT saved from YT_MCP_TOKEN");
124
+ console.log(` ✅ PAT saved from YT_MCP_TOKEN to shared auth`);
155
125
  }
156
126
  else if (hasBrowser) {
157
127
  // Local or cloud desktop — let user choose auth method
@@ -172,7 +142,7 @@ export async function runSetup() {
172
142
  const patInput = await prompt(" 粘贴你的 PAT token (pat_xxx): ");
173
143
  if (patInput) {
174
144
  await tokenManager.savePAT(patInput);
175
- console.log(" ✅ PAT saved");
145
+ console.log(" ✅ PAT saved to shared auth");
176
146
  }
177
147
  else {
178
148
  console.log(" ⚠️ 未输入 token,稍后可通过环境变量配置:");
@@ -189,6 +159,7 @@ export async function runSetup() {
189
159
  const tokens = await runOAuthFlow(config.auth_url);
190
160
  await tokenManager.saveTokens(tokens.accessToken, tokens.refreshToken, tokens.expiresIn, tokens.clientId);
191
161
  console.log(" ✅ OAuth login successful");
162
+ console.log(" ℹ️ Other first-party local MCPs on this machine can reuse this login.");
192
163
  }
193
164
  catch (err) {
194
165
  // OAuth failed — auto fallback to PAT
@@ -202,7 +173,7 @@ export async function runSetup() {
202
173
  const patInput = await prompt(" 粘贴你的 PAT token (pat_xxx), 或直接回车跳过: ");
203
174
  if (patInput) {
204
175
  await tokenManager.savePAT(patInput);
205
- console.log(" ✅ PAT saved");
176
+ console.log(" ✅ PAT saved to shared auth");
206
177
  }
207
178
  else {
208
179
  console.log(" ⚠️ 稍后可通过环境变量配置:");
@@ -220,7 +191,7 @@ export async function runSetup() {
220
191
  const patInput = await prompt(" 粘贴你的 PAT token (pat_xxx), 或直接回车跳过: ");
221
192
  if (patInput) {
222
193
  await tokenManager.savePAT(patInput);
223
- console.log(" ✅ PAT saved");
194
+ console.log(" ✅ PAT saved to shared auth");
224
195
  }
225
196
  else {
226
197
  console.log(" ⚠️ 稍后可通过环境变量配置:");
@@ -253,35 +224,32 @@ export async function runSetup() {
253
224
  // ── Step 5: MCP Registration ──
254
225
  console.log("Step 5/5: Registering MCP in AI clients...");
255
226
  const launcherCommand = buildLauncherCommand();
256
- const mcpArgs = [launcherCommand.command, ...launcherCommand.args].map(quoteArg).join(" ");
257
227
  let registered = false;
258
228
  const cliCandidates = [
259
229
  // Claude Code: {bin} mcp add yt-mcp -- npx ... serve
260
230
  { bin: "claude-internal", label: "Claude Code (internal)",
261
- cmd: (b, a) => `${b} mcp add -s user yt-mcp -- ${a}` },
231
+ cmd: (b, launcher) => ({ file: b, args: ["mcp", "add", "-s", "user", "yt-mcp", "--", launcher.command, ...launcher.args] }) },
262
232
  { bin: "claude", label: "Claude Code",
263
- cmd: (b, a) => `${b} mcp add -s user yt-mcp -- ${a}` },
233
+ cmd: (b, launcher) => ({ file: b, args: ["mcp", "add", "-s", "user", "yt-mcp", "--", launcher.command, ...launcher.args] }) },
264
234
  // Codex (public): {bin} mcp add yt-mcp -- npx ... serve
265
235
  { bin: "codex", label: "Codex CLI / Codex App",
266
- cmd: (b, a) => `${b} mcp add yt-mcp -- ${a}` },
236
+ cmd: (b, launcher) => ({ file: b, args: ["mcp", "add", "yt-mcp", "--", launcher.command, ...launcher.args] }) },
267
237
  // Codex-internal doesn't support mcp add — needs manual config
268
238
  { bin: "codex-internal", label: "Codex CLI (internal)",
269
239
  cmd: () => null },
270
240
  // Gemini: {bin} mcp add -s user yt-mcp node <launcher> serve (no --)
271
241
  { bin: "gemini-internal", label: "Gemini CLI (internal)",
272
- cmd: (b, a) => `${b} mcp add -s user yt-mcp ${a}` },
242
+ cmd: (b, launcher) => ({ file: b, args: ["mcp", "add", "-s", "user", "yt-mcp", launcher.command, ...launcher.args] }) },
273
243
  { bin: "gemini", label: "Gemini CLI",
274
- cmd: (b, a) => `${b} mcp add -s user yt-mcp ${a}` },
244
+ cmd: (b, launcher) => ({ file: b, args: ["mcp", "add", "-s", "user", "yt-mcp", launcher.command, ...launcher.args] }) },
275
245
  // Others: assume Claude-style syntax
276
246
  { 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}` },
247
+ cmd: (b, launcher) => ({ file: b, args: ["mcp", "add", "yt-mcp", "--", launcher.command, ...launcher.args] }) },
280
248
  ];
281
249
  for (const { bin, label, cmd } of cliCandidates) {
282
250
  if (!detectCli(bin))
283
251
  continue;
284
- const command = cmd(bin, mcpArgs);
252
+ const command = cmd(bin, launcherCommand);
285
253
  if (!command) {
286
254
  // CLI detected but doesn't support auto-registration
287
255
  console.log(` ⚠️ ${label} detected but requires manual MCP config.`);
@@ -291,6 +259,17 @@ export async function runSetup() {
291
259
  registered = true;
292
260
  }
293
261
  }
262
+ if (isOpenClawInstallLikelyInstalled(detectCli)) {
263
+ try {
264
+ const status = writeOpenClawConfig("yt-mcp", launcherCommand);
265
+ const suffix = status === "created" ? "created" : "updated";
266
+ console.log(` ✅ MCP registered in OpenClaw (${suffix} mcporter.json)`);
267
+ registered = true;
268
+ }
269
+ catch (err) {
270
+ console.log(` ⚠️ OpenClaw auto-register failed: ${err instanceof Error ? err.message : String(err)}`);
271
+ }
272
+ }
294
273
  if (!registered) {
295
274
  console.log(" ℹ️ No supported CLI found. Add manually to your AI client:");
296
275
  }
@@ -307,6 +286,26 @@ export async function runSetup() {
307
286
  }
308
287
  }
309
288
  `);
289
+ console.log(` OpenClaw mcporter config: ${getOpenClawConfigPath()}`);
290
+ console.log(" OpenClaw manual config:");
291
+ console.log(`
292
+ {
293
+ "servers": {
294
+ "yt-mcp": {
295
+ "transport": "stdio",
296
+ "command": "node",
297
+ "args": [${JSON.stringify(PATHS.launcherJs)}, "serve"]
298
+ }
299
+ }
300
+ }
301
+ `);
302
+ console.log(` OpenClaw uses ${PATHS.sharedAuthJson} for PAT/JWT, so env.YT_MCP_TOKEN is optional after setup.`);
303
+ console.log(" Media downloads:");
304
+ console.log(" - `start_download_job` / `poll_download_job` are job-based local tools");
305
+ console.log(" - Batch limit: 5 YouTube videos per job");
306
+ console.log(" - Output path: ~/Downloads/yt-mcp/YYYY-MM-DD_<video_id>");
307
+ console.log(" - `ffmpeg` is required for video download modes");
308
+ console.log("");
310
309
  console.log("✅ Setup complete!");
311
310
  if (hasBrowser) {
312
311
  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 {};