@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 +2 -0
- package/dist/auth/oauthFlow.d.ts +4 -1
- package/dist/auth/oauthFlow.js +8 -8
- package/dist/cli/setup.d.ts +3 -0
- package/dist/cli/setup.js +46 -13
- package/dist/cli/setupCookies.d.ts +1 -0
- package/dist/cli/setupCookies.js +20 -6
- package/dist/server.js +4 -5
- package/dist/utils/browserLaunch.d.ts +5 -0
- package/dist/utils/browserLaunch.js +22 -0
- package/package.json +1 -1
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:
|
package/dist/auth/oauthFlow.d.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
export
|
|
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;
|
package/dist/auth/oauthFlow.js
CHANGED
|
@@ -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(
|
|
49
|
-
},
|
|
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(({
|
|
128
|
-
const
|
|
129
|
-
|
|
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
|
});
|
package/dist/cli/setup.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
88
|
-
|
|
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
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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 (
|
|
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) {
|
package/dist/cli/setupCookies.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
381
|
-
|
|
382
|
-
|
|
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 {
|
|
39
|
-
const
|
|
40
|
-
|
|
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,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
|
+
}
|