@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;
@@ -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
- const importedCookies = await deps.readImportedBrowserCookies(candidate, chromium, deps);
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 (19 tools)
13
+ * ├─ YT_MCP_TOKEN env var? ──▶ PAT mode ──▶ full server (20 tools)
14
14
  * │
15
- * ├─ auth.json exists? ──▶ JWT mode ──▶ full server (19 tools)
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 (19 tools)
13
+ * ├─ YT_MCP_TOKEN env var? ──▶ PAT mode ──▶ full server (20 tools)
14
14
  * │
15
- * ├─ auth.json exists? ──▶ JWT mode ──▶ full server (19 tools)
15
+ * ├─ auth.json exists? ──▶ JWT mode ──▶ full server (20 tools)
16
16
  * │
17
17
  * └─ no token? ──▶ register "setup_required" tool only
18
18
  * │
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mkterswingman/5mghost-yonder",
3
- "version": "0.0.24",
3
+ "version": "0.0.26",
4
4
  "description": "Internal MCP client with local data tools and remote API proxy",
5
5
  "type": "module",
6
6
  "bin": {
@@ -10,14 +10,14 @@ description: |
10
10
  Keywords: YouTube, 频道, 视频, 字幕, 评论, 均播, 播放量, trending, yt-mcp, youtube.com, youtu.be.
11
11
  ---
12
12
 
13
- # Use yt-mcp
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`, or `Use YouTube Data MCP` when `yt-mcp` can answer.
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.