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/CHANGELOG.md +95 -0
- package/README.md +77 -27
- package/completions/ghfind.bash +5 -1
- package/completions/ghfind.fish +2 -1
- package/completions/ghfind.zsh +5 -0
- package/dist/cli.js +1117 -103
- package/package.json +1 -1
- package/release-notes/9.6.0.md +67 -0
- package/release-notes/9.7.0.md +53 -0
- package/scripts/brew.rb +31 -10
- package/scripts/install.sh +118 -0
- package/src/agent-skill.ts +141 -0
- package/src/cli.ts +176 -4
- package/src/compare.ts +76 -1
- package/src/deepdive.ts +195 -23
- package/src/doctor.ts +4 -0
- package/src/mcp-server.ts +865 -0
- package/src/search.ts +88 -5
- package/src/types.ts +2 -0
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
|
|
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.
|
|
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
|
|
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 (!
|
|
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";
|