@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
@@ -1,8 +1,63 @@
1
1
  import { spawn } from "node:child_process";
2
+ import { spawnSync } from "node:child_process";
2
3
  import { existsSync } from "node:fs";
3
4
  import { PATHS } from "./config.js";
4
5
  import { getYtDlpPath } from "./ytdlpPath.js";
5
- export function runYtDlp(args, timeoutMs = 45_000) {
6
+ export function createYtDlpStderrLineSplitter(onLine) {
7
+ let buffer = "";
8
+ let pendingCarriageReturn = false;
9
+ const emit = () => {
10
+ if (buffer.length === 0) {
11
+ return;
12
+ }
13
+ try {
14
+ onLine(buffer);
15
+ }
16
+ catch {
17
+ // Why: progress observers are best-effort and must not break downloads.
18
+ }
19
+ buffer = "";
20
+ };
21
+ return {
22
+ push(chunk) {
23
+ const text = typeof chunk === "string" ? chunk : chunk.toString("utf8");
24
+ for (const ch of text) {
25
+ if (pendingCarriageReturn) {
26
+ if (ch === "\n") {
27
+ pendingCarriageReturn = false;
28
+ emit();
29
+ continue;
30
+ }
31
+ pendingCarriageReturn = false;
32
+ emit();
33
+ }
34
+ if (ch === "\r") {
35
+ pendingCarriageReturn = true;
36
+ continue;
37
+ }
38
+ if (ch === "\n") {
39
+ emit();
40
+ continue;
41
+ }
42
+ buffer += ch;
43
+ }
44
+ },
45
+ flush() {
46
+ if (pendingCarriageReturn) {
47
+ pendingCarriageReturn = false;
48
+ }
49
+ emit();
50
+ },
51
+ };
52
+ }
53
+ export function hasYtDlp(runSpawnSync = spawnSync) {
54
+ const result = runSpawnSync(getYtDlpPath(), ["--version"], {
55
+ stdio: "ignore",
56
+ timeout: 30_000,
57
+ });
58
+ return result.status === 0 && result.error == null;
59
+ }
60
+ export function runYtDlp(args, timeoutMs = 45_000, onStderrLine) {
6
61
  return new Promise((resolve, reject) => {
7
62
  const start = Date.now();
8
63
  const finalArgs = ["--force-ipv4", "--no-warnings", ...args];
@@ -14,13 +69,23 @@ export function runYtDlp(args, timeoutMs = 45_000) {
14
69
  });
15
70
  const stdoutChunks = [];
16
71
  const stderrChunks = [];
72
+ const stderrSplitter = onStderrLine
73
+ ? createYtDlpStderrLineSplitter(onStderrLine)
74
+ : null;
17
75
  proc.stdout.on("data", (chunk) => stdoutChunks.push(chunk));
18
- proc.stderr.on("data", (chunk) => stderrChunks.push(chunk));
76
+ proc.stderr.on("data", (chunk) => {
77
+ stderrChunks.push(chunk);
78
+ stderrSplitter?.push(chunk);
79
+ });
80
+ function flushStderrLineBuffer() {
81
+ stderrSplitter?.flush();
82
+ }
19
83
  let settled = false;
20
84
  const timer = setTimeout(() => {
21
85
  if (!settled) {
22
86
  settled = true;
23
87
  proc.kill("SIGKILL");
88
+ flushStderrLineBuffer();
24
89
  resolve({
25
90
  exitCode: -1,
26
91
  stdout: Buffer.concat(stdoutChunks).toString("utf8"),
@@ -33,6 +98,7 @@ export function runYtDlp(args, timeoutMs = 45_000) {
33
98
  clearTimeout(timer);
34
99
  if (!settled) {
35
100
  settled = true;
101
+ flushStderrLineBuffer();
36
102
  resolve({
37
103
  exitCode: code ?? 1,
38
104
  stdout: Buffer.concat(stdoutChunks).toString("utf8"),
@@ -50,3 +116,24 @@ export function runYtDlp(args, timeoutMs = 45_000) {
50
116
  });
51
117
  });
52
118
  }
119
+ export async function runYtDlpJson(args, timeoutMs = 45_000) {
120
+ const result = await runYtDlp(args, timeoutMs);
121
+ if (result.exitCode !== 0) {
122
+ return {
123
+ ok: false,
124
+ error: result.stderr.slice(0, 500) || `yt-dlp exited with ${result.exitCode}`,
125
+ };
126
+ }
127
+ try {
128
+ return {
129
+ ok: true,
130
+ value: JSON.parse(result.stdout),
131
+ };
132
+ }
133
+ catch {
134
+ return {
135
+ ok: false,
136
+ error: "yt-dlp returned invalid JSON metadata.",
137
+ };
138
+ }
139
+ }
@@ -3,9 +3,16 @@
3
3
  *
4
4
  * Priority:
5
5
  * 1. YT_DLP_PATH env var (explicit override)
6
- * 2. Bundled binary at <pkg>/bin/yt-dlp (downloaded by postinstall)
6
+ * 2. Runtime-managed binary at ~/.yt-mcp/runtime/bin/yt-dlp
7
7
  * 3. "yt-dlp" — fall back to system PATH
8
8
  */
9
+ export interface ResolveYtDlpPathOptions {
10
+ envPath?: string | null;
11
+ runtimePath?: string;
12
+ runtimeExists?: boolean;
13
+ }
14
+ export declare function getRuntimeYtDlpPath(runtimeBinDir?: string): string;
15
+ export declare function resolveYtDlpPath(options?: ResolveYtDlpPathOptions): string;
9
16
  /**
10
17
  * Returns the absolute path to the yt-dlp binary, or the bare command name
11
18
  * "yt-dlp" if only available on system PATH.
@@ -13,7 +20,7 @@
13
20
  export declare function getYtDlpPath(): string;
14
21
  export interface YtDlpVersionInfo {
15
22
  version: string;
16
- source: "bundled" | "system" | "env";
23
+ source: "runtime" | "system" | "env";
17
24
  }
18
25
  /**
19
26
  * Get the version string and source of the resolved yt-dlp binary.
@@ -3,35 +3,34 @@
3
3
  *
4
4
  * Priority:
5
5
  * 1. YT_DLP_PATH env var (explicit override)
6
- * 2. Bundled binary at <pkg>/bin/yt-dlp (downloaded by postinstall)
6
+ * 2. Runtime-managed binary at ~/.yt-mcp/runtime/bin/yt-dlp
7
7
  * 3. "yt-dlp" — fall back to system PATH
8
8
  */
9
9
  import { existsSync } from "node:fs";
10
10
  import { execFileSync } from "node:child_process";
11
- import { join, dirname } from "node:path";
12
- import { fileURLToPath } from "node:url";
13
- const __dirname = dirname(fileURLToPath(import.meta.url));
14
- // From dist/utils/ → package root is ../../
15
- const pkgRoot = join(__dirname, "..", "..");
16
- function bundledPath() {
11
+ import { PATHS, buildRuntimeBinaryPath } from "./config.js";
12
+ export function getRuntimeYtDlpPath(runtimeBinDir = PATHS.runtimeBinDir) {
17
13
  const name = process.platform === "win32" ? "yt-dlp.exe" : "yt-dlp";
18
- return join(pkgRoot, "bin", name);
14
+ return buildRuntimeBinaryPath(runtimeBinDir, name);
15
+ }
16
+ export function resolveYtDlpPath(options = {}) {
17
+ const envPath = options.envPath ?? process.env.YT_DLP_PATH ?? null;
18
+ if (envPath) {
19
+ return envPath;
20
+ }
21
+ const runtimePath = options.runtimePath ?? getRuntimeYtDlpPath();
22
+ const runtimeExists = options.runtimeExists ?? existsSync(runtimePath);
23
+ if (runtimeExists) {
24
+ return runtimePath;
25
+ }
26
+ return "yt-dlp";
19
27
  }
20
28
  /**
21
29
  * Returns the absolute path to the yt-dlp binary, or the bare command name
22
30
  * "yt-dlp" if only available on system PATH.
23
31
  */
24
32
  export function getYtDlpPath() {
25
- // 1. Explicit env override
26
- const envPath = process.env.YT_DLP_PATH;
27
- if (envPath && existsSync(envPath))
28
- return envPath;
29
- // 2. Bundled binary
30
- const bundled = bundledPath();
31
- if (existsSync(bundled))
32
- return bundled;
33
- // 3. System PATH
34
- return "yt-dlp";
33
+ return resolveYtDlpPath();
35
34
  }
36
35
  /**
37
36
  * Get the version string and source of the resolved yt-dlp binary.
@@ -47,8 +46,8 @@ export function getYtDlpVersion(binPath) {
47
46
  if (process.env.YT_DLP_PATH && resolved === process.env.YT_DLP_PATH) {
48
47
  source = "env";
49
48
  }
50
- else if (resolved === bundledPath()) {
51
- source = "bundled";
49
+ else if (resolved === getRuntimeYtDlpPath()) {
50
+ source = "runtime";
52
51
  }
53
52
  else {
54
53
  source = "system";
@@ -0,0 +1,13 @@
1
+ export interface ParsedYtDlpProgress {
2
+ progress: string;
3
+ downloaded_size?: string;
4
+ total_size?: string;
5
+ speed?: string;
6
+ eta?: string;
7
+ }
8
+ /**
9
+ * Parse the canonical yt-dlp download progress line used by this integration.
10
+ * Narrow contract: intentionally supports only representative `[download] ...`
11
+ * lines with percent, total size, speed, and ETA fields.
12
+ */
13
+ export declare function parseYtDlpProgressLine(line: string): ParsedYtDlpProgress | null;
@@ -0,0 +1,77 @@
1
+ import { formatBytes, formatEta, formatPercent, formatSpeed } from "./formatters.js";
2
+ const SIZE_UNITS = {
3
+ b: 1,
4
+ kb: 1000,
5
+ kib: 1024,
6
+ mb: 1000 ** 2,
7
+ mib: 1024 ** 2,
8
+ gb: 1000 ** 3,
9
+ gib: 1024 ** 3,
10
+ tb: 1000 ** 4,
11
+ tib: 1024 ** 4,
12
+ };
13
+ function parseSizeToBytes(text) {
14
+ const match = text.trim().match(/^(\d+(?:\.\d+)?)([kmgt]?i?b)$/i);
15
+ if (!match) {
16
+ return null;
17
+ }
18
+ const value = Number(match[1]);
19
+ const unit = match[2].toLowerCase();
20
+ const multiplier = SIZE_UNITS[unit];
21
+ if (!Number.isFinite(value) || multiplier == null) {
22
+ return null;
23
+ }
24
+ return value * multiplier;
25
+ }
26
+ function parseEtaToSeconds(text) {
27
+ const trimmed = text.trim();
28
+ if (!trimmed || trimmed === "N/A") {
29
+ return null;
30
+ }
31
+ const hms = trimmed.match(/^(\d+):(\d{2}):(\d{2})$/);
32
+ if (hms) {
33
+ return Number(hms[1]) * 3600 + Number(hms[2]) * 60 + Number(hms[3]);
34
+ }
35
+ const ms = trimmed.match(/^(\d{1,2}):(\d{2})$/);
36
+ if (ms) {
37
+ return Number(ms[1]) * 60 + Number(ms[2]);
38
+ }
39
+ const unitMatch = trimmed.match(/^(\d+(?:\.\d+)?)([smhd])$/i);
40
+ if (unitMatch) {
41
+ const value = Number(unitMatch[1]);
42
+ const unit = unitMatch[2].toLowerCase();
43
+ const multiplier = unit === "s" ? 1 : unit === "m" ? 60 : unit === "h" ? 3600 : 86400;
44
+ return value * multiplier;
45
+ }
46
+ return null;
47
+ }
48
+ /**
49
+ * Parse the canonical yt-dlp download progress line used by this integration.
50
+ * Narrow contract: intentionally supports only representative `[download] ...`
51
+ * lines with percent, total size, speed, and ETA fields.
52
+ */
53
+ export function parseYtDlpProgressLine(line) {
54
+ const match = line.match(/^\[download\]\s+(?<progress>\d+(?:\.\d+)?)% of (?<total>\S+)(?: at (?<speed>\S+?\/s))?(?: ETA (?<eta>\S+))?(?: \((?<details>.+)\))?$/);
55
+ if (!match?.groups?.progress) {
56
+ return null;
57
+ }
58
+ const progress = formatPercent(Number(match.groups.progress));
59
+ const totalBytes = match.groups.total ? parseSizeToBytes(match.groups.total) : null;
60
+ const speedBytes = match.groups.speed ? parseSizeToBytes(match.groups.speed.replace(/\/s$/i, "")) : null;
61
+ const etaSeconds = match.groups.eta ? parseEtaToSeconds(match.groups.eta) : null;
62
+ const parsed = { progress };
63
+ if (totalBytes != null) {
64
+ parsed.total_size = formatBytes(totalBytes);
65
+ }
66
+ if (speedBytes != null) {
67
+ parsed.speed = formatSpeed(speedBytes);
68
+ }
69
+ if (etaSeconds != null) {
70
+ parsed.eta = formatEta(etaSeconds);
71
+ }
72
+ if (totalBytes != null && Number.isFinite(Number(match.groups.progress))) {
73
+ const downloadedBytes = (totalBytes * Number(match.groups.progress)) / 100;
74
+ parsed.downloaded_size = formatBytes(downloadedBytes);
75
+ }
76
+ return parsed;
77
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mkterswingman/5mghost-yonder",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "description": "Internal MCP client with local data tools and remote API proxy",
5
5
  "type": "module",
6
6
  "bin": {
@@ -11,9 +11,9 @@
11
11
  },
12
12
  "scripts": {
13
13
  "build": "tsc -p tsconfig.json",
14
+ "test": "npm run build && node --test tests/*.test.mjs",
14
15
  "dev": "tsx src/cli/index.ts",
15
- "start": "node dist/cli/index.js",
16
- "postinstall": "node scripts/download-ytdlp.mjs"
16
+ "start": "node dist/cli/index.js"
17
17
  },
18
18
  "dependencies": {
19
19
  "@modelcontextprotocol/sdk": "^1.28.0",
@@ -35,6 +35,8 @@
35
35
  "files": [
36
36
  "dist/",
37
37
  "scripts/download-ytdlp.mjs",
38
+ "scripts/install.sh",
39
+ "scripts/install.ps1",
38
40
  "README.md"
39
41
  ],
40
42
  "devDependencies": {
@@ -39,7 +39,7 @@ const PLATFORM_MAP = {
39
39
 
40
40
  const __dirname = dirname(fileURLToPath(import.meta.url));
41
41
  const pkgRoot = join(__dirname, "..");
42
- const binDir = join(pkgRoot, "bin");
42
+ const binDir = process.env.YT_MCP_BINARY_DIR || join(pkgRoot, "bin");
43
43
 
44
44
  function localName() {
45
45
  return process.platform === "win32" ? "yt-dlp.exe" : "yt-dlp";
@@ -0,0 +1,9 @@
1
+ $ErrorActionPreference = "Stop"
2
+
3
+ if (-not (Get-Command node -ErrorAction SilentlyContinue)) {
4
+ winget install OpenJS.NodeJS.LTS
5
+ }
6
+
7
+ npm install -g @mkterswingman/5mghost-yonder
8
+ yt-mcp runtime install
9
+ yt-mcp setup
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ if ! command -v node >/dev/null 2>&1; then
5
+ if command -v brew >/dev/null 2>&1; then
6
+ brew install node
7
+ else
8
+ echo "Node.js is required. Install it first, then rerun this script." >&2
9
+ exit 1
10
+ fi
11
+ fi
12
+
13
+ npm install -g @mkterswingman/5mghost-yonder
14
+ yt-mcp runtime install
15
+ yt-mcp setup