github-search-cli 9.5.1 → 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/dist/cli.js CHANGED
@@ -501,6 +501,66 @@ function appendPage(url, page, perPage) {
501
501
  return u.toString();
502
502
  }
503
503
  var TRENDING_URL = "https://github.com/trending";
504
+ var TRENDING_LANGUAGES = /* @__PURE__ */ new Set([
505
+ "c",
506
+ "c#",
507
+ "c++",
508
+ "clojure",
509
+ "crystal",
510
+ "css",
511
+ "dart",
512
+ "dockerfile",
513
+ "elixir",
514
+ "go",
515
+ "haskell",
516
+ "html",
517
+ "java",
518
+ "javascript",
519
+ "jupyter-notebook",
520
+ "kotlin",
521
+ "lua",
522
+ "objective-c",
523
+ "ocaml",
524
+ "perl",
525
+ "php",
526
+ "powershell",
527
+ "python",
528
+ "r",
529
+ "ruby",
530
+ "rust",
531
+ "scala",
532
+ "shell",
533
+ "solidity",
534
+ "svelte",
535
+ "swift",
536
+ "typescript",
537
+ "vue",
538
+ "zig"
539
+ ]);
540
+ function trendingLanguageSlug(language) {
541
+ return language.trim().toLowerCase().replace(/\s+/g, "-");
542
+ }
543
+ function resolveTrendingLanguage(language) {
544
+ const trimmed = typeof language === "string" ? language.trim() : "";
545
+ if (!trimmed) return void 0;
546
+ const slug = trendingLanguageSlug(trimmed);
547
+ if (!TRENDING_LANGUAGES.has(slug)) {
548
+ throw new Error(
549
+ `"${trimmed}" is not a trending language filter. Try one of: rust, python, typescript, javascript, go, zig.`
550
+ );
551
+ }
552
+ return slug;
553
+ }
554
+ function buildTrendingUrl(since, language) {
555
+ const params = new URLSearchParams();
556
+ if (since !== "daily") params.set("since", since);
557
+ if (language) {
558
+ const slug = trendingLanguageSlug(language);
559
+ if (TRENDING_LANGUAGES.has(slug)) params.set("language", slug);
560
+ }
561
+ const qs = params.toString();
562
+ return qs ? `${TRENDING_URL}?${qs}` : TRENDING_URL;
563
+ }
504
564
  function trendingRepoToRepo(r) {
505
565
  return {
506
566
  id: 0,
@@ -531,7 +591,7 @@ var TrendingAdapter = class {
531
591
  }
532
592
  async search(_query, options) {
533
593
  const since = options.trendingSince ?? "daily";
534
- const url = since === "daily" ? TRENDING_URL : `${TRENDING_URL}?since=${since}`;
594
+ const url = buildTrendingUrl(since, options.trendingLanguage);
535
595
  let res;
536
596
  try {
537
597
  res = await fetch(url, { headers: { "User-Agent": "ghfind" } });
@@ -560,7 +620,8 @@ var SearchModule = class _SearchModule {
560
620
  options.sort,
561
621
  String(options.limit),
562
622
  String(options.page ?? 1),
563
- options.trendingSince ?? "none"
623
+ options.trendingSince ?? "none",
624
+ options.trendingLanguage ?? "none"
564
625
  );
565
626
  const cached2 = _SearchModule.cache.get(cacheKey);
566
627
  if (cached2) {
@@ -1315,7 +1376,9 @@ function checkDirs() {
1315
1376
  }
1316
1377
  }
1317
1378
  function printReport() {
1379
+ const noColor = process.env.NO_COLOR !== void 0 || !process.stdout.isTTY;
1318
1380
  const colors2 = (s, n) => {
1381
+ if (noColor) return s;
1319
1382
  const code = n === "PASS" ? "\x1B[32m" : n === "WARN" ? "\x1B[33m" : "\x1B[31m";
1320
1383
  return `${code}${s}\x1B[0m`;
1321
1384
  };
@@ -1376,7 +1439,7 @@ var cached = null;
1376
1439
  function getVersion() {
1377
1440
  if (cached) return cached;
1378
1441
  if (true) {
1379
- cached = "9.5.1";
1442
+ cached = "9.7.0";
1380
1443
  return cached;
1381
1444
  }
1382
1445
  try {
@@ -2536,6 +2599,250 @@ function buildComparisonTable(repos) {
2536
2599
  lines.push("\u2514" + sep.map((s) => "\u2500" + s + "\u2500").join("\u2534") + "\u2518");
2537
2600
  return lines.join("\n");
2538
2601
  }
2602
+ function comparisonRepo(r) {
2603
+ return {
2604
+ fullName: r.fullName,
2605
+ url: r.url,
2606
+ description: r.description,
2607
+ stars: r.stars,
2608
+ forks: r.forks,
2609
+ language: r.language,
2610
+ createdAt: r.createdAt,
2611
+ updatedAt: r.updatedAt,
2612
+ topics: r.topics,
2613
+ archived: r.archived
2614
+ };
2615
+ }
2616
+ function comparisonJson(repos) {
2617
+ return JSON.stringify(repos.map(comparisonRepo), null, 2);
2618
+ }
2619
+ function csvEscape3(val) {
2620
+ const str = String(val ?? "");
2621
+ if (str.includes('"') || str.includes(",") || str.includes("\n")) {
2622
+ return `"${str.replace(/"/g, '""')}"`;
2623
+ }
2624
+ return str;
2625
+ }
2626
+ function comparisonCsv(repos) {
2627
+ const header = "full_name,stars,forks,language,created,updated,archived,url";
2628
+ const rows = repos.map(
2629
+ (r) => [
2630
+ r.fullName,
2631
+ r.stars,
2632
+ r.forks,
2633
+ r.language ?? "",
2634
+ r.createdAt?.slice(0, 10) ?? "",
2635
+ r.updatedAt?.slice(0, 10) ?? "",
2636
+ String(r.archived),
2637
+ r.url
2638
+ ].map(csvEscape3).join(",")
2639
+ );
2640
+ return [header, ...rows].join("\n");
2641
+ }
2642
+ function comparisonMarkdown(repos) {
2643
+ const header = "| Repo | Stars | Forks | Language | Created | Updated | Topics |";
2644
+ const sep = "|------|-------|-------|----------|---------|---------|--------|";
2645
+ const rows = repos.map(
2646
+ (r) => `| [${r.fullName}](${r.url}) | ${r.stars.toLocaleString()} | ${r.forks.toLocaleString()} | ${r.language ?? "\u2014"} | ${r.createdAt?.slice(0, 7) ?? "\u2014"} | ${r.updatedAt?.slice(0, 10) ?? "\u2014"} | ${(r.topics?.slice(0, 5).join(", ") ?? "") || "\u2014"} |`
2647
+ );
2648
+ return [header, sep, ...rows].join("\n");
2649
+ }
2650
+
2651
+ // src/deepdive.ts
2652
+ var USER_AGENT = "ghfind/1.0";
2653
+ function buildHeaders(token) {
2654
+ const headers = { "User-Agent": USER_AGENT };
2655
+ if (token) headers.Authorization = `Bearer ${token}`;
2656
+ return headers;
2657
+ }
2658
+ var REPO_REF_RE = /^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/;
2659
+ function parseRepoRef(input) {
2660
+ const fullName = input.trim().replace(/\.git$/, "").replace(/^@/, "");
2661
+ if (!REPO_REF_RE.test(fullName)) {
2662
+ throw new Error(
2663
+ `"${input}" is not a repo name \u2014 use owner/name, e.g. facebook/react.`
2664
+ );
2665
+ }
2666
+ const [owner, name] = fullName.split("/");
2667
+ return { owner, name };
2668
+ }
2669
+ function repoFromRef(fullName) {
2670
+ const { owner, name } = parseRepoRef(fullName);
2671
+ return {
2672
+ id: 0,
2673
+ fullName: `${owner}/${name}`,
2674
+ name,
2675
+ owner,
2676
+ description: null,
2677
+ url: `https://github.com/${owner}/${name}`,
2678
+ stars: 0,
2679
+ forks: 0,
2680
+ watchers: 0,
2681
+ language: null,
2682
+ topics: [],
2683
+ archived: false,
2684
+ isFork: false,
2685
+ private: false,
2686
+ createdAt: "",
2687
+ updatedAt: "",
2688
+ pushedAt: "",
2689
+ score: 0
2690
+ };
2691
+ }
2692
+ async function resolveRepoFromRef(fullName, token) {
2693
+ const ref = parseRepoRef(fullName);
2694
+ const canonical = `${ref.owner}/${ref.name}`;
2695
+ try {
2696
+ const res = await fetch(`https://api.github.com/repos/${canonical}`, {
2697
+ headers: buildHeaders(token)
2698
+ });
2699
+ if (res.status === 404) {
2700
+ throw new Error(`Repo not found: ${canonical}`);
2701
+ }
2702
+ if (res.ok) {
2703
+ const api = await res.json();
2704
+ return {
2705
+ id: 0,
2706
+ fullName: canonical,
2707
+ name: ref.name,
2708
+ owner: ref.owner,
2709
+ description: api.description,
2710
+ url: api.html_url,
2711
+ stars: api.stargazers_count ?? 0,
2712
+ forks: api.forks_count ?? 0,
2713
+ watchers: api.subscribers_count ?? 0,
2714
+ language: api.language,
2715
+ topics: api.topics ?? [],
2716
+ archived: api.archived ?? false,
2717
+ isFork: api.fork ?? false,
2718
+ private: false,
2719
+ createdAt: api.created_at ?? "",
2720
+ updatedAt: api.updated_at ?? "",
2721
+ pushedAt: api.pushed_at ?? "",
2722
+ score: 0
2723
+ };
2724
+ }
2725
+ } catch (err) {
2726
+ if (err instanceof Error && err.message.startsWith("Repo not found")) {
2727
+ throw err;
2728
+ }
2729
+ }
2730
+ return repoFromRef(canonical);
2731
+ }
2732
+ var README_MAX_CHARS = 5e3;
2733
+ async function fetchDeepDiveRaw(repo, token) {
2734
+ const headers = buildHeaders(token);
2735
+ const [languagesRes, contributorsRes] = await Promise.all([
2736
+ fetch(`https://api.github.com/repos/${repo.owner}/${repo.name}/languages`, {
2737
+ headers
2738
+ }),
2739
+ fetch(
2740
+ `https://api.github.com/repos/${repo.owner}/${repo.name}/contributors?per_page=5`,
2741
+ { headers }
2742
+ )
2743
+ ]);
2744
+ let readmeText = "";
2745
+ for (const path of [
2746
+ `${repo.owner}/${repo.name}/main/README.md`,
2747
+ `${repo.owner}/${repo.name}/master/README.md`,
2748
+ `${repo.owner}/${repo.name}/main/README.rst`
2749
+ ]) {
2750
+ const r = await fetch(`https://raw.githubusercontent.com/${path}`, {
2751
+ headers
2752
+ });
2753
+ if (r.ok) {
2754
+ readmeText = await r.text();
2755
+ break;
2756
+ }
2757
+ }
2758
+ const langEntries = languagesRes.ok ? Object.entries(
2759
+ await languagesRes.json().catch(() => ({}))
2760
+ ) : [];
2761
+ const totalBytes = langEntries.reduce((s, [, v]) => s + v, 0);
2762
+ const contributors = contributorsRes.ok ? await contributorsRes.json().catch(() => []) : [];
2763
+ return {
2764
+ repo: {
2765
+ fullName: repo.fullName,
2766
+ url: repo.url,
2767
+ description: repo.description,
2768
+ stars: repo.stars,
2769
+ forks: repo.forks,
2770
+ language: repo.language,
2771
+ topics: repo.topics
2772
+ },
2773
+ languages: langEntries.sort(([, a], [, b]) => b - a).map(([name, bytes]) => ({
2774
+ name,
2775
+ bytes,
2776
+ percent: totalBytes > 0 ? bytes / totalBytes * 100 : 0
2777
+ })),
2778
+ contributors: contributors.map((c) => ({
2779
+ login: c.login,
2780
+ contributions: c.contributions
2781
+ })),
2782
+ readme: readmeText.slice(0, README_MAX_CHARS)
2783
+ };
2784
+ }
2785
+ function buildDeepDiveJson(data) {
2786
+ return JSON.stringify(data, null, 2);
2787
+ }
2788
+ function buildDeepDiveData(repo, raw) {
2789
+ return {
2790
+ summary: formatSummary(repo),
2791
+ languages: formatLanguages(raw.languages),
2792
+ contributors: formatContributors(raw.contributors),
2793
+ readme: formatReadme(raw.readme)
2794
+ };
2795
+ }
2796
+ async function fetchDeepDive(repo, token) {
2797
+ return buildDeepDiveData(repo, await fetchDeepDiveRaw(repo, token));
2798
+ }
2799
+ function formatSummary(repo) {
2800
+ const lines = [
2801
+ ` ${repo.fullName}`,
2802
+ ` \u2605 ${repo.stars.toLocaleString()} \u25C6 ${repo.forks.toLocaleString()} ${repo.language ?? "\u2014"}`,
2803
+ repo.description ? ` ${repo.description}` : "",
2804
+ repo.topics.length ? ` topics: ${repo.topics.slice(0, 8).join(", ")}` : "",
2805
+ ` ${repo.url}`
2806
+ ];
2807
+ return lines.filter(Boolean).join("\n");
2808
+ }
2809
+ function formatLanguages(langs) {
2810
+ if (langs.length === 0) return " (no language data)";
2811
+ const barW = 20;
2812
+ return langs.slice(0, 8).map(({ name, percent }) => {
2813
+ const pct = percent.toFixed(1);
2814
+ const filled = Math.round(percent / 100 * barW);
2815
+ const bar = "\u2588".repeat(filled) + "\u2591".repeat(barW - filled);
2816
+ return ` ${name.padEnd(16)} ${bar} ${pct}%`;
2817
+ }).join("\n");
2818
+ }
2819
+ function formatContributors(contributors) {
2820
+ if (contributors.length === 0) return " (no contributor data)";
2821
+ return contributors.slice(0, 5).map((c) => ` \u25B2 ${c.login.padEnd(20)} ${c.contributions} commits`).join("\n");
2822
+ }
2823
+ function formatReadme(readme) {
2824
+ if (!readme) return " (no README)";
2825
+ const stripped = readme.replace(/```[\s\S]*?```/g, "[code block]").replace(/#{1,6}\s/g, "").replace(/\*\*(.+?)\*\*/g, "$1").replace(/\[(.+?)\]\(.+?\)/g, "$1").replace(/`([^`]+)`/g, "$1").split("\n").map((l) => l.trim()).filter(Boolean);
2826
+ const lines = stripped.slice(0, 30);
2827
+ if (stripped.length > 30)
2828
+ lines.push("... (more lines, open in browser to read full)");
2829
+ return lines.map((l) => ` ${l}`).join("\n");
2830
+ }
2831
+ function buildDeepDiveText(data) {
2832
+ return [
2833
+ "\u2500\u2500 Summary \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
2834
+ data.summary,
2835
+ "",
2836
+ "\u2500\u2500 Languages \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
2837
+ data.languages,
2838
+ "",
2839
+ "\u2500\u2500 Top Contributors \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
2840
+ data.contributors,
2841
+ "",
2842
+ "\u2500\u2500 README \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
2843
+ data.readme
2844
+ ].join("\n");
2845
+ }
2539
2846
 
2540
2847
  // src/cli.ts
2541
2848
  import { readFileSync as readFileSync6 } from "fs";
@@ -2850,99 +3157,6 @@ function restoreSession() {
2850
3157
  return readJSON(SESSION_FILE, null);
2851
3158
  }
2852
3159
 
2853
- // src/deepdive.ts
2854
- var USER_AGENT = "ghfind/1.0";
2855
- function buildHeaders(token) {
2856
- const headers = { "User-Agent": USER_AGENT };
2857
- if (token) headers.Authorization = `Bearer ${token}`;
2858
- return headers;
2859
- }
2860
- async function fetchDeepDive(repo, token) {
2861
- const headers = buildHeaders(token);
2862
- const [languagesRes, contributorsRes] = await Promise.all([
2863
- fetch(`https://api.github.com/repos/${repo.owner}/${repo.name}/languages`, {
2864
- headers
2865
- }),
2866
- fetch(
2867
- `https://api.github.com/repos/${repo.owner}/${repo.name}/contributors?per_page=5`,
2868
- { headers }
2869
- )
2870
- ]);
2871
- let readmeText = "";
2872
- for (const path of [
2873
- `${repo.owner}/${repo.name}/main/README.md`,
2874
- `${repo.owner}/${repo.name}/master/README.md`,
2875
- `${repo.owner}/${repo.name}/main/README.rst`
2876
- ]) {
2877
- const r = await fetch(`https://raw.githubusercontent.com/${path}`, {
2878
- headers
2879
- });
2880
- if (r.ok) {
2881
- readmeText = await r.text();
2882
- break;
2883
- }
2884
- }
2885
- return {
2886
- summary: formatSummary(repo),
2887
- languages: formatLanguages(
2888
- languagesRes.ok ? await languagesRes.json().catch(() => ({})) : {}
2889
- ),
2890
- contributors: formatContributors(
2891
- contributorsRes.ok ? await contributorsRes.json().catch(() => []) : []
2892
- ),
2893
- readme: formatReadme(readmeText)
2894
- };
2895
- }
2896
- function formatSummary(repo) {
2897
- const lines = [
2898
- ` ${repo.fullName}`,
2899
- ` \u2605 ${repo.stars.toLocaleString()} \u25C6 ${repo.forks.toLocaleString()} ${repo.language ?? "\u2014"}`,
2900
- repo.description ? ` ${repo.description}` : "",
2901
- repo.topics.length ? ` topics: ${repo.topics.slice(0, 8).join(", ")}` : "",
2902
- ` ${repo.url}`
2903
- ];
2904
- return lines.filter(Boolean).join("\n");
2905
- }
2906
- function formatLanguages(langs) {
2907
- const entries = Object.entries(langs);
2908
- if (entries.length === 0) return " (no language data)";
2909
- const total = entries.reduce((s, [, v]) => s + v, 0);
2910
- const barW = 20;
2911
- return entries.sort(([, a], [, b]) => b - a).slice(0, 8).map(([name, bytes]) => {
2912
- const pct = (bytes / total * 100).toFixed(1);
2913
- const filled = Math.round(bytes / total * barW);
2914
- const bar = "\u2588".repeat(filled) + "\u2591".repeat(barW - filled);
2915
- return ` ${name.padEnd(16)} ${bar} ${pct}%`;
2916
- }).join("\n");
2917
- }
2918
- function formatContributors(contributors) {
2919
- if (contributors.length === 0) return " (no contributor data)";
2920
- return contributors.slice(0, 5).map((c) => ` \u25B2 ${c.login.padEnd(20)} ${c.contributions} commits`).join("\n");
2921
- }
2922
- function formatReadme(readme) {
2923
- if (!readme) return " (no README)";
2924
- const stripped = readme.replace(/```[\s\S]*?```/g, "[code block]").replace(/#{1,6}\s/g, "").replace(/\*\*(.+?)\*\*/g, "$1").replace(/\[(.+?)\]\(.+?\)/g, "$1").replace(/`([^`]+)`/g, "$1").split("\n").map((l) => l.trim()).filter(Boolean);
2925
- const lines = stripped.slice(0, 30);
2926
- if (stripped.length > 30)
2927
- lines.push("... (more lines, open in browser to read full)");
2928
- return lines.map((l) => ` ${l}`).join("\n");
2929
- }
2930
- function buildDeepDiveText(data) {
2931
- return [
2932
- "\u2500\u2500 Summary \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
2933
- data.summary,
2934
- "",
2935
- "\u2500\u2500 Languages \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
2936
- data.languages,
2937
- "",
2938
- "\u2500\u2500 Top Contributors \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
2939
- data.contributors,
2940
- "",
2941
- "\u2500\u2500 README \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
2942
- data.readme
2943
- ].join("\n");
2944
- }
2945
-
2946
3160
  // src/markdown-render.ts
2947
3161
  function renderMarkdown(md, opts = {}) {
2948
3162
  const width = opts.width ?? 80;
@@ -5954,6 +6168,688 @@ if (import.meta.main && false) {
5954
6168
  launchBrowser();
5955
6169
  }
5956
6170
 
6171
+ // src/agent-skill.ts
6172
+ function buildGhfindSkill() {
6173
+ return {
6174
+ name: "ghfind-cli",
6175
+ title: "ghfind CLI for agents",
6176
+ description: "Search GitHub (repos, trending, npm packages, org/user profiles) from the terminal via the `ghfind` command. Use when the user asks to find, compare, or track GitHub repositories or npm packages.",
6177
+ builtin: true,
6178
+ content: `# ghfind \u2014 GitHub search for agents
6179
+
6180
+ ghfind searches GitHub from the terminal: repositories, trending, npm
6181
+ packages, org/user profiles. This guide covers the non-interactive surface
6182
+ only \u2014 ignore anything about a TUI in other docs; agents cannot drive it.
6183
+
6184
+ ## Search GitHub repositories
6185
+
6186
+ ghfind "QUERY" --json [--limit N] [--sort best-match|stars|updated|forks]
6187
+
6188
+ Query language (GitHub search qualifiers):
6189
+ language:Rust stars:>1000 topic:cli org:vercel user:torvalds
6190
+ repo:facebook/react pushed:>2026-01-01 license:mit archived:false
6191
+ "exact phrase" -language:JavaScript # quotes, negation
6192
+
6193
+ Formats: --json | --csv | --markdown | --count | --format urls|names|ssh-urls|clone-commands|ids
6194
+
6195
+ ## Trending repositories
6196
+
6197
+ ghfind --trending --json [--since daily|weekly|monthly] [--language rust]
6198
+ ghfind trending rust --json # subcommand form of the same thing
6199
+
6200
+ ## npm packages
6201
+
6202
+ ghfind pkg "QUERY" --json [--limit N] [--sort best-match|score|downloads|name]
6203
+
6204
+ ## Org & user profiles
6205
+
6206
+ ghfind org vercel --json
6207
+ ghfind user torvalds --json
6208
+
6209
+ ## Compare repositories
6210
+
6211
+ ghfind --compare owner/a owner/b [--json|--csv|--markdown]
6212
+ Errors if fewer than 2 of the requested repos resolve; unresolved names are
6213
+ reported in text output and via the error message.
6214
+
6215
+ ## Deep-dive a repo (languages, contributors, README)
6216
+
6217
+ ghfind deep-dive owner/repo --json
6218
+ Text output (no flag) prints the same sections formatted for the terminal.
6219
+
6220
+ ## Rate limits
6221
+
6222
+ 60 req/hr unauthenticated, 5,000 with GITHUB_TOKEN (env var or \`ghfind login\`).
6223
+ Responses are cached ~5 min, so repeated identical queries are cheap.
6224
+
6225
+ ## Bookmarking & release tracking
6226
+
6227
+ Bookmarks live in ghfind's local state, not GitHub. An agent can read the
6228
+ release feed with:
6229
+ ghfind --releases --json
6230
+ Marking releases seen and editing bookmarks is a TUI action; skip it.
6231
+
6232
+ ## Token efficiency
6233
+
6234
+ Prefer --count when only the number of results matters, --format names when
6235
+ you only need fullNames, and --limit to cap payload size (default 50, max 100).
6236
+ `
6237
+ };
6238
+ }
6239
+ var registry = /* @__PURE__ */ new Map();
6240
+ function registerSkill(skill) {
6241
+ registry.set(skill.name, skill);
6242
+ }
6243
+ function getSkill(name) {
6244
+ return registry.get(name);
6245
+ }
6246
+ function listSkills() {
6247
+ return [...registry.values()].sort((a, b) => {
6248
+ if (a.builtin !== b.builtin) return a.builtin ? -1 : 1;
6249
+ return a.name.localeCompare(b.name);
6250
+ });
6251
+ }
6252
+ function ensureBuiltinSkill() {
6253
+ if (!registry.has("ghfind-cli")) {
6254
+ registerSkill(buildGhfindSkill());
6255
+ }
6256
+ }
6257
+ function formatSkillCatalog() {
6258
+ ensureBuiltinSkill();
6259
+ const lines = listSkills().map(
6260
+ (s) => ` ${s.name.padEnd(16)} ${s.description}`
6261
+ );
6262
+ return [
6263
+ "Available agent skills:",
6264
+ ...lines,
6265
+ "",
6266
+ `Run \`ghfind skill <name>\` to print one (ghfind v${getVersion()}).`
6267
+ ].join("\n");
6268
+ }
6269
+
6270
+ // src/mcp-server.ts
6271
+ var SUPPORTED_PROTOCOL_VERSIONS = ["2025-06-18", "2025-03-26", "2024-11-05"];
6272
+ var LATEST_PROTOCOL_VERSION = SUPPORTED_PROTOCOL_VERSIONS[0];
6273
+ var JSONRPC_VERSION = "2.0";
6274
+ var PARSE_ERROR = -32700;
6275
+ var INVALID_REQUEST = -32600;
6276
+ var METHOD_NOT_FOUND = -32601;
6277
+ var INVALID_PARAMS = -32602;
6278
+ var McpToolError = class extends Error {
6279
+ constructor(message) {
6280
+ super(message);
6281
+ this.name = "McpToolError";
6282
+ }
6283
+ };
6284
+ function resolveToken(state, arg) {
6285
+ if (typeof arg === "string" && arg.trim()) return arg.trim();
6286
+ if (state.token) return state.token;
6287
+ return loadConfig().githubToken ?? process.env.GITHUB_TOKEN;
6288
+ }
6289
+ function coerceInt(value, fallback, min, max) {
6290
+ const n = typeof value === "number" ? value : Number(value);
6291
+ if (!Number.isFinite(n)) return fallback;
6292
+ return Math.min(Math.max(Math.trunc(n), min), max);
6293
+ }
6294
+ function coerceEnum(value, allowed, fallback) {
6295
+ return typeof value === "string" && allowed.includes(value) ? value : fallback;
6296
+ }
6297
+ var fmtInt = (n) => n.toLocaleString("en-US");
6298
+ function truncate(s, max) {
6299
+ return s.length <= max ? s : `${s.slice(0, max - 1)}\u2026`;
6300
+ }
6301
+ function slimRepo(r) {
6302
+ return {
6303
+ fullName: r.fullName,
6304
+ description: r.description,
6305
+ url: r.url,
6306
+ stars: r.stars,
6307
+ forks: r.forks,
6308
+ language: r.language,
6309
+ topics: r.topics,
6310
+ pushedAt: r.pushedAt,
6311
+ archived: r.archived
6312
+ };
6313
+ }
6314
+ function formatRepoLines(repos) {
6315
+ return repos.map((r, i) => {
6316
+ const desc = r.description ? ` \u2014 ${truncate(r.description, 100)}` : "";
6317
+ const lang = r.language ? ` [${r.language}]` : "";
6318
+ return `${i + 1}. ${r.fullName} \u2605 ${fmtInt(r.stars)}${lang}${desc}`;
6319
+ }).join("\n");
6320
+ }
6321
+ function formatPackageLines(pkgs) {
6322
+ return pkgs.map((p, i) => {
6323
+ const desc = p.description ? ` \u2014 ${truncate(p.description, 90)}` : "";
6324
+ return `${i + 1}. ${p.name}@${p.version}${desc} (\u2193 ${fmtInt(p.downloads)}/mo)`;
6325
+ }).join("\n");
6326
+ }
6327
+ function asMcpToolError(err) {
6328
+ return new McpToolError(err instanceof Error ? err.message : String(err));
6329
+ }
6330
+ var TRENDING_QUERY = { keywords: [], qualifiers: [], raw: "trending" };
6331
+ var REPO_SORTS = ["best-match", "stars", "updated", "forks"];
6332
+ var TRENDING_SINCE = ["daily", "weekly", "monthly"];
6333
+ var PKG_SORTS = ["best-match", "score", "downloads", "name"];
6334
+ var TOOLS = [
6335
+ {
6336
+ name: "ghfind_search_repos",
6337
+ title: "Search GitHub repositories",
6338
+ description: "Search GitHub repositories with the GitHub search syntax. Supports qualifiers like language:, stars:>1000, topic:, org:, user:, pushed:>2026-01-01, quoted phrases, and -negation. Returns fullNames, stars, languages, descriptions, and URLs.",
6339
+ inputSchema: {
6340
+ type: "object",
6341
+ properties: {
6342
+ query: {
6343
+ type: "string",
6344
+ description: 'GitHub search query, e.g. "rust cli stars:>500" or "language:TypeScript topic:mcp".'
6345
+ },
6346
+ limit: {
6347
+ type: "number",
6348
+ description: "Max results, 1-100 (default 20).",
6349
+ minimum: 1,
6350
+ maximum: 100
6351
+ },
6352
+ sort: {
6353
+ type: "string",
6354
+ description: "Sort strategy (default best-match).",
6355
+ enum: [...REPO_SORTS]
6356
+ },
6357
+ token: {
6358
+ type: "string",
6359
+ description: "Optional GitHub token (raises the rate limit from 60/hr to 5,000/hr)."
6360
+ }
6361
+ },
6362
+ required: ["query"]
6363
+ },
6364
+ async run(args, state) {
6365
+ const query = String(args.query ?? "").trim();
6366
+ if (!query) throw new McpToolError("query must be a non-empty string.");
6367
+ const limit = coerceInt(args.limit, 20, 1, 100);
6368
+ const sort = coerceEnum(
6369
+ args.sort,
6370
+ REPO_SORTS,
6371
+ "best-match"
6372
+ );
6373
+ const token = resolveToken(state, args.token);
6374
+ const provider = createGitHubSearch(void 0, token ? [token] : []);
6375
+ const parsed = parseQuery(query);
6376
+ const options = {
6377
+ limit,
6378
+ sort,
6379
+ json: false,
6380
+ verbose: false,
6381
+ token
6382
+ };
6383
+ const response = await provider.search(parsed, options);
6384
+ const repos = rankRepos(response.repos, sort);
6385
+ try {
6386
+ appendHistory({
6387
+ query,
6388
+ mode: "search",
6389
+ timestamp: Date.now(),
6390
+ resultCount: repos.length
6391
+ });
6392
+ } catch {
6393
+ }
6394
+ const text = repos.length === 0 ? `No repositories found for "${query}" (total_count ${response.totalCount}).` : formatRepoLines(repos) + (response.totalCount > repos.length ? `
6395
+
6396
+ (${fmtInt(response.totalCount)} total matches; showing ${repos.length}.)` : "");
6397
+ return {
6398
+ text,
6399
+ data: {
6400
+ totalCount: response.totalCount,
6401
+ repos: repos.map(slimRepo),
6402
+ ...response.rateLimited ? { rateLimited: true } : {}
6403
+ }
6404
+ };
6405
+ }
6406
+ },
6407
+ {
6408
+ name: "ghfind_trending",
6409
+ title: "Trending GitHub repositories",
6410
+ description: "List repositories trending on GitHub today, this week, or this month, optionally filtered by language. Uses github.com/trending, so results carry stars-today momentum the Search API cannot provide.",
6411
+ inputSchema: {
6412
+ type: "object",
6413
+ properties: {
6414
+ since: {
6415
+ type: "string",
6416
+ description: "Trending period (default daily).",
6417
+ enum: [...TRENDING_SINCE]
6418
+ },
6419
+ language: {
6420
+ type: "string",
6421
+ description: 'Optional language filter slug, e.g. "rust", "python", "typescript".'
6422
+ },
6423
+ limit: {
6424
+ type: "number",
6425
+ description: "Max results, 1-50 (default 25).",
6426
+ minimum: 1,
6427
+ maximum: 50
6428
+ },
6429
+ token: { type: "string", description: "Optional GitHub token." }
6430
+ }
6431
+ },
6432
+ async run(args, state) {
6433
+ const since = coerceEnum(args.since, TRENDING_SINCE, "daily");
6434
+ const limit = coerceInt(args.limit, 25, 1, 50);
6435
+ const token = resolveToken(state, args.token);
6436
+ let language;
6437
+ try {
6438
+ language = resolveTrendingLanguage(
6439
+ typeof args.language === "string" ? args.language : void 0
6440
+ );
6441
+ } catch (err) {
6442
+ throw asMcpToolError(err);
6443
+ }
6444
+ const trending = createTrendingSearch();
6445
+ const options = {
6446
+ limit,
6447
+ sort: "stars",
6448
+ json: false,
6449
+ verbose: false,
6450
+ trendingSince: since,
6451
+ ...language ? { trendingLanguage: language } : {},
6452
+ token
6453
+ };
6454
+ const response = await trending.search(TRENDING_QUERY, options);
6455
+ const repos = response.repos.slice(0, limit);
6456
+ const text = repos.length === 0 ? `No trending repositories returned (${since}${language ? `, ${language}` : ""}).` : formatRepoLines(repos);
6457
+ return {
6458
+ text,
6459
+ data: {
6460
+ since,
6461
+ ...language ? { language } : {},
6462
+ repos: repos.map(slimRepo)
6463
+ }
6464
+ };
6465
+ }
6466
+ },
6467
+ {
6468
+ name: "ghfind_npm_packages",
6469
+ title: "Search npm packages",
6470
+ description: "Search the npm registry: name, version, monthly downloads, quality score, and description for each package.",
6471
+ inputSchema: {
6472
+ type: "object",
6473
+ properties: {
6474
+ query: {
6475
+ type: "string",
6476
+ description: 'Package search text, e.g. "terminal ui".'
6477
+ },
6478
+ limit: {
6479
+ type: "number",
6480
+ description: "Max results, 1-100 (default 20).",
6481
+ minimum: 1,
6482
+ maximum: 100
6483
+ },
6484
+ sort: {
6485
+ type: "string",
6486
+ description: "Sort strategy (default best-match).",
6487
+ enum: [...PKG_SORTS]
6488
+ }
6489
+ },
6490
+ required: ["query"]
6491
+ },
6492
+ async run(args) {
6493
+ const query = String(args.query ?? "").trim();
6494
+ if (!query) throw new McpToolError("query must be a non-empty string.");
6495
+ const limit = coerceInt(args.limit, 20, 1, 100);
6496
+ const sort = coerceEnum(
6497
+ args.sort,
6498
+ PKG_SORTS,
6499
+ "best-match"
6500
+ );
6501
+ const { totalCount, packages } = await createPackageSearch().searchPackage(query, limit);
6502
+ const sorted = sortPackages(packages, sort);
6503
+ const text = sorted.length === 0 ? `No npm packages found for "${query}".` : formatPackageLines(sorted);
6504
+ return { text, data: { totalCount, packages: sorted } };
6505
+ }
6506
+ },
6507
+ {
6508
+ name: "ghfind_org_profile",
6509
+ title: "GitHub org profile",
6510
+ description: "Aggregate profile for a GitHub organization: repo count, total stars/forks of fetched repos, top languages, top repos by stars, and recently active repos.",
6511
+ inputSchema: {
6512
+ type: "object",
6513
+ properties: {
6514
+ org: { type: "string", description: 'Org login, e.g. "vercel".' },
6515
+ limit: {
6516
+ type: "number",
6517
+ description: "How many repos to aggregate, 1-500 (default 100).",
6518
+ minimum: 1,
6519
+ maximum: 500
6520
+ },
6521
+ token: { type: "string", description: "Optional GitHub token." }
6522
+ },
6523
+ required: ["org"]
6524
+ },
6525
+ async run(args, state) {
6526
+ const org = String(args.org ?? "").trim();
6527
+ if (!org) throw new McpToolError("org must be a non-empty string.");
6528
+ const limit = coerceInt(args.limit, 100, 1, 500);
6529
+ const profile = await fetchOrgProfile(org, {
6530
+ token: resolveToken(state, args.token),
6531
+ limit
6532
+ });
6533
+ return { text: formatOrgText(profile), data: { ...profile } };
6534
+ }
6535
+ },
6536
+ {
6537
+ name: "ghfind_user_profile",
6538
+ title: "GitHub user profile",
6539
+ description: "Aggregate profile for a GitHub user: repo count, total stars/forks, followers, top languages, top repos, and recently active repos.",
6540
+ inputSchema: {
6541
+ type: "object",
6542
+ properties: {
6543
+ user: { type: "string", description: 'GitHub login, e.g. "torvalds".' },
6544
+ limit: {
6545
+ type: "number",
6546
+ description: "How many repos to aggregate, 1-500 (default 100).",
6547
+ minimum: 1,
6548
+ maximum: 500
6549
+ },
6550
+ token: { type: "string", description: "Optional GitHub token." }
6551
+ },
6552
+ required: ["user"]
6553
+ },
6554
+ async run(args, state) {
6555
+ const user = String(args.user ?? "").trim();
6556
+ if (!user) throw new McpToolError("user must be a non-empty string.");
6557
+ const limit = coerceInt(args.limit, 100, 1, 500);
6558
+ const profile = await fetchUserProfile(user, {
6559
+ token: resolveToken(state, args.token),
6560
+ limit
6561
+ });
6562
+ return { text: formatUserJson(profile), data: { ...profile } };
6563
+ }
6564
+ },
6565
+ {
6566
+ name: "ghfind_compare_repos",
6567
+ title: "Compare GitHub repositories",
6568
+ description: "Fetch 2-5 repositories and return a side-by-side comparison (stars, forks, language, age, topics, descriptions).",
6569
+ inputSchema: {
6570
+ type: "object",
6571
+ properties: {
6572
+ repos: {
6573
+ type: "array",
6574
+ description: 'Repo fullNames to compare, e.g. ["oven-sh/bun", "nodejs/node"].',
6575
+ items: { type: "string" },
6576
+ minItems: 2,
6577
+ maxItems: 5
6578
+ },
6579
+ token: { type: "string", description: "Optional GitHub token." }
6580
+ },
6581
+ required: ["repos"]
6582
+ },
6583
+ async run(args, state) {
6584
+ const names = Array.isArray(args.repos) ? args.repos.map((r) => String(r)) : [];
6585
+ if (names.length < 2 || names.length > 5) {
6586
+ throw new McpToolError("repos must contain 2-5 repo fullNames.");
6587
+ }
6588
+ const token = resolveToken(state, args.token);
6589
+ const provider = createGitHubSearch(void 0, token ? [token] : []);
6590
+ const found = [];
6591
+ const missing = [];
6592
+ for (const fullName of names) {
6593
+ const res = await provider.search(parseQuery(fullName), {
6594
+ limit: 1,
6595
+ sort: "stars",
6596
+ json: false,
6597
+ verbose: false,
6598
+ token
6599
+ });
6600
+ if (res.repos.length > 0) found.push(res.repos[0]);
6601
+ else missing.push(fullName);
6602
+ }
6603
+ if (found.length < 2) {
6604
+ throw new McpToolError(
6605
+ `Could not resolve enough repos to compare (found ${found.length}${missing.length ? `; not found: ${missing.join(", ")}` : ""}).`
6606
+ );
6607
+ }
6608
+ const text = buildComparisonTable(found) + (missing.length ? `
6609
+
6610
+ Not found: ${missing.join(", ")}` : "");
6611
+ return { text, data: { repos: found.map(slimRepo) } };
6612
+ }
6613
+ },
6614
+ {
6615
+ name: "ghfind_deep_dive",
6616
+ title: "GitHub repo deep-dive",
6617
+ description: "Rich detail for one repository: language breakdown (bytes), top contributors, and a README excerpt.",
6618
+ inputSchema: {
6619
+ type: "object",
6620
+ properties: {
6621
+ repo: {
6622
+ type: "string",
6623
+ description: 'Repo fullName, e.g. "facebook/react".'
6624
+ },
6625
+ token: { type: "string", description: "Optional GitHub token." }
6626
+ },
6627
+ required: ["repo"]
6628
+ },
6629
+ async run(args, state) {
6630
+ const repo = String(args.repo ?? "").trim();
6631
+ if (!repo) throw new McpToolError("repo must be a non-empty string.");
6632
+ let stub;
6633
+ try {
6634
+ stub = await resolveRepoFromRef(repo, resolveToken(state, args.token));
6635
+ } catch (err) {
6636
+ throw asMcpToolError(err);
6637
+ }
6638
+ const data = await fetchDeepDive(stub, resolveToken(state, args.token));
6639
+ return { text: buildDeepDiveText(data), data: { ...data } };
6640
+ }
6641
+ },
6642
+ {
6643
+ name: "ghfind_bookmarked_releases",
6644
+ title: "Bookmarked repo releases",
6645
+ description: "The cached release feed for the user's ghfind bookmarks (read-only). Run `ghfind --releases` in a terminal first to refresh the cache.",
6646
+ inputSchema: { type: "object", properties: {} },
6647
+ async run() {
6648
+ const releases = allCachedReleases();
6649
+ const bookmarkCount = getBookmarks().length;
6650
+ const text = releases.length === 0 ? bookmarkCount === 0 ? "No bookmarks yet. Bookmark repos in the ghfind TUI (Space \u2192 Bookmark)." : `${bookmarkCount} bookmarks, no cached releases. Run \`ghfind --releases\` in a terminal to refresh.` : releases.map(
6651
+ (r) => `${r.fullName} ${r.tagName}${r.prerelease ? " [pre]" : ""} \u2014 ${r.publishedAt.slice(0, 10)}
6652
+ ${r.url}`
6653
+ ).join("\n");
6654
+ return {
6655
+ text,
6656
+ data: {
6657
+ bookmarkCount,
6658
+ releases: releases.map((r) => ({ ...r }))
6659
+ }
6660
+ };
6661
+ }
6662
+ },
6663
+ {
6664
+ name: "ghfind_skill",
6665
+ title: "ghfind CLI usage guide for agents",
6666
+ description: "Print a token-efficient guide to running ghfind directly in a shell (search syntax, flags, rate limits, token-efficiency tips). Prefer this over parsing --help output.",
6667
+ inputSchema: { type: "object", properties: {} },
6668
+ async run() {
6669
+ ensureBuiltinSkill();
6670
+ const skill = getSkill("ghfind-cli");
6671
+ return {
6672
+ text: skill?.content ?? "ghfind skill unavailable.",
6673
+ data: { name: "ghfind-cli" }
6674
+ };
6675
+ }
6676
+ }
6677
+ ];
6678
+ function toolSummaries() {
6679
+ return TOOLS.map((t3) => ({
6680
+ name: t3.name,
6681
+ title: t3.title,
6682
+ description: t3.description,
6683
+ inputSchema: t3.inputSchema
6684
+ }));
6685
+ }
6686
+ function respond(id, result) {
6687
+ return JSON.stringify({ jsonrpc: JSONRPC_VERSION, id, result });
6688
+ }
6689
+ function respondError(id, code, message, data) {
6690
+ return JSON.stringify({
6691
+ jsonrpc: JSONRPC_VERSION,
6692
+ id,
6693
+ error: { code, message, ...data !== void 0 ? { data } : {} }
6694
+ });
6695
+ }
6696
+ function findTool(name) {
6697
+ return TOOLS.find((t3) => t3.name === name);
6698
+ }
6699
+ async function callTool(name, args, state) {
6700
+ const tool = findTool(name);
6701
+ if (!tool) throw new McpToolError(`Unknown tool: ${name}`);
6702
+ try {
6703
+ return await tool.run(args, state);
6704
+ } catch (err) {
6705
+ if (err instanceof McpToolError) throw err;
6706
+ const msg = err instanceof Error ? err.message : String(err);
6707
+ throw new McpToolError(`ghfind ${name} failed: ${msg}`);
6708
+ }
6709
+ }
6710
+ async function processServerLine(line, state, config) {
6711
+ let msg;
6712
+ try {
6713
+ msg = JSON.parse(line);
6714
+ } catch {
6715
+ config.write(respondError(null, PARSE_ERROR, "Parse error"));
6716
+ return;
6717
+ }
6718
+ if (typeof msg !== "object" || msg === null || typeof msg.method !== "string") {
6719
+ config.write(respondError(null, INVALID_REQUEST, "Invalid Request"));
6720
+ return;
6721
+ }
6722
+ const rawId = msg.id;
6723
+ const isNotification = rawId === void 0 || rawId === null;
6724
+ const id = isNotification ? null : rawId;
6725
+ if (msg.method === "notifications/initialized") {
6726
+ state.initialized = true;
6727
+ return;
6728
+ }
6729
+ if (isNotification) return;
6730
+ if (!state.initialized && msg.method !== "initialize" && msg.method !== "ping") {
6731
+ config.write(
6732
+ respondError(
6733
+ id,
6734
+ INVALID_REQUEST,
6735
+ `Server not initialized \u2014 send "initialize" first (method was "${msg.method}").`
6736
+ )
6737
+ );
6738
+ return;
6739
+ }
6740
+ switch (msg.method) {
6741
+ case "initialize": {
6742
+ const requested = String(msg.params?.protocolVersion ?? "");
6743
+ const negotiated = SUPPORTED_PROTOCOL_VERSIONS.includes(requested) ? requested : LATEST_PROTOCOL_VERSION;
6744
+ state.initialized = true;
6745
+ config.write(
6746
+ respond(id, {
6747
+ protocolVersion: negotiated,
6748
+ capabilities: { tools: { listChanged: false } },
6749
+ serverInfo: {
6750
+ name: "ghfind",
6751
+ title: "ghfind \u2014 GitHub search for agents",
6752
+ version: getVersion()
6753
+ },
6754
+ instructions: "ghfind gives agents GitHub search: repositories, trending, npm packages, org/user profiles, repo comparison and deep-dives. Call ghfind_skill for the full CLI guide. Unauthenticated requests are rate-limited to 60/hr; pass a token or set GITHUB_TOKEN for 5,000/hr."
6755
+ })
6756
+ );
6757
+ return;
6758
+ }
6759
+ case "ping": {
6760
+ config.write(respond(id, {}));
6761
+ return;
6762
+ }
6763
+ case "tools/list": {
6764
+ config.write(respond(id, { tools: toolSummaries() }));
6765
+ return;
6766
+ }
6767
+ case "tools/call": {
6768
+ const name = String(msg.params?.name ?? "");
6769
+ const tool = findTool(name);
6770
+ if (!tool) {
6771
+ config.write(respondError(id, INVALID_PARAMS, `Unknown tool: ${name}`));
6772
+ return;
6773
+ }
6774
+ const rawArgs = msg.params?.arguments;
6775
+ const args = typeof rawArgs === "object" && rawArgs !== null ? rawArgs : {};
6776
+ try {
6777
+ const outcome = await callTool(name, args, state);
6778
+ config.write(
6779
+ respond(id, {
6780
+ content: [{ type: "text", text: outcome.text }],
6781
+ ...outcome.data ? { structuredContent: outcome.data } : {},
6782
+ isError: false
6783
+ })
6784
+ );
6785
+ } catch (err) {
6786
+ const message = err instanceof McpToolError ? err.message : `ghfind tool "${name}" failed: ${err instanceof Error ? err.message : String(err)}`;
6787
+ config.write(
6788
+ respond(id, {
6789
+ content: [{ type: "text", text: message }],
6790
+ isError: true
6791
+ })
6792
+ );
6793
+ }
6794
+ return;
6795
+ }
6796
+ default: {
6797
+ config.write(
6798
+ respondError(id, METHOD_NOT_FOUND, `Method not found: ${msg.method}`)
6799
+ );
6800
+ }
6801
+ }
6802
+ }
6803
+ function writeStdout(line) {
6804
+ process.stdout.write(line + "\n");
6805
+ }
6806
+ async function runMcpServer(options = {}) {
6807
+ const state = {
6808
+ initialized: false,
6809
+ ...options.token ? { token: options.token } : {}
6810
+ };
6811
+ const config = { write: writeStdout };
6812
+ let pending = 0;
6813
+ let stdinClosed = false;
6814
+ let shutdownTimer;
6815
+ const maybeExit = () => {
6816
+ if (!stdinClosed || pending > 0) return;
6817
+ process.exit(0);
6818
+ };
6819
+ process.stdin.setEncoding("utf8");
6820
+ let buffer = "";
6821
+ process.stdin.on("data", (chunk) => {
6822
+ buffer += chunk;
6823
+ let idx;
6824
+ while ((idx = buffer.indexOf("\n")) >= 0) {
6825
+ const line = buffer.slice(0, idx).trim();
6826
+ buffer = buffer.slice(idx + 1);
6827
+ if (!line) continue;
6828
+ pending++;
6829
+ void processServerLine(line, state, config).finally(() => {
6830
+ pending--;
6831
+ if (shutdownTimer) {
6832
+ clearTimeout(shutdownTimer);
6833
+ shutdownTimer = void 0;
6834
+ }
6835
+ maybeExit();
6836
+ });
6837
+ }
6838
+ });
6839
+ const close = () => {
6840
+ stdinClosed = true;
6841
+ if (pending === 0) {
6842
+ process.exit(0);
6843
+ return;
6844
+ }
6845
+ shutdownTimer = setTimeout(() => process.exit(0), 3e4);
6846
+ };
6847
+ process.stdin.on("end", close);
6848
+ process.stdin.on("error", close);
6849
+ await new Promise(() => {
6850
+ });
6851
+ }
6852
+
5957
6853
  // src/cli.ts
5958
6854
  process.env.GHFIND_CLI_RUN = "1";
5959
6855
  var __dirname3 = dirname4(fileURLToPath4(import.meta.url));
@@ -5980,7 +6876,8 @@ function parseArgs(args) {
5980
6876
  pkg: false,
5981
6877
  registry: "npm",
5982
6878
  compare: [],
5983
- doctor: false
6879
+ doctor: false,
6880
+ mcp: false
5984
6881
  };
5985
6882
  let queryParts = [];
5986
6883
  for (let i = 0; i < args.length; i++) {
@@ -6001,6 +6898,16 @@ function parseArgs(args) {
6001
6898
  case "--trending":
6002
6899
  flags.trending = true;
6003
6900
  break;
6901
+ case "trending": {
6902
+ flags.trending = true;
6903
+ if (flags.query === "" && i + 1 < args.length && !args[i + 1].startsWith("-")) {
6904
+ flags.language = args[++i];
6905
+ }
6906
+ break;
6907
+ }
6908
+ case "--language":
6909
+ flags.language = args[++i];
6910
+ break;
6004
6911
  case "--watch":
6005
6912
  flags.watch = true;
6006
6913
  break;
@@ -6059,6 +6966,27 @@ function parseArgs(args) {
6059
6966
  case "--completion":
6060
6967
  flags.completion = args[++i];
6061
6968
  break;
6969
+ case "mcp":
6970
+ flags.mcp = true;
6971
+ break;
6972
+ case "deep-dive": {
6973
+ const next = args[i + 1];
6974
+ if (next && !next.startsWith("-")) {
6975
+ flags.deepDive = next;
6976
+ i++;
6977
+ }
6978
+ break;
6979
+ }
6980
+ case "skill": {
6981
+ const next = args[i + 1];
6982
+ if (next && !next.startsWith("-")) {
6983
+ flags.skill = next;
6984
+ i++;
6985
+ } else {
6986
+ flags.skill = "";
6987
+ }
6988
+ break;
6989
+ }
6062
6990
  case "org": {
6063
6991
  const parts = [];
6064
6992
  while (i + 1 < args.length && !args[i + 1].startsWith("-")) {
@@ -6117,12 +7045,18 @@ Usage:
6117
7045
  ghfind <query> --format <fmt> Format lines (urls|names|ssh-urls|clone-commands|ids)
6118
7046
  ghfind <query> --pipe <target> Pipe to clone/open
6119
7047
  ghfind --trending --json Trending repos as JSON
7048
+ ghfind trending <lang> Trending repos, optionally by language
6120
7049
  ghfind --releases Check bookmarks for new releases
6121
7050
  ghfind --compare <r1> <r2> Compare two+ repos side-by-side
7051
+ ghfind --compare a/b c/d --json Compare as JSON|CSV|Markdown
7052
+ ghfind deep-dive <owner/repo> Deep-dive: languages, contributors, README
7053
+ ghfind deep-dive a/b --json Deep-dive as JSON
6122
7054
  ghfind org <name> --json Org profile: repos, stars, top languages
6123
7055
  ghfind user <name> --json User profile: repos, stars, top languages
6124
7056
  ghfind pkg <query> --json Search npm packages, output JSON
6125
7057
  ghfind pkg <query> Search npm packages, text list
7058
+ ghfind mcp Run the MCP server for AI agents (stdio)
7059
+ ghfind skill [name] Print the agent skill guide (or catalog)
6126
7060
  ghfind --watch <query> Watch mode (poll every Ns)
6127
7061
  ghfind init Run setup wizard
6128
7062
  ghfind login Import the gh CLI token or paste one
@@ -6137,6 +7071,7 @@ Options:
6137
7071
  --sort <strategy> Sort: best-match|stars|updated|forks
6138
7072
  --theme <name> Theme: tokyo-night|premium-dark (default: tokyo-night)
6139
7073
  --trending Trending mode
7074
+ --language <lang> Trending language filter (e.g. rust)
6140
7075
  --since <period> Trending period: daily|weekly|monthly
6141
7076
  --pipe <target> Pipe target: clone|open
6142
7077
  --format <fmt> Line format: urls|names|ssh-urls|...
@@ -6171,6 +7106,14 @@ Options:
6171
7106
  await runLoginWizard();
6172
7107
  return;
6173
7108
  }
7109
+ if (flags.mcp) {
7110
+ await runMcpServer({ token: flags.token });
7111
+ return;
7112
+ }
7113
+ if (flags.skill !== void 0) {
7114
+ printAgentSkill(flags.skill);
7115
+ return;
7116
+ }
6174
7117
  if (flags.pkg) {
6175
7118
  const interactive = !flags.query && !flags.json && !flags.csv && !flags.markdown && !flags.count && !flags.format;
6176
7119
  if (interactive) {
@@ -6181,6 +7124,10 @@ Options:
6181
7124
  return;
6182
7125
  }
6183
7126
  const outputFormat = flags.json ? "json" : flags.csv ? "csv" : flags.markdown ? "markdown" : void 0;
7127
+ if (flags.deepDive !== void 0) {
7128
+ await runDeepDive(flags, outputFormat);
7129
+ return;
7130
+ }
6184
7131
  if (flags.org !== void 0) {
6185
7132
  await runOrgProfile(flags, outputFormat);
6186
7133
  return;
@@ -6189,7 +7136,9 @@ Options:
6189
7136
  await runUserProfile(flags, outputFormat);
6190
7137
  return;
6191
7138
  }
6192
- const isNonInteractive = outputFormat || flags.count || flags.format || flags.pipe || flags.watch || flags.releases;
7139
+ const isNonInteractive = outputFormat || flags.count || flags.format || flags.pipe || flags.watch || flags.releases || // `ghfind trending rust` / `ghfind --trending rust` print a list —
7140
+ // a language filter implies non-interactive output
7141
+ flags.trending && (!!flags.language || !!flags.query);
6193
7142
  if (isNonInteractive) {
6194
7143
  if (flags.releases) {
6195
7144
  await runReleases(flags, outputFormat);
@@ -6208,6 +7157,21 @@ function buildSearchContext(flags) {
6208
7157
  provider: createGitHubSearch(void 0, flags.token ? [flags.token] : [])
6209
7158
  };
6210
7159
  }
7160
+ function printAgentSkill(name) {
7161
+ ensureBuiltinSkill();
7162
+ if (!name) {
7163
+ console.log(formatSkillCatalog());
7164
+ return;
7165
+ }
7166
+ const skill = getSkill(name);
7167
+ if (!skill) {
7168
+ console.error(
7169
+ `Unknown skill: ${name}. Available: ${listSkills().map((s) => s.name).join(", ")}`
7170
+ );
7171
+ process.exit(1);
7172
+ }
7173
+ console.log(skill.content);
7174
+ }
6211
7175
  async function runUserProfile(flags, outputFormat) {
6212
7176
  try {
6213
7177
  const profile = await fetchUserProfile(flags.user, {
@@ -6252,19 +7216,40 @@ async function runOrgProfile(flags, outputFormat) {
6252
7216
  process.exit(1);
6253
7217
  }
6254
7218
  }
7219
+ async function runDeepDive(flags, outputFormat) {
7220
+ const ref = flags.deepDive;
7221
+ if (!ref) {
7222
+ console.error(
7223
+ "Usage: ghfind deep-dive <owner/repo> [--json] [--token <t>]"
7224
+ );
7225
+ process.exit(1);
7226
+ }
7227
+ try {
7228
+ const repo = await resolveRepoFromRef(ref, flags.token);
7229
+ const raw = await fetchDeepDiveRaw(repo, flags.token);
7230
+ if (outputFormat === "json") {
7231
+ console.log(buildDeepDiveJson(raw));
7232
+ } else {
7233
+ console.log(buildDeepDiveText(buildDeepDiveData(repo, raw)));
7234
+ }
7235
+ } catch (err) {
7236
+ console.error(err instanceof Error ? err.message : String(err));
7237
+ process.exit(1);
7238
+ }
7239
+ }
6255
7240
  async function runPackageSearch(flags) {
6256
7241
  if (flags.registry !== "npm") {
6257
7242
  console.error(`Registry "${flags.registry}" not yet supported (only npm).`);
6258
7243
  process.exit(1);
6259
7244
  }
6260
- const PKG_SORTS = [
7245
+ const PKG_SORTS2 = [
6261
7246
  "best-match",
6262
7247
  "score",
6263
7248
  "downloads",
6264
7249
  "name"
6265
7250
  ];
6266
7251
  const pkgSort = flags.sort;
6267
- if (!PKG_SORTS.includes(pkgSort)) {
7252
+ if (!PKG_SORTS2.includes(pkgSort)) {
6268
7253
  console.error(
6269
7254
  `Invalid sort for pkg: "${flags.sort}" (use best-match|score|downloads|name).`
6270
7255
  );
@@ -6345,6 +7330,15 @@ async function runNonInteractive(flags, outputFormat) {
6345
7330
  trending: async (ctx2) => {
6346
7331
  const since = flags.since === "weekly" ? "weekly" : flags.since === "monthly" ? "monthly" : "daily";
6347
7332
  const trendingSearch = createTrendingSearch();
7333
+ let language;
7334
+ try {
7335
+ language = resolveTrendingLanguage(
7336
+ flags.language ?? (flags.query ? flags.query.split(/\s+/)[0] : void 0)
7337
+ );
7338
+ } catch (err) {
7339
+ console.error(err instanceof Error ? err.message : String(err));
7340
+ process.exit(1);
7341
+ }
6348
7342
  const response = await trendingSearch.search(
6349
7343
  { keywords: [], qualifiers: [], raw: "trending" },
6350
7344
  {
@@ -6352,7 +7346,8 @@ async function runNonInteractive(flags, outputFormat) {
6352
7346
  sort: "stars",
6353
7347
  json: false,
6354
7348
  verbose: false,
6355
- trendingSince: since
7349
+ trendingSince: since,
7350
+ ...language ? { trendingLanguage: language } : {}
6356
7351
  }
6357
7352
  );
6358
7353
  if (outputFormat) {
@@ -6436,6 +7431,7 @@ async function runNonInteractive(flags, outputFormat) {
6436
7431
  compare: async (_ctx) => {
6437
7432
  const search = createGitHubSearch(void 0, [flags.token ?? ""]);
6438
7433
  const repos = [];
7434
+ const missing = [];
6439
7435
  for (const fullName of flags.compare) {
6440
7436
  const res = await search.search(
6441
7437
  { keywords: [fullName], qualifiers: [], raw: fullName },
@@ -6447,9 +7443,27 @@ async function runNonInteractive(flags, outputFormat) {
6447
7443
  token: flags.token
6448
7444
  }
6449
7445
  );
6450
- repos.push(...res.repos);
7446
+ if (res.repos.length > 0) repos.push(...res.repos);
7447
+ else missing.push(fullName);
7448
+ }
7449
+ if (repos.length < 2) {
7450
+ console.error(
7451
+ `Could not resolve enough repos to compare (found ${repos.length}${missing.length ? `; not found: ${missing.join(", ")}` : ""}).`
7452
+ );
7453
+ process.exit(1);
7454
+ }
7455
+ const notFound = missing.length ? `
7456
+
7457
+ Not found: ${missing.join(", ")}` : "";
7458
+ if (outputFormat === "json") {
7459
+ console.log(comparisonJson(repos));
7460
+ } else if (outputFormat === "csv") {
7461
+ console.log(comparisonCsv(repos));
7462
+ } else if (outputFormat === "markdown") {
7463
+ console.log(comparisonMarkdown(repos));
7464
+ } else {
7465
+ console.log(buildComparisonTable(repos) + notFound);
6451
7466
  }
6452
- console.log(buildComparisonTable(repos));
6453
7467
  }
6454
7468
  };
6455
7469
  const key = flags.compare.length > 0 ? "compare" : flags.trending && !flags.watch ? "trending" : flags.watch ? "watch" : flags.pipe ? "pipe" : flags.format ? "format" : "search";