github-search-cli 9.8.0 → 9.8.1
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 +46 -0
- package/README.md +70 -14
- package/completions/ghfind.bash +5 -1
- package/completions/ghfind.fish +6 -1
- package/completions/ghfind.zsh +14 -1
- package/dist/cli.js +694 -33
- package/package.json +1 -1
- package/release-notes/9.8.1.md +74 -0
- package/src/agent-skill.ts +14 -0
- package/src/cli.ts +413 -1
- package/src/image-loader.ts +30 -1
- package/src/markdown-view.ts +20 -4
- package/src/mcp-server.ts +60 -0
- package/src/readme.ts +54 -0
- package/src/skill-finder.ts +445 -0
- package/src/tui.ts +8 -22
package/dist/cli.js
CHANGED
|
@@ -1538,7 +1538,7 @@ var cached = null;
|
|
|
1538
1538
|
function getVersion() {
|
|
1539
1539
|
if (cached) return cached;
|
|
1540
1540
|
if (true) {
|
|
1541
|
-
cached = "9.8.
|
|
1541
|
+
cached = "9.8.1";
|
|
1542
1542
|
return cached;
|
|
1543
1543
|
}
|
|
1544
1544
|
try {
|
|
@@ -2309,6 +2309,12 @@ function removeBookmark(fullName) {
|
|
|
2309
2309
|
const bookmarks = loadRaw().filter((b) => b.repo.fullName !== fullName);
|
|
2310
2310
|
saveRaw(bookmarks);
|
|
2311
2311
|
}
|
|
2312
|
+
function searchBookmarks(query) {
|
|
2313
|
+
const q = query.toLowerCase();
|
|
2314
|
+
return getBookmarks().filter(
|
|
2315
|
+
(b) => b.repo.fullName.toLowerCase().includes(q) || (b.repo.description ?? "").toLowerCase().includes(q) || b.tags.some((t3) => t3.toLowerCase().includes(q))
|
|
2316
|
+
);
|
|
2317
|
+
}
|
|
2312
2318
|
function getLastSeen(fullName) {
|
|
2313
2319
|
return loadRaw().find((b) => b.repo.fullName === fullName)?.lastSeenAt ?? 0;
|
|
2314
2320
|
}
|
|
@@ -2946,8 +2952,8 @@ function buildDeepDiveText(data) {
|
|
|
2946
2952
|
}
|
|
2947
2953
|
|
|
2948
2954
|
// src/cli.ts
|
|
2949
|
-
import { readFileSync as
|
|
2950
|
-
import { join as
|
|
2955
|
+
import { readFileSync as readFileSync7 } from "fs";
|
|
2956
|
+
import { join as join7, dirname as dirname4 } from "path";
|
|
2951
2957
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
2952
2958
|
|
|
2953
2959
|
// src/tui.ts
|
|
@@ -3577,7 +3583,25 @@ var RAW_HOSTS = /* @__PURE__ */ new Set([
|
|
|
3577
3583
|
]);
|
|
3578
3584
|
var RASTER_EXT = /\.(png|jpe?g|webp|gif|bmp|avif|heic|tiff?)(?:[?#]|$)/i;
|
|
3579
3585
|
var CACHE_LIMIT = 24;
|
|
3586
|
+
var MAX_CONCURRENT_IMAGE_FETCHES = 4;
|
|
3580
3587
|
var cache = /* @__PURE__ */ new Map();
|
|
3588
|
+
var activeFetches = 0;
|
|
3589
|
+
var waiting = [];
|
|
3590
|
+
function releaseSlot() {
|
|
3591
|
+
activeFetches--;
|
|
3592
|
+
waiting.shift()?.();
|
|
3593
|
+
}
|
|
3594
|
+
async function withSlot(job) {
|
|
3595
|
+
if (activeFetches >= MAX_CONCURRENT_IMAGE_FETCHES) {
|
|
3596
|
+
await new Promise((resolve) => waiting.push(resolve));
|
|
3597
|
+
}
|
|
3598
|
+
activeFetches++;
|
|
3599
|
+
try {
|
|
3600
|
+
return await job();
|
|
3601
|
+
} finally {
|
|
3602
|
+
releaseSlot();
|
|
3603
|
+
}
|
|
3604
|
+
}
|
|
3581
3605
|
function decodeDataUri(uri) {
|
|
3582
3606
|
const match = /^data:([^;,]+)?(;base64)?,(.*)$/is.exec(uri);
|
|
3583
3607
|
if (!match) return null;
|
|
@@ -3660,7 +3684,7 @@ function isGitHubHost(hostname) {
|
|
|
3660
3684
|
async function fetchImageBytes(url, opts = {}) {
|
|
3661
3685
|
const cached2 = cache.get(url);
|
|
3662
3686
|
if (cached2) return cached2;
|
|
3663
|
-
const pending = loadImageBytes(url, opts);
|
|
3687
|
+
const pending = withSlot(() => loadImageBytes(url, opts));
|
|
3664
3688
|
cache.set(url, pending);
|
|
3665
3689
|
while (cache.size > CACHE_LIMIT) {
|
|
3666
3690
|
const oldest = cache.keys().next().value;
|
|
@@ -3793,6 +3817,7 @@ function extractImageRefs(token) {
|
|
|
3793
3817
|
|
|
3794
3818
|
// src/markdown-view.ts
|
|
3795
3819
|
var MAX_IMAGE_ROWS = 24;
|
|
3820
|
+
var RASTER_CACHE_LIMIT = 32;
|
|
3796
3821
|
function createMarkdownStyle(colors2) {
|
|
3797
3822
|
const surface = colors2.surface ?? colors2.bg;
|
|
3798
3823
|
return SyntaxStyle.fromStyles({
|
|
@@ -3887,6 +3912,11 @@ function createMarkdownView(opts) {
|
|
|
3887
3912
|
});
|
|
3888
3913
|
})();
|
|
3889
3914
|
rasterCache.set(key, pending);
|
|
3915
|
+
while (rasterCache.size > RASTER_CACHE_LIMIT) {
|
|
3916
|
+
const oldest = rasterCache.keys().next().value;
|
|
3917
|
+
if (oldest === void 0) break;
|
|
3918
|
+
rasterCache.delete(oldest);
|
|
3919
|
+
}
|
|
3890
3920
|
return pending;
|
|
3891
3921
|
};
|
|
3892
3922
|
const imageNode = (image) => {
|
|
@@ -3902,9 +3932,6 @@ function createMarkdownView(opts) {
|
|
|
3902
3932
|
container.add(placeholder);
|
|
3903
3933
|
void rasterize(url).then(
|
|
3904
3934
|
(cells) => {
|
|
3905
|
-
console.error(
|
|
3906
|
-
`[dbg] image resolved: cells=${cells ? `${cells.cols}x${cells.rows}` : "null"} destroyed=${container.isDestroyed} parent=${!!container.parent} enabled=${imagesEnabled}`
|
|
3907
|
-
);
|
|
3908
3935
|
if (container.isDestroyed || !container.parent) return;
|
|
3909
3936
|
container.remove(placeholder);
|
|
3910
3937
|
placeholder.destroy();
|
|
@@ -3923,7 +3950,7 @@ function createMarkdownView(opts) {
|
|
|
3923
3950
|
renderable.requestRender();
|
|
3924
3951
|
},
|
|
3925
3952
|
(err) => {
|
|
3926
|
-
|
|
3953
|
+
void err;
|
|
3927
3954
|
}
|
|
3928
3955
|
);
|
|
3929
3956
|
return container;
|
|
@@ -3978,6 +4005,8 @@ function createMarkdownView(opts) {
|
|
|
3978
4005
|
renderable.fg = text();
|
|
3979
4006
|
renderable.bg = bg();
|
|
3980
4007
|
renderable.content = markdown;
|
|
4008
|
+
renderable.streaming = false;
|
|
4009
|
+
renderable.requestRender();
|
|
3981
4010
|
},
|
|
3982
4011
|
setImagesEnabled(enabled) {
|
|
3983
4012
|
if (imagesEnabled === enabled) return;
|
|
@@ -3988,6 +4017,29 @@ function createMarkdownView(opts) {
|
|
|
3988
4017
|
};
|
|
3989
4018
|
}
|
|
3990
4019
|
|
|
4020
|
+
// src/readme.ts
|
|
4021
|
+
var CANDIDATES = [
|
|
4022
|
+
["main", "README.md"],
|
|
4023
|
+
["master", "README.md"],
|
|
4024
|
+
["main", "README.rst"]
|
|
4025
|
+
];
|
|
4026
|
+
async function fetchReadme(owner, repo, opts = {}) {
|
|
4027
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
4028
|
+
const headers = { "User-Agent": "ghfind/1.0" };
|
|
4029
|
+
if (opts.token) headers.Authorization = `Bearer ${opts.token}`;
|
|
4030
|
+
const attempts = CANDIDATES.map(async ([branch, file]) => {
|
|
4031
|
+
const url = readmeBaseUrl(owner, repo, branch, file);
|
|
4032
|
+
const res = await fetchImpl(url, { headers });
|
|
4033
|
+
if (!res.ok) throw new Error(`no readme at ${url}`);
|
|
4034
|
+
return { text: await res.text(), sourceUrl: url };
|
|
4035
|
+
});
|
|
4036
|
+
try {
|
|
4037
|
+
return await Promise.any(attempts);
|
|
4038
|
+
} catch {
|
|
4039
|
+
return null;
|
|
4040
|
+
}
|
|
4041
|
+
}
|
|
4042
|
+
|
|
3991
4043
|
// src/explore.ts
|
|
3992
4044
|
async function fetchTopics() {
|
|
3993
4045
|
const res = await fetch(
|
|
@@ -5889,27 +5941,13 @@ async function launchBrowser(theme_override) {
|
|
|
5889
5941
|
readmeScroll.scrollTop = 0;
|
|
5890
5942
|
showOverlay("readme");
|
|
5891
5943
|
try {
|
|
5892
|
-
const
|
|
5893
|
-
|
|
5894
|
-
|
|
5895
|
-
|
|
5896
|
-
|
|
5897
|
-
["main", "README.md"],
|
|
5898
|
-
["master", "README.md"],
|
|
5899
|
-
["main", "README.rst"]
|
|
5900
|
-
]) {
|
|
5901
|
-
const url = readmeBaseUrl(repo.owner, repo.name, branch, file);
|
|
5902
|
-
const r = await fetch(url, { headers });
|
|
5903
|
-
if (r.ok) {
|
|
5904
|
-
text = await r.text();
|
|
5905
|
-
sourceUrl = url;
|
|
5906
|
-
break;
|
|
5907
|
-
}
|
|
5908
|
-
}
|
|
5909
|
-
if (!text) {
|
|
5910
|
-
readmeView.setContent(`_(no README found for ${repo.fullName})_`);
|
|
5944
|
+
const readme = await fetchReadme(repo.owner, repo.name, {
|
|
5945
|
+
token: githubToken
|
|
5946
|
+
});
|
|
5947
|
+
if (readme) {
|
|
5948
|
+
readmeView.setContent(readme.text, readme.sourceUrl);
|
|
5911
5949
|
} else {
|
|
5912
|
-
readmeView.setContent(
|
|
5950
|
+
readmeView.setContent(`_(no README found for ${repo.fullName})_`);
|
|
5913
5951
|
}
|
|
5914
5952
|
} catch {
|
|
5915
5953
|
readmeView.setContent("_Failed to load README._");
|
|
@@ -6931,6 +6969,12 @@ reported in text output and via the error message.
|
|
|
6931
6969
|
ghfind deep-dive owner/repo --json
|
|
6932
6970
|
Text output (no flag) prints the same sections formatted for the terminal.
|
|
6933
6971
|
|
|
6972
|
+
## Finding skills
|
|
6973
|
+
ghfind skill search QUERY # skills installed on this machine
|
|
6974
|
+
ghfind skill search QUERY --remote # the public skills.sh ecosystem
|
|
6975
|
+
Results carry path+description (local) or install command (registry, read-only --
|
|
6976
|
+
nothing is installed). The MCP server exposes the same search as
|
|
6977
|
+
ghfind_skill_search.
|
|
6934
6978
|
## Rate limits
|
|
6935
6979
|
|
|
6936
6980
|
60 req/hr unauthenticated, 5,000 with GITHUB_TOKEN (env var or \`ghfind login\`).
|
|
@@ -6942,6 +6986,14 @@ Bookmarks live in ghfind's local state, not GitHub. An agent can read the
|
|
|
6942
6986
|
release feed with:
|
|
6943
6987
|
ghfind --releases --json
|
|
6944
6988
|
Marking releases seen and editing bookmarks is a TUI action; skip it.
|
|
6989
|
+
## Local state (read-only)
|
|
6990
|
+
ghfind bookmarks [QUERY] --json # saved repos, newest first
|
|
6991
|
+
ghfind history [QUERY] --json # past searches
|
|
6992
|
+
ghfind saved --json # named searches
|
|
6993
|
+
ghfind topics --json # popular GitHub topics
|
|
6994
|
+
ghfind readme owner/repo # print a repo README
|
|
6995
|
+
ghfind share owner/repo --as gh-cli|markdown|plain|short
|
|
6996
|
+
These never write local state; skip them when nothing needs reading.
|
|
6945
6997
|
|
|
6946
6998
|
## Token efficiency
|
|
6947
6999
|
|
|
@@ -6981,6 +7033,233 @@ function formatSkillCatalog() {
|
|
|
6981
7033
|
].join("\n");
|
|
6982
7034
|
}
|
|
6983
7035
|
|
|
7036
|
+
// src/skill-finder.ts
|
|
7037
|
+
import { readdirSync, readFileSync as readFileSync6 } from "fs";
|
|
7038
|
+
import { homedir as homedir3 } from "os";
|
|
7039
|
+
import { delimiter, join as join6 } from "path";
|
|
7040
|
+
var REGISTRY_SEARCH_ENDPOINT = "https://skills.sh/api/search";
|
|
7041
|
+
var MIN_QUERY_LENGTH = 2;
|
|
7042
|
+
var MAX_SKILL_RESULTS = 100;
|
|
7043
|
+
function displayPath(path) {
|
|
7044
|
+
const home = homedir3();
|
|
7045
|
+
const normalized = path.replace(/\\/g, "/");
|
|
7046
|
+
const homeNorm = home.replace(/\\/g, "/").replace(/\/$/, "");
|
|
7047
|
+
if (normalized === homeNorm) return "~";
|
|
7048
|
+
if (normalized.startsWith(`${homeNorm}/`)) {
|
|
7049
|
+
return `~${normalized.slice(homeNorm.length)}`;
|
|
7050
|
+
}
|
|
7051
|
+
return normalized;
|
|
7052
|
+
}
|
|
7053
|
+
function defaultSkillRoots(cwd = process.cwd()) {
|
|
7054
|
+
const home = homedir3();
|
|
7055
|
+
return [
|
|
7056
|
+
join6(home, ".codex", "skills"),
|
|
7057
|
+
join6(home, ".agents", "skills"),
|
|
7058
|
+
join6(cwd, ".codex", "skills"),
|
|
7059
|
+
join6(cwd, ".github", "skills")
|
|
7060
|
+
];
|
|
7061
|
+
}
|
|
7062
|
+
function resolveSkillRoots(cwd = process.cwd()) {
|
|
7063
|
+
const override = process.env.GHFIND_SKILL_ROOTS;
|
|
7064
|
+
const paths = override?.trim() ? override.split(delimiter).filter((p) => p.trim()).map((p) => p.trim()) : defaultSkillRoots(cwd);
|
|
7065
|
+
return paths.map((path) => ({ path, label: displayPath(path) }));
|
|
7066
|
+
}
|
|
7067
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git"]);
|
|
7068
|
+
var MAX_DEPTH = 4;
|
|
7069
|
+
function unquote(value) {
|
|
7070
|
+
const trimmed = value.trim();
|
|
7071
|
+
if (trimmed.length >= 2 && (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'"))) {
|
|
7072
|
+
return trimmed.slice(1, -1).trim();
|
|
7073
|
+
}
|
|
7074
|
+
return trimmed;
|
|
7075
|
+
}
|
|
7076
|
+
function parseSkillFrontmatter(text) {
|
|
7077
|
+
const match = /^---\r?\n([\s\S]*?)\r?\n---/.exec(text);
|
|
7078
|
+
if (!match) return null;
|
|
7079
|
+
const lines = match[1].split(/\r?\n/);
|
|
7080
|
+
const fields = {};
|
|
7081
|
+
for (let i = 0; i < lines.length; i++) {
|
|
7082
|
+
const field = /^([A-Za-z_][\w-]*):[ \t]*(.*)$/.exec(lines[i]);
|
|
7083
|
+
if (!field) continue;
|
|
7084
|
+
const key = field[1].toLowerCase();
|
|
7085
|
+
const inline = field[2].trim();
|
|
7086
|
+
if (inline === ">" || inline === "|" || /^[>|][-+]?$/.test(inline)) {
|
|
7087
|
+
const literal = inline.startsWith("|");
|
|
7088
|
+
const parts = [];
|
|
7089
|
+
while (i + 1 < lines.length && /^[ \t]+\S/.test(lines[i + 1])) {
|
|
7090
|
+
parts.push(lines[++i].trim());
|
|
7091
|
+
}
|
|
7092
|
+
fields[key] = literal ? parts.join("\n") : parts.join(" ");
|
|
7093
|
+
} else {
|
|
7094
|
+
fields[key] = unquote(inline);
|
|
7095
|
+
}
|
|
7096
|
+
}
|
|
7097
|
+
const name = fields.name ?? "";
|
|
7098
|
+
if (!name) return null;
|
|
7099
|
+
return { name, description: fields.description ?? "" };
|
|
7100
|
+
}
|
|
7101
|
+
function scanLocalSkills(roots = resolveSkillRoots()) {
|
|
7102
|
+
const byName = /* @__PURE__ */ new Map();
|
|
7103
|
+
for (const root of roots) {
|
|
7104
|
+
for (const dir of walkDirs(root.path)) {
|
|
7105
|
+
const entries = listDir(dir);
|
|
7106
|
+
if (!entries) continue;
|
|
7107
|
+
if (!entries.some((e) => e.isFile() && e.name === "SKILL.md")) continue;
|
|
7108
|
+
const file = join6(dir, "SKILL.md");
|
|
7109
|
+
try {
|
|
7110
|
+
const parsed = parseSkillFrontmatter(readFileSync6(file, "utf-8"));
|
|
7111
|
+
if (!parsed || byName.has(parsed.name)) continue;
|
|
7112
|
+
byName.set(parsed.name, {
|
|
7113
|
+
name: parsed.name,
|
|
7114
|
+
description: parsed.description.replace(/\s+/g, " ").trim(),
|
|
7115
|
+
path: file,
|
|
7116
|
+
origin: root.label
|
|
7117
|
+
});
|
|
7118
|
+
} catch {
|
|
7119
|
+
}
|
|
7120
|
+
}
|
|
7121
|
+
}
|
|
7122
|
+
return [...byName.values()];
|
|
7123
|
+
}
|
|
7124
|
+
function listDir(dir) {
|
|
7125
|
+
try {
|
|
7126
|
+
return readdirSync(dir, { withFileTypes: true });
|
|
7127
|
+
} catch {
|
|
7128
|
+
return null;
|
|
7129
|
+
}
|
|
7130
|
+
}
|
|
7131
|
+
function* walkDirs(root) {
|
|
7132
|
+
const queue = [[root, 0]];
|
|
7133
|
+
while (queue.length > 0) {
|
|
7134
|
+
const [dir, depth] = queue.shift();
|
|
7135
|
+
const entries = listDir(dir);
|
|
7136
|
+
if (!entries) continue;
|
|
7137
|
+
yield dir;
|
|
7138
|
+
if (depth >= MAX_DEPTH) continue;
|
|
7139
|
+
for (const entry of entries) {
|
|
7140
|
+
if (!entry.isDirectory() || SKIP_DIRS.has(entry.name)) continue;
|
|
7141
|
+
queue.push([join6(dir, entry.name), depth + 1]);
|
|
7142
|
+
}
|
|
7143
|
+
}
|
|
7144
|
+
}
|
|
7145
|
+
function localMatchTier(hit, needle) {
|
|
7146
|
+
const name = hit.name.toLowerCase();
|
|
7147
|
+
if (name === needle) return 0;
|
|
7148
|
+
if (name.startsWith(needle)) return 1;
|
|
7149
|
+
if (name.includes(needle)) return 2;
|
|
7150
|
+
if (hit.description.toLowerCase().includes(needle)) return 3;
|
|
7151
|
+
return null;
|
|
7152
|
+
}
|
|
7153
|
+
function rankLocalSkills(hits, query, limit = MAX_SKILL_RESULTS) {
|
|
7154
|
+
const needle = query.trim().toLowerCase();
|
|
7155
|
+
return hits.map((hit) => ({ hit, tier: localMatchTier(hit, needle) })).filter(
|
|
7156
|
+
(entry) => entry.tier !== null
|
|
7157
|
+
).sort((a, b) => a.tier - b.tier || a.hit.name.localeCompare(b.hit.name)).slice(0, clampLimit(limit)).map((entry) => entry.hit);
|
|
7158
|
+
}
|
|
7159
|
+
function clampLimit(limit) {
|
|
7160
|
+
const n = Math.trunc(Number(limit));
|
|
7161
|
+
if (!Number.isFinite(n)) return MAX_SKILL_RESULTS;
|
|
7162
|
+
return Math.min(Math.max(n, 1), MAX_SKILL_RESULTS);
|
|
7163
|
+
}
|
|
7164
|
+
function assertSearchableQuery(query) {
|
|
7165
|
+
const trimmed = query.trim();
|
|
7166
|
+
if (trimmed.length < MIN_QUERY_LENGTH) {
|
|
7167
|
+
throw new Error(
|
|
7168
|
+
`Query must be at least ${MIN_QUERY_LENGTH} characters (got "${trimmed}").`
|
|
7169
|
+
);
|
|
7170
|
+
}
|
|
7171
|
+
return trimmed;
|
|
7172
|
+
}
|
|
7173
|
+
function searchLocalSkills(query, opts = {}) {
|
|
7174
|
+
const trimmed = assertSearchableQuery(query);
|
|
7175
|
+
const skills = rankLocalSkills(
|
|
7176
|
+
scanLocalSkills(opts.roots ?? resolveSkillRoots()),
|
|
7177
|
+
trimmed,
|
|
7178
|
+
opts.limit ?? MAX_SKILL_RESULTS
|
|
7179
|
+
);
|
|
7180
|
+
return { query: trimmed, source: "local", count: skills.length, skills };
|
|
7181
|
+
}
|
|
7182
|
+
function normalizeRegistrySkill(raw) {
|
|
7183
|
+
if (typeof raw !== "object" || raw === null) return null;
|
|
7184
|
+
const obj = raw;
|
|
7185
|
+
const source = typeof obj.source === "string" ? obj.source : "";
|
|
7186
|
+
const skillId = typeof obj.skillId === "string" ? obj.skillId : "";
|
|
7187
|
+
if (!source || !skillId) return null;
|
|
7188
|
+
const name = typeof obj.name === "string" && obj.name.trim() ? obj.name : skillId;
|
|
7189
|
+
const installs = typeof obj.installs === "number" ? obj.installs : 0;
|
|
7190
|
+
const id = typeof obj.id === "string" && obj.id ? obj.id : `${source}/${skillId}`;
|
|
7191
|
+
return {
|
|
7192
|
+
id,
|
|
7193
|
+
source,
|
|
7194
|
+
skillId,
|
|
7195
|
+
name,
|
|
7196
|
+
installs,
|
|
7197
|
+
installCommand: `npx skills add ${source}@${skillId}`,
|
|
7198
|
+
url: `https://skills.sh/${source}/${skillId}`
|
|
7199
|
+
};
|
|
7200
|
+
}
|
|
7201
|
+
async function searchRegistrySkills(query, opts = {}) {
|
|
7202
|
+
const trimmed = assertSearchableQuery(query);
|
|
7203
|
+
const limit = clampLimit(opts.limit ?? MAX_SKILL_RESULTS);
|
|
7204
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
7205
|
+
const url = `${REGISTRY_SEARCH_ENDPOINT}?q=${encodeURIComponent(trimmed)}&limit=${limit}`;
|
|
7206
|
+
let res;
|
|
7207
|
+
try {
|
|
7208
|
+
res = await fetchImpl(url, {
|
|
7209
|
+
headers: { Accept: "application/json", "User-Agent": "ghfind/1.0" }
|
|
7210
|
+
});
|
|
7211
|
+
} catch (err) {
|
|
7212
|
+
throw new Error(
|
|
7213
|
+
`Could not reach skills.sh: ${err instanceof Error ? err.message : String(err)}`
|
|
7214
|
+
);
|
|
7215
|
+
}
|
|
7216
|
+
if (!res.ok) {
|
|
7217
|
+
throw new Error(
|
|
7218
|
+
`skills.sh returned HTTP ${res.status} for "${trimmed}". Retry, or search locally with --local.`
|
|
7219
|
+
);
|
|
7220
|
+
}
|
|
7221
|
+
let payload;
|
|
7222
|
+
try {
|
|
7223
|
+
payload = await res.json();
|
|
7224
|
+
} catch {
|
|
7225
|
+
throw new Error("skills.sh returned a response that was not JSON.");
|
|
7226
|
+
}
|
|
7227
|
+
const items = payload?.skills ?? [];
|
|
7228
|
+
const skills = items.map(normalizeRegistrySkill).filter((s) => s !== null).slice(0, limit);
|
|
7229
|
+
return { query: trimmed, source: "registry", count: skills.length, skills };
|
|
7230
|
+
}
|
|
7231
|
+
async function searchSkills(query, opts = {}) {
|
|
7232
|
+
if ((opts.source ?? "local") === "registry") {
|
|
7233
|
+
return searchRegistrySkills(query, opts);
|
|
7234
|
+
}
|
|
7235
|
+
return searchLocalSkills(query, opts);
|
|
7236
|
+
}
|
|
7237
|
+
function formatSkillSearchJson(result) {
|
|
7238
|
+
return JSON.stringify(result, null, 2);
|
|
7239
|
+
}
|
|
7240
|
+
function shortDescription(description, width = 100) {
|
|
7241
|
+
if (description.length <= width) return description;
|
|
7242
|
+
return `${description.slice(0, width - 1).trimEnd()}\u2026`;
|
|
7243
|
+
}
|
|
7244
|
+
function formatSkillSearchText(result) {
|
|
7245
|
+
if (result.count === 0) {
|
|
7246
|
+
return result.source === "local" ? `No installed skills match "${result.query}". Try --remote to search the skills.sh ecosystem.` : `No skills.sh entries match "${result.query}".`;
|
|
7247
|
+
}
|
|
7248
|
+
if (result.source === "local") {
|
|
7249
|
+
return result.skills.map(
|
|
7250
|
+
(s) => `${s.name} (${s.origin})
|
|
7251
|
+
${shortDescription(s.description)}`
|
|
7252
|
+
).join("\n");
|
|
7253
|
+
}
|
|
7254
|
+
return result.skills.map(
|
|
7255
|
+
(s) => `${s.name} ${s.installs.toLocaleString()} installs [${s.source}]
|
|
7256
|
+
${s.installCommand}`
|
|
7257
|
+
).join("\n");
|
|
7258
|
+
}
|
|
7259
|
+
function formatSkillSearchNames(result) {
|
|
7260
|
+
return result.skills.map((s) => s.name).join("\n");
|
|
7261
|
+
}
|
|
7262
|
+
|
|
6984
7263
|
// src/mcp-server.ts
|
|
6985
7264
|
var SUPPORTED_PROTOCOL_VERSIONS = ["2025-06-18", "2025-03-26", "2024-11-05"];
|
|
6986
7265
|
var LATEST_PROTOCOL_VERSION = SUPPORTED_PROTOCOL_VERSIONS[0];
|
|
@@ -7043,6 +7322,7 @@ function asMcpToolError(err) {
|
|
|
7043
7322
|
}
|
|
7044
7323
|
var TRENDING_QUERY = { keywords: [], qualifiers: [], raw: "trending" };
|
|
7045
7324
|
var REPO_SORTS = ["best-match", "stars", "updated", "forks"];
|
|
7325
|
+
var SKILL_SOURCES = ["local", "registry"];
|
|
7046
7326
|
var TRENDING_SINCE = ["daily", "weekly", "monthly"];
|
|
7047
7327
|
var PKG_SORTS = ["best-match", "score", "downloads", "name"];
|
|
7048
7328
|
var TOOLS = [
|
|
@@ -7387,6 +7667,52 @@ Not found: ${missing.join(", ")}` : "");
|
|
|
7387
7667
|
data: { name: "ghfind-cli" }
|
|
7388
7668
|
};
|
|
7389
7669
|
}
|
|
7670
|
+
},
|
|
7671
|
+
{
|
|
7672
|
+
name: "ghfind_skill_search",
|
|
7673
|
+
title: "Search agent skills",
|
|
7674
|
+
description: "Search installed agent skills (SKILL.md files under the user's skill directories) or the public skills.sh ecosystem by keyword, and return matching skills with their descriptions. Read-only: it never installs anything - registry hits include the `npx skills add` command for the user to run deliberately. Use when the user asks to find a skill for a task, or asks what skills are available.",
|
|
7675
|
+
inputSchema: {
|
|
7676
|
+
type: "object",
|
|
7677
|
+
properties: {
|
|
7678
|
+
query: {
|
|
7679
|
+
type: "string",
|
|
7680
|
+
description: 'Keyword to match against skill names and descriptions, e.g. "changelog" or "testing" (2+ characters).'
|
|
7681
|
+
},
|
|
7682
|
+
source: {
|
|
7683
|
+
type: "string",
|
|
7684
|
+
description: "Where to search: 'local' for skills installed on this machine (default), 'registry' for the public skills.sh ecosystem.",
|
|
7685
|
+
enum: [...SKILL_SOURCES]
|
|
7686
|
+
},
|
|
7687
|
+
limit: {
|
|
7688
|
+
type: "number",
|
|
7689
|
+
description: "Max results, 1-100 (default 20).",
|
|
7690
|
+
minimum: 1,
|
|
7691
|
+
maximum: 100
|
|
7692
|
+
}
|
|
7693
|
+
},
|
|
7694
|
+
required: ["query"]
|
|
7695
|
+
},
|
|
7696
|
+
async run(args) {
|
|
7697
|
+
const source = coerceEnum(
|
|
7698
|
+
args.source,
|
|
7699
|
+
SKILL_SOURCES,
|
|
7700
|
+
"local"
|
|
7701
|
+
);
|
|
7702
|
+
const limit = coerceInt(args.limit, 20, 1, 100);
|
|
7703
|
+
const result = await searchSkills(
|
|
7704
|
+
String(args.query ?? ""),
|
|
7705
|
+
{ source, limit }
|
|
7706
|
+
);
|
|
7707
|
+
const where = result.source === "local" ? "installed skills on this machine" : "the public skills.sh ecosystem";
|
|
7708
|
+
const header = `${result.count} match(es) for "${result.query}" in ${where}:`;
|
|
7709
|
+
return {
|
|
7710
|
+
text: `${header}
|
|
7711
|
+
|
|
7712
|
+
${formatSkillSearchText(result)}`,
|
|
7713
|
+
data: { ...result }
|
|
7714
|
+
};
|
|
7715
|
+
}
|
|
7390
7716
|
}
|
|
7391
7717
|
];
|
|
7392
7718
|
function toolSummaries() {
|
|
@@ -7465,7 +7791,7 @@ async function processServerLine(line, state, config) {
|
|
|
7465
7791
|
title: "ghfind \u2014 GitHub search for agents",
|
|
7466
7792
|
version: getVersion()
|
|
7467
7793
|
},
|
|
7468
|
-
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."
|
|
7794
|
+
instructions: "ghfind gives agents GitHub search: repositories, trending, npm packages, org/user profiles, repo comparison and deep-dives. ghfind_skill_search finds agent skills, either installed on this machine or in the public skills.sh ecosystem. 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."
|
|
7469
7795
|
})
|
|
7470
7796
|
);
|
|
7471
7797
|
return;
|
|
@@ -7569,7 +7895,7 @@ async function runMcpServer(options = {}) {
|
|
|
7569
7895
|
// src/cli.ts
|
|
7570
7896
|
process.env.GHFIND_CLI_RUN = "1";
|
|
7571
7897
|
var __dirname3 = dirname4(fileURLToPath4(import.meta.url));
|
|
7572
|
-
var PKG_DIR3 =
|
|
7898
|
+
var PKG_DIR3 = join7(__dirname3, "..");
|
|
7573
7899
|
function parseArgs(args) {
|
|
7574
7900
|
const flags = {
|
|
7575
7901
|
query: "",
|
|
@@ -7593,7 +7919,16 @@ function parseArgs(args) {
|
|
|
7593
7919
|
registry: "npm",
|
|
7594
7920
|
compare: [],
|
|
7595
7921
|
doctor: false,
|
|
7596
|
-
mcp: false
|
|
7922
|
+
mcp: false,
|
|
7923
|
+
bookmarks: false,
|
|
7924
|
+
history: false,
|
|
7925
|
+
topics: false,
|
|
7926
|
+
saved: false,
|
|
7927
|
+
remote: false,
|
|
7928
|
+
local: false,
|
|
7929
|
+
copy: false,
|
|
7930
|
+
raw: false,
|
|
7931
|
+
limitExplicit: false
|
|
7597
7932
|
};
|
|
7598
7933
|
const queryParts = [];
|
|
7599
7934
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -7650,6 +7985,7 @@ function parseArgs(args) {
|
|
|
7650
7985
|
break;
|
|
7651
7986
|
case "--limit": {
|
|
7652
7987
|
const parsed = parseInt(args[++i], 10);
|
|
7988
|
+
flags.limitExplicit = true;
|
|
7653
7989
|
if (Number.isNaN(parsed) || parsed < 1 || parsed > 100) {
|
|
7654
7990
|
console.error(`Invalid limit: must be 1-100. Using default 50.`);
|
|
7655
7991
|
flags.limit = 50;
|
|
@@ -7682,9 +8018,56 @@ function parseArgs(args) {
|
|
|
7682
8018
|
case "--completion":
|
|
7683
8019
|
flags.completion = args[++i];
|
|
7684
8020
|
break;
|
|
8021
|
+
case "--remote":
|
|
8022
|
+
flags.remote = true;
|
|
8023
|
+
break;
|
|
8024
|
+
case "--local":
|
|
8025
|
+
flags.local = true;
|
|
8026
|
+
break;
|
|
8027
|
+
case "--copy":
|
|
8028
|
+
flags.copy = true;
|
|
8029
|
+
break;
|
|
8030
|
+
case "--raw":
|
|
8031
|
+
flags.raw = true;
|
|
8032
|
+
break;
|
|
8033
|
+
case "--as":
|
|
8034
|
+
flags.sharedAs = args[++i];
|
|
8035
|
+
break;
|
|
7685
8036
|
case "mcp":
|
|
7686
8037
|
flags.mcp = true;
|
|
7687
8038
|
break;
|
|
8039
|
+
case "bookmarks":
|
|
8040
|
+
flags.bookmarks = true;
|
|
8041
|
+
break;
|
|
8042
|
+
case "history":
|
|
8043
|
+
flags.history = true;
|
|
8044
|
+
break;
|
|
8045
|
+
case "topics":
|
|
8046
|
+
flags.topics = true;
|
|
8047
|
+
break;
|
|
8048
|
+
case "saved":
|
|
8049
|
+
flags.saved = true;
|
|
8050
|
+
break;
|
|
8051
|
+
case "readme": {
|
|
8052
|
+
const next = args[i + 1];
|
|
8053
|
+
if (next && !next.startsWith("-")) {
|
|
8054
|
+
flags.readme = next;
|
|
8055
|
+
i++;
|
|
8056
|
+
} else {
|
|
8057
|
+
flags.readme = "";
|
|
8058
|
+
}
|
|
8059
|
+
break;
|
|
8060
|
+
}
|
|
8061
|
+
case "share": {
|
|
8062
|
+
const next = args[i + 1];
|
|
8063
|
+
if (next && !next.startsWith("-")) {
|
|
8064
|
+
flags.share = next;
|
|
8065
|
+
i++;
|
|
8066
|
+
} else {
|
|
8067
|
+
flags.share = "";
|
|
8068
|
+
}
|
|
8069
|
+
break;
|
|
8070
|
+
}
|
|
7688
8071
|
case "deep-dive": {
|
|
7689
8072
|
const next = args[i + 1];
|
|
7690
8073
|
if (next && !next.startsWith("-")) {
|
|
@@ -7695,6 +8078,17 @@ function parseArgs(args) {
|
|
|
7695
8078
|
}
|
|
7696
8079
|
case "skill": {
|
|
7697
8080
|
const next = args[i + 1];
|
|
8081
|
+
if (next === "search") {
|
|
8082
|
+
i++;
|
|
8083
|
+
const query = args[i + 1];
|
|
8084
|
+
if (query && !query.startsWith("-")) {
|
|
8085
|
+
flags.skillSearch = query;
|
|
8086
|
+
i++;
|
|
8087
|
+
} else {
|
|
8088
|
+
flags.skillSearch = "";
|
|
8089
|
+
}
|
|
8090
|
+
break;
|
|
8091
|
+
}
|
|
7698
8092
|
if (next && !next.startsWith("-")) {
|
|
7699
8093
|
flags.skill = next;
|
|
7700
8094
|
i++;
|
|
@@ -7773,6 +8167,13 @@ Usage:
|
|
|
7773
8167
|
ghfind pkg <query> Search npm packages, text list
|
|
7774
8168
|
ghfind mcp Run the MCP server for AI agents (stdio)
|
|
7775
8169
|
ghfind skill [name] Print the agent skill guide (or catalog)
|
|
8170
|
+
ghfind bookmarks [query] Saved repos, newest first
|
|
8171
|
+
ghfind history [query] Past searches, newest first
|
|
8172
|
+
ghfind topics Browse popular GitHub topics
|
|
8173
|
+
ghfind saved Named searches saved in the TUI
|
|
8174
|
+
ghfind readme <owner/repo> Print a repository README
|
|
8175
|
+
ghfind share a/b --as gh-cli Share snippet (--copy to clipboard)
|
|
8176
|
+
ghfind skill search <query> Find skills locally, or --remote
|
|
7776
8177
|
ghfind --watch <query> Watch mode (poll every Ns)
|
|
7777
8178
|
ghfind init Run setup wizard
|
|
7778
8179
|
ghfind login Import the gh CLI token or paste one
|
|
@@ -7797,14 +8198,19 @@ Options:
|
|
|
7797
8198
|
--releases New-release feed for bookmarks
|
|
7798
8199
|
--interval <s> Watch interval in seconds (default: 300)
|
|
7799
8200
|
--completion <shell> Generate completions
|
|
8201
|
+
--remote skill search: query skills.sh instead of this machine
|
|
8202
|
+
--local skill search: force the local scan (default)
|
|
8203
|
+
--as <fmt> share format: markdown|plain|gh-cli|short
|
|
8204
|
+
--copy share: also copy the snippet to the clipboard
|
|
8205
|
+
--raw readme: print the raw markdown with no header
|
|
7800
8206
|
`);
|
|
7801
8207
|
return;
|
|
7802
8208
|
}
|
|
7803
8209
|
if (flags.completion) {
|
|
7804
8210
|
const shell = flags.completion;
|
|
7805
8211
|
try {
|
|
7806
|
-
const content =
|
|
7807
|
-
|
|
8212
|
+
const content = readFileSync7(
|
|
8213
|
+
join7(PKG_DIR3, "completions", `ghfind.${shell}`),
|
|
7808
8214
|
"utf-8"
|
|
7809
8215
|
);
|
|
7810
8216
|
console.log(content);
|
|
@@ -7826,10 +8232,38 @@ Options:
|
|
|
7826
8232
|
await runMcpServer({ token: flags.token });
|
|
7827
8233
|
return;
|
|
7828
8234
|
}
|
|
8235
|
+
if (flags.skillSearch !== void 0) {
|
|
8236
|
+
await runSkillSearch(flags);
|
|
8237
|
+
return;
|
|
8238
|
+
}
|
|
7829
8239
|
if (flags.skill !== void 0) {
|
|
7830
8240
|
printAgentSkill(flags.skill);
|
|
7831
8241
|
return;
|
|
7832
8242
|
}
|
|
8243
|
+
if (flags.bookmarks) {
|
|
8244
|
+
runBookmarks(flags);
|
|
8245
|
+
return;
|
|
8246
|
+
}
|
|
8247
|
+
if (flags.history) {
|
|
8248
|
+
runHistory(flags);
|
|
8249
|
+
return;
|
|
8250
|
+
}
|
|
8251
|
+
if (flags.saved) {
|
|
8252
|
+
runSavedSearches(flags);
|
|
8253
|
+
return;
|
|
8254
|
+
}
|
|
8255
|
+
if (flags.topics) {
|
|
8256
|
+
await runTopics(flags);
|
|
8257
|
+
return;
|
|
8258
|
+
}
|
|
8259
|
+
if (flags.readme !== void 0) {
|
|
8260
|
+
await runReadme(flags);
|
|
8261
|
+
return;
|
|
8262
|
+
}
|
|
8263
|
+
if (flags.share !== void 0) {
|
|
8264
|
+
await runShare(flags);
|
|
8265
|
+
return;
|
|
8266
|
+
}
|
|
7833
8267
|
if (flags.pkg) {
|
|
7834
8268
|
const interactive = !flags.query && !flags.json && !flags.csv && !flags.markdown && !flags.count && !flags.format;
|
|
7835
8269
|
if (interactive) {
|
|
@@ -7963,6 +8397,233 @@ async function runDeepDive(flags, outputFormat) {
|
|
|
7963
8397
|
process.exit(1);
|
|
7964
8398
|
}
|
|
7965
8399
|
}
|
|
8400
|
+
async function runSkillSearch(flags) {
|
|
8401
|
+
const query = flags.skillSearch ?? "";
|
|
8402
|
+
if (!query.trim()) {
|
|
8403
|
+
console.error(
|
|
8404
|
+
"Usage: ghfind skill search <query> [--remote] [--limit <n>] [--json]"
|
|
8405
|
+
);
|
|
8406
|
+
process.exit(1);
|
|
8407
|
+
}
|
|
8408
|
+
if (flags.remote && flags.local) {
|
|
8409
|
+
console.error(
|
|
8410
|
+
"Contradictory flags: pass either --remote or --local, not both."
|
|
8411
|
+
);
|
|
8412
|
+
process.exit(1);
|
|
8413
|
+
}
|
|
8414
|
+
const source = flags.remote ? "registry" : "local";
|
|
8415
|
+
try {
|
|
8416
|
+
const result = await searchSkills(query, {
|
|
8417
|
+
source,
|
|
8418
|
+
limit: flags.limit
|
|
8419
|
+
});
|
|
8420
|
+
if (flags.json) {
|
|
8421
|
+
console.log(formatSkillSearchJson(result));
|
|
8422
|
+
} else if (flags.format === "names") {
|
|
8423
|
+
console.log(formatSkillSearchNames(result));
|
|
8424
|
+
} else {
|
|
8425
|
+
console.log(formatSkillSearchText(result));
|
|
8426
|
+
}
|
|
8427
|
+
} catch (err) {
|
|
8428
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
8429
|
+
process.exit(1);
|
|
8430
|
+
}
|
|
8431
|
+
}
|
|
8432
|
+
function runBookmarks(flags) {
|
|
8433
|
+
const query = flags.query.trim();
|
|
8434
|
+
const bookmarks = query ? searchBookmarks(query) : getBookmarks();
|
|
8435
|
+
if (flags.json) {
|
|
8436
|
+
console.log(JSON.stringify(bookmarks, null, 2));
|
|
8437
|
+
return;
|
|
8438
|
+
}
|
|
8439
|
+
if (bookmarks.length === 0) {
|
|
8440
|
+
console.log(
|
|
8441
|
+
query ? `No bookmarks match "${query}".` : "No bookmarks yet. Press Space \u2192 Bookmark in the TUI to save one."
|
|
8442
|
+
);
|
|
8443
|
+
return;
|
|
8444
|
+
}
|
|
8445
|
+
if (flags.csv || flags.markdown) {
|
|
8446
|
+
const header = ["full_name", "stars", "language", "tags", "saved_at"];
|
|
8447
|
+
const rows = bookmarks.map((b) => [
|
|
8448
|
+
b.repo.fullName,
|
|
8449
|
+
String(b.repo.stars),
|
|
8450
|
+
b.repo.language ?? "",
|
|
8451
|
+
b.tags.join(" "),
|
|
8452
|
+
new Date(b.savedAt).toISOString().slice(0, 10)
|
|
8453
|
+
]);
|
|
8454
|
+
console.log(
|
|
8455
|
+
flags.csv ? rowsToCsv(header, rows) : rowsToMarkdown(header, rows)
|
|
8456
|
+
);
|
|
8457
|
+
return;
|
|
8458
|
+
}
|
|
8459
|
+
for (const b of bookmarks) {
|
|
8460
|
+
const tags = b.tags.length > 0 ? ` [${b.tags.join(", ")}]` : "";
|
|
8461
|
+
console.log(
|
|
8462
|
+
`${b.repo.fullName} \u2014 ${b.repo.stars.toLocaleString()} ${b.repo.language ?? "n/a"}${tags}`
|
|
8463
|
+
);
|
|
8464
|
+
}
|
|
8465
|
+
}
|
|
8466
|
+
function runHistory(flags) {
|
|
8467
|
+
const limit = flags.limitExplicit ? flags.limit : 20;
|
|
8468
|
+
const query = flags.query.trim().toLowerCase();
|
|
8469
|
+
const entries = readHistory().filter((e) => !query || e.query.toLowerCase().includes(query)).slice(0, limit);
|
|
8470
|
+
if (flags.json) {
|
|
8471
|
+
console.log(JSON.stringify(entries, null, 2));
|
|
8472
|
+
return;
|
|
8473
|
+
}
|
|
8474
|
+
if (entries.length === 0) {
|
|
8475
|
+
console.log(
|
|
8476
|
+
query ? `No history matches "${query}".` : "No search history yet."
|
|
8477
|
+
);
|
|
8478
|
+
return;
|
|
8479
|
+
}
|
|
8480
|
+
if (flags.csv || flags.markdown) {
|
|
8481
|
+
const header = ["query", "mode", "results", "when"];
|
|
8482
|
+
const rows = entries.map((e) => [
|
|
8483
|
+
e.query,
|
|
8484
|
+
e.mode,
|
|
8485
|
+
String(e.resultCount),
|
|
8486
|
+
new Date(e.timestamp).toISOString()
|
|
8487
|
+
]);
|
|
8488
|
+
console.log(
|
|
8489
|
+
flags.csv ? rowsToCsv(header, rows) : rowsToMarkdown(header, rows)
|
|
8490
|
+
);
|
|
8491
|
+
return;
|
|
8492
|
+
}
|
|
8493
|
+
for (const e of entries) {
|
|
8494
|
+
const when = new Date(e.timestamp).toISOString().slice(0, 10);
|
|
8495
|
+
console.log(
|
|
8496
|
+
`${e.query} \u2014 ${e.mode} \u2014 ${e.resultCount} results \u2014 ${when}`
|
|
8497
|
+
);
|
|
8498
|
+
}
|
|
8499
|
+
}
|
|
8500
|
+
function runSavedSearches(flags) {
|
|
8501
|
+
const saved = getSavedSearches();
|
|
8502
|
+
if (flags.json) {
|
|
8503
|
+
console.log(JSON.stringify(saved, null, 2));
|
|
8504
|
+
return;
|
|
8505
|
+
}
|
|
8506
|
+
if (saved.length === 0) {
|
|
8507
|
+
console.log("No saved searches yet. Save one from the TUI command menu.");
|
|
8508
|
+
return;
|
|
8509
|
+
}
|
|
8510
|
+
if (flags.csv || flags.markdown) {
|
|
8511
|
+
const header = ["name", "query", "mode", "limit"];
|
|
8512
|
+
const rows = saved.map((s) => [s.name, s.query, s.mode, String(s.limit)]);
|
|
8513
|
+
console.log(
|
|
8514
|
+
flags.csv ? rowsToCsv(header, rows) : rowsToMarkdown(header, rows)
|
|
8515
|
+
);
|
|
8516
|
+
return;
|
|
8517
|
+
}
|
|
8518
|
+
for (const s of saved) {
|
|
8519
|
+
console.log(`${s.name} \u2014 ${s.query} \u2014 ${s.mode} \u2014 limit ${s.limit}`);
|
|
8520
|
+
}
|
|
8521
|
+
}
|
|
8522
|
+
function rowsToCsv(header, rows) {
|
|
8523
|
+
const quote = (value) => /[",\n]/.test(value) ? `"${value.replace(/"/g, '""')}"` : value;
|
|
8524
|
+
return [header, ...rows].map((row) => row.map(quote).join(",")).join("\n");
|
|
8525
|
+
}
|
|
8526
|
+
function rowsToMarkdown(header, rows) {
|
|
8527
|
+
return [
|
|
8528
|
+
`| ${header.join(" | ")} |`,
|
|
8529
|
+
`|${header.map(() => "---").join("|")}|`,
|
|
8530
|
+
...rows.map((row) => `| ${row.join(" | ")} |`)
|
|
8531
|
+
].join("\n");
|
|
8532
|
+
}
|
|
8533
|
+
function truncate2(text, width) {
|
|
8534
|
+
if (text.length <= width) return text;
|
|
8535
|
+
return `${text.slice(0, width - 1).trimEnd()}\u2026`;
|
|
8536
|
+
}
|
|
8537
|
+
function plainText(html) {
|
|
8538
|
+
return html.replace(/<\/p>|<br\s*\/?>/gi, " ").replace(/<[^>]*>/g, "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/ /g, " ").replace(/\s+/g, " ").trim();
|
|
8539
|
+
}
|
|
8540
|
+
async function runTopics(flags) {
|
|
8541
|
+
try {
|
|
8542
|
+
const topics = (await fetchTopics()).slice(0, flags.limit);
|
|
8543
|
+
if (flags.json) {
|
|
8544
|
+
console.log(JSON.stringify(topics, null, 2));
|
|
8545
|
+
return;
|
|
8546
|
+
}
|
|
8547
|
+
if (topics.length === 0) {
|
|
8548
|
+
console.log("No topics returned.");
|
|
8549
|
+
return;
|
|
8550
|
+
}
|
|
8551
|
+
for (const t3 of topics) {
|
|
8552
|
+
const description = truncate2(plainText(t3.description), 140);
|
|
8553
|
+
console.log(description ? `${t3.name} ${description}` : t3.name);
|
|
8554
|
+
}
|
|
8555
|
+
} catch (err) {
|
|
8556
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
8557
|
+
process.exit(1);
|
|
8558
|
+
}
|
|
8559
|
+
}
|
|
8560
|
+
async function runReadme(flags) {
|
|
8561
|
+
const ref = flags.readme ?? "";
|
|
8562
|
+
if (!ref) {
|
|
8563
|
+
console.error(
|
|
8564
|
+
"Usage: ghfind readme <owner/repo> [--json] [--raw] [--token <t>]"
|
|
8565
|
+
);
|
|
8566
|
+
process.exit(1);
|
|
8567
|
+
}
|
|
8568
|
+
try {
|
|
8569
|
+
const { owner, name } = repoFromRef(ref);
|
|
8570
|
+
const readme = await fetchReadme(owner, name, { token: flags.token });
|
|
8571
|
+
if (!readme) {
|
|
8572
|
+
console.error(`No README found for ${owner}/${name}.`);
|
|
8573
|
+
process.exit(1);
|
|
8574
|
+
}
|
|
8575
|
+
if (flags.json) {
|
|
8576
|
+
console.log(
|
|
8577
|
+
JSON.stringify(
|
|
8578
|
+
{ owner, name, sourceUrl: readme.sourceUrl, text: readme.text },
|
|
8579
|
+
null,
|
|
8580
|
+
2
|
|
8581
|
+
)
|
|
8582
|
+
);
|
|
8583
|
+
return;
|
|
8584
|
+
}
|
|
8585
|
+
if (!flags.raw) {
|
|
8586
|
+
console.log(`# ${owner}/${name} README (${readme.sourceUrl})`);
|
|
8587
|
+
console.log("");
|
|
8588
|
+
}
|
|
8589
|
+
console.log(readme.text);
|
|
8590
|
+
} catch (err) {
|
|
8591
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
8592
|
+
process.exit(1);
|
|
8593
|
+
}
|
|
8594
|
+
}
|
|
8595
|
+
async function runShare(flags) {
|
|
8596
|
+
const ref = flags.share ?? "";
|
|
8597
|
+
if (!ref) {
|
|
8598
|
+
console.error(
|
|
8599
|
+
"Usage: ghfind share <owner/repo> [--as markdown|plain|gh-cli|short] [--copy]"
|
|
8600
|
+
);
|
|
8601
|
+
process.exit(1);
|
|
8602
|
+
}
|
|
8603
|
+
const SHARE_FORMATS = ["markdown", "plain", "gh-cli", "short"];
|
|
8604
|
+
const as = flags.sharedAs ?? "markdown";
|
|
8605
|
+
if (!SHARE_FORMATS.includes(as)) {
|
|
8606
|
+
console.error(
|
|
8607
|
+
`Invalid --as value: "${flags.sharedAs}" (use markdown|plain|gh-cli|short).`
|
|
8608
|
+
);
|
|
8609
|
+
process.exit(1);
|
|
8610
|
+
}
|
|
8611
|
+
try {
|
|
8612
|
+
repoFromRef(ref);
|
|
8613
|
+
const repo = await resolveRepoFromRef(ref, flags.token);
|
|
8614
|
+
const text = formatShare(repo, as);
|
|
8615
|
+
console.log(text);
|
|
8616
|
+
if (flags.copy) {
|
|
8617
|
+
const copied = await copyToClipboard(text);
|
|
8618
|
+
console.error(
|
|
8619
|
+
copied ? "Copied to clipboard." : "Could not copy to the clipboard on this platform."
|
|
8620
|
+
);
|
|
8621
|
+
}
|
|
8622
|
+
} catch (err) {
|
|
8623
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
8624
|
+
process.exit(1);
|
|
8625
|
+
}
|
|
8626
|
+
}
|
|
7966
8627
|
async function runPackageSearch(flags) {
|
|
7967
8628
|
if (flags.registry !== "npm") {
|
|
7968
8629
|
console.error(`Registry "${flags.registry}" not yet supported (only npm).`);
|