@mkterswingman/5mghost-yonder 0.0.24 → 0.0.26
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.
|
@@ -55,6 +55,9 @@ export interface SetupCookiesOptions {
|
|
|
55
55
|
importOnly?: boolean;
|
|
56
56
|
headed?: boolean;
|
|
57
57
|
}
|
|
58
|
+
export declare class BrowserProfileLockedError extends Error {
|
|
59
|
+
constructor(message?: string);
|
|
60
|
+
}
|
|
58
61
|
type SetupCookiesChromium = Awaited<ReturnType<SetupCookiesDeps["loadChromium"]>>;
|
|
59
62
|
interface CdpCookie {
|
|
60
63
|
name: string;
|
package/dist/cli/setupCookies.js
CHANGED
|
@@ -109,6 +109,12 @@ async function loadPlaywrightChromium() {
|
|
|
109
109
|
const pw = await import("playwright");
|
|
110
110
|
return pw.chromium;
|
|
111
111
|
}
|
|
112
|
+
export class BrowserProfileLockedError extends Error {
|
|
113
|
+
constructor(message = "Chrome/Edge profile is locked by a running browser") {
|
|
114
|
+
super(message);
|
|
115
|
+
this.name = "BrowserProfileLockedError";
|
|
116
|
+
}
|
|
117
|
+
}
|
|
112
118
|
function buildSetupCookiesDeps(overrides = {}) {
|
|
113
119
|
return {
|
|
114
120
|
ensureConfigDir,
|
|
@@ -194,6 +200,7 @@ async function terminateImportedBrowser(processHandle) {
|
|
|
194
200
|
}
|
|
195
201
|
export async function tryImportBrowserCookies(chromium, deps) {
|
|
196
202
|
const candidates = deps.findImportableBrowserProfileCandidates();
|
|
203
|
+
let sawLockedProfile = false;
|
|
197
204
|
if (candidates.length === 0) {
|
|
198
205
|
return false;
|
|
199
206
|
}
|
|
@@ -201,7 +208,19 @@ export async function tryImportBrowserCookies(chromium, deps) {
|
|
|
201
208
|
const candidate = candidates[index];
|
|
202
209
|
const isLastCandidate = index === candidates.length - 1;
|
|
203
210
|
deps.log(`Trying to import existing YouTube login from ${candidate.label} (${candidate.profileLabel})...`);
|
|
204
|
-
|
|
211
|
+
let importedCookies = null;
|
|
212
|
+
try {
|
|
213
|
+
importedCookies = await deps.readImportedBrowserCookies(candidate, chromium, deps);
|
|
214
|
+
}
|
|
215
|
+
catch (err) {
|
|
216
|
+
if (err instanceof BrowserProfileLockedError) {
|
|
217
|
+
sawLockedProfile = true;
|
|
218
|
+
deps.log(`${candidate.label} (${candidate.profileLabel}) profile is locked by a running browser.`);
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
throw err;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
205
224
|
if (importedCookies) {
|
|
206
225
|
await deps.saveCookiesAndClose({ close: async () => { } }, importedCookies, true);
|
|
207
226
|
deps.log(`✅ Imported YouTube session from ${candidate.label} (${candidate.profileLabel})\n`);
|
|
@@ -212,6 +231,9 @@ export async function tryImportBrowserCookies(chromium, deps) {
|
|
|
212
231
|
deps.log(`${candidate.label} (${candidate.profileLabel}) import unavailable, trying ${nextCandidate.label} (${nextCandidate.profileLabel})...`);
|
|
213
232
|
}
|
|
214
233
|
}
|
|
234
|
+
if (sawLockedProfile) {
|
|
235
|
+
throw new BrowserProfileLockedError("Chrome/Edge profile is locked by a running browser. Close Chrome/Edge and rerun `yt-mcp setup-cookies`, or continue with headed browser login.");
|
|
236
|
+
}
|
|
215
237
|
deps.log("Browser profile import failed, falling back to manual login...\n");
|
|
216
238
|
return false;
|
|
217
239
|
}
|
|
@@ -255,7 +277,14 @@ export async function readImportedBrowserCookies(candidate, chromium, deps) {
|
|
|
255
277
|
}
|
|
256
278
|
return cookies;
|
|
257
279
|
}
|
|
258
|
-
catch {
|
|
280
|
+
catch (err) {
|
|
281
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
282
|
+
const code = typeof err === "object" && err && "code" in err ? String(err.code ?? "") : "";
|
|
283
|
+
if (code === "EBUSY" ||
|
|
284
|
+
code === "EPERM" ||
|
|
285
|
+
/resource busy|being used by another process|used by another process|device or resource busy|operation not permitted/i.test(message)) {
|
|
286
|
+
throw new BrowserProfileLockedError();
|
|
287
|
+
}
|
|
259
288
|
return null;
|
|
260
289
|
}
|
|
261
290
|
finally {
|
package/dist/server.d.ts
CHANGED
|
@@ -10,9 +10,9 @@ import type { DownloadToolDeps } from "./tools/downloads.js";
|
|
|
10
10
|
*
|
|
11
11
|
* serve startup
|
|
12
12
|
* │
|
|
13
|
-
* ├─ YT_MCP_TOKEN env var? ──▶ PAT mode ──▶ full server (
|
|
13
|
+
* ├─ YT_MCP_TOKEN env var? ──▶ PAT mode ──▶ full server (20 tools)
|
|
14
14
|
* │
|
|
15
|
-
* ├─ auth.json exists? ──▶ JWT mode ──▶ full server (
|
|
15
|
+
* ├─ auth.json exists? ──▶ JWT mode ──▶ full server (20 tools)
|
|
16
16
|
* │
|
|
17
17
|
* └─ no token? ──▶ register "setup_required" tool only
|
|
18
18
|
* │
|
package/dist/server.js
CHANGED
|
@@ -10,9 +10,9 @@ import { registerRemoteTools } from "./tools/remote.js";
|
|
|
10
10
|
*
|
|
11
11
|
* serve startup
|
|
12
12
|
* │
|
|
13
|
-
* ├─ YT_MCP_TOKEN env var? ──▶ PAT mode ──▶ full server (
|
|
13
|
+
* ├─ YT_MCP_TOKEN env var? ──▶ PAT mode ──▶ full server (20 tools)
|
|
14
14
|
* │
|
|
15
|
-
* ├─ auth.json exists? ──▶ JWT mode ──▶ full server (
|
|
15
|
+
* ├─ auth.json exists? ──▶ JWT mode ──▶ full server (20 tools)
|
|
16
16
|
* │
|
|
17
17
|
* └─ no token? ──▶ register "setup_required" tool only
|
|
18
18
|
* │
|
package/dist/tools/remote.js
CHANGED
|
@@ -134,6 +134,15 @@ const REMOTE_TOOLS = [
|
|
|
134
134
|
},
|
|
135
135
|
remotePath: "api/trending",
|
|
136
136
|
},
|
|
137
|
+
{
|
|
138
|
+
name: "search_channels",
|
|
139
|
+
description: "Search YouTube channels by keyword for candidate discovery.",
|
|
140
|
+
schema: {
|
|
141
|
+
query: z.string().min(1).max(200),
|
|
142
|
+
max_results: z.number().int().min(1).max(50).optional(),
|
|
143
|
+
},
|
|
144
|
+
remotePath: "api/channel/search",
|
|
145
|
+
},
|
|
137
146
|
{
|
|
138
147
|
name: "get_channel_stats",
|
|
139
148
|
description: "Get YouTube channel statistics for up to 50 channel inputs.",
|
|
@@ -93,6 +93,10 @@ export function parseBrowserProfilesFromLocalState(localStateText) {
|
|
|
93
93
|
function buildProfileLabel(profileName, meta) {
|
|
94
94
|
return meta?.name?.trim() || meta?.user_name?.trim() || meta?.gaia_name?.trim() || profileName;
|
|
95
95
|
}
|
|
96
|
+
function hasReusableCookieStore(profileDir, exists) {
|
|
97
|
+
// Why: recent Chromium builds often moved the SQLite cookie DB under Network/Cookies.
|
|
98
|
+
return exists(join(profileDir, "Cookies")) || exists(join(profileDir, "Network", "Cookies"));
|
|
99
|
+
}
|
|
96
100
|
export function findImportableBrowserProfileCandidates(input) {
|
|
97
101
|
const exists = input?.exists ?? existsSync;
|
|
98
102
|
const readFile = input?.readFile ?? ((path) => readFileSync(path, "utf8"));
|
|
@@ -114,7 +118,7 @@ export function findImportableBrowserProfileCandidates(input) {
|
|
|
114
118
|
}
|
|
115
119
|
for (const profileName of parsedState.orderedProfileNames) {
|
|
116
120
|
const profileDir = join(installation.rootDir, profileName);
|
|
117
|
-
if (!exists(profileDir)) {
|
|
121
|
+
if (!exists(profileDir) || !hasReusableCookieStore(profileDir, exists)) {
|
|
118
122
|
continue;
|
|
119
123
|
}
|
|
120
124
|
candidates.push({
|
package/package.json
CHANGED
|
@@ -10,14 +10,14 @@ description: |
|
|
|
10
10
|
Keywords: YouTube, 频道, 视频, 字幕, 评论, 均播, 播放量, trending, yt-mcp, youtube.com, youtu.be.
|
|
11
11
|
---
|
|
12
12
|
|
|
13
|
-
#
|
|
13
|
+
# use-yt-mcp
|
|
14
14
|
|
|
15
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
16
|
|
|
17
17
|
## 0. Hard Constraints
|
|
18
18
|
|
|
19
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
|
|
20
|
+
- Do not route YouTube queries to legacy MCPs or legacy setup skills such as `youtube-data`, `mcp_youtube_data`, or `use-youtube-data-mcp` when `yt-mcp` can answer.
|
|
21
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
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
23
|
|
|
@@ -31,6 +31,8 @@ Focus on YouTube analysis with yt-mcp MCP tools. Answer the analysis question fi
|
|
|
31
31
|
| User Intent | Tool(s) |
|
|
32
32
|
|---|---|
|
|
33
33
|
| Search videos by keyword | `search_videos` |
|
|
34
|
+
| Name-only creator request without explicit platform | confirm platform first, then `search_channels` |
|
|
35
|
+
| Search channel candidates by fuzzy name | `search_channels` |
|
|
34
36
|
| Get video metrics in bulk | `get_video_stats` |
|
|
35
37
|
| Large video stats job | `start_video_stats_job` -> `poll_video_stats_job` |
|
|
36
38
|
| Get subtitles / transcript | `get_subtitles`, `batch_get_subtitles` |
|
|
@@ -48,6 +50,10 @@ All tools are prefixed `mcp__yt-mcp__` in actual calls.
|
|
|
48
50
|
## 2. Analysis Rules
|
|
49
51
|
|
|
50
52
|
- Prefer batch-capable tools when comparing multiple videos or channels.
|
|
53
|
+
- If the user only gives a creator name but does not explicitly say platform, ask first whether they mean a YouTube creator/channel before running yt-mcp tools.
|
|
54
|
+
- If the user provides a fuzzy channel name (for example only a brand/person name), call `search_channels` first and present 3-5 candidates before doing channel analysis.
|
|
55
|
+
- After `search_channels` returns candidates, explicitly ask the user which channel they mean; do not auto-select candidate #1.
|
|
56
|
+
- If the user already provides a channel URL, `@handle`, or explicit `channel_id`, skip candidate search and go directly to `get_channel_stats` / `list_channel_uploads`.
|
|
51
57
|
- `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
58
|
- 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
59
|
- When using async jobs, poll until status is `completed`, and tell the user if the result is still partial or in progress.
|