@mkterswingman/5mghost-yonder 0.0.21 → 0.0.23

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 CHANGED
@@ -21,6 +21,8 @@ The bootstrap installer:
21
21
  - first tries a headless YouTube cookie import from your local Chrome/Edge session
22
22
  - only asks to open a visible browser for YouTube login if the headless import fails
23
23
  - in unattended installs, defaults to OAuth auth mode and defaults to headed cookie setup after the prompt timeout
24
+ - in installer mode, OAuth waits up to `180s` and prints PAT fallback commands before waiting
25
+ - if auth is still incomplete after `setup`, the installer stops before smoke tests and YouTube cookie setup instead of pretending the install fully passed
24
26
  - runs a subtitle smoke check immediately after cookies are available
25
27
 
26
28
  If you are working inside the repo instead of using the hosted installer:
@@ -1,4 +1,7 @@
1
- export declare function runOAuthFlow(authUrl: string): Promise<{
1
+ export interface OAuthFlowOptions {
2
+ timeoutMs?: number;
3
+ }
4
+ export declare function runOAuthFlow(authUrl: string, options?: OAuthFlowOptions): Promise<{
2
5
  accessToken: string;
3
6
  refreshToken: string;
4
7
  expiresIn: number;
@@ -1,6 +1,7 @@
1
1
  import { createServer } from "node:http";
2
2
  import { randomBytes, createHash } from "node:crypto";
3
3
  import { URL } from "node:url";
4
+ import { buildBrowserOpenCommand } from "../utils/browserLaunch.js";
4
5
  function base64url(buf) {
5
6
  return buf
6
7
  .toString("base64")
@@ -8,7 +9,7 @@ function base64url(buf) {
8
9
  .replace(/\//g, "_")
9
10
  .replace(/=+$/, "");
10
11
  }
11
- export async function runOAuthFlow(authUrl) {
12
+ export async function runOAuthFlow(authUrl, options = {}) {
12
13
  // 1. Generate PKCE + state
13
14
  const codeVerifier = base64url(randomBytes(32));
14
15
  const codeChallenge = base64url(createHash("sha256").update(codeVerifier).digest());
@@ -43,10 +44,11 @@ export async function runOAuthFlow(authUrl) {
43
44
  }
44
45
  // 4. Wait for OAuth callback
45
46
  return new Promise((resolve, reject) => {
47
+ const timeoutMs = options.timeoutMs ?? 5 * 60 * 1000;
46
48
  const timeout = setTimeout(() => {
47
49
  httpServer.close();
48
- reject(new Error("OAuth flow timed out after 5 minutes"));
49
- }, 5 * 60 * 1000);
50
+ reject(new Error(`OAuth flow timed out after ${Math.round(timeoutMs / 1000)}s`));
51
+ }, timeoutMs);
50
52
  function cleanup() {
51
53
  clearTimeout(timeout);
52
54
  httpServer.close();
@@ -124,11 +126,9 @@ export async function runOAuthFlow(authUrl) {
124
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)}`;
125
127
  console.log("\n\x1b[1mOpen this URL in your browser to authorize:\x1b[0m");
126
128
  console.log(`\n ${authorizeUrl}\n`);
127
- import("node:child_process").then(({ exec }) => {
128
- const cmd = process.platform === "darwin" ? "open" :
129
- process.platform === "win32" ? "start" :
130
- "xdg-open";
131
- exec(`${cmd} "${authorizeUrl}"`);
129
+ import("node:child_process").then(({ execFile }) => {
130
+ const command = buildBrowserOpenCommand(authorizeUrl);
131
+ execFile(command.file, command.args);
132
132
  }).catch(() => {
133
133
  // ignore — user can open manually
134
134
  });
@@ -22,6 +22,9 @@ export interface PromptWithDefaultOptions {
22
22
  log?: (message: string) => void;
23
23
  questionFn?: PromptQuestionFn;
24
24
  }
25
+ export declare const INSTALLER_OAUTH_TIMEOUT_MS = 180000;
26
+ export declare function getOAuthRecoveryHint(authUrl: string): string[];
27
+ export declare function getCookieSetupRecoveryHint(): string[];
25
28
  export declare function promptWithDefault(question: string, options: PromptWithDefaultOptions): Promise<string>;
26
29
  export declare function getNoBrowserSessionNotice(): string;
27
30
  export declare function getNoBrowserPatHint(authUrl: string): string[];
package/dist/cli/setup.js CHANGED
@@ -1,10 +1,12 @@
1
- import { execFileSync, execSync } from "node:child_process";
1
+ import { execFileSync } from "node:child_process";
2
+ import { existsSync } from "node:fs";
2
3
  import { createInterface } from "node:readline/promises";
3
4
  import { loadConfig, saveConfig, PATHS, ensureConfigDir } from "../utils/config.js";
4
5
  import { TokenManager } from "../auth/tokenManager.js";
5
6
  import { runOAuthFlow } from "../auth/oauthFlow.js";
6
7
  import { hasSIDCookies } from "../utils/cookies.js";
7
8
  import { buildLauncherCommand, writeLauncherFile } from "../utils/launcher.js";
9
+ import { buildBrowserOpenCommand } from "../utils/browserLaunch.js";
8
10
  import { checkAll } from "../runtime/installers.js";
9
11
  import { runInstallSkills } from "./installSkills.js";
10
12
  import { getOpenClawConfigPath, isOpenClawInstallLikelyInstalled, writeOpenClawConfig, } from "../utils/openClaw.js";
@@ -84,15 +86,29 @@ function canOpenBrowser() {
84
86
  }
85
87
  function openUrl(url) {
86
88
  try {
87
- const cmd = process.platform === "darwin" ? `open "${url}"` :
88
- process.platform === "win32" ? `start "${url}"` :
89
- `xdg-open "${url}"`;
90
- execSync(cmd, { stdio: "ignore" });
89
+ const command = buildBrowserOpenCommand(url);
90
+ execFileSync(command.file, command.args, { stdio: "ignore" });
91
91
  }
92
92
  catch {
93
93
  // Can't open — user will see the URL in console
94
94
  }
95
95
  }
96
+ export const INSTALLER_OAUTH_TIMEOUT_MS = 180_000;
97
+ export function getOAuthRecoveryHint(authUrl) {
98
+ return [
99
+ " 如果浏览器没有出现,或你之后想改用 PAT,可直接继续:",
100
+ ` PAT 登录页:${authUrl}/pat/login`,
101
+ " macOS / Linux: YT_MCP_TOKEN='pat_xxx' yt-mcp setup",
102
+ " Windows: $env:YT_MCP_TOKEN='pat_xxx'; yt-mcp setup",
103
+ ];
104
+ }
105
+ export function getCookieSetupRecoveryHint() {
106
+ return [
107
+ " 如果浏览器没有出现,或这次没完成 YouTube 登录,可稍后继续:",
108
+ " 重新执行:yt-mcp setup-cookies",
109
+ " 验证字幕:yt-mcp smoke --subtitles",
110
+ ];
111
+ }
96
112
  function createPromptQuestionFn() {
97
113
  return async (question, signal) => {
98
114
  const rl = createInterface({ input: process.stdin, output: process.stdout });
@@ -237,12 +253,17 @@ export async function runSetup() {
237
253
  }
238
254
  else {
239
255
  // User chose OAuth (default)
256
+ const oauthTimeoutMs = installerMode ? INSTALLER_OAUTH_TIMEOUT_MS : 5 * 60 * 1000;
240
257
  console.log("");
241
258
  console.log(" 🌐 Opening browser for OAuth login...");
242
259
  console.log(" ⚠️ 如果你在云桌面上运行,请在云桌面的浏览器中完成登录!");
243
260
  console.log(" 在本地电脑打开链接将无法完成回调。");
261
+ for (const line of getOAuthRecoveryHint(config.auth_url)) {
262
+ console.log(line);
263
+ }
264
+ console.log(` OAuth 等待上限:${Math.round(oauthTimeoutMs / 1000)}s`);
244
265
  try {
245
- const tokens = await runOAuthFlow(config.auth_url);
266
+ const tokens = await runOAuthFlow(config.auth_url, { timeoutMs: oauthTimeoutMs });
246
267
  await tokenManager.saveTokens(tokens.accessToken, tokens.refreshToken, tokens.expiresIn, tokens.clientId);
247
268
  console.log(" ✅ OAuth login successful");
248
269
  console.log(" ℹ️ Other first-party local MCPs on this machine can reuse this login.");
@@ -250,12 +271,20 @@ export async function runSetup() {
250
271
  catch (err) {
251
272
  // OAuth failed — auto fallback to PAT
252
273
  console.log(` ⚠️ OAuth failed: ${err instanceof Error ? err.message : String(err)}`);
253
- console.log("");
254
- console.log(" 📋 Falling back to PAT login...");
255
- const patUrl = `${config.auth_url}/pat/login`;
256
- console.log(` 🔗 Opening PAT login page: ${patUrl}`);
257
- openUrl(patUrl);
258
- console.log("");
274
+ for (const line of getOAuthRecoveryHint(config.auth_url)) {
275
+ console.log(line);
276
+ }
277
+ if (process.stdin.isTTY) {
278
+ console.log("");
279
+ console.log(" 📋 Falling back to PAT login...");
280
+ const patUrl = `${config.auth_url}/pat/login`;
281
+ console.log(` 🔗 Opening PAT login page: ${patUrl}`);
282
+ openUrl(patUrl);
283
+ console.log("");
284
+ }
285
+ else {
286
+ console.log(" ℹ️ 当前安装会话无交互输入,请拿到 PAT 后直接带 YT_MCP_TOKEN 重跑。");
287
+ }
259
288
  const patInput = process.stdin.isTTY
260
289
  ? await promptWithDefault(" 粘贴你的 PAT token (pat_xxx), 或直接回车跳过: ", {
261
290
  defaultValue: "",
@@ -293,9 +322,13 @@ export async function runSetup() {
293
322
  console.log(' "env": { "YT_MCP_TOKEN": "pat_xxx" }');
294
323
  }
295
324
  }
325
+ const hasSharedAuth = Boolean(pat) || existsSync(PATHS.sharedAuthJson);
296
326
  // ── Step 4: YouTube Cookies ──
297
327
  console.log("Step 4/5: YouTube cookies...");
298
- if (skipCookieStep) {
328
+ if (!hasSharedAuth) {
329
+ console.log(" ℹ️ Deferred because authentication is not complete yet");
330
+ }
331
+ else if (skipCookieStep) {
299
332
  console.log(" ℹ️ Deferred to installer cookie flow");
300
333
  }
301
334
  else if (!hasBrowser) {
@@ -53,6 +53,7 @@ export interface SetupCookiesDeps {
53
53
  }
54
54
  export interface SetupCookiesOptions {
55
55
  importOnly?: boolean;
56
+ headed?: boolean;
56
57
  }
57
58
  type SetupCookiesChromium = Awaited<ReturnType<SetupCookiesDeps["loadChromium"]>>;
58
59
  interface CdpCookie {
@@ -3,6 +3,7 @@ import { writeFileSync } from "node:fs";
3
3
  import { createServer } from "node:net";
4
4
  import { PATHS, ensureConfigDir } from "../utils/config.js";
5
5
  import { cookiesToNetscape } from "../utils/cookies.js";
6
+ import { getCookieSetupRecoveryHint } from "./setup.js";
6
7
  import { cleanupImportedBrowserWorkspace, findImportableBrowserProfileCandidates, prepareImportedBrowserWorkspace, } from "../utils/browserProfileImport.js";
7
8
  /**
8
9
  * Detect which browser channel is available on the system.
@@ -341,10 +342,17 @@ export async function runManualCookieSetup(chromium, deps) {
341
342
  const LOGIN_TIMEOUT_MS = 2 * 60 * 1000;
342
343
  deps.log("⏳ Waiting for login (up to 2 minutes)...");
343
344
  deps.log(" Login will be detected automatically once you sign in.\n");
345
+ for (const line of getCookieSetupRecoveryHint()) {
346
+ deps.log(line);
347
+ }
348
+ deps.log("");
344
349
  const finalCookies = await deps.waitForLogin(context, () => browserClosed, LOGIN_TIMEOUT_MS);
345
350
  if (browserClosed) {
346
351
  deps.log("\n⚠️ Browser was closed before login completed.");
347
- deps.log(" Run again: npx @mkterswingman/5mghost-yonder setup-cookies\n");
352
+ for (const line of getCookieSetupRecoveryHint()) {
353
+ deps.log(line);
354
+ }
355
+ deps.log("");
348
356
  return;
349
357
  }
350
358
  if (!finalCookies) {
@@ -353,7 +361,10 @@ export async function runManualCookieSetup(chromium, deps) {
353
361
  }
354
362
  catch { /* already closed */ }
355
363
  deps.log("\n⏰ Login timed out (2 minutes).");
356
- deps.log(" Run again: npx @mkterswingman/5mghost-yonder setup-cookies\n");
364
+ for (const line of getCookieSetupRecoveryHint()) {
365
+ deps.log(line);
366
+ }
367
+ deps.log("");
357
368
  return;
358
369
  }
359
370
  await deps.saveCookiesAndClose(context, finalCookies);
@@ -364,6 +375,7 @@ export async function runManualCookieSetup(chromium, deps) {
364
375
  export function parseSetupCookiesArgs(argv) {
365
376
  return {
366
377
  importOnly: argv.includes("--import-only"),
378
+ headed: argv.includes("--headed"),
367
379
  };
368
380
  }
369
381
  export async function runSetupCookies(overrides = {}, options = {}) {
@@ -377,11 +389,13 @@ export async function runSetupCookies(overrides = {}, options = {}) {
377
389
  catch {
378
390
  throw new Error("Playwright runtime is not installed.\nRun: yt-mcp runtime install");
379
391
  }
380
- const imported = await deps.tryImportBrowserCookies(chromium, deps);
381
- if (imported) {
382
- return;
392
+ if (!options.headed) {
393
+ const imported = await deps.tryImportBrowserCookies(chromium, deps);
394
+ if (imported) {
395
+ return;
396
+ }
383
397
  }
384
- if (options.importOnly) {
398
+ if (options.importOnly && !options.headed) {
385
399
  throw new Error("No reusable YouTube session found in local Chrome/Edge profiles");
386
400
  }
387
401
  await deps.runManualCookieSetup(chromium, deps);
package/dist/server.js CHANGED
@@ -35,11 +35,9 @@ export async function createServer(config, tokenManager, downloadJobManager = ne
35
35
  const patUrl = `${config.auth_url}/pat/login`;
36
36
  let opened = false;
37
37
  try {
38
- const { exec } = await import("node:child_process");
39
- const cmd = process.platform === "darwin" ? `open "${patUrl}"` :
40
- process.platform === "win32" ? `start "${patUrl}"` :
41
- `xdg-open "${patUrl}"`;
42
- exec(cmd);
38
+ const { execFile } = await import("node:child_process");
39
+ const command = buildBrowserOpenCommand(patUrl);
40
+ execFile(command.file, command.args);
43
41
  opened = true;
44
42
  }
45
43
  catch { /* can't open browser, fall through */ }
@@ -80,3 +78,4 @@ export async function createServer(config, tokenManager, downloadJobManager = ne
80
78
  registerRemoteTools(server, config, tokenManager);
81
79
  return server;
82
80
  }
81
+ import { buildBrowserOpenCommand } from "./utils/browserLaunch.js";
@@ -0,0 +1,5 @@
1
+ export interface BrowserOpenCommand {
2
+ file: string;
3
+ args: string[];
4
+ }
5
+ export declare function buildBrowserOpenCommand(url: string, platform?: NodeJS.Platform): BrowserOpenCommand;
@@ -0,0 +1,22 @@
1
+ function escapePowerShellSingleQuoted(value) {
2
+ return value.replace(/'/g, "''");
3
+ }
4
+ export function buildBrowserOpenCommand(url, platform = process.platform) {
5
+ if (platform === "darwin") {
6
+ return { file: "open", args: [url] };
7
+ }
8
+ if (platform === "win32") {
9
+ return {
10
+ file: "powershell",
11
+ args: [
12
+ "-NoProfile",
13
+ "-NonInteractive",
14
+ "-ExecutionPolicy",
15
+ "Bypass",
16
+ "-Command",
17
+ `Start-Process '${escapePowerShellSingleQuoted(url)}'`,
18
+ ],
19
+ };
20
+ }
21
+ return { file: "xdg-open", args: [url] };
22
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mkterswingman/5mghost-yonder",
3
- "version": "0.0.21",
3
+ "version": "0.0.23",
4
4
  "description": "Internal MCP client with local data tools and remote API proxy",
5
5
  "type": "module",
6
6
  "bin": {