@mkterswingman/5mghost-yonder 0.0.37 → 0.0.38

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/dist/cli/check.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { PATHS, loadConfig } from "../utils/config.js";
3
- import { TokenManager } from "../auth/tokenManager.js";
3
+ import { TokenManager } from "@mkterswingman/5mghost-shared-client/auth";
4
4
  import { hasSIDCookies, areCookiesExpired } from "../utils/cookies.js";
5
5
  import { getYtDlpVersion } from "../utils/ytdlpPath.js";
6
6
  import { runRuntimeCommand } from "./runtime.js";
@@ -9,7 +9,11 @@ import { runRuntimeCommand } from "./runtime.js";
9
9
  */
10
10
  export async function runCheck() {
11
11
  const config = loadConfig();
12
- const tm = new TokenManager(config.auth_url);
12
+ const tm = new TokenManager({
13
+ authUrl: config.auth_url,
14
+ authJsonPath: PATHS.sharedAuthJson,
15
+ envTokenName: "YT_MCP_TOKEN",
16
+ });
13
17
  console.log("yt-mcp check\n");
14
18
  // 1. Token
15
19
  const token = await tm.getValidToken();
package/dist/cli/index.js CHANGED
File without changes
@@ -1,16 +1,7 @@
1
1
  import { fileURLToPath } from "node:url";
2
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
- }
3
+ import { buildSkillInstallPlan, installSkillTarget, } from "@mkterswingman/5mghost-shared-client/registration";
4
+ import { detectCli } from "@mkterswingman/5mghost-shared-client";
14
5
  function resolvePackageRoot() {
15
6
  return join(dirname(fileURLToPath(import.meta.url)), "..", "..");
16
7
  }
@@ -24,7 +15,7 @@ export async function runInstallSkills() {
24
15
  "gemini-internal",
25
16
  "gemini",
26
17
  ].filter(detectCli);
27
- const plan = buildSkillInstallPlan(packageRoot, {
18
+ const plan = buildSkillInstallPlan(packageRoot, "use-yt-mcp", {
28
19
  availableCliNames,
29
20
  includeOpenClaw: process.env.YT_MCP_INSTALL_OPENCLAW_SKILL === "1",
30
21
  });
package/dist/cli/serve.js CHANGED
@@ -1,11 +1,15 @@
1
1
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
2
- import { loadConfig } from "../utils/config.js";
3
- import { TokenManager } from "../auth/tokenManager.js";
2
+ import { loadConfig, PATHS } from "../utils/config.js";
3
+ import { TokenManager } from "@mkterswingman/5mghost-shared-client/auth";
4
4
  import { DownloadJobManager } from "../download/jobManager.js";
5
5
  import { createServer } from "../server.js";
6
6
  export async function runServe() {
7
7
  const config = loadConfig();
8
- const tokenManager = new TokenManager(config.auth_url);
8
+ const tokenManager = new TokenManager({
9
+ authUrl: config.auth_url,
10
+ authJsonPath: PATHS.sharedAuthJson,
11
+ envTokenName: "YT_MCP_TOKEN",
12
+ });
9
13
  const downloadJobManager = new DownloadJobManager();
10
14
  // PAT mode via env var (don't persist — just keep in memory for this session)
11
15
  const pat = process.env.YT_MCP_TOKEN;
package/dist/cli/setup.js CHANGED
@@ -2,25 +2,15 @@ import { execFileSync } from "node:child_process";
2
2
  import { existsSync } from "node:fs";
3
3
  import { createInterface } from "node:readline/promises";
4
4
  import { loadConfig, saveConfig, PATHS, ensureConfigDir } from "../utils/config.js";
5
- import { TokenManager } from "../auth/tokenManager.js";
6
- import { runOAuthFlow } from "../auth/oauthFlow.js";
5
+ import { TokenManager } from "@mkterswingman/5mghost-shared-client/auth";
6
+ import { runOAuthFlow } from "@mkterswingman/5mghost-shared-client/auth";
7
7
  import { hasSIDCookies } from "../utils/cookies.js";
8
8
  import { buildLauncherCommand, writeLauncherFile } from "../utils/launcher.js";
9
9
  import { buildBrowserOpenCommand } from "../utils/browserLaunch.js";
10
10
  import { checkAll } from "../runtime/installers.js";
11
11
  import { runInstallSkills } from "./installSkills.js";
12
- import { getOpenClawConfigPath, isOpenClawInstallLikelyInstalled, writeOpenClawConfig, } from "../utils/openClaw.js";
13
- import { getCodexInternalConfigPath, writeCodexInternalConfig, } from "../utils/codexInternal.js";
14
- import { MCP_REGISTER_TIMEOUT_MS, classifyRegistrationFailure, } from "../utils/mcpRegistration.js";
15
- function detectCli(name) {
16
- try {
17
- execFileSync(name, ["--version"], { stdio: "pipe" });
18
- return true;
19
- }
20
- catch {
21
- return false;
22
- }
23
- }
12
+ import { registerCliCandidates, registerOpenClaw, } from "@mkterswingman/5mghost-shared-client/registration";
13
+ import { getOpenClawConfigPath, getCodexInternalConfigPath, writeCodexInternalConfig, detectCli, isOpenClawInstallLikelyInstalled, MCP_REGISTER_TIMEOUT_MS, classifyRegistrationFailure, } from "@mkterswingman/5mghost-shared-client";
24
14
  function tryRegisterMcp(cmd, label) {
25
15
  try {
26
16
  execFileSync(cmd.file, cmd.args, { stdio: "pipe", timeout: MCP_REGISTER_TIMEOUT_MS });
@@ -215,7 +205,11 @@ export async function runSetup() {
215
205
  console.log(` ✅ Shared auth: ${PATHS.sharedAuthJson}`);
216
206
  // ── Step 3: Authentication ──
217
207
  console.log("Step 3/5: Authentication...");
218
- const tokenManager = new TokenManager(config.auth_url);
208
+ const tokenManager = new TokenManager({
209
+ authUrl: config.auth_url,
210
+ authJsonPath: PATHS.sharedAuthJson,
211
+ envTokenName: "YT_MCP_TOKEN",
212
+ });
219
213
  const pat = process.env.YT_MCP_TOKEN;
220
214
  if (pat) {
221
215
  // Explicit PAT provided via env var
@@ -270,7 +264,11 @@ export async function runSetup() {
270
264
  }
271
265
  console.log(` OAuth 等待上限:${Math.round(oauthTimeoutMs / 1000)}s`);
272
266
  try {
273
- const tokens = await runOAuthFlow(config.auth_url, { timeoutMs: oauthTimeoutMs });
267
+ const tokens = await runOAuthFlow({
268
+ clientName: "yt-mcp-cli",
269
+ authUrl: config.auth_url,
270
+ timeoutMs: oauthTimeoutMs,
271
+ });
274
272
  await tokenManager.saveTokens(tokens.accessToken, tokens.refreshToken, tokens.expiresIn, tokens.clientId);
275
273
  console.log(" ✅ OAuth login successful");
276
274
  console.log(" ℹ️ Other first-party local MCPs on this machine can reuse this login.");
@@ -363,18 +361,10 @@ export async function runSetup() {
363
361
  // ── Step 5: MCP Registration ──
364
362
  console.log("Step 5/5: Registering MCP in AI clients...");
365
363
  const launcherCommand = buildLauncherCommand();
366
- let registered = false;
367
- let skillsInstalled = false;
368
- for (const { bin, label, command } of buildSetupCliCandidates({
369
- file: launcherCommand.command,
370
- args: launcherCommand.args,
371
- })) {
372
- if (!detectCli(bin))
373
- continue;
374
- if (tryRegisterMcp(command, label)) {
375
- registered = true;
376
- }
377
- }
364
+ let registered = registerCliCandidates({
365
+ serverName: "yt-mcp",
366
+ launcherCommand,
367
+ });
378
368
  if (isOpenClawInstallLikelyInstalled(detectCli)) {
379
369
  try {
380
370
  let openClawRegistered = false;
@@ -385,10 +375,11 @@ export async function runSetup() {
385
375
  }), "OpenClaw (mcporter config)");
386
376
  }
387
377
  if (!openClawRegistered) {
388
- const status = writeOpenClawConfig("yt-mcp", launcherCommand);
389
- const suffix = status === "created" ? "created" : "updated";
390
- console.log(` ✅ MCP registered in OpenClaw (${suffix} ${getOpenClawConfigPath()})`);
391
- openClawRegistered = true;
378
+ const result = registerOpenClaw("yt-mcp", launcherCommand);
379
+ if (result) {
380
+ console.log(` ✅ MCP registered in OpenClaw (${result.method === "cli" ? "mcporter config" : result.status} ${getOpenClawConfigPath()})`);
381
+ openClawRegistered = true;
382
+ }
392
383
  }
393
384
  if (openClawRegistered) {
394
385
  registered = true;
@@ -409,6 +400,7 @@ export async function runSetup() {
409
400
  console.log(` ⚠️ Codex CLI (internal) auto-register failed: ${err instanceof Error ? err.message : String(err)}`);
410
401
  }
411
402
  }
403
+ let skillsInstalled = false;
412
404
  try {
413
405
  process.env.YT_MCP_INSTALL_OPENCLAW_SKILL = isOpenClawInstallLikelyInstalled(detectCli) ? "1" : "0";
414
406
  await runInstallSkills();
@@ -1,17 +1,8 @@
1
1
  import { execFileSync, spawn } from "node:child_process";
2
2
  import { existsSync, rmSync } from "node:fs";
3
3
  import { PATHS } from "../utils/config.js";
4
- import { getOpenClawConfigPath, removeOpenClawConfig } from "../utils/openClaw.js";
5
- import { getCodexInternalConfigPath, removeCodexInternalConfig, } from "../utils/codexInternal.js";
6
- function detectCli(name) {
7
- try {
8
- execFileSync(name, ["--version"], { stdio: "pipe" });
9
- return true;
10
- }
11
- catch {
12
- return false;
13
- }
14
- }
4
+ import { getOpenClawConfigPath, getCodexInternalConfigPath, removeCodexInternalConfig, unregisterOpenClaw, detectCli, } from "@mkterswingman/5mghost-shared-client";
5
+ import { unregisterCliCandidates } from "@mkterswingman/5mghost-shared-client/registration";
15
6
  function tryRemoveMcp(command, label) {
16
7
  try {
17
8
  execFileSync(command.file, command.args, { stdio: "pipe" });
@@ -64,13 +55,9 @@ function scheduleSelfUninstall() {
64
55
  export async function runUninstall() {
65
56
  console.log("\n🧹 yt-mcp uninstall\n");
66
57
  console.log("Removing MCP client registrations...");
67
- for (const candidate of buildUninstallCliCandidates()) {
68
- if (!detectCli(candidate.bin))
69
- continue;
70
- tryRemoveMcp(candidate.command, candidate.label);
71
- }
72
- const openClawStatus = removeOpenClawConfig("yt-mcp");
73
- if (openClawStatus === "removed") {
58
+ unregisterCliCandidates({ serverName: "yt-mcp" });
59
+ const openClawResult = unregisterOpenClaw("yt-mcp");
60
+ if (openClawResult === "removed") {
74
61
  console.log(` ✅ Removed MCP registration from OpenClaw (${getOpenClawConfigPath()})`);
75
62
  }
76
63
  else {
package/dist/server.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import type { YtMcpConfig } from "./utils/config.js";
3
- import type { TokenManager } from "./auth/tokenManager.js";
3
+ import type { TokenManager } from "@mkterswingman/5mghost-shared-client/auth";
4
4
  import { DownloadJobManager } from "./download/jobManager.js";
5
5
  import type { DownloadToolDeps } from "./tools/downloads.js";
6
6
  /**
@@ -1,6 +1,6 @@
1
1
  import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import type { YtMcpConfig } from "../utils/config.js";
3
- import type { TokenManager } from "../auth/tokenManager.js";
3
+ import type { TokenManager } from "@mkterswingman/5mghost-shared-client/auth";
4
4
  import { downloadOneItem } from "../download/downloader.js";
5
5
  import type { DownloadJobManager } from "../download/jobManager.js";
6
6
  export interface DownloadToolDeps {
@@ -1,4 +1,4 @@
1
1
  import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import type { YtMcpConfig } from "../utils/config.js";
3
- import type { TokenManager } from "../auth/tokenManager.js";
3
+ import type { TokenManager } from "@mkterswingman/5mghost-shared-client/auth";
4
4
  export declare function registerRemoteTools(server: McpServer, config: YtMcpConfig, tokenManager: TokenManager): void;
@@ -1,6 +1,6 @@
1
1
  import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import type { YtMcpConfig } from "../utils/config.js";
3
- import type { TokenManager } from "../auth/tokenManager.js";
3
+ import type { TokenManager } from "@mkterswingman/5mghost-shared-client/auth";
4
4
  export declare function toReadableSubtitleJobError(error: unknown): string;
5
5
  /**
6
6
  * Convert VTT subtitle content to clean, human-readable CSV.
@@ -1,12 +1,7 @@
1
- export interface LauncherSourceOptions {
2
- packageSpec?: string;
3
- npmCacheDir?: string;
4
- }
5
- export interface LauncherCommand {
6
- command: string;
7
- args: string[];
8
- }
1
+ import { isRepairableNpxFailure } from "@mkterswingman/5mghost-shared-client/launcher";
2
+ import type { LauncherCommand, LauncherSourceOptions } from "@mkterswingman/5mghost-shared-client/launcher";
3
+ export type { LauncherCommand, LauncherSourceOptions };
4
+ export { isRepairableNpxFailure };
9
5
  export declare function buildLauncherCommand(launcherPath?: string): LauncherCommand;
10
- export declare function isRepairableNpxFailure(stderr: string): boolean;
11
- export declare function buildLauncherSource(options?: LauncherSourceOptions): string;
12
- export declare function writeLauncherFile(options?: LauncherSourceOptions): string;
6
+ export declare function buildLauncherSource(options?: Partial<LauncherSourceOptions>): string;
7
+ export declare function writeLauncherFile(options?: Partial<LauncherSourceOptions>): string;
@@ -1,90 +1,19 @@
1
- import { mkdirSync, writeFileSync } from "node:fs";
2
- import { dirname } from "node:path";
3
1
  import { PATHS } from "./config.js";
2
+ import { buildLauncherCommand as _buildLauncherCommand, buildLauncherSource as _buildLauncherSource, writeLauncherFile as _writeLauncherFile, isRepairableNpxFailure, } from "@mkterswingman/5mghost-shared-client/launcher";
3
+ export { isRepairableNpxFailure };
4
4
  const DEFAULT_PACKAGE_SPEC = "@mkterswingman/5mghost-yonder@latest";
5
5
  export function buildLauncherCommand(launcherPath = PATHS.launcherJs) {
6
- return {
7
- command: "node",
8
- args: [launcherPath, "serve"],
9
- };
10
- }
11
- export function isRepairableNpxFailure(stderr) {
12
- const lower = stderr.toLowerCase();
13
- const referencesNpxDir = lower.includes("_npx/") || lower.includes("_npx\\");
14
- return referencesNpxDir && (lower.includes("enotempty") || lower.includes("rename"));
6
+ return _buildLauncherCommand(launcherPath);
15
7
  }
16
8
  export function buildLauncherSource(options = {}) {
17
- const packageSpec = options.packageSpec ?? DEFAULT_PACKAGE_SPEC;
18
- const npmCacheDir = options.npmCacheDir ?? PATHS.npmCacheDir;
19
- return `#!/usr/bin/env node
20
- import { existsSync, mkdirSync, renameSync } from "node:fs";
21
- import { join } from "node:path";
22
- import { spawnSync } from "node:child_process";
23
-
24
- const packageSpec = ${JSON.stringify(packageSpec)};
25
- const npmCacheDir = ${JSON.stringify(npmCacheDir)};
26
- const args = process.argv.slice(2);
27
- const targetArgs = args.length > 0 ? args : ["serve"];
28
-
29
- function isSkillInstallerMode(subArgs) {
30
- return subArgs[0] === "install-skills";
31
- }
32
-
33
- function isRepairableNpxFailure(stderr) {
34
- const lower = stderr.toLowerCase();
35
- return (lower.includes("_npx/") || lower.includes("_npx\\\\"))
36
- && (lower.includes("enotempty") || lower.includes("rename"));
37
- }
38
-
39
- function runNpx(subArgs, captureStdErrOnly = false) {
40
- mkdirSync(npmCacheDir, { recursive: true });
41
- const npxBin = process.platform === "win32" ? "npx.cmd" : "npx";
42
- return spawnSync(npxBin, ["--yes", packageSpec, ...subArgs], {
43
- env: { ...process.env, npm_config_cache: npmCacheDir },
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.
46
- stdio: captureStdErrOnly ? ["ignore", "ignore", "pipe"] : "inherit",
47
- });
48
- }
49
-
50
- function rotateNpxDir() {
51
- const npxDir = join(npmCacheDir, "_npx");
52
- if (!existsSync(npxDir)) return false;
53
- const backup = \`\${npxDir}.bad.\${new Date().toISOString().replace(/[^0-9]/g, "").slice(0, 14)}\`;
54
- renameSync(npxDir, backup);
55
- process.stderr.write(\`[yt-mcp launcher] repaired corrupted npx cache: \${backup}\\n\`);
56
- return true;
57
- }
58
-
59
- function ensurePackageReady() {
60
- const first = runNpx(["version"], true);
61
- if ((first.status ?? 1) === 0) return;
62
-
63
- const stderr = first.stderr ? String(first.stderr) : "";
64
- if (!isRepairableNpxFailure(stderr) || !rotateNpxDir()) {
65
- if (stderr) process.stderr.write(stderr);
66
- process.exit(first.status ?? 1);
67
- }
68
-
69
- const second = runNpx(["version"], true);
70
- const secondStderr = second.stderr ? String(second.stderr) : "";
71
- if ((second.status ?? 1) !== 0) {
72
- if (secondStderr) process.stderr.write(secondStderr);
73
- process.exit(second.status ?? 1);
74
- }
75
- }
76
-
77
- ensurePackageReady();
78
- if (isSkillInstallerMode(targetArgs)) {
79
- process.env.YT_MCP_INSTALL_SKILLS = "1";
80
- }
81
- const finalRun = runNpx(targetArgs, false);
82
- process.exit(finalRun.status ?? 0);
83
- `;
9
+ return _buildLauncherSource({
10
+ packageSpec: options.packageSpec ?? DEFAULT_PACKAGE_SPEC,
11
+ npmCacheDir: options.npmCacheDir ?? PATHS.npmCacheDir,
12
+ });
84
13
  }
85
14
  export function writeLauncherFile(options = {}) {
86
- mkdirSync(dirname(PATHS.launcherJs), { recursive: true });
87
- const source = buildLauncherSource(options);
88
- writeFileSync(PATHS.launcherJs, source, { encoding: "utf8", mode: 0o755 });
89
- return PATHS.launcherJs;
15
+ return _writeLauncherFile({
16
+ packageSpec: options.packageSpec ?? DEFAULT_PACKAGE_SPEC,
17
+ npmCacheDir: options.npmCacheDir ?? PATHS.npmCacheDir,
18
+ }, PATHS.launcherJs);
90
19
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mkterswingman/5mghost-yonder",
3
- "version": "0.0.37",
3
+ "version": "0.0.38",
4
4
  "description": "Internal MCP client with local data tools and remote API proxy",
5
5
  "type": "module",
6
6
  "bin": {
@@ -17,6 +17,7 @@
17
17
  "start": "node dist/cli/index.js"
18
18
  },
19
19
  "dependencies": {
20
+ "@mkterswingman/5mghost-shared-client": "^0.0.2",
20
21
  "@modelcontextprotocol/sdk": "^1.28.0",
21
22
  "playwright": "^1.58.0",
22
23
  "zod": "^4.3.6"
@@ -1,9 +0,0 @@
1
- export interface OAuthFlowOptions {
2
- timeoutMs?: number;
3
- }
4
- export declare function runOAuthFlow(authUrl: string, options?: OAuthFlowOptions): Promise<{
5
- accessToken: string;
6
- refreshToken: string;
7
- expiresIn: number;
8
- clientId: string;
9
- }>;
@@ -1,151 +0,0 @@
1
- import { createServer } from "node:http";
2
- import { randomBytes, createHash } from "node:crypto";
3
- import { URL } from "node:url";
4
- import { buildBrowserOpenCommand } from "../utils/browserLaunch.js";
5
- function base64url(buf) {
6
- return buf
7
- .toString("base64")
8
- .replace(/\+/g, "-")
9
- .replace(/\//g, "_")
10
- .replace(/=+$/, "");
11
- }
12
- export async function runOAuthFlow(authUrl, options = {}) {
13
- // 1. Generate PKCE + state
14
- const codeVerifier = base64url(randomBytes(32));
15
- const codeChallenge = base64url(createHash("sha256").update(codeVerifier).digest());
16
- const state = base64url(randomBytes(32));
17
- // 2. Start temp HTTP server to get the actual port for redirect_uri
18
- const { server: httpServer, port } = await startCallbackServer();
19
- const redirectUri = `http://127.0.0.1:${port}`;
20
- // 3. DCR register client with actual redirect_uri (including port)
21
- let clientId;
22
- let clientSecret;
23
- try {
24
- const dcrRes = await fetch(`${authUrl}/oauth/register`, {
25
- method: "POST",
26
- headers: { "Content-Type": "application/json" },
27
- body: JSON.stringify({
28
- client_name: "yt-mcp-cli",
29
- redirect_uris: [redirectUri],
30
- }),
31
- });
32
- if (!dcrRes.ok) {
33
- const text = await dcrRes.text().catch(() => "");
34
- httpServer.close();
35
- throw new Error(`DCR registration failed: ${dcrRes.status} ${text}`);
36
- }
37
- const dcrBody = (await dcrRes.json());
38
- clientId = dcrBody.client_id;
39
- clientSecret = dcrBody.client_secret;
40
- }
41
- catch (err) {
42
- httpServer.close();
43
- throw err;
44
- }
45
- // 4. Wait for OAuth callback
46
- return new Promise((resolve, reject) => {
47
- const timeoutMs = options.timeoutMs ?? 5 * 60 * 1000;
48
- const timeout = setTimeout(() => {
49
- httpServer.close();
50
- reject(new Error(`OAuth flow timed out after ${Math.round(timeoutMs / 1000)}s`));
51
- }, timeoutMs);
52
- function cleanup() {
53
- clearTimeout(timeout);
54
- httpServer.close();
55
- }
56
- httpServer.on("request", async (req, res) => {
57
- try {
58
- const url = new URL(req.url ?? "/", `http://127.0.0.1`);
59
- const code = url.searchParams.get("code");
60
- const error = url.searchParams.get("error");
61
- const returnedState = url.searchParams.get("state");
62
- if (error) {
63
- const safeError = error.replace(/[<>&"']/g, (c) => `&#${c.charCodeAt(0)};`);
64
- res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
65
- res.end(`<h1>Authorization failed</h1><p>${safeError}</p>`);
66
- cleanup();
67
- reject(new Error(`OAuth error: ${error}`));
68
- return;
69
- }
70
- if (!code) {
71
- res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
72
- res.end("<h1>Waiting for authorization...</h1>");
73
- return;
74
- }
75
- // Verify state to prevent CSRF
76
- if (returnedState !== state) {
77
- res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
78
- res.end("<h1>Authorization failed</h1><p>State mismatch — possible CSRF attack.</p>");
79
- cleanup();
80
- reject(new Error("OAuth state mismatch"));
81
- return;
82
- }
83
- // Exchange code for tokens
84
- const tokenBody = {
85
- grant_type: "authorization_code",
86
- code,
87
- redirect_uri: redirectUri,
88
- client_id: clientId,
89
- code_verifier: codeVerifier,
90
- };
91
- if (clientSecret) {
92
- tokenBody.client_secret = clientSecret;
93
- }
94
- const tokenRes = await fetch(`${authUrl}/oauth/token`, {
95
- method: "POST",
96
- headers: { "Content-Type": "application/json" },
97
- body: JSON.stringify(tokenBody),
98
- });
99
- if (!tokenRes.ok) {
100
- const text = await tokenRes.text().catch(() => "");
101
- res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
102
- res.end(`<h1>Token exchange failed</h1><p>${text}</p>`);
103
- cleanup();
104
- reject(new Error(`Token exchange failed: ${tokenRes.status} ${text}`));
105
- return;
106
- }
107
- const tokens = (await tokenRes.json());
108
- res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
109
- res.end("<h1>Authorization successful!</h1><p>You can close this window.</p>");
110
- cleanup();
111
- resolve({
112
- accessToken: tokens.access_token,
113
- refreshToken: tokens.refresh_token,
114
- expiresIn: tokens.expires_in,
115
- clientId,
116
- });
117
- }
118
- catch (err) {
119
- res.writeHead(500);
120
- res.end("Internal error");
121
- cleanup();
122
- reject(err);
123
- }
124
- });
125
- // Open browser
126
- const authorizeUrl = `${authUrl}/oauth/authorize?response_type=code&client_id=${encodeURIComponent(clientId)}&redirect_uri=${encodeURIComponent(redirectUri)}&code_challenge=${encodeURIComponent(codeChallenge)}&code_challenge_method=S256&state=${encodeURIComponent(state)}`;
127
- console.log("\n\x1b[1mOpen this URL in your browser to authorize:\x1b[0m");
128
- console.log(`\n ${authorizeUrl}\n`);
129
- import("node:child_process").then(({ execFile }) => {
130
- const command = buildBrowserOpenCommand(authorizeUrl);
131
- execFile(command.file, command.args);
132
- }).catch(() => {
133
- // ignore — user can open manually
134
- });
135
- });
136
- }
137
- /** Start an HTTP server on a random port and return the server + port. */
138
- async function startCallbackServer() {
139
- const server = createServer();
140
- return new Promise((resolve, reject) => {
141
- server.listen(0, "127.0.0.1", () => {
142
- const addr = server.address();
143
- if (!addr || typeof addr === "string") {
144
- server.close();
145
- reject(new Error("Failed to start callback server"));
146
- return;
147
- }
148
- resolve({ server, port: addr.port });
149
- });
150
- });
151
- }
@@ -1,10 +0,0 @@
1
- export interface SharedAuthData {
2
- type: "jwt" | "pat";
3
- access_token?: string;
4
- refresh_token?: string;
5
- expires_at?: number;
6
- client_id?: string;
7
- pat?: string;
8
- }
9
- export declare function readSharedAuth(authPath: string): SharedAuthData | null;
10
- export declare function writeSharedAuth(authPath: string, data: SharedAuthData): void;
@@ -1,31 +0,0 @@
1
- import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
2
- import { dirname } from "node:path";
3
- export function readSharedAuth(authPath) {
4
- if (!existsSync(authPath))
5
- return null;
6
- try {
7
- return JSON.parse(readFileSync(authPath, "utf8"));
8
- }
9
- catch {
10
- return null;
11
- }
12
- }
13
- export function writeSharedAuth(authPath, data) {
14
- const authDir = dirname(authPath);
15
- mkdirSync(authDir, { recursive: true });
16
- try {
17
- chmodSync(authDir, 0o700);
18
- }
19
- catch {
20
- // Why: best-effort hardening; Windows may ignore POSIX-style directory modes.
21
- }
22
- const tempPath = `${authPath}.tmp`;
23
- writeFileSync(tempPath, JSON.stringify(data, null, 2), { encoding: "utf8", mode: 0o600 });
24
- renameSync(tempPath, authPath);
25
- try {
26
- chmodSync(authPath, 0o600);
27
- }
28
- catch {
29
- // Why: chmod is best-effort on Windows and should not block auth persistence.
30
- }
31
- }
@@ -1,18 +0,0 @@
1
- interface TokenManagerOptions {
2
- authPath?: string;
3
- env?: NodeJS.ProcessEnv;
4
- fetchImpl?: typeof fetch;
5
- }
6
- export declare class TokenManager {
7
- private authUrl;
8
- private authPath;
9
- private env;
10
- private fetchImpl;
11
- constructor(authUrl: string, options?: TokenManagerOptions);
12
- getValidToken(): Promise<string | null>;
13
- saveTokens(accessToken: string, refreshToken: string, expiresIn: number, clientId?: string): Promise<void>;
14
- savePAT(pat: string): Promise<void>;
15
- private readAuth;
16
- private refreshTokens;
17
- }
18
- export {};
@@ -1,92 +0,0 @@
1
- import { PATHS } from "../utils/config.js";
2
- import { readSharedAuth, writeSharedAuth } from "./sharedAuth.js";
3
- export class TokenManager {
4
- authUrl;
5
- authPath;
6
- env;
7
- fetchImpl;
8
- constructor(authUrl, options = {}) {
9
- this.authUrl = authUrl;
10
- this.authPath = options.authPath ?? PATHS.sharedAuthJson;
11
- this.env = options.env ?? process.env;
12
- this.fetchImpl = options.fetchImpl ?? fetch;
13
- }
14
- async getValidToken() {
15
- // Check env var PAT first
16
- const envPat = this.env.YT_MCP_TOKEN;
17
- if (envPat)
18
- return envPat;
19
- const auth = this.readAuth();
20
- if (!auth)
21
- return null;
22
- if (auth.type === "pat" && auth.pat) {
23
- return auth.pat;
24
- }
25
- if (auth.type === "jwt") {
26
- if (auth.access_token && auth.expires_at && Date.now() < auth.expires_at) {
27
- return auth.access_token;
28
- }
29
- // Try refresh
30
- if (auth.refresh_token) {
31
- try {
32
- const refreshed = await this.refreshTokens(auth.refresh_token, auth.client_id);
33
- if (refreshed) {
34
- await this.saveTokens(refreshed.access_token, refreshed.refresh_token, refreshed.expires_in, auth.client_id);
35
- return refreshed.access_token;
36
- }
37
- }
38
- catch {
39
- // refresh failed → AUTH_EXPIRED
40
- }
41
- }
42
- return null;
43
- }
44
- return null;
45
- }
46
- async saveTokens(accessToken, refreshToken, expiresIn, clientId) {
47
- const data = {
48
- type: "jwt",
49
- access_token: accessToken,
50
- refresh_token: refreshToken,
51
- expires_at: Date.now() + expiresIn * 1000,
52
- client_id: clientId,
53
- };
54
- writeSharedAuth(this.authPath, data);
55
- }
56
- async savePAT(pat) {
57
- const data = {
58
- type: "pat",
59
- pat,
60
- };
61
- writeSharedAuth(this.authPath, data);
62
- }
63
- readAuth() {
64
- return readSharedAuth(this.authPath);
65
- }
66
- async refreshTokens(refreshToken, clientId) {
67
- const url = `${this.authUrl}/oauth/token`;
68
- const body = {
69
- grant_type: "refresh_token",
70
- refresh_token: refreshToken,
71
- };
72
- if (clientId) {
73
- body.client_id = clientId;
74
- }
75
- const res = await this.fetchImpl(url, {
76
- method: "POST",
77
- headers: { "Content-Type": "application/json" },
78
- body: JSON.stringify(body),
79
- });
80
- if (!res.ok)
81
- return null;
82
- try {
83
- const body = (await res.json());
84
- if (!body.access_token)
85
- return null;
86
- return body;
87
- }
88
- catch {
89
- return null;
90
- }
91
- }
92
- }
@@ -1,9 +0,0 @@
1
- import type { LauncherCommand } from "./launcher.js";
2
- export declare function getCodexInternalConfigPath(homeDir?: string): string;
3
- export declare function upsertCodexInternalConfigText(currentText: string | null, serverName: string, launcherCommand: LauncherCommand): string;
4
- export declare function removeCodexInternalConfigEntryText(currentText: string | null, serverName: string): {
5
- changed: boolean;
6
- nextText: string | null;
7
- };
8
- export declare function writeCodexInternalConfig(serverName: string, launcherCommand: LauncherCommand, configPath?: string): "created" | "updated";
9
- export declare function removeCodexInternalConfig(serverName: string, configPath?: string): "removed" | "missing";
@@ -1,60 +0,0 @@
1
- import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
- import { homedir } from "node:os";
3
- import { dirname, join } from "node:path";
4
- export function getCodexInternalConfigPath(homeDir = homedir()) {
5
- return join(homeDir, ".codex-internal", "config.toml");
6
- }
7
- function formatCodexInternalSection(serverName, launcherCommand) {
8
- const escapedName = serverName.replace(/"/g, '\\"');
9
- const args = launcherCommand.args.map((arg) => JSON.stringify(arg)).join(", ");
10
- return `[mcp_servers.${JSON.stringify(escapedName)}]
11
- command = ${JSON.stringify(launcherCommand.command)}
12
- args = [${args}]
13
- `;
14
- }
15
- export function upsertCodexInternalConfigText(currentText, serverName, launcherCommand) {
16
- const section = formatCodexInternalSection(serverName, launcherCommand).trimEnd();
17
- const sectionPattern = new RegExp(String.raw `(?:^|\n)\[mcp_servers\.${JSON.stringify(serverName).replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\]\n(?:.+\n?)*?(?=\n\[|$)`, "m");
18
- if (!currentText || currentText.trim() === "") {
19
- return `${section}\n`;
20
- }
21
- if (sectionPattern.test(currentText)) {
22
- return `${currentText.replace(sectionPattern, `\n${section}\n`).trim()}\n`;
23
- }
24
- return `${currentText.trimEnd()}\n\n${section}\n`;
25
- }
26
- export function removeCodexInternalConfigEntryText(currentText, serverName) {
27
- if (!currentText) {
28
- return { changed: false, nextText: null };
29
- }
30
- const sectionPattern = new RegExp(String.raw `(?:^|\n)\[mcp_servers\.${JSON.stringify(serverName).replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\]\n(?:.+\n?)*?(?=\n\[|$)`, "m");
31
- if (!sectionPattern.test(currentText)) {
32
- return { changed: false, nextText: currentText };
33
- }
34
- const nextText = currentText.replace(sectionPattern, "\n").replace(/\n{3,}/g, "\n\n").trim();
35
- return {
36
- changed: true,
37
- nextText: nextText ? `${nextText}\n` : null,
38
- };
39
- }
40
- export function writeCodexInternalConfig(serverName, launcherCommand, configPath = getCodexInternalConfigPath()) {
41
- const existingText = existsSync(configPath) ? readFileSync(configPath, "utf8") : null;
42
- const created = existingText === null;
43
- const nextText = upsertCodexInternalConfigText(existingText, serverName, launcherCommand);
44
- mkdirSync(dirname(configPath), { recursive: true });
45
- writeFileSync(configPath, nextText, "utf8");
46
- return created ? "created" : "updated";
47
- }
48
- export function removeCodexInternalConfig(serverName, configPath = getCodexInternalConfigPath()) {
49
- const existingText = existsSync(configPath) ? readFileSync(configPath, "utf8") : null;
50
- const result = removeCodexInternalConfigEntryText(existingText, serverName);
51
- if (!result.changed) {
52
- return "missing";
53
- }
54
- if (!result.nextText) {
55
- rmSync(configPath, { force: true });
56
- return "removed";
57
- }
58
- writeFileSync(configPath, result.nextText, "utf8");
59
- return "removed";
60
- }
@@ -1,7 +0,0 @@
1
- export declare const MCP_REGISTER_TIMEOUT_MS = 5000;
2
- export type RegistrationFailureKind = "already_exists" | "authentication" | "timeout" | "other";
3
- export interface RegistrationFailure {
4
- kind: RegistrationFailureKind;
5
- output: string;
6
- }
7
- export declare function classifyRegistrationFailure(err: unknown): RegistrationFailure;
@@ -1,23 +0,0 @@
1
- export const MCP_REGISTER_TIMEOUT_MS = 5000;
2
- function readOutput(err) {
3
- if (!(err instanceof Error))
4
- return "";
5
- const stderr = "stderr" in err ? String(err.stderr ?? "") : "";
6
- const stdout = "stdout" in err ? String(err.stdout ?? "") : "";
7
- return `${stderr}${stdout}`;
8
- }
9
- export function classifyRegistrationFailure(err) {
10
- const output = readOutput(err);
11
- const lower = output.toLowerCase();
12
- if (lower.includes("already exists")) {
13
- return { kind: "already_exists", output };
14
- }
15
- if (lower.includes("waiting for authentication")) {
16
- return { kind: "authentication", output };
17
- }
18
- if (err instanceof Error &&
19
- ("code" in err && err.code === "ETIMEDOUT")) {
20
- return { kind: "timeout", output };
21
- }
22
- return { kind: "other", output };
23
- }
@@ -1,18 +0,0 @@
1
- import type { LauncherCommand } from "./launcher.js";
2
- export interface OpenClawServerConfig {
3
- transport: "stdio";
4
- command: string;
5
- args: string[];
6
- env?: Record<string, string>;
7
- }
8
- export declare function getOpenClawConfigPath(homeDir?: string): string;
9
- export declare function getOpenClawSkillsDir(homeDir?: string): string;
10
- export declare function buildOpenClawServerConfig(launcherCommand: LauncherCommand): OpenClawServerConfig;
11
- export declare function upsertOpenClawConfigText(currentText: string | null, serverName: string, launcherCommand: LauncherCommand): string;
12
- export declare function removeOpenClawConfigEntryText(currentText: string | null, serverName: string): {
13
- changed: boolean;
14
- nextText: string | null;
15
- };
16
- export declare function isOpenClawInstallLikelyInstalled(detectBinary: (name: string) => boolean, configPath?: string): boolean;
17
- export declare function writeOpenClawConfig(serverName: string, launcherCommand: LauncherCommand, configPath?: string): "created" | "updated";
18
- export declare function removeOpenClawConfig(serverName: string, configPath?: string): "removed" | "missing";
@@ -1,82 +0,0 @@
1
- import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
- import { homedir } from "node:os";
3
- import { dirname, join } from "node:path";
4
- export function getOpenClawConfigPath(homeDir = homedir()) {
5
- return join(homeDir, ".openclaw", "workspace", "config", "mcporter.json");
6
- }
7
- export function getOpenClawSkillsDir(homeDir = homedir()) {
8
- return join(homeDir, ".openclaw", "skills");
9
- }
10
- export function buildOpenClawServerConfig(launcherCommand) {
11
- return {
12
- transport: "stdio",
13
- command: launcherCommand.command,
14
- args: [...launcherCommand.args],
15
- };
16
- }
17
- function parseOpenClawConfig(raw) {
18
- const parsed = JSON.parse(raw);
19
- if (parsed && typeof parsed === "object") {
20
- return parsed;
21
- }
22
- throw new Error("OpenClaw config must be a JSON object");
23
- }
24
- export function upsertOpenClawConfigText(currentText, serverName, launcherCommand) {
25
- const config = currentText ? parseOpenClawConfig(currentText) : {};
26
- const servers = config.servers && typeof config.servers === "object" ? { ...config.servers } : {};
27
- servers[serverName] = buildOpenClawServerConfig(launcherCommand);
28
- const nextConfig = {
29
- ...config,
30
- servers,
31
- };
32
- return JSON.stringify(nextConfig, null, 2);
33
- }
34
- export function removeOpenClawConfigEntryText(currentText, serverName) {
35
- if (!currentText) {
36
- return { changed: false, nextText: null };
37
- }
38
- const config = parseOpenClawConfig(currentText);
39
- if (!config.servers || typeof config.servers !== "object" || !(serverName in config.servers)) {
40
- return { changed: false, nextText: currentText };
41
- }
42
- const servers = { ...config.servers };
43
- delete servers[serverName];
44
- const nextConfig = { ...config };
45
- if (Object.keys(servers).length === 0) {
46
- delete nextConfig.servers;
47
- }
48
- else {
49
- nextConfig.servers = servers;
50
- }
51
- return {
52
- changed: true,
53
- nextText: JSON.stringify(nextConfig, null, 2),
54
- };
55
- }
56
- export function isOpenClawInstallLikelyInstalled(detectBinary, configPath = getOpenClawConfigPath()) {
57
- if (detectBinary("mcporter") || detectBinary("openclaw")) {
58
- return true;
59
- }
60
- return existsSync(configPath) || existsSync(dirname(configPath));
61
- }
62
- export function writeOpenClawConfig(serverName, launcherCommand, configPath = getOpenClawConfigPath()) {
63
- const existingText = existsSync(configPath) ? readFileSync(configPath, "utf8") : null;
64
- const created = existingText === null;
65
- const nextText = upsertOpenClawConfigText(existingText, serverName, launcherCommand);
66
- mkdirSync(dirname(configPath), { recursive: true });
67
- writeFileSync(configPath, `${nextText}\n`, "utf8");
68
- return created ? "created" : "updated";
69
- }
70
- export function removeOpenClawConfig(serverName, configPath = getOpenClawConfigPath()) {
71
- const existingText = existsSync(configPath) ? readFileSync(configPath, "utf8") : null;
72
- const result = removeOpenClawConfigEntryText(existingText, serverName);
73
- if (!result.changed) {
74
- return "missing";
75
- }
76
- if (!result.nextText) {
77
- rmSync(configPath, { force: true });
78
- return "removed";
79
- }
80
- writeFileSync(configPath, `${result.nextText}\n`, "utf8");
81
- return "removed";
82
- }
@@ -1,16 +0,0 @@
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;
@@ -1,56 +0,0 @@
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
- }