github-search-cli 9.5.0 → 9.6.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 +85 -3
- package/README.md +65 -31
- package/completions/ghfind.bash +1 -1
- package/completions/ghfind.fish +1 -1
- package/completions/ghfind.zsh +2 -0
- package/dist/cli.js +813 -10
- package/package.json +1 -1
- package/release-notes/9.5.1.md +36 -0
- package/release-notes/9.6.0.md +67 -0
- package/scripts/brew.rb +31 -10
- package/scripts/install.sh +118 -0
- package/src/agent-skill.ts +140 -0
- package/src/cli.ts +60 -4
- package/src/doctor.ts +4 -0
- package/src/help.ts +4 -0
- package/src/mcp-server.ts +894 -0
- package/src/search.ts +69 -5
- package/src/types.ts +2 -0
package/dist/cli.js
CHANGED
|
@@ -501,6 +501,55 @@ 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 buildTrendingUrl(since, language) {
|
|
544
|
+
const params = new URLSearchParams();
|
|
545
|
+
if (since !== "daily") params.set("since", since);
|
|
546
|
+
if (language) {
|
|
547
|
+
const slug = trendingLanguageSlug(language);
|
|
548
|
+
if (TRENDING_LANGUAGES.has(slug)) params.set("language", slug);
|
|
549
|
+
}
|
|
550
|
+
const qs = params.toString();
|
|
551
|
+
return qs ? `${TRENDING_URL}?${qs}` : TRENDING_URL;
|
|
552
|
+
}
|
|
504
553
|
function trendingRepoToRepo(r) {
|
|
505
554
|
return {
|
|
506
555
|
id: 0,
|
|
@@ -531,7 +580,7 @@ var TrendingAdapter = class {
|
|
|
531
580
|
}
|
|
532
581
|
async search(_query, options) {
|
|
533
582
|
const since = options.trendingSince ?? "daily";
|
|
534
|
-
const url = since
|
|
583
|
+
const url = buildTrendingUrl(since, options.trendingLanguage);
|
|
535
584
|
let res;
|
|
536
585
|
try {
|
|
537
586
|
res = await fetch(url, { headers: { "User-Agent": "ghfind" } });
|
|
@@ -560,7 +609,8 @@ var SearchModule = class _SearchModule {
|
|
|
560
609
|
options.sort,
|
|
561
610
|
String(options.limit),
|
|
562
611
|
String(options.page ?? 1),
|
|
563
|
-
options.trendingSince ?? "none"
|
|
612
|
+
options.trendingSince ?? "none",
|
|
613
|
+
options.trendingLanguage ?? "none"
|
|
564
614
|
);
|
|
565
615
|
const cached2 = _SearchModule.cache.get(cacheKey);
|
|
566
616
|
if (cached2) {
|
|
@@ -1315,7 +1365,9 @@ function checkDirs() {
|
|
|
1315
1365
|
}
|
|
1316
1366
|
}
|
|
1317
1367
|
function printReport() {
|
|
1368
|
+
const noColor = process.env.NO_COLOR !== void 0 || !process.stdout.isTTY;
|
|
1318
1369
|
const colors2 = (s, n) => {
|
|
1370
|
+
if (noColor) return s;
|
|
1319
1371
|
const code = n === "PASS" ? "\x1B[32m" : n === "WARN" ? "\x1B[33m" : "\x1B[31m";
|
|
1320
1372
|
return `${code}${s}\x1B[0m`;
|
|
1321
1373
|
};
|
|
@@ -1376,7 +1428,7 @@ var cached = null;
|
|
|
1376
1428
|
function getVersion() {
|
|
1377
1429
|
if (cached) return cached;
|
|
1378
1430
|
if (true) {
|
|
1379
|
-
cached = "9.
|
|
1431
|
+
cached = "9.6.0";
|
|
1380
1432
|
return cached;
|
|
1381
1433
|
}
|
|
1382
1434
|
try {
|
|
@@ -2659,6 +2711,10 @@ function buildHelpSections() {
|
|
|
2659
2711
|
{ keys: "Packages", action: "Switch to npm packages" },
|
|
2660
2712
|
{ keys: "Export", action: "Export results to file" },
|
|
2661
2713
|
{ keys: "Share", action: "Share repo as text" },
|
|
2714
|
+
{
|
|
2715
|
+
keys: "Org profile",
|
|
2716
|
+
action: "Org summary: repos, stars, top languages"
|
|
2717
|
+
},
|
|
2662
2718
|
{ keys: "Topics", action: "Browse GitHub topics" },
|
|
2663
2719
|
{ keys: "Saved", action: "View saved searches" },
|
|
2664
2720
|
{ keys: "Notifs", action: "Check GitHub notifications" },
|
|
@@ -5950,6 +6006,715 @@ if (import.meta.main && false) {
|
|
|
5950
6006
|
launchBrowser();
|
|
5951
6007
|
}
|
|
5952
6008
|
|
|
6009
|
+
// src/agent-skill.ts
|
|
6010
|
+
function buildGhfindSkill() {
|
|
6011
|
+
return {
|
|
6012
|
+
name: "ghfind-cli",
|
|
6013
|
+
title: "ghfind CLI for agents",
|
|
6014
|
+
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.",
|
|
6015
|
+
builtin: true,
|
|
6016
|
+
content: `# ghfind \u2014 GitHub search for agents
|
|
6017
|
+
|
|
6018
|
+
ghfind searches GitHub from the terminal: repositories, trending, npm
|
|
6019
|
+
packages, org/user profiles. This guide covers the non-interactive surface
|
|
6020
|
+
only \u2014 ignore anything about a TUI in other docs; agents cannot drive it.
|
|
6021
|
+
|
|
6022
|
+
## Search GitHub repositories
|
|
6023
|
+
|
|
6024
|
+
ghfind "QUERY" --json [--limit N] [--sort best-match|stars|updated|forks]
|
|
6025
|
+
|
|
6026
|
+
Query language (GitHub search qualifiers):
|
|
6027
|
+
language:Rust stars:>1000 topic:cli org:vercel user:torvalds
|
|
6028
|
+
repo:facebook/react pushed:>2026-01-01 license:mit archived:false
|
|
6029
|
+
"exact phrase" -language:JavaScript # quotes, negation
|
|
6030
|
+
|
|
6031
|
+
Formats: --json | --csv | --markdown | --count | --format urls|names|ssh-urls|clone-commands|ids
|
|
6032
|
+
|
|
6033
|
+
## Trending repositories
|
|
6034
|
+
|
|
6035
|
+
ghfind --trending --json [--since daily|weekly|monthly]
|
|
6036
|
+
|
|
6037
|
+
## npm packages
|
|
6038
|
+
|
|
6039
|
+
ghfind pkg "QUERY" --json [--limit N] [--sort best-match|score|downloads|name]
|
|
6040
|
+
|
|
6041
|
+
## Org & user profiles
|
|
6042
|
+
|
|
6043
|
+
ghfind org vercel --json
|
|
6044
|
+
ghfind user torvalds --json
|
|
6045
|
+
|
|
6046
|
+
## Compare repositories
|
|
6047
|
+
|
|
6048
|
+
ghfind --compare owner/a owner/b [--json]
|
|
6049
|
+
Compare is text-only; there is no JSON formatter for it yet.
|
|
6050
|
+
|
|
6051
|
+
## Deep-dive a repo (languages, contributors, README)
|
|
6052
|
+
|
|
6053
|
+
Not exposed on the CLI. Fetch directly instead:
|
|
6054
|
+
curl -s https://api.github.com/repos/OWNER/REPO/languages
|
|
6055
|
+
curl -s https://raw.githubusercontent.com/OWNER/REPO/main/README.md
|
|
6056
|
+
|
|
6057
|
+
## Rate limits
|
|
6058
|
+
|
|
6059
|
+
60 req/hr unauthenticated, 5,000 with GITHUB_TOKEN (env var or \`ghfind login\`).
|
|
6060
|
+
Responses are cached ~5 min, so repeated identical queries are cheap.
|
|
6061
|
+
|
|
6062
|
+
## Bookmarking & release tracking
|
|
6063
|
+
|
|
6064
|
+
Bookmarks live in ghfind's local state, not GitHub. An agent can read the
|
|
6065
|
+
release feed with:
|
|
6066
|
+
ghfind --releases --json
|
|
6067
|
+
Marking releases seen and editing bookmarks is a TUI action; skip it.
|
|
6068
|
+
|
|
6069
|
+
## Token efficiency
|
|
6070
|
+
|
|
6071
|
+
Prefer --count when only the number of results matters, --format names when
|
|
6072
|
+
you only need fullNames, and --limit to cap payload size (default 50, max 100).
|
|
6073
|
+
`
|
|
6074
|
+
};
|
|
6075
|
+
}
|
|
6076
|
+
var registry = /* @__PURE__ */ new Map();
|
|
6077
|
+
function registerSkill(skill) {
|
|
6078
|
+
registry.set(skill.name, skill);
|
|
6079
|
+
}
|
|
6080
|
+
function getSkill(name) {
|
|
6081
|
+
return registry.get(name);
|
|
6082
|
+
}
|
|
6083
|
+
function listSkills() {
|
|
6084
|
+
return [...registry.values()].sort((a, b) => {
|
|
6085
|
+
if (a.builtin !== b.builtin) return a.builtin ? -1 : 1;
|
|
6086
|
+
return a.name.localeCompare(b.name);
|
|
6087
|
+
});
|
|
6088
|
+
}
|
|
6089
|
+
function ensureBuiltinSkill() {
|
|
6090
|
+
if (!registry.has("ghfind-cli")) {
|
|
6091
|
+
registerSkill(buildGhfindSkill());
|
|
6092
|
+
}
|
|
6093
|
+
}
|
|
6094
|
+
function formatSkillCatalog() {
|
|
6095
|
+
ensureBuiltinSkill();
|
|
6096
|
+
const lines = listSkills().map(
|
|
6097
|
+
(s) => ` ${s.name.padEnd(16)} ${s.description}`
|
|
6098
|
+
);
|
|
6099
|
+
return [
|
|
6100
|
+
"Available agent skills:",
|
|
6101
|
+
...lines,
|
|
6102
|
+
"",
|
|
6103
|
+
`Run \`ghfind skill <name>\` to print one (ghfind v${getVersion()}).`
|
|
6104
|
+
].join("\n");
|
|
6105
|
+
}
|
|
6106
|
+
|
|
6107
|
+
// src/mcp-server.ts
|
|
6108
|
+
var SUPPORTED_PROTOCOL_VERSIONS = ["2025-06-18", "2025-03-26", "2024-11-05"];
|
|
6109
|
+
var LATEST_PROTOCOL_VERSION = SUPPORTED_PROTOCOL_VERSIONS[0];
|
|
6110
|
+
var JSONRPC_VERSION = "2.0";
|
|
6111
|
+
var PARSE_ERROR = -32700;
|
|
6112
|
+
var INVALID_REQUEST = -32600;
|
|
6113
|
+
var METHOD_NOT_FOUND = -32601;
|
|
6114
|
+
var INVALID_PARAMS = -32602;
|
|
6115
|
+
var McpToolError = class extends Error {
|
|
6116
|
+
constructor(message) {
|
|
6117
|
+
super(message);
|
|
6118
|
+
this.name = "McpToolError";
|
|
6119
|
+
}
|
|
6120
|
+
};
|
|
6121
|
+
function resolveToken(state, arg) {
|
|
6122
|
+
if (typeof arg === "string" && arg.trim()) return arg.trim();
|
|
6123
|
+
if (state.token) return state.token;
|
|
6124
|
+
return loadConfig().githubToken ?? process.env.GITHUB_TOKEN;
|
|
6125
|
+
}
|
|
6126
|
+
function coerceInt(value, fallback, min, max) {
|
|
6127
|
+
const n = typeof value === "number" ? value : Number(value);
|
|
6128
|
+
if (!Number.isFinite(n)) return fallback;
|
|
6129
|
+
return Math.min(Math.max(Math.trunc(n), min), max);
|
|
6130
|
+
}
|
|
6131
|
+
function coerceEnum(value, allowed, fallback) {
|
|
6132
|
+
return typeof value === "string" && allowed.includes(value) ? value : fallback;
|
|
6133
|
+
}
|
|
6134
|
+
var fmtInt = (n) => n.toLocaleString("en-US");
|
|
6135
|
+
function truncate(s, max) {
|
|
6136
|
+
return s.length <= max ? s : `${s.slice(0, max - 1)}\u2026`;
|
|
6137
|
+
}
|
|
6138
|
+
function slimRepo(r) {
|
|
6139
|
+
return {
|
|
6140
|
+
fullName: r.fullName,
|
|
6141
|
+
description: r.description,
|
|
6142
|
+
url: r.url,
|
|
6143
|
+
stars: r.stars,
|
|
6144
|
+
forks: r.forks,
|
|
6145
|
+
language: r.language,
|
|
6146
|
+
topics: r.topics,
|
|
6147
|
+
pushedAt: r.pushedAt,
|
|
6148
|
+
archived: r.archived
|
|
6149
|
+
};
|
|
6150
|
+
}
|
|
6151
|
+
function formatRepoLines(repos) {
|
|
6152
|
+
return repos.map((r, i) => {
|
|
6153
|
+
const desc = r.description ? ` \u2014 ${truncate(r.description, 100)}` : "";
|
|
6154
|
+
const lang = r.language ? ` [${r.language}]` : "";
|
|
6155
|
+
return `${i + 1}. ${r.fullName} \u2605 ${fmtInt(r.stars)}${lang}${desc}`;
|
|
6156
|
+
}).join("\n");
|
|
6157
|
+
}
|
|
6158
|
+
function formatPackageLines(pkgs) {
|
|
6159
|
+
return pkgs.map((p, i) => {
|
|
6160
|
+
const desc = p.description ? ` \u2014 ${truncate(p.description, 90)}` : "";
|
|
6161
|
+
return `${i + 1}. ${p.name}@${p.version}${desc} (\u2193 ${fmtInt(p.downloads)}/mo)`;
|
|
6162
|
+
}).join("\n");
|
|
6163
|
+
}
|
|
6164
|
+
var REPO_NAME_RE = /^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/;
|
|
6165
|
+
function parseRepoName(input) {
|
|
6166
|
+
const fullName = input.trim().replace(/\.git$/, "").replace(/^@/, "");
|
|
6167
|
+
if (!REPO_NAME_RE.test(fullName)) {
|
|
6168
|
+
throw new McpToolError(
|
|
6169
|
+
`"${input}" is not a repo name \u2014 use owner/name, e.g. facebook/react.`
|
|
6170
|
+
);
|
|
6171
|
+
}
|
|
6172
|
+
const [owner, name] = fullName.split("/");
|
|
6173
|
+
return { owner, name };
|
|
6174
|
+
}
|
|
6175
|
+
function stubRepo(fullName) {
|
|
6176
|
+
const { owner, name } = parseRepoName(fullName);
|
|
6177
|
+
return {
|
|
6178
|
+
id: 0,
|
|
6179
|
+
fullName: `${owner}/${name}`,
|
|
6180
|
+
name,
|
|
6181
|
+
owner,
|
|
6182
|
+
description: null,
|
|
6183
|
+
url: `https://github.com/${owner}/${name}`,
|
|
6184
|
+
stars: 0,
|
|
6185
|
+
forks: 0,
|
|
6186
|
+
watchers: 0,
|
|
6187
|
+
language: null,
|
|
6188
|
+
topics: [],
|
|
6189
|
+
archived: false,
|
|
6190
|
+
isFork: false,
|
|
6191
|
+
private: false,
|
|
6192
|
+
createdAt: "",
|
|
6193
|
+
updatedAt: "",
|
|
6194
|
+
pushedAt: "",
|
|
6195
|
+
score: 0
|
|
6196
|
+
};
|
|
6197
|
+
}
|
|
6198
|
+
var TRENDING_QUERY = { keywords: [], qualifiers: [], raw: "trending" };
|
|
6199
|
+
var REPO_SORTS = ["best-match", "stars", "updated", "forks"];
|
|
6200
|
+
var TRENDING_SINCE = ["daily", "weekly", "monthly"];
|
|
6201
|
+
var PKG_SORTS = ["best-match", "score", "downloads", "name"];
|
|
6202
|
+
var TOOLS = [
|
|
6203
|
+
{
|
|
6204
|
+
name: "ghfind_search_repos",
|
|
6205
|
+
title: "Search GitHub repositories",
|
|
6206
|
+
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.",
|
|
6207
|
+
inputSchema: {
|
|
6208
|
+
type: "object",
|
|
6209
|
+
properties: {
|
|
6210
|
+
query: {
|
|
6211
|
+
type: "string",
|
|
6212
|
+
description: 'GitHub search query, e.g. "rust cli stars:>500" or "language:TypeScript topic:mcp".'
|
|
6213
|
+
},
|
|
6214
|
+
limit: {
|
|
6215
|
+
type: "number",
|
|
6216
|
+
description: "Max results, 1-100 (default 20).",
|
|
6217
|
+
minimum: 1,
|
|
6218
|
+
maximum: 100
|
|
6219
|
+
},
|
|
6220
|
+
sort: {
|
|
6221
|
+
type: "string",
|
|
6222
|
+
description: "Sort strategy (default best-match).",
|
|
6223
|
+
enum: [...REPO_SORTS]
|
|
6224
|
+
},
|
|
6225
|
+
token: {
|
|
6226
|
+
type: "string",
|
|
6227
|
+
description: "Optional GitHub token (raises the rate limit from 60/hr to 5,000/hr)."
|
|
6228
|
+
}
|
|
6229
|
+
},
|
|
6230
|
+
required: ["query"]
|
|
6231
|
+
},
|
|
6232
|
+
async run(args, state) {
|
|
6233
|
+
const query = String(args.query ?? "").trim();
|
|
6234
|
+
if (!query) throw new McpToolError("query must be a non-empty string.");
|
|
6235
|
+
const limit = coerceInt(args.limit, 20, 1, 100);
|
|
6236
|
+
const sort = coerceEnum(
|
|
6237
|
+
args.sort,
|
|
6238
|
+
REPO_SORTS,
|
|
6239
|
+
"best-match"
|
|
6240
|
+
);
|
|
6241
|
+
const token = resolveToken(state, args.token);
|
|
6242
|
+
const provider = createGitHubSearch(void 0, token ? [token] : []);
|
|
6243
|
+
const parsed = parseQuery(query);
|
|
6244
|
+
const options = {
|
|
6245
|
+
limit,
|
|
6246
|
+
sort,
|
|
6247
|
+
json: false,
|
|
6248
|
+
verbose: false,
|
|
6249
|
+
token
|
|
6250
|
+
};
|
|
6251
|
+
const response = await provider.search(parsed, options);
|
|
6252
|
+
const repos = rankRepos(response.repos, sort);
|
|
6253
|
+
try {
|
|
6254
|
+
appendHistory({
|
|
6255
|
+
query,
|
|
6256
|
+
mode: "search",
|
|
6257
|
+
timestamp: Date.now(),
|
|
6258
|
+
resultCount: repos.length
|
|
6259
|
+
});
|
|
6260
|
+
} catch {
|
|
6261
|
+
}
|
|
6262
|
+
const text = repos.length === 0 ? `No repositories found for "${query}" (total_count ${response.totalCount}).` : formatRepoLines(repos) + (response.totalCount > repos.length ? `
|
|
6263
|
+
|
|
6264
|
+
(${fmtInt(response.totalCount)} total matches; showing ${repos.length}.)` : "");
|
|
6265
|
+
return {
|
|
6266
|
+
text,
|
|
6267
|
+
data: {
|
|
6268
|
+
totalCount: response.totalCount,
|
|
6269
|
+
repos: repos.map(slimRepo),
|
|
6270
|
+
...response.rateLimited ? { rateLimited: true } : {}
|
|
6271
|
+
}
|
|
6272
|
+
};
|
|
6273
|
+
}
|
|
6274
|
+
},
|
|
6275
|
+
{
|
|
6276
|
+
name: "ghfind_trending",
|
|
6277
|
+
title: "Trending GitHub repositories",
|
|
6278
|
+
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.",
|
|
6279
|
+
inputSchema: {
|
|
6280
|
+
type: "object",
|
|
6281
|
+
properties: {
|
|
6282
|
+
since: {
|
|
6283
|
+
type: "string",
|
|
6284
|
+
description: "Trending period (default daily).",
|
|
6285
|
+
enum: [...TRENDING_SINCE]
|
|
6286
|
+
},
|
|
6287
|
+
language: {
|
|
6288
|
+
type: "string",
|
|
6289
|
+
description: 'Optional language filter slug, e.g. "rust", "python", "typescript".'
|
|
6290
|
+
},
|
|
6291
|
+
limit: {
|
|
6292
|
+
type: "number",
|
|
6293
|
+
description: "Max results, 1-50 (default 25).",
|
|
6294
|
+
minimum: 1,
|
|
6295
|
+
maximum: 50
|
|
6296
|
+
},
|
|
6297
|
+
token: { type: "string", description: "Optional GitHub token." }
|
|
6298
|
+
}
|
|
6299
|
+
},
|
|
6300
|
+
async run(args, state) {
|
|
6301
|
+
const since = coerceEnum(args.since, TRENDING_SINCE, "daily");
|
|
6302
|
+
const limit = coerceInt(args.limit, 25, 1, 50);
|
|
6303
|
+
const token = resolveToken(state, args.token);
|
|
6304
|
+
let language;
|
|
6305
|
+
const langArg = typeof args.language === "string" ? args.language.trim() : "";
|
|
6306
|
+
if (langArg) {
|
|
6307
|
+
const slug = trendingLanguageSlug(langArg);
|
|
6308
|
+
if (!TRENDING_LANGUAGES.has(slug)) {
|
|
6309
|
+
throw new McpToolError(
|
|
6310
|
+
`"${langArg}" is not a trending language filter. Try one of: rust, python, typescript, javascript, go, zig.`
|
|
6311
|
+
);
|
|
6312
|
+
}
|
|
6313
|
+
language = slug;
|
|
6314
|
+
}
|
|
6315
|
+
const trending = createTrendingSearch();
|
|
6316
|
+
const options = {
|
|
6317
|
+
limit,
|
|
6318
|
+
sort: "stars",
|
|
6319
|
+
json: false,
|
|
6320
|
+
verbose: false,
|
|
6321
|
+
trendingSince: since,
|
|
6322
|
+
...language ? { trendingLanguage: language } : {},
|
|
6323
|
+
token
|
|
6324
|
+
};
|
|
6325
|
+
const response = await trending.search(TRENDING_QUERY, options);
|
|
6326
|
+
const repos = response.repos.slice(0, limit);
|
|
6327
|
+
const text = repos.length === 0 ? `No trending repositories returned (${since}${language ? `, ${language}` : ""}).` : formatRepoLines(repos);
|
|
6328
|
+
return {
|
|
6329
|
+
text,
|
|
6330
|
+
data: {
|
|
6331
|
+
since,
|
|
6332
|
+
...language ? { language } : {},
|
|
6333
|
+
repos: repos.map(slimRepo)
|
|
6334
|
+
}
|
|
6335
|
+
};
|
|
6336
|
+
}
|
|
6337
|
+
},
|
|
6338
|
+
{
|
|
6339
|
+
name: "ghfind_npm_packages",
|
|
6340
|
+
title: "Search npm packages",
|
|
6341
|
+
description: "Search the npm registry: name, version, monthly downloads, quality score, and description for each package.",
|
|
6342
|
+
inputSchema: {
|
|
6343
|
+
type: "object",
|
|
6344
|
+
properties: {
|
|
6345
|
+
query: {
|
|
6346
|
+
type: "string",
|
|
6347
|
+
description: 'Package search text, e.g. "terminal ui".'
|
|
6348
|
+
},
|
|
6349
|
+
limit: {
|
|
6350
|
+
type: "number",
|
|
6351
|
+
description: "Max results, 1-100 (default 20).",
|
|
6352
|
+
minimum: 1,
|
|
6353
|
+
maximum: 100
|
|
6354
|
+
},
|
|
6355
|
+
sort: {
|
|
6356
|
+
type: "string",
|
|
6357
|
+
description: "Sort strategy (default best-match).",
|
|
6358
|
+
enum: [...PKG_SORTS]
|
|
6359
|
+
}
|
|
6360
|
+
},
|
|
6361
|
+
required: ["query"]
|
|
6362
|
+
},
|
|
6363
|
+
async run(args) {
|
|
6364
|
+
const query = String(args.query ?? "").trim();
|
|
6365
|
+
if (!query) throw new McpToolError("query must be a non-empty string.");
|
|
6366
|
+
const limit = coerceInt(args.limit, 20, 1, 100);
|
|
6367
|
+
const sort = coerceEnum(
|
|
6368
|
+
args.sort,
|
|
6369
|
+
PKG_SORTS,
|
|
6370
|
+
"best-match"
|
|
6371
|
+
);
|
|
6372
|
+
const { totalCount, packages } = await createPackageSearch().searchPackage(query, limit);
|
|
6373
|
+
const sorted = sortPackages(packages, sort);
|
|
6374
|
+
const text = sorted.length === 0 ? `No npm packages found for "${query}".` : formatPackageLines(sorted);
|
|
6375
|
+
return { text, data: { totalCount, packages: sorted } };
|
|
6376
|
+
}
|
|
6377
|
+
},
|
|
6378
|
+
{
|
|
6379
|
+
name: "ghfind_org_profile",
|
|
6380
|
+
title: "GitHub org profile",
|
|
6381
|
+
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.",
|
|
6382
|
+
inputSchema: {
|
|
6383
|
+
type: "object",
|
|
6384
|
+
properties: {
|
|
6385
|
+
org: { type: "string", description: 'Org login, e.g. "vercel".' },
|
|
6386
|
+
limit: {
|
|
6387
|
+
type: "number",
|
|
6388
|
+
description: "How many repos to aggregate, 1-500 (default 100).",
|
|
6389
|
+
minimum: 1,
|
|
6390
|
+
maximum: 500
|
|
6391
|
+
},
|
|
6392
|
+
token: { type: "string", description: "Optional GitHub token." }
|
|
6393
|
+
},
|
|
6394
|
+
required: ["org"]
|
|
6395
|
+
},
|
|
6396
|
+
async run(args, state) {
|
|
6397
|
+
const org = String(args.org ?? "").trim();
|
|
6398
|
+
if (!org) throw new McpToolError("org must be a non-empty string.");
|
|
6399
|
+
const limit = coerceInt(args.limit, 100, 1, 500);
|
|
6400
|
+
const profile = await fetchOrgProfile(org, {
|
|
6401
|
+
token: resolveToken(state, args.token),
|
|
6402
|
+
limit
|
|
6403
|
+
});
|
|
6404
|
+
return { text: formatOrgText(profile), data: { ...profile } };
|
|
6405
|
+
}
|
|
6406
|
+
},
|
|
6407
|
+
{
|
|
6408
|
+
name: "ghfind_user_profile",
|
|
6409
|
+
title: "GitHub user profile",
|
|
6410
|
+
description: "Aggregate profile for a GitHub user: repo count, total stars/forks, followers, top languages, top repos, and recently active repos.",
|
|
6411
|
+
inputSchema: {
|
|
6412
|
+
type: "object",
|
|
6413
|
+
properties: {
|
|
6414
|
+
user: { type: "string", description: 'GitHub login, e.g. "torvalds".' },
|
|
6415
|
+
limit: {
|
|
6416
|
+
type: "number",
|
|
6417
|
+
description: "How many repos to aggregate, 1-500 (default 100).",
|
|
6418
|
+
minimum: 1,
|
|
6419
|
+
maximum: 500
|
|
6420
|
+
},
|
|
6421
|
+
token: { type: "string", description: "Optional GitHub token." }
|
|
6422
|
+
},
|
|
6423
|
+
required: ["user"]
|
|
6424
|
+
},
|
|
6425
|
+
async run(args, state) {
|
|
6426
|
+
const user = String(args.user ?? "").trim();
|
|
6427
|
+
if (!user) throw new McpToolError("user must be a non-empty string.");
|
|
6428
|
+
const limit = coerceInt(args.limit, 100, 1, 500);
|
|
6429
|
+
const profile = await fetchUserProfile(user, {
|
|
6430
|
+
token: resolveToken(state, args.token),
|
|
6431
|
+
limit
|
|
6432
|
+
});
|
|
6433
|
+
return { text: formatUserJson(profile), data: { ...profile } };
|
|
6434
|
+
}
|
|
6435
|
+
},
|
|
6436
|
+
{
|
|
6437
|
+
name: "ghfind_compare_repos",
|
|
6438
|
+
title: "Compare GitHub repositories",
|
|
6439
|
+
description: "Fetch 2-5 repositories and return a side-by-side comparison (stars, forks, language, age, topics, descriptions).",
|
|
6440
|
+
inputSchema: {
|
|
6441
|
+
type: "object",
|
|
6442
|
+
properties: {
|
|
6443
|
+
repos: {
|
|
6444
|
+
type: "array",
|
|
6445
|
+
description: 'Repo fullNames to compare, e.g. ["oven-sh/bun", "nodejs/node"].',
|
|
6446
|
+
items: { type: "string" },
|
|
6447
|
+
minItems: 2,
|
|
6448
|
+
maxItems: 5
|
|
6449
|
+
},
|
|
6450
|
+
token: { type: "string", description: "Optional GitHub token." }
|
|
6451
|
+
},
|
|
6452
|
+
required: ["repos"]
|
|
6453
|
+
},
|
|
6454
|
+
async run(args, state) {
|
|
6455
|
+
const names = Array.isArray(args.repos) ? args.repos.map((r) => String(r)) : [];
|
|
6456
|
+
if (names.length < 2 || names.length > 5) {
|
|
6457
|
+
throw new McpToolError("repos must contain 2-5 repo fullNames.");
|
|
6458
|
+
}
|
|
6459
|
+
const token = resolveToken(state, args.token);
|
|
6460
|
+
const provider = createGitHubSearch(void 0, token ? [token] : []);
|
|
6461
|
+
const found = [];
|
|
6462
|
+
const missing = [];
|
|
6463
|
+
for (const fullName of names) {
|
|
6464
|
+
const res = await provider.search(parseQuery(fullName), {
|
|
6465
|
+
limit: 1,
|
|
6466
|
+
sort: "stars",
|
|
6467
|
+
json: false,
|
|
6468
|
+
verbose: false,
|
|
6469
|
+
token
|
|
6470
|
+
});
|
|
6471
|
+
if (res.repos.length > 0) found.push(res.repos[0]);
|
|
6472
|
+
else missing.push(fullName);
|
|
6473
|
+
}
|
|
6474
|
+
if (found.length < 2) {
|
|
6475
|
+
throw new McpToolError(
|
|
6476
|
+
`Could not resolve enough repos to compare (found ${found.length}${missing.length ? `; not found: ${missing.join(", ")}` : ""}).`
|
|
6477
|
+
);
|
|
6478
|
+
}
|
|
6479
|
+
const text = buildComparisonTable(found) + (missing.length ? `
|
|
6480
|
+
|
|
6481
|
+
Not found: ${missing.join(", ")}` : "");
|
|
6482
|
+
return { text, data: { repos: found.map(slimRepo) } };
|
|
6483
|
+
}
|
|
6484
|
+
},
|
|
6485
|
+
{
|
|
6486
|
+
name: "ghfind_deep_dive",
|
|
6487
|
+
title: "GitHub repo deep-dive",
|
|
6488
|
+
description: "Rich detail for one repository: language breakdown (bytes), top contributors, and a README excerpt.",
|
|
6489
|
+
inputSchema: {
|
|
6490
|
+
type: "object",
|
|
6491
|
+
properties: {
|
|
6492
|
+
repo: {
|
|
6493
|
+
type: "string",
|
|
6494
|
+
description: 'Repo fullName, e.g. "facebook/react".'
|
|
6495
|
+
},
|
|
6496
|
+
token: { type: "string", description: "Optional GitHub token." }
|
|
6497
|
+
},
|
|
6498
|
+
required: ["repo"]
|
|
6499
|
+
},
|
|
6500
|
+
async run(args, state) {
|
|
6501
|
+
const repo = String(args.repo ?? "").trim();
|
|
6502
|
+
if (!repo) throw new McpToolError("repo must be a non-empty string.");
|
|
6503
|
+
const stub = stubRepo(repo);
|
|
6504
|
+
const data = await fetchDeepDive(stub, resolveToken(state, args.token));
|
|
6505
|
+
return { text: buildDeepDiveText(data), data: { ...data } };
|
|
6506
|
+
}
|
|
6507
|
+
},
|
|
6508
|
+
{
|
|
6509
|
+
name: "ghfind_bookmarked_releases",
|
|
6510
|
+
title: "Bookmarked repo releases",
|
|
6511
|
+
description: "The cached release feed for the user's ghfind bookmarks (read-only). Run `ghfind --releases` in a terminal first to refresh the cache.",
|
|
6512
|
+
inputSchema: { type: "object", properties: {} },
|
|
6513
|
+
async run() {
|
|
6514
|
+
const releases = allCachedReleases();
|
|
6515
|
+
const bookmarkCount = getBookmarks().length;
|
|
6516
|
+
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(
|
|
6517
|
+
(r) => `${r.fullName} ${r.tagName}${r.prerelease ? " [pre]" : ""} \u2014 ${r.publishedAt.slice(0, 10)}
|
|
6518
|
+
${r.url}`
|
|
6519
|
+
).join("\n");
|
|
6520
|
+
return {
|
|
6521
|
+
text,
|
|
6522
|
+
data: {
|
|
6523
|
+
bookmarkCount,
|
|
6524
|
+
releases: releases.map((r) => ({ ...r }))
|
|
6525
|
+
}
|
|
6526
|
+
};
|
|
6527
|
+
}
|
|
6528
|
+
},
|
|
6529
|
+
{
|
|
6530
|
+
name: "ghfind_skill",
|
|
6531
|
+
title: "ghfind CLI usage guide for agents",
|
|
6532
|
+
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.",
|
|
6533
|
+
inputSchema: { type: "object", properties: {} },
|
|
6534
|
+
async run() {
|
|
6535
|
+
ensureBuiltinSkill();
|
|
6536
|
+
const skill = getSkill("ghfind-cli");
|
|
6537
|
+
return {
|
|
6538
|
+
text: skill?.content ?? "ghfind skill unavailable.",
|
|
6539
|
+
data: { name: "ghfind-cli" }
|
|
6540
|
+
};
|
|
6541
|
+
}
|
|
6542
|
+
}
|
|
6543
|
+
];
|
|
6544
|
+
function toolSummaries() {
|
|
6545
|
+
return TOOLS.map((t3) => ({
|
|
6546
|
+
name: t3.name,
|
|
6547
|
+
title: t3.title,
|
|
6548
|
+
description: t3.description,
|
|
6549
|
+
inputSchema: t3.inputSchema
|
|
6550
|
+
}));
|
|
6551
|
+
}
|
|
6552
|
+
function respond(id, result) {
|
|
6553
|
+
return JSON.stringify({ jsonrpc: JSONRPC_VERSION, id, result });
|
|
6554
|
+
}
|
|
6555
|
+
function respondError(id, code, message, data) {
|
|
6556
|
+
return JSON.stringify({
|
|
6557
|
+
jsonrpc: JSONRPC_VERSION,
|
|
6558
|
+
id,
|
|
6559
|
+
error: { code, message, ...data !== void 0 ? { data } : {} }
|
|
6560
|
+
});
|
|
6561
|
+
}
|
|
6562
|
+
function findTool(name) {
|
|
6563
|
+
return TOOLS.find((t3) => t3.name === name);
|
|
6564
|
+
}
|
|
6565
|
+
async function callTool(name, args, state) {
|
|
6566
|
+
const tool = findTool(name);
|
|
6567
|
+
if (!tool) throw new McpToolError(`Unknown tool: ${name}`);
|
|
6568
|
+
try {
|
|
6569
|
+
return await tool.run(args, state);
|
|
6570
|
+
} catch (err) {
|
|
6571
|
+
if (err instanceof McpToolError) throw err;
|
|
6572
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
6573
|
+
throw new McpToolError(`ghfind ${name} failed: ${msg}`);
|
|
6574
|
+
}
|
|
6575
|
+
}
|
|
6576
|
+
async function processServerLine(line, state, config) {
|
|
6577
|
+
let msg;
|
|
6578
|
+
try {
|
|
6579
|
+
msg = JSON.parse(line);
|
|
6580
|
+
} catch {
|
|
6581
|
+
config.write(respondError(null, PARSE_ERROR, "Parse error"));
|
|
6582
|
+
return;
|
|
6583
|
+
}
|
|
6584
|
+
if (typeof msg !== "object" || msg === null || typeof msg.method !== "string") {
|
|
6585
|
+
config.write(respondError(null, INVALID_REQUEST, "Invalid Request"));
|
|
6586
|
+
return;
|
|
6587
|
+
}
|
|
6588
|
+
const isNotification = msg.id === void 0 || msg.id === null;
|
|
6589
|
+
const id = isNotification ? null : msg.id;
|
|
6590
|
+
if (msg.method === "notifications/initialized") {
|
|
6591
|
+
state.initialized = true;
|
|
6592
|
+
return;
|
|
6593
|
+
}
|
|
6594
|
+
if (isNotification) return;
|
|
6595
|
+
if (!state.initialized && msg.method !== "initialize" && msg.method !== "ping") {
|
|
6596
|
+
config.write(
|
|
6597
|
+
respondError(
|
|
6598
|
+
id,
|
|
6599
|
+
INVALID_REQUEST,
|
|
6600
|
+
`Server not initialized \u2014 send "initialize" first (method was "${msg.method}").`
|
|
6601
|
+
)
|
|
6602
|
+
);
|
|
6603
|
+
return;
|
|
6604
|
+
}
|
|
6605
|
+
switch (msg.method) {
|
|
6606
|
+
case "initialize": {
|
|
6607
|
+
const requested = String(msg.params?.protocolVersion ?? "");
|
|
6608
|
+
const negotiated = SUPPORTED_PROTOCOL_VERSIONS.includes(requested) ? requested : LATEST_PROTOCOL_VERSION;
|
|
6609
|
+
state.initialized = true;
|
|
6610
|
+
config.write(
|
|
6611
|
+
respond(id, {
|
|
6612
|
+
protocolVersion: negotiated,
|
|
6613
|
+
capabilities: { tools: { listChanged: false } },
|
|
6614
|
+
serverInfo: {
|
|
6615
|
+
name: "ghfind",
|
|
6616
|
+
title: "ghfind \u2014 GitHub search for agents",
|
|
6617
|
+
version: getVersion()
|
|
6618
|
+
},
|
|
6619
|
+
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."
|
|
6620
|
+
})
|
|
6621
|
+
);
|
|
6622
|
+
return;
|
|
6623
|
+
}
|
|
6624
|
+
case "ping": {
|
|
6625
|
+
config.write(respond(id, {}));
|
|
6626
|
+
return;
|
|
6627
|
+
}
|
|
6628
|
+
case "tools/list": {
|
|
6629
|
+
config.write(respond(id, { tools: toolSummaries() }));
|
|
6630
|
+
return;
|
|
6631
|
+
}
|
|
6632
|
+
case "tools/call": {
|
|
6633
|
+
const name = String(msg.params?.name ?? "");
|
|
6634
|
+
const tool = findTool(name);
|
|
6635
|
+
if (!tool) {
|
|
6636
|
+
config.write(respondError(id, INVALID_PARAMS, `Unknown tool: ${name}`));
|
|
6637
|
+
return;
|
|
6638
|
+
}
|
|
6639
|
+
const rawArgs = msg.params?.arguments;
|
|
6640
|
+
const args = typeof rawArgs === "object" && rawArgs !== null ? rawArgs : {};
|
|
6641
|
+
try {
|
|
6642
|
+
const outcome = await callTool(name, args, state);
|
|
6643
|
+
config.write(
|
|
6644
|
+
respond(id, {
|
|
6645
|
+
content: [{ type: "text", text: outcome.text }],
|
|
6646
|
+
...outcome.data ? { structuredContent: outcome.data } : {},
|
|
6647
|
+
isError: false
|
|
6648
|
+
})
|
|
6649
|
+
);
|
|
6650
|
+
} catch (err) {
|
|
6651
|
+
const message = err instanceof McpToolError ? err.message : `ghfind tool "${name}" failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
6652
|
+
config.write(
|
|
6653
|
+
respond(id, {
|
|
6654
|
+
content: [{ type: "text", text: message }],
|
|
6655
|
+
isError: true
|
|
6656
|
+
})
|
|
6657
|
+
);
|
|
6658
|
+
}
|
|
6659
|
+
return;
|
|
6660
|
+
}
|
|
6661
|
+
default: {
|
|
6662
|
+
config.write(
|
|
6663
|
+
respondError(id, METHOD_NOT_FOUND, `Method not found: ${msg.method}`)
|
|
6664
|
+
);
|
|
6665
|
+
}
|
|
6666
|
+
}
|
|
6667
|
+
}
|
|
6668
|
+
function writeStdout(line) {
|
|
6669
|
+
process.stdout.write(line + "\n");
|
|
6670
|
+
}
|
|
6671
|
+
async function runMcpServer(options = {}) {
|
|
6672
|
+
const state = {
|
|
6673
|
+
initialized: false,
|
|
6674
|
+
...options.token ? { token: options.token } : {}
|
|
6675
|
+
};
|
|
6676
|
+
const config = { write: writeStdout };
|
|
6677
|
+
let pending = 0;
|
|
6678
|
+
let stdinClosed = false;
|
|
6679
|
+
let shutdownTimer;
|
|
6680
|
+
const maybeExit = () => {
|
|
6681
|
+
if (!stdinClosed || pending > 0) return;
|
|
6682
|
+
process.exit(0);
|
|
6683
|
+
};
|
|
6684
|
+
process.stdin.setEncoding("utf8");
|
|
6685
|
+
let buffer = "";
|
|
6686
|
+
process.stdin.on("data", (chunk) => {
|
|
6687
|
+
buffer += chunk;
|
|
6688
|
+
let idx;
|
|
6689
|
+
while ((idx = buffer.indexOf("\n")) >= 0) {
|
|
6690
|
+
const line = buffer.slice(0, idx).trim();
|
|
6691
|
+
buffer = buffer.slice(idx + 1);
|
|
6692
|
+
if (!line) continue;
|
|
6693
|
+
pending++;
|
|
6694
|
+
void processServerLine(line, state, config).finally(() => {
|
|
6695
|
+
pending--;
|
|
6696
|
+
if (shutdownTimer) {
|
|
6697
|
+
clearTimeout(shutdownTimer);
|
|
6698
|
+
shutdownTimer = void 0;
|
|
6699
|
+
}
|
|
6700
|
+
maybeExit();
|
|
6701
|
+
});
|
|
6702
|
+
}
|
|
6703
|
+
});
|
|
6704
|
+
const close = () => {
|
|
6705
|
+
stdinClosed = true;
|
|
6706
|
+
if (pending === 0) {
|
|
6707
|
+
process.exit(0);
|
|
6708
|
+
return;
|
|
6709
|
+
}
|
|
6710
|
+
shutdownTimer = setTimeout(() => process.exit(0), 3e4);
|
|
6711
|
+
};
|
|
6712
|
+
process.stdin.on("end", close);
|
|
6713
|
+
process.stdin.on("error", close);
|
|
6714
|
+
await new Promise(() => {
|
|
6715
|
+
});
|
|
6716
|
+
}
|
|
6717
|
+
|
|
5953
6718
|
// src/cli.ts
|
|
5954
6719
|
process.env.GHFIND_CLI_RUN = "1";
|
|
5955
6720
|
var __dirname3 = dirname4(fileURLToPath4(import.meta.url));
|
|
@@ -5976,7 +6741,8 @@ function parseArgs(args) {
|
|
|
5976
6741
|
pkg: false,
|
|
5977
6742
|
registry: "npm",
|
|
5978
6743
|
compare: [],
|
|
5979
|
-
doctor: false
|
|
6744
|
+
doctor: false,
|
|
6745
|
+
mcp: false
|
|
5980
6746
|
};
|
|
5981
6747
|
let queryParts = [];
|
|
5982
6748
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -6055,6 +6821,19 @@ function parseArgs(args) {
|
|
|
6055
6821
|
case "--completion":
|
|
6056
6822
|
flags.completion = args[++i];
|
|
6057
6823
|
break;
|
|
6824
|
+
case "mcp":
|
|
6825
|
+
flags.mcp = true;
|
|
6826
|
+
break;
|
|
6827
|
+
case "skill": {
|
|
6828
|
+
const next = args[i + 1];
|
|
6829
|
+
if (next && !next.startsWith("-")) {
|
|
6830
|
+
flags.skill = next;
|
|
6831
|
+
i++;
|
|
6832
|
+
} else {
|
|
6833
|
+
flags.skill = "";
|
|
6834
|
+
}
|
|
6835
|
+
break;
|
|
6836
|
+
}
|
|
6058
6837
|
case "org": {
|
|
6059
6838
|
const parts = [];
|
|
6060
6839
|
while (i + 1 < args.length && !args[i + 1].startsWith("-")) {
|
|
@@ -6114,12 +6893,13 @@ Usage:
|
|
|
6114
6893
|
ghfind <query> --pipe <target> Pipe to clone/open
|
|
6115
6894
|
ghfind --trending --json Trending repos as JSON
|
|
6116
6895
|
ghfind --releases Check bookmarks for new releases
|
|
6117
|
-
ghfind --compare <
|
|
6896
|
+
ghfind --compare <r1> <r2> Compare two+ repos side-by-side
|
|
6118
6897
|
ghfind org <name> --json Org profile: repos, stars, top languages
|
|
6119
6898
|
ghfind user <name> --json User profile: repos, stars, top languages
|
|
6120
|
-
ghfind
|
|
6121
|
-
ghfind pkg <query>
|
|
6122
|
-
ghfind
|
|
6899
|
+
ghfind pkg <query> --json Search npm packages, output JSON
|
|
6900
|
+
ghfind pkg <query> Search npm packages, text list
|
|
6901
|
+
ghfind mcp Run the MCP server for AI agents (stdio)
|
|
6902
|
+
ghfind skill [name] Print the agent skill guide (or catalog)
|
|
6123
6903
|
ghfind --watch <query> Watch mode (poll every Ns)
|
|
6124
6904
|
ghfind init Run setup wizard
|
|
6125
6905
|
ghfind login Import the gh CLI token or paste one
|
|
@@ -6168,6 +6948,14 @@ Options:
|
|
|
6168
6948
|
await runLoginWizard();
|
|
6169
6949
|
return;
|
|
6170
6950
|
}
|
|
6951
|
+
if (flags.mcp) {
|
|
6952
|
+
await runMcpServer({ token: flags.token });
|
|
6953
|
+
return;
|
|
6954
|
+
}
|
|
6955
|
+
if (flags.skill !== void 0) {
|
|
6956
|
+
printAgentSkill(flags.skill);
|
|
6957
|
+
return;
|
|
6958
|
+
}
|
|
6171
6959
|
if (flags.pkg) {
|
|
6172
6960
|
const interactive = !flags.query && !flags.json && !flags.csv && !flags.markdown && !flags.count && !flags.format;
|
|
6173
6961
|
if (interactive) {
|
|
@@ -6205,6 +6993,21 @@ function buildSearchContext(flags) {
|
|
|
6205
6993
|
provider: createGitHubSearch(void 0, flags.token ? [flags.token] : [])
|
|
6206
6994
|
};
|
|
6207
6995
|
}
|
|
6996
|
+
function printAgentSkill(name) {
|
|
6997
|
+
ensureBuiltinSkill();
|
|
6998
|
+
if (!name) {
|
|
6999
|
+
console.log(formatSkillCatalog());
|
|
7000
|
+
return;
|
|
7001
|
+
}
|
|
7002
|
+
const skill = getSkill(name);
|
|
7003
|
+
if (!skill) {
|
|
7004
|
+
console.error(
|
|
7005
|
+
`Unknown skill: ${name}. Available: ${listSkills().map((s) => s.name).join(", ")}`
|
|
7006
|
+
);
|
|
7007
|
+
process.exit(1);
|
|
7008
|
+
}
|
|
7009
|
+
console.log(skill.content);
|
|
7010
|
+
}
|
|
6208
7011
|
async function runUserProfile(flags, outputFormat) {
|
|
6209
7012
|
try {
|
|
6210
7013
|
const profile = await fetchUserProfile(flags.user, {
|
|
@@ -6254,14 +7057,14 @@ async function runPackageSearch(flags) {
|
|
|
6254
7057
|
console.error(`Registry "${flags.registry}" not yet supported (only npm).`);
|
|
6255
7058
|
process.exit(1);
|
|
6256
7059
|
}
|
|
6257
|
-
const
|
|
7060
|
+
const PKG_SORTS2 = [
|
|
6258
7061
|
"best-match",
|
|
6259
7062
|
"score",
|
|
6260
7063
|
"downloads",
|
|
6261
7064
|
"name"
|
|
6262
7065
|
];
|
|
6263
7066
|
const pkgSort = flags.sort;
|
|
6264
|
-
if (!
|
|
7067
|
+
if (!PKG_SORTS2.includes(pkgSort)) {
|
|
6265
7068
|
console.error(
|
|
6266
7069
|
`Invalid sort for pkg: "${flags.sort}" (use best-match|score|downloads|name).`
|
|
6267
7070
|
);
|