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/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 === "daily" ? TRENDING_URL : `${TRENDING_URL}?since=${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.5.0";
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 <repo1> <repo2> Compare two+ repos side-by-side
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 user <name> --json User profile: repos, stars, top languages
6121
- ghfind pkg <query> --json Search npm packages, output JSON
6122
- ghfind pkg <query> Search npm packages, text list
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 PKG_SORTS = [
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 (!PKG_SORTS.includes(pkgSort)) {
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
  );