@mkterswingman/5mghost-yonder 0.0.20 → 0.0.22

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
@@ -18,7 +18,12 @@ The bootstrap installer:
18
18
  - installs required runtimes (`playwright`, `yt-dlp`, `ffmpeg`)
19
19
  - runs `yt-mcp setup`
20
20
  - runs `yt-mcp smoke` to verify MCP startup and authenticated remote access
21
- - asks whether to configure YouTube cookies now; if you confirm, it runs `yt-mcp setup-cookies` and a subtitle smoke check immediately
21
+ - first tries a headless YouTube cookie import from your local Chrome/Edge session
22
+ - only asks to open a visible browser for YouTube login if the headless import fails
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
26
+ - runs a subtitle smoke check immediately after cookies are available
22
27
 
23
28
  If you are working inside the repo instead of using the hosted installer:
24
29
 
@@ -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
  });
@@ -13,6 +13,19 @@ export declare function canOpenBrowserForEnv(input: {
13
13
  env: NodeJS.ProcessEnv;
14
14
  stdinIsTTY: boolean;
15
15
  }): boolean;
16
+ type PromptQuestionFn = (question: string, signal: AbortSignal) => Promise<string>;
17
+ export interface PromptWithDefaultOptions {
18
+ defaultValue: string;
19
+ defaultLabel?: string;
20
+ timeoutMs?: number;
21
+ stdinIsTTY?: boolean;
22
+ log?: (message: string) => void;
23
+ questionFn?: PromptQuestionFn;
24
+ }
25
+ export declare const INSTALLER_OAUTH_TIMEOUT_MS = 180000;
26
+ export declare function getOAuthRecoveryHint(authUrl: string): string[];
27
+ export declare function getCookieSetupRecoveryHint(): string[];
28
+ export declare function promptWithDefault(question: string, options: PromptWithDefaultOptions): Promise<string>;
16
29
  export declare function getNoBrowserSessionNotice(): string;
17
30
  export declare function getNoBrowserPatHint(authUrl: string): string[];
18
31
  export declare function getCookieSetupDeferredHint(): string[];
package/dist/cli/setup.js CHANGED
@@ -1,10 +1,12 @@
1
- import { execFileSync, execSync } from "node:child_process";
2
- import { createInterface } from "node:readline";
1
+ import { execFileSync } from "node:child_process";
2
+ import { existsSync } from "node:fs";
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,26 +86,76 @@ 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
- /**
97
- * Prompt user for input in terminal.
98
- */
99
- function prompt(question) {
100
- const rl = createInterface({ input: process.stdin, output: process.stdout });
101
- return new Promise((resolve) => {
102
- rl.question(question, (answer) => {
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
+ }
112
+ function createPromptQuestionFn() {
113
+ return async (question, signal) => {
114
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
115
+ try {
116
+ return (await rl.question(question, { signal })).trim();
117
+ }
118
+ finally {
103
119
  rl.close();
104
- resolve(answer.trim());
105
- });
106
- });
120
+ }
121
+ };
122
+ }
123
+ export async function promptWithDefault(question, options) {
124
+ const stdinIsTTY = options.stdinIsTTY ?? Boolean(process.stdin.isTTY);
125
+ const defaultLabel = options.defaultLabel ?? options.defaultValue;
126
+ const log = options.log ?? console.log;
127
+ if (!stdinIsTTY) {
128
+ log(` ℹ️ No interactive terminal detected; defaulting to ${defaultLabel}.`);
129
+ return options.defaultValue;
130
+ }
131
+ const questionFn = options.questionFn ?? createPromptQuestionFn();
132
+ const timeoutMs = options.timeoutMs ?? 15_000;
133
+ const controller = new AbortController();
134
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
135
+ try {
136
+ const answer = await questionFn(question, controller.signal);
137
+ return answer || options.defaultValue;
138
+ }
139
+ catch (err) {
140
+ if (err instanceof Error && err.name === "AbortError") {
141
+ log(` ℹ️ No input received within ${Math.round(timeoutMs / 1000)}s; defaulting to ${defaultLabel}.`);
142
+ return options.defaultValue;
143
+ }
144
+ throw err;
145
+ }
146
+ finally {
147
+ clearTimeout(timer);
148
+ }
149
+ }
150
+ function getRequestedAuthModeFromEnv(env) {
151
+ const value = env.YT_MCP_AUTH_MODE?.trim().toLowerCase();
152
+ if (value === "oauth" || value === "1") {
153
+ return "oauth";
154
+ }
155
+ if (value === "pat" || value === "2") {
156
+ return "pat";
157
+ }
158
+ return null;
107
159
  }
108
160
  export function getNoBrowserSessionNotice() {
109
161
  return " ℹ️ 当前安装会话无法自动拉起浏览器 — 使用 PAT 模式\n";
@@ -169,8 +221,13 @@ export async function runSetup() {
169
221
  console.log(" [1] OAuth 自动登录(推荐 — 自动弹出浏览器完成登录)");
170
222
  console.log(" [2] PAT Token 登录(手动 — 在浏览器中生成 token 后粘贴)");
171
223
  console.log("");
172
- const choice = await prompt(" 请输入 1 或 2 (默认 1): ");
173
- const usePAT = choice === "2";
224
+ const forcedAuthMode = getRequestedAuthModeFromEnv(process.env);
225
+ const usePAT = forcedAuthMode
226
+ ? forcedAuthMode === "pat"
227
+ : (await promptWithDefault(" 请输入 1 或 2 (默认 1): ", {
228
+ defaultValue: "1",
229
+ defaultLabel: "1",
230
+ })) === "2";
174
231
  if (usePAT) {
175
232
  // User chose PAT
176
233
  const patUrl = `${config.auth_url}/pat/login`;
@@ -179,7 +236,12 @@ export async function runSetup() {
179
236
  openUrl(patUrl);
180
237
  console.log("");
181
238
  console.log(" 请在浏览器中登录并生成 PAT token。");
182
- const patInput = await prompt(" 粘贴你的 PAT token (pat_xxx): ");
239
+ const patInput = process.stdin.isTTY
240
+ ? await promptWithDefault(" 粘贴你的 PAT token (pat_xxx): ", {
241
+ defaultValue: "",
242
+ defaultLabel: "skip",
243
+ })
244
+ : "";
183
245
  if (patInput) {
184
246
  await tokenManager.savePAT(patInput);
185
247
  console.log(" ✅ PAT saved to shared auth");
@@ -191,12 +253,17 @@ export async function runSetup() {
191
253
  }
192
254
  else {
193
255
  // User chose OAuth (default)
256
+ const oauthTimeoutMs = installerMode ? INSTALLER_OAUTH_TIMEOUT_MS : 5 * 60 * 1000;
194
257
  console.log("");
195
258
  console.log(" 🌐 Opening browser for OAuth login...");
196
259
  console.log(" ⚠️ 如果你在云桌面上运行,请在云桌面的浏览器中完成登录!");
197
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`);
198
265
  try {
199
- const tokens = await runOAuthFlow(config.auth_url);
266
+ const tokens = await runOAuthFlow(config.auth_url, { timeoutMs: oauthTimeoutMs });
200
267
  await tokenManager.saveTokens(tokens.accessToken, tokens.refreshToken, tokens.expiresIn, tokens.clientId);
201
268
  console.log(" ✅ OAuth login successful");
202
269
  console.log(" ℹ️ Other first-party local MCPs on this machine can reuse this login.");
@@ -204,13 +271,26 @@ export async function runSetup() {
204
271
  catch (err) {
205
272
  // OAuth failed — auto fallback to PAT
206
273
  console.log(` ⚠️ OAuth failed: ${err instanceof Error ? err.message : String(err)}`);
207
- console.log("");
208
- console.log(" 📋 Falling back to PAT login...");
209
- const patUrl = `${config.auth_url}/pat/login`;
210
- console.log(` 🔗 Opening PAT login page: ${patUrl}`);
211
- openUrl(patUrl);
212
- console.log("");
213
- const patInput = await prompt(" 粘贴你的 PAT token (pat_xxx), 或直接回车跳过: ");
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
+ }
288
+ const patInput = process.stdin.isTTY
289
+ ? await promptWithDefault(" 粘贴你的 PAT token (pat_xxx), 或直接回车跳过: ", {
290
+ defaultValue: "",
291
+ defaultLabel: "skip",
292
+ })
293
+ : "";
214
294
  if (patInput) {
215
295
  await tokenManager.savePAT(patInput);
216
296
  console.log(" ✅ PAT saved to shared auth");
@@ -227,7 +307,12 @@ export async function runSetup() {
227
307
  for (const line of getNoBrowserPatHint(config.auth_url)) {
228
308
  console.log(line);
229
309
  }
230
- const patInput = await prompt(" 粘贴你的 PAT token (pat_xxx), 或直接回车跳过: ");
310
+ const patInput = process.stdin.isTTY
311
+ ? await promptWithDefault(" 粘贴你的 PAT token (pat_xxx), 或直接回车跳过: ", {
312
+ defaultValue: "",
313
+ defaultLabel: "skip",
314
+ })
315
+ : "";
231
316
  if (patInput) {
232
317
  await tokenManager.savePAT(patInput);
233
318
  console.log(" ✅ PAT saved to shared auth");
@@ -237,9 +322,13 @@ export async function runSetup() {
237
322
  console.log(' "env": { "YT_MCP_TOKEN": "pat_xxx" }');
238
323
  }
239
324
  }
325
+ const hasSharedAuth = Boolean(pat) || existsSync(PATHS.sharedAuthJson);
240
326
  // ── Step 4: YouTube Cookies ──
241
327
  console.log("Step 4/5: YouTube cookies...");
242
- if (skipCookieStep) {
328
+ if (!hasSharedAuth) {
329
+ console.log(" ℹ️ Deferred because authentication is not complete yet");
330
+ }
331
+ else if (skipCookieStep) {
243
332
  console.log(" ℹ️ Deferred to installer cookie flow");
244
333
  }
245
334
  else if (!hasBrowser) {
@@ -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);
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.20",
3
+ "version": "0.0.22",
4
4
  "description": "Internal MCP client with local data tools and remote API proxy",
5
5
  "type": "module",
6
6
  "bin": {