github-search-cli 9.6.0 → 9.7.0

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/src/mcp-server.ts CHANGED
@@ -27,8 +27,7 @@ import {
27
27
  rankRepos,
28
28
  createGitHubSearch,
29
29
  createTrendingSearch,
30
- TRENDING_LANGUAGES,
31
- trendingLanguageSlug,
30
+ resolveTrendingLanguage,
32
31
  } from "./search";
33
32
  import {
34
33
  createPackageSearch,
@@ -37,7 +36,11 @@ import {
37
36
  } from "./package";
38
37
  import { fetchOrgProfile, formatOrgText } from "./org";
39
38
  import { fetchUserProfile, formatUserJson } from "./user";
40
- import { fetchDeepDive, buildDeepDiveText } from "./deepdive";
39
+ import {
40
+ fetchDeepDive,
41
+ buildDeepDiveText,
42
+ resolveRepoFromRef,
43
+ } from "./deepdive";
41
44
  import { buildComparisonTable } from "./compare";
42
45
  import { allCachedReleases } from "./releases";
43
46
  import { getBookmarks } from "./bookmarks";
@@ -199,45 +202,9 @@ function formatPackageLines(
199
202
  .join("\n");
200
203
  }
201
204
 
202
- const REPO_NAME_RE = /^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/;
203
-
204
- function parseRepoName(input: string): { owner: string; name: string } {
205
- const fullName = input
206
- .trim()
207
- .replace(/\.git$/, "")
208
- .replace(/^@/, "");
209
- if (!REPO_NAME_RE.test(fullName)) {
210
- throw new McpToolError(
211
- `"${input}" is not a repo name — use owner/name, e.g. facebook/react.`,
212
- );
213
- }
214
- const [owner, name] = fullName.split("/");
215
- return { owner, name };
216
- }
217
-
218
- /** Minimal Repo for deep-dive (fetchDeepDive only reads owner/name). */
219
- function stubRepo(fullName: string): Repo {
220
- const { owner, name } = parseRepoName(fullName);
221
- return {
222
- id: 0,
223
- fullName: `${owner}/${name}`,
224
- name,
225
- owner,
226
- description: null,
227
- url: `https://github.com/${owner}/${name}`,
228
- stars: 0,
229
- forks: 0,
230
- watchers: 0,
231
- language: null,
232
- topics: [],
233
- archived: false,
234
- isFork: false,
235
- private: false,
236
- createdAt: "",
237
- updatedAt: "",
238
- pushedAt: "",
239
- score: 0,
240
- };
205
+ /** Wrap a domain error as McpToolError so its message surfaces verbatim. */
206
+ function asMcpToolError(err: unknown): McpToolError {
207
+ return new McpToolError(err instanceof Error ? err.message : String(err));
241
208
  }
242
209
 
243
210
  const TRENDING_QUERY = { keywords: [], qualifiers: [], raw: "trending" };
@@ -366,16 +333,12 @@ const TOOLS: ToolDefinition[] = [
366
333
  const token = resolveToken(state, args.token);
367
334
 
368
335
  let language: string | undefined;
369
- const langArg =
370
- typeof args.language === "string" ? args.language.trim() : "";
371
- if (langArg) {
372
- const slug = trendingLanguageSlug(langArg);
373
- if (!TRENDING_LANGUAGES.has(slug)) {
374
- throw new McpToolError(
375
- `"${langArg}" is not a trending language filter. Try one of: rust, python, typescript, javascript, go, zig.`,
376
- );
377
- }
378
- language = slug;
336
+ try {
337
+ language = resolveTrendingLanguage(
338
+ typeof args.language === "string" ? args.language : undefined,
339
+ );
340
+ } catch (err) {
341
+ throw asMcpToolError(err);
379
342
  }
380
343
 
381
344
  const trending = createTrendingSearch();
@@ -582,7 +545,12 @@ const TOOLS: ToolDefinition[] = [
582
545
  async run(args, state) {
583
546
  const repo = String(args.repo ?? "").trim();
584
547
  if (!repo) throw new McpToolError("repo must be a non-empty string.");
585
- const stub = stubRepo(repo);
548
+ let stub: Repo;
549
+ try {
550
+ stub = await resolveRepoFromRef(repo, resolveToken(state, args.token));
551
+ } catch (err) {
552
+ throw asMcpToolError(err);
553
+ }
586
554
  const data = await fetchDeepDive(stub, resolveToken(state, args.token));
587
555
  return { text: buildDeepDiveText(data), data: { ...data } };
588
556
  },
@@ -720,8 +688,11 @@ export async function processServerLine(
720
688
  return;
721
689
  }
722
690
 
723
- const isNotification = msg.id === undefined || msg.id === null;
724
- const id = isNotification ? null : msg.id;
691
+ // Extract the id into a const so TS narrows `undefined` out of it below
692
+ // (aliased conditional narrowing doesn't apply through the `let msg` bind).
693
+ const rawId = msg.id;
694
+ const isNotification = rawId === undefined || rawId === null;
695
+ const id: JsonRpcId = isNotification ? null : rawId;
725
696
 
726
697
  // Notifications never get responses.
727
698
  if (msg.method === "notifications/initialized") {
package/src/search.ts CHANGED
@@ -570,6 +570,25 @@ export function trendingLanguageSlug(language: string): string {
570
570
  return language.trim().toLowerCase().replace(/\s+/g, "-");
571
571
  }
572
572
 
573
+ /**
574
+ * Validate a user-supplied trending language and return its slug, or null if
575
+ * empty. Throws with a user-facing suggestion list for unknown languages.
576
+ * Shared by the MCP trending tool and the CLI trending filter.
577
+ */
578
+ export function resolveTrendingLanguage(
579
+ language: string | undefined | null,
580
+ ): string | undefined {
581
+ const trimmed = typeof language === "string" ? language.trim() : "";
582
+ if (!trimmed) return undefined;
583
+ const slug = trendingLanguageSlug(trimmed);
584
+ if (!TRENDING_LANGUAGES.has(slug)) {
585
+ throw new Error(
586
+ `"${trimmed}" is not a trending language filter. Try one of: rust, python, typescript, javascript, go, zig.`,
587
+ );
588
+ }
589
+ return slug;
590
+ }
591
+
573
592
  /** Build the github.com/trending URL for a period + optional language filter. */
574
593
  export function buildTrendingUrl(
575
594
  since: "daily" | "weekly" | "monthly",