@mkterswingman/5mghost-yonder 0.0.1 → 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.
- 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.d.ts +4 -0
- package/dist/cli/check.js +90 -0
- package/dist/cli/index.d.ts +15 -1
- package/dist/cli/index.js +76 -27
- 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.js +60 -61
- 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.
|
|
@@ -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:
|
|
107
|
-
console.log("Step 1/5: Checking
|
|
108
|
-
const
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
137
|
-
console.log(
|
|
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(
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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 教程"');
|
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 {};
|