@mkterswingman/5mghost-yonder 0.0.18 → 0.0.20

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.
@@ -5,6 +5,7 @@ export interface UnifiedUpdateDeps {
5
5
  getLatestVersion(): Promise<string>;
6
6
  installLatestPackage(): Promise<void>;
7
7
  updateRuntime(): Promise<import("../runtime/installers.js").RuntimeSummary>;
8
+ postUpdate?: () => Promise<void>;
8
9
  }
9
10
  export declare function compareVersions(currentVersion: string, latestVersion: string): number;
10
11
  export declare function runUnifiedUpdate(deps: UnifiedUpdateDeps): Promise<{
package/dist/cli/index.js CHANGED
@@ -24,6 +24,7 @@ Commands:
24
24
  setup Run first-time setup (OAuth + cookies + MCP registration)
25
25
  serve Start the MCP server (stdio transport)
26
26
  smoke Run installer smoke checks
27
+ install-skills Install the bundled yt-mcp analysis skill into supported AI clients
27
28
  setup-cookies Refresh YouTube cookies using browser login
28
29
  runtime Manage required runtimes
29
30
  check Check auth, runtime, cookies, and connectivity
@@ -58,6 +59,9 @@ export async function runUnifiedUpdate(deps) {
58
59
  updated = true;
59
60
  }
60
61
  const runtime = await deps.updateRuntime();
62
+ if (deps.postUpdate) {
63
+ await deps.postUpdate();
64
+ }
61
65
  return {
62
66
  package: { currentVersion, latestVersion, updated },
63
67
  runtime,
@@ -81,6 +85,11 @@ async function main() {
81
85
  await runSmoke(process.argv.slice(3));
82
86
  break;
83
87
  }
88
+ case "install-skills": {
89
+ const { runInstallSkills } = await import("./installSkills.js");
90
+ await runInstallSkills();
91
+ break;
92
+ }
84
93
  case "setup-cookies": {
85
94
  const { runSetupCookies, parseSetupCookiesArgs } = await import("./setupCookies.js");
86
95
  await runSetupCookies({}, parseSetupCookiesArgs(process.argv.slice(3)));
@@ -107,6 +116,7 @@ async function main() {
107
116
  console.log("Checking for updates...\n");
108
117
  try {
109
118
  const { updateAll } = await import("../runtime/installers.js");
119
+ const { runInstallSkills } = await import("./installSkills.js");
110
120
  const summary = await runUnifiedUpdate({
111
121
  getCurrentVersion: getVersion,
112
122
  getLatestVersion: async () => execSync("npm view @mkterswingman/5mghost-yonder version", {
@@ -119,6 +129,7 @@ async function main() {
119
129
  });
120
130
  },
121
131
  updateRuntime: updateAll,
132
+ postUpdate: runInstallSkills,
122
133
  });
123
134
  if (summary.package.updated) {
124
135
  console.log(`\n✅ Updated package to ${summary.package.latestVersion}`);
@@ -0,0 +1 @@
1
+ export declare function runInstallSkills(): Promise<void>;
@@ -0,0 +1,39 @@
1
+ import { fileURLToPath } from "node:url";
2
+ import { dirname, join } from "node:path";
3
+ import { execFileSync } from "node:child_process";
4
+ import { buildSkillInstallPlan, installSkillTarget, } from "../utils/skills.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 resolvePackageRoot() {
15
+ return join(dirname(fileURLToPath(import.meta.url)), "..", "..");
16
+ }
17
+ export async function runInstallSkills() {
18
+ const packageRoot = resolvePackageRoot();
19
+ const availableCliNames = [
20
+ "claude-internal",
21
+ "claude",
22
+ "codex-internal",
23
+ "codex",
24
+ "gemini-internal",
25
+ "gemini",
26
+ ].filter(detectCli);
27
+ const plan = buildSkillInstallPlan(packageRoot, {
28
+ availableCliNames,
29
+ includeOpenClaw: process.env.YT_MCP_INSTALL_OPENCLAW_SKILL === "1",
30
+ });
31
+ if (plan.length === 0) {
32
+ console.log("[yt-mcp] No supported AI client detected for bundled skill install.");
33
+ return;
34
+ }
35
+ for (const target of plan) {
36
+ installSkillTarget(target);
37
+ console.log(`[yt-mcp] Installed bundled skill for ${target.label}: ${target.targetDir}`);
38
+ }
39
+ }
package/dist/cli/setup.js CHANGED
@@ -6,6 +6,7 @@ import { runOAuthFlow } from "../auth/oauthFlow.js";
6
6
  import { hasSIDCookies } from "../utils/cookies.js";
7
7
  import { buildLauncherCommand, writeLauncherFile } from "../utils/launcher.js";
8
8
  import { checkAll } from "../runtime/installers.js";
9
+ import { runInstallSkills } from "./installSkills.js";
9
10
  import { getOpenClawConfigPath, isOpenClawInstallLikelyInstalled, writeOpenClawConfig, } from "../utils/openClaw.js";
10
11
  import { getCodexInternalConfigPath, writeCodexInternalConfig, } from "../utils/codexInternal.js";
11
12
  import { MCP_REGISTER_TIMEOUT_MS, classifyRegistrationFailure, } from "../utils/mcpRegistration.js";
@@ -267,6 +268,7 @@ export async function runSetup() {
267
268
  console.log("Step 5/5: Registering MCP in AI clients...");
268
269
  const launcherCommand = buildLauncherCommand();
269
270
  let registered = false;
271
+ let skillsInstalled = false;
270
272
  for (const { bin, label, command } of buildSetupCliCandidates({
271
273
  file: launcherCommand.command,
272
274
  args: launcherCommand.args,
@@ -299,6 +301,14 @@ export async function runSetup() {
299
301
  console.log(` ⚠️ Codex CLI (internal) auto-register failed: ${err instanceof Error ? err.message : String(err)}`);
300
302
  }
301
303
  }
304
+ try {
305
+ process.env.YT_MCP_INSTALL_OPENCLAW_SKILL = isOpenClawInstallLikelyInstalled(detectCli) ? "1" : "0";
306
+ await runInstallSkills();
307
+ skillsInstalled = true;
308
+ }
309
+ catch (err) {
310
+ console.log(` ⚠️ Bundled skill install failed: ${err instanceof Error ? err.message : String(err)}`);
311
+ }
302
312
  if (!registered) {
303
313
  console.log(" ℹ️ No supported CLI found. Add manually to your AI client:");
304
314
  }
@@ -329,6 +339,9 @@ export async function runSetup() {
329
339
  }
330
340
  `);
331
341
  console.log(` OpenClaw uses ${PATHS.sharedAuthJson} for PAT/JWT, so env.YT_MCP_TOKEN is optional after setup.`);
342
+ if (skillsInstalled) {
343
+ console.log(" ✅ Installed bundled yt-mcp analysis skill for detected AI clients");
344
+ }
332
345
  console.log(" Media downloads:");
333
346
  console.log(" - `start_download_job` / `poll_download_job` are job-based local tools");
334
347
  console.log(" - Batch limit: 5 YouTube videos per job");
@@ -26,6 +26,10 @@ const npmCacheDir = ${JSON.stringify(npmCacheDir)};
26
26
  const args = process.argv.slice(2);
27
27
  const targetArgs = args.length > 0 ? args : ["serve"];
28
28
 
29
+ function isSkillInstallerMode(subArgs) {
30
+ return subArgs[0] === "install-skills";
31
+ }
32
+
29
33
  function isRepairableNpxFailure(stderr) {
30
34
  const lower = stderr.toLowerCase();
31
35
  return (lower.includes("_npx/") || lower.includes("_npx\\\\"))
@@ -38,6 +42,7 @@ function runNpx(subArgs, captureStdErrOnly = false) {
38
42
  return spawnSync(npxBin, ["--yes", packageSpec, ...subArgs], {
39
43
  env: { ...process.env, npm_config_cache: npmCacheDir },
40
44
  // Why: probe runs before MCP starts; stdout must stay silent or it will corrupt stdio transport.
45
+ // The skill installer is an explicit one-shot CLI path, so it can inherit stdio safely.
41
46
  stdio: captureStdErrOnly ? ["ignore", "ignore", "pipe"] : "inherit",
42
47
  });
43
48
  }
@@ -70,6 +75,9 @@ function ensurePackageReady() {
70
75
  }
71
76
 
72
77
  ensurePackageReady();
78
+ if (isSkillInstallerMode(targetArgs)) {
79
+ process.env.YT_MCP_INSTALL_SKILLS = "1";
80
+ }
73
81
  const finalRun = runNpx(targetArgs, false);
74
82
  process.exit(finalRun.status ?? 0);
75
83
  `;
@@ -6,6 +6,7 @@ export interface OpenClawServerConfig {
6
6
  env?: Record<string, string>;
7
7
  }
8
8
  export declare function getOpenClawConfigPath(homeDir?: string): string;
9
+ export declare function getOpenClawSkillsDir(homeDir?: string): string;
9
10
  export declare function buildOpenClawServerConfig(launcherCommand: LauncherCommand): OpenClawServerConfig;
10
11
  export declare function upsertOpenClawConfigText(currentText: string | null, serverName: string, launcherCommand: LauncherCommand): string;
11
12
  export declare function removeOpenClawConfigEntryText(currentText: string | null, serverName: string): {
@@ -4,6 +4,9 @@ import { dirname, join } from "node:path";
4
4
  export function getOpenClawConfigPath(homeDir = homedir()) {
5
5
  return join(homeDir, ".openclaw", "workspace", "config", "mcporter.json");
6
6
  }
7
+ export function getOpenClawSkillsDir(homeDir = homedir()) {
8
+ return join(homeDir, ".openclaw", "skills");
9
+ }
7
10
  export function buildOpenClawServerConfig(launcherCommand) {
8
11
  return {
9
12
  transport: "stdio",
@@ -0,0 +1,16 @@
1
+ import type { LauncherCommand } from "./launcher.js";
2
+ export interface SkillInstallTarget {
3
+ client: string;
4
+ label: string;
5
+ sourceDir: string;
6
+ targetDir: string;
7
+ }
8
+ export interface SkillInstallPlanOptions {
9
+ homeDir?: string;
10
+ availableCliNames?: string[];
11
+ includeOpenClaw?: boolean;
12
+ }
13
+ export declare function getSkillPackageSourcePath(packageRoot: string): string;
14
+ export declare function buildSkillLauncherCommand(launcherPath: string): LauncherCommand;
15
+ export declare function buildSkillInstallPlan(packageRoot: string, options?: SkillInstallPlanOptions): SkillInstallTarget[];
16
+ export declare function installSkillTarget(target: SkillInstallTarget): void;
@@ -0,0 +1,56 @@
1
+ import { cpSync, existsSync, mkdirSync, rmSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { dirname, join } from "node:path";
4
+ import { getOpenClawSkillsDir } from "./openClaw.js";
5
+ const SKILL_NAME = "use-yt-mcp";
6
+ export function getSkillPackageSourcePath(packageRoot) {
7
+ return join(packageRoot, "skills", SKILL_NAME);
8
+ }
9
+ export function buildSkillLauncherCommand(launcherPath) {
10
+ return {
11
+ command: "node",
12
+ args: [launcherPath, "install-skills"],
13
+ };
14
+ }
15
+ export function buildSkillInstallPlan(packageRoot, options = {}) {
16
+ const homeDir = options.homeDir ?? homedir();
17
+ const availableCliNames = new Set(options.availableCliNames ?? []);
18
+ const sourceDir = getSkillPackageSourcePath(packageRoot);
19
+ const plan = [];
20
+ const cliTargets = [
21
+ { client: "claude-internal", label: "Claude Code (internal)", dir: join(homeDir, ".claude-internal", "skills") },
22
+ { client: "claude", label: "Claude Code", dir: join(homeDir, ".claude", "skills") },
23
+ { client: "codex-internal", label: "Codex CLI (internal)", dir: join(homeDir, ".codex-internal", "skills") },
24
+ { client: "codex", label: "Codex CLI / Codex App", dir: join(homeDir, ".codex", "skills") },
25
+ { client: "gemini-internal", label: "Gemini CLI (internal)", dir: join(homeDir, ".gemini-internal", "skills") },
26
+ { client: "gemini", label: "Gemini CLI", dir: join(homeDir, ".gemini", "skills") },
27
+ ];
28
+ for (const target of cliTargets) {
29
+ if (!availableCliNames.has(target.client)) {
30
+ continue;
31
+ }
32
+ plan.push({
33
+ client: target.client,
34
+ label: target.label,
35
+ sourceDir,
36
+ targetDir: join(target.dir, SKILL_NAME),
37
+ });
38
+ }
39
+ if (options.includeOpenClaw) {
40
+ plan.push({
41
+ client: "openclaw",
42
+ label: "OpenClaw",
43
+ sourceDir,
44
+ targetDir: join(getOpenClawSkillsDir(homeDir), SKILL_NAME),
45
+ });
46
+ }
47
+ return plan;
48
+ }
49
+ export function installSkillTarget(target) {
50
+ if (!existsSync(target.sourceDir)) {
51
+ throw new Error(`Bundled skill not found: ${target.sourceDir}`);
52
+ }
53
+ mkdirSync(dirname(target.targetDir), { recursive: true });
54
+ rmSync(target.targetDir, { recursive: true, force: true });
55
+ cpSync(target.sourceDir, target.targetDir, { recursive: true });
56
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mkterswingman/5mghost-yonder",
3
- "version": "0.0.18",
3
+ "version": "0.0.20",
4
4
  "description": "Internal MCP client with local data tools and remote API proxy",
5
5
  "type": "module",
6
6
  "bin": {
@@ -34,6 +34,7 @@
34
34
  },
35
35
  "files": [
36
36
  "dist/",
37
+ "skills/",
37
38
  "scripts/download-ytdlp.mjs",
38
39
  "scripts/install.sh",
39
40
  "scripts/install.ps1",
@@ -0,0 +1,117 @@
1
+ ---
2
+ name: use-yt-mcp
3
+ preamble-tier: 3
4
+ version: 1.2.0
5
+ description: |
6
+ Use when the user wants YouTube analysis through yt-mcp: channel performance,
7
+ video stats, subtitles, comments, trending, or batch YouTube data work.
8
+ Also use when the user pastes YouTube links such as youtube.com/watch,
9
+ youtube.com/shorts, youtube.com/live, or youtu.be URLs.
10
+ Keywords: YouTube, 频道, 视频, 字幕, 评论, 均播, 播放量, trending, yt-mcp, youtube.com, youtu.be.
11
+ ---
12
+
13
+ # Use yt-mcp
14
+
15
+ Focus on YouTube analysis with yt-mcp MCP tools. Answer the analysis question first. Only switch into setup or troubleshooting when the user explicitly asks for it or a tool call fails.
16
+
17
+ ## 0. Hard Constraints
18
+
19
+ - For YouTube data tasks, always use `mcp__yt-mcp__*` tools first.
20
+ - Do not route YouTube queries to legacy MCPs or legacy setup skills such as `youtube-data`, `mcp_youtube_data`, `use-youtube-data-mcp`, or `Use YouTube Data MCP` when `yt-mcp` can answer.
21
+ - Do not fetch YouTube metrics, subtitles, comments, channel data, or trending data by browsing the web, scraping pages, or using generic web search. Use `yt-mcp` instead.
22
+ - If `yt-mcp` is unavailable or fails, stop and tell the user it needs setup or troubleshooting. Do not silently fall back to old MCPs or ad-hoc web lookup.
23
+
24
+ ## Trigger Hints
25
+
26
+ - Treat pasted YouTube links as an automatic trigger for this skill, even if the user does not explicitly say "YouTube".
27
+ - Common URL forms: `https://www.youtube.com/watch?v=...`, `https://youtu.be/...`, `https://www.youtube.com/shorts/...`, `https://www.youtube.com/live/...`.
28
+
29
+ ## 1. Intent -> Tool Routing
30
+
31
+ | User Intent | Tool(s) |
32
+ |---|---|
33
+ | Search videos by keyword | `search_videos` |
34
+ | Get video metrics in bulk | `get_video_stats` |
35
+ | Large video stats job | `start_video_stats_job` -> `poll_video_stats_job` |
36
+ | Get subtitles / transcript | `get_subtitles`, `batch_get_subtitles` |
37
+ | Check subtitle languages | `list_available_subtitles` |
38
+ | Get comments | `get_comments` |
39
+ | Large comments export | `start_comments_job` -> `poll_comments_job` |
40
+ | Get comment replies | `get_comment_replies` |
41
+ | Channel stats | `get_channel_stats` |
42
+ | List channel uploads | `list_channel_uploads` |
43
+ | Trending videos | `get_trending` |
44
+ | Local download | `start_download_job` -> `poll_download_job` |
45
+
46
+ All tools are prefixed `mcp__yt-mcp__` in actual calls.
47
+
48
+ ## 2. Analysis Rules
49
+
50
+ - Prefer batch-capable tools when comparing multiple videos or channels.
51
+ - `get_video_stats` accepts up to 1000 items. If the response returns `mode: "file"`, then inline `preview_items` is only a preview sample from a larger dataset and must not be used as the full basis of analysis. Tell the user the complete result is in `output_file` or `download_url`.
52
+ - For large video/comment analyses where completeness matters, prefer async jobs: `start_video_stats_job` -> `poll_video_stats_job`, or `start_comments_job` -> `poll_comments_job`.
53
+ - When using async jobs, poll until status is `completed`, and tell the user if the result is still partial or in progress.
54
+ - Use normalized response fields in analysis: `published_at`, `duration`, `view_count`, `like_count`, `comment_count`, `is_live_replay`.
55
+ - Default channel analysis window to 30 days unless the user specifies another range.
56
+ - Shorts heuristic: parsed ISO 8601 `duration` <= 90 seconds. Live replay: `is_live_replay === true`. Otherwise treat as long-form.
57
+ - Follow the user's language. Use thousand separators like `1,234,567`; avoid `K/M/B` unless the user asks.
58
+ - Do not dump raw JSON unless the user explicitly wants raw output.
59
+
60
+ ## 3. Common Workflows
61
+
62
+ ### Channel Average Views
63
+
64
+ 1. `list_channel_uploads(channel or channel_id, max_results=N)`
65
+ 2. For moderate sets, `get_video_stats(videos=[...ids])`; for large sets or when full coverage matters, `start_video_stats_job(videos=[...ids])` then `poll_video_stats_job(job_id)` until completed
66
+ 3. Filter by `published_at` within the requested window
67
+ 4. Segment into long-form, Shorts, live replay
68
+ 5. Report video count, total views, average views, max, min
69
+
70
+ Default output:
71
+
72
+ ```md
73
+ ## {频道名} 均播分析(近 {N} 天)
74
+
75
+ | 类型 | 视频数 | 总播放 | 均播 | 最高 | 最低 |
76
+ |---|---:|---:|---:|---:|---:|
77
+ | 长视频 | 12 | 1,234,567 | 102,880 | 456,789 | 12,345 |
78
+ | Shorts | 24 | 2,345,678 | 97,736 | 234,567 | 23,456 |
79
+ | 直播回放 | 3 | 345,678 | 115,226 | 200,000 | 45,678 |
80
+ ```
81
+
82
+ ### Video Summary
83
+
84
+ 1. `get_subtitles(video, format="csv")`
85
+ 2. Build a timestamped summary from the transcript
86
+ 3. Output section headers with timestamps, not a single long paragraph
87
+
88
+ ### Comment Analysis
89
+
90
+ 1. For normal analysis, `get_comments(video, max_comments=N, order="relevance")`; for larger exports, `start_comments_job` then `poll_comments_job`
91
+ 2. Cluster by sentiment or topic
92
+ 3. Quote representative comments and keep like counts when useful
93
+
94
+ ### Channel Stability
95
+
96
+ 1. Fetch uploads and stats
97
+ 2. Measure posting frequency, average views, and volatility
98
+ 3. Compare recent videos vs earlier videos to judge trend direction
99
+
100
+ ## 4. Output Rules
101
+
102
+ - Answer the user's actual question first, then show supporting metrics.
103
+ - For comparisons, prefer compact tables over long prose.
104
+ - State assumptions when the time window, sample size, or language is inferred.
105
+ - If data is partial, say so directly.
106
+
107
+ ## 5. Setup Boundary
108
+
109
+ Only discuss CLI when the user asks to install or troubleshoot yt-mcp, or when a tool call fails because of auth/cookies/runtime.
110
+
111
+ Useful commands:
112
+
113
+ - `yt-mcp setup`
114
+ - `yt-mcp setup-cookies`
115
+ - `yt-mcp check`
116
+
117
+ Do not turn a normal analysis request into a setup checklist.