@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.
- package/README.md +44 -3
- package/dist/auth/sharedAuth.d.ts +10 -0
- package/dist/auth/sharedAuth.js +24 -0
- package/dist/auth/tokenManager.d.ts +10 -1
- package/dist/auth/tokenManager.js +14 -22
- package/dist/cli/check.js +6 -3
- package/dist/cli/index.d.ts +15 -1
- package/dist/cli/index.js +74 -31
- package/dist/cli/runtime.d.ts +9 -0
- package/dist/cli/runtime.js +35 -0
- package/dist/cli/serve.js +3 -1
- package/dist/cli/setup.d.ts +3 -0
- package/dist/cli/setup.js +84 -68
- package/dist/cli/setupCookies.js +2 -2
- package/dist/cli/smoke.d.ts +27 -0
- package/dist/cli/smoke.js +108 -0
- package/dist/cli/uninstall.d.ts +1 -0
- package/dist/cli/uninstall.js +67 -0
- package/dist/download/downloader.d.ts +64 -0
- package/dist/download/downloader.js +264 -0
- package/dist/download/jobManager.d.ts +21 -0
- package/dist/download/jobManager.js +198 -0
- package/dist/download/types.d.ts +43 -0
- package/dist/download/types.js +1 -0
- package/dist/runtime/ffmpegRuntime.d.ts +13 -0
- package/dist/runtime/ffmpegRuntime.js +51 -0
- package/dist/runtime/installers.d.ts +12 -0
- package/dist/runtime/installers.js +45 -0
- package/dist/runtime/manifest.d.ts +18 -0
- package/dist/runtime/manifest.js +43 -0
- package/dist/runtime/playwrightRuntime.d.ts +13 -0
- package/dist/runtime/playwrightRuntime.js +37 -0
- package/dist/runtime/systemDeps.d.ts +3 -0
- package/dist/runtime/systemDeps.js +30 -0
- package/dist/runtime/ytdlpRuntime.d.ts +14 -0
- package/dist/runtime/ytdlpRuntime.js +58 -0
- package/dist/server.d.ts +3 -1
- package/dist/server.js +4 -1
- package/dist/tools/downloads.d.ts +11 -0
- package/dist/tools/downloads.js +220 -0
- package/dist/tools/subtitles.d.ts +25 -0
- package/dist/tools/subtitles.js +135 -47
- package/dist/utils/config.d.ts +28 -0
- package/dist/utils/config.js +40 -11
- package/dist/utils/ffmpeg.d.ts +5 -0
- package/dist/utils/ffmpeg.js +16 -0
- package/dist/utils/ffmpegPath.d.ts +8 -0
- package/dist/utils/ffmpegPath.js +21 -0
- package/dist/utils/formatters.d.ts +4 -0
- package/dist/utils/formatters.js +42 -0
- package/dist/utils/mediaPaths.d.ts +7 -0
- package/dist/utils/mediaPaths.js +10 -0
- package/dist/utils/openClaw.d.ts +17 -0
- package/dist/utils/openClaw.js +79 -0
- package/dist/utils/videoInput.js +3 -0
- package/dist/utils/videoMetadata.d.ts +11 -0
- package/dist/utils/videoMetadata.js +1 -0
- package/dist/utils/ytdlp.d.ts +17 -1
- package/dist/utils/ytdlp.js +89 -2
- package/dist/utils/ytdlpPath.d.ts +9 -2
- package/dist/utils/ytdlpPath.js +19 -20
- package/dist/utils/ytdlpProgress.d.ts +13 -0
- package/dist/utils/ytdlpProgress.js +77 -0
- package/package.json +5 -3
- package/scripts/download-ytdlp.mjs +1 -1
- package/scripts/install.ps1 +9 -0
- package/scripts/install.sh +15 -0
package/dist/cli/setup.js
CHANGED
|
@@ -1,18 +1,16 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
137
|
-
console.log(
|
|
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(
|
|
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
|
|
217
|
-
|
|
218
|
-
|
|
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
|
-
|
|
234
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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 教程"');
|
package/dist/cli/setupCookies.js
CHANGED
|
@@ -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:
|
|
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:
|
|
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 {};
|