@rakeshroushan/reposcout 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Rakesh Roushan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,98 @@
1
+ # reposcout
2
+
3
+ LLM-driven GitHub repo discovery and ranking, exposed as a Model Context Protocol (MCP) server.
4
+
5
+ When an AI coding agent needs "the best open-source library for X", it usually falls back on stale training-data recall. reposcout gives the agent a fresh, evidence-backed view of the GitHub ecosystem: it searches by intent, pulls READMEs and metadata, computes deterministic popularity / maintenance / completeness signals, and combines them with the agent's own relevance judgment into a ranked, explainable shortlist.
6
+
7
+ It ships as a stdio MCP server with three composable tools, plus a companion Claude Code skill that orchestrates them.
8
+
9
+ ## How it works
10
+
11
+ Discovery is split into three tools so the deterministic math stays server-side and the judgment stays with the LLM driving the loop:
12
+
13
+ | Tool | What it does |
14
+ |------|--------------|
15
+ | `repo_search` | Runs several GitHub search queries built from the objective, unions and dedupes them, and returns compact records with computed `popularity_score` and `maintenance_score`. |
16
+ | `repo_enrich` | For a shortlist, fetches the cleaned + truncated README plus completeness signals (license, homepage, description, topic count, README size). Results are cached locally. |
17
+ | `repo_rank` | Combines four sub-scores — relevance, popularity, maintenance, completeness — into a weighted ranking. The agent supplies relevance + completeness; reposcout owns popularity + maintenance and the final math. |
18
+
19
+ Scoring is deterministic: log-scaled popularity (star/fork caps), exponential-decay maintenance (180-day half-life) with an open-issues penalty. Default weights are relevance 0.4 / popularity 0.2 / maintenance 0.2 / completeness 0.2 (normalized, overridable per call). Archived repos are excluded by default.
20
+
21
+ ## Requirements
22
+
23
+ - Node.js >= 22 (uses the built-in `node:sqlite`).
24
+ - A GitHub token. reposcout reads `GITHUB_TOKEN` or `GH_TOKEN`; if neither is set it falls back to `gh auth token`. So if your `gh` CLI is logged in (`gh auth login`), no extra setup is needed.
25
+
26
+ ## Install
27
+
28
+ reposcout runs as a local stdio MCP server — point any MCP client at it.
29
+
30
+ ### Claude Code
31
+
32
+ ```bash
33
+ claude mcp add reposcout -- npx -y @rakeshroushan/reposcout
34
+ ```
35
+
36
+ ### Claude Desktop / Cursor / other MCP clients
37
+
38
+ Add to the client's MCP config (for Claude Desktop, `claude_desktop_config.json`):
39
+
40
+ ```json
41
+ {
42
+ "mcpServers": {
43
+ "reposcout": {
44
+ "command": "npx",
45
+ "args": ["-y", "@rakeshroushan/reposcout"],
46
+ "env": { "GITHUB_TOKEN": "ghp_..." }
47
+ }
48
+ }
49
+ }
50
+ ```
51
+
52
+ Omit `env` to use your `gh` CLI login instead.
53
+
54
+ ### From source
55
+
56
+ ```bash
57
+ git clone https://github.com/Rakesh1002/reposcout
58
+ cd reposcout
59
+ pnpm install
60
+ pnpm build
61
+ ```
62
+
63
+ Then point the client at the built server:
64
+
65
+ ```json
66
+ {
67
+ "mcpServers": {
68
+ "reposcout": {
69
+ "command": "node",
70
+ "args": ["/absolute/path/to/reposcout/dist/server.js"]
71
+ }
72
+ }
73
+ }
74
+ ```
75
+
76
+ ## Using it
77
+
78
+ Once connected, state an objective in plain language:
79
+
80
+ > Find the best TypeScript library to generate OpenAPI types from Zod schemas — min 100 stars, actively maintained.
81
+
82
+ The agent expands that into complementary GitHub queries, calls `repo_search`, triages a shortlist, calls `repo_enrich`, scores relevance + completeness from the READMEs, and calls `repo_rank` to return a ranked shortlist with one-line reasoning per repo. The companion `reposcout` Claude Code skill encodes that pipeline so you never touch raw JSON.
83
+
84
+ ## Development
85
+
86
+ ```bash
87
+ pnpm dev # run the server from source (tsx)
88
+ pnpm test # node --test over tests/*.test.ts
89
+ pnpm typecheck # tsc --noEmit
90
+ pnpm lint # biome check
91
+ pnpm build # tsc -> dist/
92
+ ```
93
+
94
+ The cache lives at `~/.reposcout/cache.sqlite` (24h TTL).
95
+
96
+ ## License
97
+
98
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,14 @@
1
+ export interface CacheHit<T> {
2
+ value: T;
3
+ etag?: string;
4
+ fetchedAt: number;
5
+ }
6
+ export declare const DEFAULT_CACHE_PATH: string;
7
+ export declare class RepoCache {
8
+ private db;
9
+ constructor(path?: string);
10
+ get<T>(key: string): CacheHit<T> | undefined;
11
+ getFresh<T>(key: string, ttlMs: number, now?: number): T | undefined;
12
+ set(key: string, value: unknown, etag?: string, now?: number): void;
13
+ close(): void;
14
+ }
package/dist/cache.js ADDED
@@ -0,0 +1,48 @@
1
+ import { mkdirSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { dirname, join } from "node:path";
4
+ import { DatabaseSync } from "node:sqlite";
5
+ export const DEFAULT_CACHE_PATH = join(homedir(), ".reposcout", "cache.sqlite");
6
+ export class RepoCache {
7
+ db;
8
+ constructor(path = DEFAULT_CACHE_PATH) {
9
+ if (path !== ":memory:") {
10
+ mkdirSync(dirname(path), { recursive: true });
11
+ }
12
+ this.db = new DatabaseSync(path);
13
+ this.db.exec(`CREATE TABLE IF NOT EXISTS cache (
14
+ key TEXT PRIMARY KEY,
15
+ value TEXT NOT NULL,
16
+ etag TEXT,
17
+ fetched_at INTEGER NOT NULL
18
+ )`);
19
+ }
20
+ get(key) {
21
+ const row = this.db
22
+ .prepare("SELECT value, etag, fetched_at FROM cache WHERE key = ?")
23
+ .get(key);
24
+ if (!row)
25
+ return undefined;
26
+ return {
27
+ value: JSON.parse(row.value),
28
+ etag: row.etag ?? undefined,
29
+ fetchedAt: row.fetched_at,
30
+ };
31
+ }
32
+ getFresh(key, ttlMs, now = Date.now()) {
33
+ const hit = this.get(key);
34
+ if (!hit)
35
+ return undefined;
36
+ return now - hit.fetchedAt <= ttlMs ? hit.value : undefined;
37
+ }
38
+ set(key, value, etag, now = Date.now()) {
39
+ this.db
40
+ .prepare(`INSERT INTO cache (key, value, etag, fetched_at) VALUES (?, ?, ?, ?)
41
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value, etag = excluded.etag, fetched_at = excluded.fetched_at`)
42
+ .run(key, JSON.stringify(value), etag ?? null, now);
43
+ }
44
+ close() {
45
+ this.db.close();
46
+ }
47
+ }
48
+ //# sourceMappingURL=cache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache.js","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAc3C,MAAM,CAAC,MAAM,kBAAkB,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,YAAY,EAAE,cAAc,CAAC,CAAC;AAEhF,MAAM,OAAO,SAAS;IACZ,EAAE,CAAe;IAEzB,YAAY,OAAe,kBAAkB;QAC3C,IAAI,IAAI,KAAK,UAAU,EAAE,CAAC;YACxB,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAChD,CAAC;QACD,IAAI,CAAC,EAAE,GAAG,IAAI,YAAY,CAAC,IAAI,CAAC,CAAC;QACjC,IAAI,CAAC,EAAE,CAAC,IAAI,CACV;;;;;SAKG,CACJ,CAAC;IACJ,CAAC;IAED,GAAG,CAAI,GAAW;QAChB,MAAM,GAAG,GAAG,IAAI,CAAC,EAAE;aAChB,OAAO,CAAC,yDAAyD,CAAC;aAClE,GAAG,CAAC,GAAG,CAAoB,CAAC;QAC/B,IAAI,CAAC,GAAG;YAAE,OAAO,SAAS,CAAC;QAC3B,OAAO;YACL,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,KAAK,CAAM;YACjC,IAAI,EAAE,GAAG,CAAC,IAAI,IAAI,SAAS;YAC3B,SAAS,EAAE,GAAG,CAAC,UAAU;SAC1B,CAAC;IACJ,CAAC;IAED,QAAQ,CAAI,GAAW,EAAE,KAAa,EAAE,MAAc,IAAI,CAAC,GAAG,EAAE;QAC9D,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAI,GAAG,CAAC,CAAC;QAC7B,IAAI,CAAC,GAAG;YAAE,OAAO,SAAS,CAAC;QAC3B,OAAO,GAAG,GAAG,GAAG,CAAC,SAAS,IAAI,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC;IAC9D,CAAC;IAED,GAAG,CAAC,GAAW,EAAE,KAAc,EAAE,IAAa,EAAE,MAAc,IAAI,CAAC,GAAG,EAAE;QACtE,IAAI,CAAC,EAAE;aACJ,OAAO,CACN;uHAC+G,CAChH;aACA,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,IAAI,IAAI,IAAI,EAAE,GAAG,CAAC,CAAC;IACxD,CAAC;IAED,KAAK;QACH,IAAI,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC;IAClB,CAAC;CACF"}
@@ -0,0 +1,56 @@
1
+ import { Octokit } from "octokit";
2
+ import type { RepoCache } from "./cache.js";
3
+ import type { EnrichedRepo, RepoRecord } from "./types.js";
4
+ export interface RawRepo {
5
+ full_name: string;
6
+ html_url: string;
7
+ description: string | null;
8
+ language: string | null;
9
+ topics?: string[];
10
+ stargazers_count: number;
11
+ forks_count: number;
12
+ open_issues_count: number;
13
+ pushed_at: string | null;
14
+ created_at: string | null;
15
+ license: {
16
+ spdx_id?: string | null;
17
+ } | null;
18
+ homepage: string | null;
19
+ archived: boolean;
20
+ }
21
+ export interface SearchClient {
22
+ rest: {
23
+ search: {
24
+ repos: (params: {
25
+ q: string;
26
+ per_page?: number;
27
+ page?: number;
28
+ sort?: "stars" | "forks" | "updated";
29
+ order?: "desc" | "asc";
30
+ }) => Promise<{
31
+ data: {
32
+ items: RawRepo[];
33
+ };
34
+ }>;
35
+ };
36
+ };
37
+ }
38
+ export declare function resolveToken(): string;
39
+ export declare function createClient(token?: string): Octokit;
40
+ export declare function mapSearchItem(item: RawRepo, now: Date): RepoRecord;
41
+ export interface SearchOptions {
42
+ queries: string[];
43
+ perQueryLimit?: number;
44
+ totalLimit?: number;
45
+ excludeArchived?: boolean;
46
+ now: Date;
47
+ }
48
+ export declare function searchRepos(client: SearchClient, opts: SearchOptions): Promise<RepoRecord[]>;
49
+ export interface EnrichOptions {
50
+ now: Date;
51
+ cache?: RepoCache;
52
+ cacheTtlMs?: number;
53
+ readmeChars?: number;
54
+ concurrency?: number;
55
+ }
56
+ export declare function enrichRepos(client: Octokit, fullNames: string[], opts: EnrichOptions): Promise<EnrichedRepo[]>;
package/dist/github.js ADDED
@@ -0,0 +1,136 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { Octokit } from "octokit";
3
+ import { buildCompletenessSignals, cleanReadme } from "./readme.js";
4
+ import { maintenanceScore, popularityScore } from "./score.js";
5
+ const DEFAULT_PER_QUERY = 30;
6
+ const DEFAULT_TOTAL_LIMIT = 60;
7
+ const DEFAULT_CACHE_TTL_MS = 24 * 60 * 60 * 1000;
8
+ const DEFAULT_README_CHARS = 5000;
9
+ export function resolveToken() {
10
+ const fromEnv = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
11
+ if (fromEnv)
12
+ return fromEnv;
13
+ try {
14
+ return execFileSync("gh", ["auth", "token"], { encoding: "utf8" }).trim();
15
+ }
16
+ catch {
17
+ throw new Error("No GitHub token: set GITHUB_TOKEN or authenticate the gh CLI (gh auth login).");
18
+ }
19
+ }
20
+ export function createClient(token = resolveToken()) {
21
+ return new Octokit({
22
+ auth: token,
23
+ throttle: {
24
+ onRateLimit: (_retryAfter, _options, _octokit, retryCount) => retryCount < 2,
25
+ onSecondaryRateLimit: (_retryAfter, _options, _octokit, retryCount) => retryCount < 2,
26
+ },
27
+ });
28
+ }
29
+ const normLicense = (spdx) => spdx && spdx !== "NOASSERTION" ? spdx : null;
30
+ export function mapSearchItem(item, now) {
31
+ const stars = item.stargazers_count ?? 0;
32
+ const forks = item.forks_count ?? 0;
33
+ const openIssues = item.open_issues_count ?? 0;
34
+ const pushedAt = item.pushed_at ?? new Date(0).toISOString();
35
+ return {
36
+ full_name: item.full_name,
37
+ html_url: item.html_url,
38
+ description: item.description ?? null,
39
+ language: item.language ?? null,
40
+ topics: item.topics ?? [],
41
+ stars,
42
+ forks,
43
+ open_issues: openIssues,
44
+ pushed_at: pushedAt,
45
+ created_at: item.created_at ?? pushedAt,
46
+ license: normLicense(item.license?.spdx_id),
47
+ homepage: item.homepage?.trim() ? item.homepage : null,
48
+ archived: Boolean(item.archived),
49
+ popularity_score: popularityScore(stars, forks),
50
+ maintenance_score: maintenanceScore({ pushed_at: pushedAt, archived: Boolean(item.archived), open_issues: openIssues, stars }, now),
51
+ };
52
+ }
53
+ export async function searchRepos(client, opts) {
54
+ const perPage = Math.min(opts.perQueryLimit ?? DEFAULT_PER_QUERY, 100);
55
+ const excludeArchived = opts.excludeArchived ?? true;
56
+ const byName = new Map();
57
+ for (const q of opts.queries) {
58
+ const res = await client.rest.search.repos({
59
+ q,
60
+ per_page: perPage,
61
+ sort: "stars",
62
+ order: "desc",
63
+ });
64
+ for (const item of res.data.items) {
65
+ const record = mapSearchItem(item, opts.now);
66
+ if (excludeArchived && record.archived)
67
+ continue;
68
+ if (!byName.has(record.full_name))
69
+ byName.set(record.full_name, record);
70
+ }
71
+ }
72
+ return [...byName.values()]
73
+ .sort((a, b) => b.stars - a.stars)
74
+ .slice(0, opts.totalLimit ?? DEFAULT_TOTAL_LIMIT);
75
+ }
76
+ async function mapWithConcurrency(items, limit, fn) {
77
+ const out = new Array(items.length);
78
+ let cursor = 0;
79
+ const workers = Array.from({ length: Math.min(limit, items.length) }, async () => {
80
+ while (cursor < items.length) {
81
+ const index = cursor++;
82
+ out[index] = await fn(items[index]);
83
+ }
84
+ });
85
+ await Promise.all(workers);
86
+ return out;
87
+ }
88
+ async function fetchReadmeRaw(client, owner, repo) {
89
+ try {
90
+ const res = await client.rest.repos.getReadme({
91
+ owner,
92
+ repo,
93
+ mediaType: { format: "raw" },
94
+ });
95
+ return res.data;
96
+ }
97
+ catch {
98
+ return "";
99
+ }
100
+ }
101
+ async function enrichOne(client, fullName, opts) {
102
+ const ttl = opts.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS;
103
+ const cacheKey = `repo:${fullName}`;
104
+ const cached = opts.cache?.getFresh(cacheKey, ttl);
105
+ if (cached)
106
+ return cached;
107
+ const [owner, repo] = fullName.split("/");
108
+ if (!owner || !repo)
109
+ return null;
110
+ try {
111
+ const meta = await client.rest.repos.get({ owner, repo });
112
+ const record = mapSearchItem(meta.data, opts.now);
113
+ const readmeText = cleanReadme(await fetchReadmeRaw(client, owner, repo), opts.readmeChars ?? DEFAULT_README_CHARS);
114
+ const enriched = {
115
+ ...record,
116
+ readme_excerpt: readmeText,
117
+ completeness_signals: buildCompletenessSignals({
118
+ description: record.description,
119
+ license: record.license,
120
+ homepage: record.homepage,
121
+ topics: record.topics,
122
+ readmeText,
123
+ }),
124
+ };
125
+ opts.cache?.set(cacheKey, enriched);
126
+ return enriched;
127
+ }
128
+ catch {
129
+ return null;
130
+ }
131
+ }
132
+ export async function enrichRepos(client, fullNames, opts) {
133
+ const results = await mapWithConcurrency(fullNames, opts.concurrency ?? 5, (name) => enrichOne(client, name, opts));
134
+ return results.filter((r) => r !== null);
135
+ }
136
+ //# sourceMappingURL=github.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"github.js","sourceRoot":"","sources":["../src/github.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ import type { CompletenessSignals } from "./types.js";
2
+ export declare function cleanReadme(raw: string, maxChars?: number): string;
3
+ export declare function readmeLengthBucket(len: number): CompletenessSignals["readme_length_bucket"];
4
+ export interface CompletenessInput {
5
+ description: string | null;
6
+ license: string | null;
7
+ homepage: string | null;
8
+ topics: string[];
9
+ readmeText: string;
10
+ }
11
+ export declare function buildCompletenessSignals(input: CompletenessInput): CompletenessSignals;
package/dist/readme.js ADDED
@@ -0,0 +1,38 @@
1
+ const TRUNCATE_MARKER = " [truncated]";
2
+ export function cleanReadme(raw, maxChars = 5000) {
3
+ let text = raw
4
+ .replace(/<!--[\s\S]*?-->/g, "")
5
+ .replace(/!\[[^\]]*\]\([^)]*\)/g, "")
6
+ .replace(/\[([^\]]*)\]\([^)]*\)/g, "$1")
7
+ .replace(/<[^>]+>/g, "")
8
+ .replace(/[ \t]+/g, " ")
9
+ .replace(/ *\n */g, "\n")
10
+ .replace(/\n{3,}/g, "\n\n")
11
+ .trim();
12
+ if (text.length > maxChars) {
13
+ const slice = text.slice(0, maxChars);
14
+ const lastBreak = Math.max(slice.lastIndexOf(" "), slice.lastIndexOf("\n"));
15
+ text = `${slice.slice(0, lastBreak > 0 ? lastBreak : maxChars).trimEnd()}${TRUNCATE_MARKER}`;
16
+ }
17
+ return text;
18
+ }
19
+ export function readmeLengthBucket(len) {
20
+ if (len === 0)
21
+ return "none";
22
+ if (len < 400)
23
+ return "thin";
24
+ if (len < 2000)
25
+ return "ok";
26
+ return "rich";
27
+ }
28
+ export function buildCompletenessSignals(input) {
29
+ return {
30
+ has_license: Boolean(input.license),
31
+ has_homepage: Boolean(input.homepage?.trim()),
32
+ has_description: Boolean(input.description?.trim()),
33
+ topic_count: input.topics.length,
34
+ readme_length: input.readmeText.length,
35
+ readme_length_bucket: readmeLengthBucket(input.readmeText.length),
36
+ };
37
+ }
38
+ //# sourceMappingURL=readme.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"readme.js","sourceRoot":"","sources":["../src/readme.ts"],"names":[],"mappings":"AAEA,MAAM,eAAe,GAAG,cAAc,CAAC;AAEvC,MAAM,UAAU,WAAW,CAAC,GAAW,EAAE,QAAQ,GAAG,IAAI;IACtD,IAAI,IAAI,GAAG,GAAG;SACX,OAAO,CAAC,kBAAkB,EAAE,EAAE,CAAC;SAC/B,OAAO,CAAC,uBAAuB,EAAE,EAAE,CAAC;SACpC,OAAO,CAAC,wBAAwB,EAAE,IAAI,CAAC;SACvC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC;SACvB,OAAO,CAAC,SAAS,EAAE,GAAG,CAAC;SACvB,OAAO,CAAC,SAAS,EAAE,IAAI,CAAC;SACxB,OAAO,CAAC,SAAS,EAAE,MAAM,CAAC;SAC1B,IAAI,EAAE,CAAC;IAEV,IAAI,IAAI,CAAC,MAAM,GAAG,QAAQ,EAAE,CAAC;QAC3B,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC;QACtC,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,WAAW,CAAC,GAAG,CAAC,EAAE,KAAK,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC;QAC5E,IAAI,GAAG,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,SAAS,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,OAAO,EAAE,GAAG,eAAe,EAAE,CAAC;IAC/F,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,GAAW;IAC5C,IAAI,GAAG,KAAK,CAAC;QAAE,OAAO,MAAM,CAAC;IAC7B,IAAI,GAAG,GAAG,GAAG;QAAE,OAAO,MAAM,CAAC;IAC7B,IAAI,GAAG,GAAG,IAAI;QAAE,OAAO,IAAI,CAAC;IAC5B,OAAO,MAAM,CAAC;AAChB,CAAC;AAUD,MAAM,UAAU,wBAAwB,CAAC,KAAwB;IAC/D,OAAO;QACL,WAAW,EAAE,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC;QACnC,YAAY,EAAE,OAAO,CAAC,KAAK,CAAC,QAAQ,EAAE,IAAI,EAAE,CAAC;QAC7C,eAAe,EAAE,OAAO,CAAC,KAAK,CAAC,WAAW,EAAE,IAAI,EAAE,CAAC;QACnD,WAAW,EAAE,KAAK,CAAC,MAAM,CAAC,MAAM;QAChC,aAAa,EAAE,KAAK,CAAC,UAAU,CAAC,MAAM;QACtC,oBAAoB,EAAE,kBAAkB,CAAC,KAAK,CAAC,UAAU,CAAC,MAAM,CAAC;KAClE,CAAC;AACJ,CAAC"}
@@ -0,0 +1,16 @@
1
+ import { type HardFilters, type RankCandidate, type RankResult, type Weights } from "./types.js";
2
+ export declare function popularityScore(stars: number, forks?: number): number;
3
+ export interface MaintenanceInput {
4
+ pushed_at: string;
5
+ archived: boolean;
6
+ open_issues?: number;
7
+ stars?: number;
8
+ }
9
+ export declare function maintenanceScore(input: MaintenanceInput, now: Date): number;
10
+ export declare function compositeScore(candidate: RankCandidate, weights?: Weights): number;
11
+ export interface RankOptions {
12
+ weights?: Weights;
13
+ hardFilters?: HardFilters;
14
+ now: Date;
15
+ }
16
+ export declare function rankRepos(candidates: RankCandidate[], options: RankOptions): RankResult;
package/dist/score.js ADDED
@@ -0,0 +1,86 @@
1
+ import { DEFAULT_WEIGHTS, } from "./types.js";
2
+ const STAR_CAP = 50000;
3
+ const FORK_CAP = 10000;
4
+ const FORK_BONUS = 0.05;
5
+ const MAINTENANCE_HALF_LIFE_DAYS = 180;
6
+ const ISSUE_PENALTY_MAX = 0.15;
7
+ const clamp01 = (n) => Math.max(0, Math.min(1, n));
8
+ const logScale = (value, cap) => Math.log10(value + 1) / Math.log10(cap + 1);
9
+ export function popularityScore(stars, forks = 0) {
10
+ const base = logScale(stars, STAR_CAP);
11
+ const bonus = FORK_BONUS * logScale(forks, FORK_CAP);
12
+ return clamp01(base + bonus);
13
+ }
14
+ export function maintenanceScore(input, now) {
15
+ if (input.archived)
16
+ return 0;
17
+ const days = (now.getTime() - new Date(input.pushed_at).getTime()) / 86400000;
18
+ const recency = Math.exp(-Math.max(0, days) / MAINTENANCE_HALF_LIFE_DAYS);
19
+ let penalty = 0;
20
+ if (input.stars && input.stars > 0 && input.open_issues !== undefined) {
21
+ penalty = ISSUE_PENALTY_MAX * Math.min(1, input.open_issues / input.stars);
22
+ }
23
+ return clamp01(recency - penalty);
24
+ }
25
+ function normalizeWeights(weights) {
26
+ const w = { ...DEFAULT_WEIGHTS, ...weights };
27
+ const sum = w.relevance + w.popularity + w.maintenance + w.completeness;
28
+ if (sum <= 0)
29
+ return { ...DEFAULT_WEIGHTS };
30
+ return {
31
+ relevance: w.relevance / sum,
32
+ popularity: w.popularity / sum,
33
+ maintenance: w.maintenance / sum,
34
+ completeness: w.completeness / sum,
35
+ };
36
+ }
37
+ export function compositeScore(candidate, weights) {
38
+ const w = normalizeWeights(weights);
39
+ return (w.relevance * candidate.relevance_score +
40
+ w.popularity * candidate.popularity_score +
41
+ w.maintenance * candidate.maintenance_score +
42
+ w.completeness * candidate.completeness_score);
43
+ }
44
+ function failedFilter(candidate, filters, now) {
45
+ if (filters.min_stars !== undefined && (candidate.stars ?? 0) < filters.min_stars) {
46
+ return `stars ${candidate.stars ?? 0} < min_stars ${filters.min_stars}`;
47
+ }
48
+ if (filters.language !== undefined &&
49
+ (candidate.language ?? "").toLowerCase() !== filters.language.toLowerCase()) {
50
+ return `language ${candidate.language ?? "none"} != ${filters.language}`;
51
+ }
52
+ if (filters.require_license && candidate.has_license !== true) {
53
+ return "no license";
54
+ }
55
+ if (filters.max_days_since_push !== undefined && candidate.pushed_at) {
56
+ const days = (now.getTime() - new Date(candidate.pushed_at).getTime()) / 86400000;
57
+ if (days > filters.max_days_since_push) {
58
+ return `pushed ${Math.round(days)}d ago > ${filters.max_days_since_push}d`;
59
+ }
60
+ }
61
+ return null;
62
+ }
63
+ export function rankRepos(candidates, options) {
64
+ const weights = normalizeWeights(options.weights);
65
+ const filters = options.hardFilters ?? {};
66
+ const dropped = [];
67
+ const kept = [];
68
+ for (const candidate of candidates) {
69
+ const reason = failedFilter(candidate, filters, options.now);
70
+ if (reason) {
71
+ dropped.push({ full_name: candidate.full_name, reason });
72
+ }
73
+ else {
74
+ kept.push(candidate);
75
+ }
76
+ }
77
+ const ranked = kept
78
+ .map((candidate) => ({
79
+ ...candidate,
80
+ composite: compositeScore(candidate, weights),
81
+ }))
82
+ .sort((a, b) => b.composite - a.composite)
83
+ .map((candidate, index) => ({ ...candidate, rank: index + 1 }));
84
+ return { ranked, dropped, weights };
85
+ }
86
+ //# sourceMappingURL=score.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"score.js","sourceRoot":"","sources":["../src/score.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/server.js ADDED
@@ -0,0 +1,97 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { z } from "zod";
5
+ import { RepoCache } from "./cache.js";
6
+ import { createClient, enrichRepos, searchRepos } from "./github.js";
7
+ import { rankRepos } from "./score.js";
8
+ import { HardFiltersSchema, RankCandidateSchema, WeightsSchema } from "./types.js";
9
+ const cache = new RepoCache();
10
+ let client;
11
+ const getClient = () => {
12
+ if (!client)
13
+ client = createClient();
14
+ return client;
15
+ };
16
+ const json = (value) => ({
17
+ content: [{ type: "text", text: JSON.stringify(value) }],
18
+ });
19
+ const fail = (err) => ({
20
+ isError: true,
21
+ content: [{ type: "text", text: err instanceof Error ? err.message : String(err) }],
22
+ });
23
+ const server = new McpServer({ name: "reposcout", version: "0.1.0" });
24
+ server.registerTool("repo_search", {
25
+ title: "Search GitHub repos",
26
+ description: "Run one or more GitHub repo search queries (use qualifiers like language:, stars:>N, topic:, pushed:>YYYY-MM-DD, archived:false), union and dedupe the results, and return compact records with computed popularity_score and maintenance_score. Craft 3-6 complementary queries from the user's objective.",
27
+ inputSchema: {
28
+ queries: z.array(z.string().min(1)).min(1).describe("GitHub search query strings"),
29
+ per_query_limit: z.number().int().min(1).max(100).optional(),
30
+ total_limit: z.number().int().min(1).max(200).optional(),
31
+ exclude_archived: z.boolean().optional(),
32
+ },
33
+ }, async (args) => {
34
+ try {
35
+ const records = await searchRepos(getClient(), {
36
+ queries: args.queries,
37
+ perQueryLimit: args.per_query_limit,
38
+ totalLimit: args.total_limit,
39
+ excludeArchived: args.exclude_archived,
40
+ now: new Date(),
41
+ });
42
+ return json({ count: records.length, repos: records });
43
+ }
44
+ catch (err) {
45
+ return fail(err);
46
+ }
47
+ });
48
+ server.registerTool("repo_enrich", {
49
+ title: "Enrich GitHub repos",
50
+ description: "For a shortlist of repos (owner/name), fetch the README (cleaned, truncated) plus extended metadata and deterministic completeness signals (license, homepage, description, topic count, README size). Results are cached. Use this on the ~10-20 most promising candidates from repo_search before judging relevance and completeness.",
51
+ inputSchema: {
52
+ full_names: z.array(z.string().min(1)).min(1).describe("Repos as owner/name"),
53
+ readme_chars: z.number().int().min(500).max(20000).optional(),
54
+ },
55
+ }, async (args) => {
56
+ try {
57
+ const repos = await enrichRepos(getClient(), args.full_names, {
58
+ now: new Date(),
59
+ cache,
60
+ readmeChars: args.readme_chars,
61
+ });
62
+ return json({ count: repos.length, repos });
63
+ }
64
+ catch (err) {
65
+ return fail(err);
66
+ }
67
+ });
68
+ server.registerTool("repo_rank", {
69
+ title: "Rank repos by weighted score",
70
+ description: "Combine the four sub-scores into a final ranking. popularity_score and maintenance_score come from repo_search/repo_enrich; relevance_score and completeness_score are your 0-1 judgments. Weights default to relevance 0.4, popularity 0.2, maintenance 0.2, completeness 0.2 and are normalized. Optional hard_filters drop candidates before ranking.",
71
+ inputSchema: {
72
+ candidates: z.array(RankCandidateSchema).min(1),
73
+ weights: WeightsSchema.optional(),
74
+ hard_filters: HardFiltersSchema.optional(),
75
+ },
76
+ }, async (args) => {
77
+ try {
78
+ const result = rankRepos(args.candidates, {
79
+ weights: args.weights,
80
+ hardFilters: args.hard_filters,
81
+ now: new Date(),
82
+ });
83
+ return json(result);
84
+ }
85
+ catch (err) {
86
+ return fail(err);
87
+ }
88
+ });
89
+ async function main() {
90
+ const transport = new StdioServerTransport();
91
+ await server.connect(transport);
92
+ }
93
+ main().catch((err) => {
94
+ process.stderr.write(`reposcout fatal: ${err instanceof Error ? err.stack : String(err)}\n`);
95
+ process.exit(1);
96
+ });
97
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AAGjF,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AACvC,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AACrE,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AACvC,OAAO,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEnF,MAAM,KAAK,GAAG,IAAI,SAAS,EAAE,CAAC;AAC9B,IAAI,MAA2B,CAAC;AAChC,MAAM,SAAS,GAAG,GAAY,EAAE;IAC9B,IAAI,CAAC,MAAM;QAAE,MAAM,GAAG,YAAY,EAAE,CAAC;IACrC,OAAO,MAAM,CAAC;AAChB,CAAC,CAAC;AAEF,MAAM,IAAI,GAAG,CAAC,KAAc,EAAkB,EAAE,CAAC,CAAC;IAChD,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,CAAC;CACzD,CAAC,CAAC;AAEH,MAAM,IAAI,GAAG,CAAC,GAAY,EAAkB,EAAE,CAAC,CAAC;IAC9C,OAAO,EAAE,IAAI;IACb,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC;CACpF,CAAC,CAAC;AAEH,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,CAAC;AAEtE,MAAM,CAAC,YAAY,CACjB,aAAa,EACb;IACE,KAAK,EAAE,qBAAqB;IAC5B,WAAW,EACT,6SAA6S;IAC/S,WAAW,EAAE;QACX,OAAO,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,6BAA6B,CAAC;QAClF,eAAe,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE;QAC5D,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE;QACxD,gBAAgB,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,QAAQ,EAAE;KACzC;CACF,EACD,KAAK,EAAE,IAAI,EAAE,EAAE;IACb,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,MAAM,WAAW,CAAC,SAAS,EAAE,EAAE;YAC7C,OAAO,EAAE,IAAI,CAAC,OAAO;YACrB,aAAa,EAAE,IAAI,CAAC,eAAe;YACnC,UAAU,EAAE,IAAI,CAAC,WAAW;YAC5B,eAAe,EAAE,IAAI,CAAC,gBAAgB;YACtC,GAAG,EAAE,IAAI,IAAI,EAAE;SAChB,CAAC,CAAC;QACH,OAAO,IAAI,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC,CAAC;IACzD,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC;IACnB,CAAC;AACH,CAAC,CACF,CAAC;AAEF,MAAM,CAAC,YAAY,CACjB,aAAa,EACb;IACE,KAAK,EAAE,qBAAqB;IAC5B,WAAW,EACT,yUAAyU;IAC3U,WAAW,EAAE;QACX,UAAU,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,qBAAqB,CAAC;QAC7E,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,QAAQ,EAAE;KAC9D;CACF,EACD,KAAK,EAAE,IAAI,EAAE,EAAE;IACb,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,MAAM,WAAW,CAAC,SAAS,EAAE,EAAE,IAAI,CAAC,UAAU,EAAE;YAC5D,GAAG,EAAE,IAAI,IAAI,EAAE;YACf,KAAK;YACL,WAAW,EAAE,IAAI,CAAC,YAAY;SAC/B,CAAC,CAAC;QACH,OAAO,IAAI,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC;IAC9C,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC;IACnB,CAAC;AACH,CAAC,CACF,CAAC;AAEF,MAAM,CAAC,YAAY,CACjB,WAAW,EACX;IACE,KAAK,EAAE,8BAA8B;IACrC,WAAW,EACT,0VAA0V;IAC5V,WAAW,EAAE;QACX,UAAU,EAAE,CAAC,CAAC,KAAK,CAAC,mBAAmB,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;QAC/C,OAAO,EAAE,aAAa,CAAC,QAAQ,EAAE;QACjC,YAAY,EAAE,iBAAiB,CAAC,QAAQ,EAAE;KAC3C;CACF,EACD,KAAK,EAAE,IAAI,EAAE,EAAE;IACb,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,SAAS,CAAC,IAAI,CAAC,UAAU,EAAE;YACxC,OAAO,EAAE,IAAI,CAAC,OAAO;YACrB,WAAW,EAAE,IAAI,CAAC,YAAY;YAC9B,GAAG,EAAE,IAAI,IAAI,EAAE;SAChB,CAAC,CAAC;QACH,OAAO,IAAI,CAAC,MAAM,CAAC,CAAC;IACtB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC;IACnB,CAAC;AACH,CAAC,CACF,CAAC;AAEF,KAAK,UAAU,IAAI;IACjB,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;IAC7C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;AAClC,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;IACnB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,oBAAoB,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC7F,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
@@ -0,0 +1,249 @@
1
+ import { z } from "zod";
2
+ export declare const WeightsSchema: z.ZodObject<{
3
+ relevance: z.ZodOptional<z.ZodNumber>;
4
+ popularity: z.ZodOptional<z.ZodNumber>;
5
+ maintenance: z.ZodOptional<z.ZodNumber>;
6
+ completeness: z.ZodOptional<z.ZodNumber>;
7
+ }, "strip", z.ZodTypeAny, {
8
+ relevance?: number | undefined;
9
+ popularity?: number | undefined;
10
+ maintenance?: number | undefined;
11
+ completeness?: number | undefined;
12
+ }, {
13
+ relevance?: number | undefined;
14
+ popularity?: number | undefined;
15
+ maintenance?: number | undefined;
16
+ completeness?: number | undefined;
17
+ }>;
18
+ export type Weights = z.infer<typeof WeightsSchema>;
19
+ export declare const DEFAULT_WEIGHTS: Required<Weights>;
20
+ export declare const HardFiltersSchema: z.ZodObject<{
21
+ min_stars: z.ZodOptional<z.ZodNumber>;
22
+ language: z.ZodOptional<z.ZodString>;
23
+ max_days_since_push: z.ZodOptional<z.ZodNumber>;
24
+ require_license: z.ZodOptional<z.ZodBoolean>;
25
+ }, "strict", z.ZodTypeAny, {
26
+ min_stars?: number | undefined;
27
+ language?: string | undefined;
28
+ max_days_since_push?: number | undefined;
29
+ require_license?: boolean | undefined;
30
+ }, {
31
+ min_stars?: number | undefined;
32
+ language?: string | undefined;
33
+ max_days_since_push?: number | undefined;
34
+ require_license?: boolean | undefined;
35
+ }>;
36
+ export type HardFilters = z.infer<typeof HardFiltersSchema>;
37
+ export declare const CompletenessSignalsSchema: z.ZodObject<{
38
+ has_license: z.ZodBoolean;
39
+ has_homepage: z.ZodBoolean;
40
+ has_description: z.ZodBoolean;
41
+ topic_count: z.ZodNumber;
42
+ readme_length: z.ZodNumber;
43
+ readme_length_bucket: z.ZodEnum<["none", "thin", "ok", "rich"]>;
44
+ }, "strip", z.ZodTypeAny, {
45
+ has_license: boolean;
46
+ has_homepage: boolean;
47
+ has_description: boolean;
48
+ topic_count: number;
49
+ readme_length: number;
50
+ readme_length_bucket: "none" | "thin" | "ok" | "rich";
51
+ }, {
52
+ has_license: boolean;
53
+ has_homepage: boolean;
54
+ has_description: boolean;
55
+ topic_count: number;
56
+ readme_length: number;
57
+ readme_length_bucket: "none" | "thin" | "ok" | "rich";
58
+ }>;
59
+ export type CompletenessSignals = z.infer<typeof CompletenessSignalsSchema>;
60
+ export declare const RepoRecordSchema: z.ZodObject<{
61
+ full_name: z.ZodString;
62
+ html_url: z.ZodString;
63
+ description: z.ZodNullable<z.ZodString>;
64
+ language: z.ZodNullable<z.ZodString>;
65
+ topics: z.ZodArray<z.ZodString, "many">;
66
+ stars: z.ZodNumber;
67
+ forks: z.ZodNumber;
68
+ open_issues: z.ZodNumber;
69
+ pushed_at: z.ZodString;
70
+ created_at: z.ZodString;
71
+ license: z.ZodNullable<z.ZodString>;
72
+ homepage: z.ZodNullable<z.ZodString>;
73
+ archived: z.ZodBoolean;
74
+ popularity_score: z.ZodNumber;
75
+ maintenance_score: z.ZodNumber;
76
+ }, "strip", z.ZodTypeAny, {
77
+ language: string | null;
78
+ full_name: string;
79
+ html_url: string;
80
+ description: string | null;
81
+ topics: string[];
82
+ stars: number;
83
+ forks: number;
84
+ open_issues: number;
85
+ pushed_at: string;
86
+ created_at: string;
87
+ license: string | null;
88
+ homepage: string | null;
89
+ archived: boolean;
90
+ popularity_score: number;
91
+ maintenance_score: number;
92
+ }, {
93
+ language: string | null;
94
+ full_name: string;
95
+ html_url: string;
96
+ description: string | null;
97
+ topics: string[];
98
+ stars: number;
99
+ forks: number;
100
+ open_issues: number;
101
+ pushed_at: string;
102
+ created_at: string;
103
+ license: string | null;
104
+ homepage: string | null;
105
+ archived: boolean;
106
+ popularity_score: number;
107
+ maintenance_score: number;
108
+ }>;
109
+ export type RepoRecord = z.infer<typeof RepoRecordSchema>;
110
+ export declare const EnrichedRepoSchema: z.ZodObject<{
111
+ full_name: z.ZodString;
112
+ html_url: z.ZodString;
113
+ description: z.ZodNullable<z.ZodString>;
114
+ language: z.ZodNullable<z.ZodString>;
115
+ topics: z.ZodArray<z.ZodString, "many">;
116
+ stars: z.ZodNumber;
117
+ forks: z.ZodNumber;
118
+ open_issues: z.ZodNumber;
119
+ pushed_at: z.ZodString;
120
+ created_at: z.ZodString;
121
+ license: z.ZodNullable<z.ZodString>;
122
+ homepage: z.ZodNullable<z.ZodString>;
123
+ archived: z.ZodBoolean;
124
+ popularity_score: z.ZodNumber;
125
+ maintenance_score: z.ZodNumber;
126
+ } & {
127
+ readme_excerpt: z.ZodString;
128
+ completeness_signals: z.ZodObject<{
129
+ has_license: z.ZodBoolean;
130
+ has_homepage: z.ZodBoolean;
131
+ has_description: z.ZodBoolean;
132
+ topic_count: z.ZodNumber;
133
+ readme_length: z.ZodNumber;
134
+ readme_length_bucket: z.ZodEnum<["none", "thin", "ok", "rich"]>;
135
+ }, "strip", z.ZodTypeAny, {
136
+ has_license: boolean;
137
+ has_homepage: boolean;
138
+ has_description: boolean;
139
+ topic_count: number;
140
+ readme_length: number;
141
+ readme_length_bucket: "none" | "thin" | "ok" | "rich";
142
+ }, {
143
+ has_license: boolean;
144
+ has_homepage: boolean;
145
+ has_description: boolean;
146
+ topic_count: number;
147
+ readme_length: number;
148
+ readme_length_bucket: "none" | "thin" | "ok" | "rich";
149
+ }>;
150
+ }, "strip", z.ZodTypeAny, {
151
+ language: string | null;
152
+ full_name: string;
153
+ html_url: string;
154
+ description: string | null;
155
+ topics: string[];
156
+ stars: number;
157
+ forks: number;
158
+ open_issues: number;
159
+ pushed_at: string;
160
+ created_at: string;
161
+ license: string | null;
162
+ homepage: string | null;
163
+ archived: boolean;
164
+ popularity_score: number;
165
+ maintenance_score: number;
166
+ readme_excerpt: string;
167
+ completeness_signals: {
168
+ has_license: boolean;
169
+ has_homepage: boolean;
170
+ has_description: boolean;
171
+ topic_count: number;
172
+ readme_length: number;
173
+ readme_length_bucket: "none" | "thin" | "ok" | "rich";
174
+ };
175
+ }, {
176
+ language: string | null;
177
+ full_name: string;
178
+ html_url: string;
179
+ description: string | null;
180
+ topics: string[];
181
+ stars: number;
182
+ forks: number;
183
+ open_issues: number;
184
+ pushed_at: string;
185
+ created_at: string;
186
+ license: string | null;
187
+ homepage: string | null;
188
+ archived: boolean;
189
+ popularity_score: number;
190
+ maintenance_score: number;
191
+ readme_excerpt: string;
192
+ completeness_signals: {
193
+ has_license: boolean;
194
+ has_homepage: boolean;
195
+ has_description: boolean;
196
+ topic_count: number;
197
+ readme_length: number;
198
+ readme_length_bucket: "none" | "thin" | "ok" | "rich";
199
+ };
200
+ }>;
201
+ export type EnrichedRepo = z.infer<typeof EnrichedRepoSchema>;
202
+ export declare const RankCandidateSchema: z.ZodObject<{
203
+ full_name: z.ZodString;
204
+ popularity_score: z.ZodNumber;
205
+ maintenance_score: z.ZodNumber;
206
+ relevance_score: z.ZodNumber;
207
+ completeness_score: z.ZodNumber;
208
+ stars: z.ZodOptional<z.ZodNumber>;
209
+ language: z.ZodOptional<z.ZodNullable<z.ZodString>>;
210
+ pushed_at: z.ZodOptional<z.ZodString>;
211
+ has_license: z.ZodOptional<z.ZodBoolean>;
212
+ note: z.ZodOptional<z.ZodString>;
213
+ }, "strip", z.ZodTypeAny, {
214
+ full_name: string;
215
+ popularity_score: number;
216
+ maintenance_score: number;
217
+ relevance_score: number;
218
+ completeness_score: number;
219
+ language?: string | null | undefined;
220
+ has_license?: boolean | undefined;
221
+ stars?: number | undefined;
222
+ pushed_at?: string | undefined;
223
+ note?: string | undefined;
224
+ }, {
225
+ full_name: string;
226
+ popularity_score: number;
227
+ maintenance_score: number;
228
+ relevance_score: number;
229
+ completeness_score: number;
230
+ language?: string | null | undefined;
231
+ has_license?: boolean | undefined;
232
+ stars?: number | undefined;
233
+ pushed_at?: string | undefined;
234
+ note?: string | undefined;
235
+ }>;
236
+ export type RankCandidate = z.infer<typeof RankCandidateSchema>;
237
+ export interface RankedRepo extends RankCandidate {
238
+ rank: number;
239
+ composite: number;
240
+ }
241
+ export interface RankResult {
242
+ ranked: RankedRepo[];
243
+ dropped: {
244
+ full_name: string;
245
+ reason: string;
246
+ }[];
247
+ weights: Required<Weights>;
248
+ }
249
+ export declare const SCORE_KEYS: readonly ["relevance", "popularity", "maintenance", "completeness"];
package/dist/types.js ADDED
@@ -0,0 +1,66 @@
1
+ import { z } from "zod";
2
+ export const WeightsSchema = z
3
+ .object({
4
+ relevance: z.number().min(0),
5
+ popularity: z.number().min(0),
6
+ maintenance: z.number().min(0),
7
+ completeness: z.number().min(0),
8
+ })
9
+ .partial();
10
+ export const DEFAULT_WEIGHTS = {
11
+ relevance: 0.4,
12
+ popularity: 0.2,
13
+ maintenance: 0.2,
14
+ completeness: 0.2,
15
+ };
16
+ export const HardFiltersSchema = z
17
+ .object({
18
+ min_stars: z.number().int().min(0).optional(),
19
+ language: z.string().optional(),
20
+ max_days_since_push: z.number().int().min(0).optional(),
21
+ require_license: z.boolean().optional(),
22
+ })
23
+ .strict();
24
+ export const CompletenessSignalsSchema = z.object({
25
+ has_license: z.boolean(),
26
+ has_homepage: z.boolean(),
27
+ has_description: z.boolean(),
28
+ topic_count: z.number().int().min(0),
29
+ readme_length: z.number().int().min(0),
30
+ readme_length_bucket: z.enum(["none", "thin", "ok", "rich"]),
31
+ });
32
+ export const RepoRecordSchema = z.object({
33
+ full_name: z.string(),
34
+ html_url: z.string(),
35
+ description: z.string().nullable(),
36
+ language: z.string().nullable(),
37
+ topics: z.array(z.string()),
38
+ stars: z.number().int().min(0),
39
+ forks: z.number().int().min(0),
40
+ open_issues: z.number().int().min(0),
41
+ pushed_at: z.string(),
42
+ created_at: z.string(),
43
+ license: z.string().nullable(),
44
+ homepage: z.string().nullable(),
45
+ archived: z.boolean(),
46
+ popularity_score: z.number().min(0).max(1),
47
+ maintenance_score: z.number().min(0).max(1),
48
+ });
49
+ export const EnrichedRepoSchema = RepoRecordSchema.extend({
50
+ readme_excerpt: z.string(),
51
+ completeness_signals: CompletenessSignalsSchema,
52
+ });
53
+ export const RankCandidateSchema = z.object({
54
+ full_name: z.string(),
55
+ popularity_score: z.number().min(0).max(1),
56
+ maintenance_score: z.number().min(0).max(1),
57
+ relevance_score: z.number().min(0).max(1),
58
+ completeness_score: z.number().min(0).max(1),
59
+ stars: z.number().int().min(0).optional(),
60
+ language: z.string().nullable().optional(),
61
+ pushed_at: z.string().optional(),
62
+ has_license: z.boolean().optional(),
63
+ note: z.string().optional(),
64
+ });
65
+ export const SCORE_KEYS = ["relevance", "popularity", "maintenance", "completeness"];
66
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "@rakeshroushan/reposcout",
3
+ "version": "0.1.0",
4
+ "description": "LLM-driven GitHub repo discovery and ranking, exposed as an MCP server",
5
+ "type": "module",
6
+ "mcpName": "io.github.rakesh1002/reposcout",
7
+ "license": "MIT",
8
+ "author": "Rakesh Roushan",
9
+ "homepage": "https://github.com/Rakesh1002/reposcout#readme",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/Rakesh1002/reposcout.git"
13
+ },
14
+ "bugs": {
15
+ "url": "https://github.com/Rakesh1002/reposcout/issues"
16
+ },
17
+ "keywords": [
18
+ "mcp",
19
+ "modelcontextprotocol",
20
+ "model-context-protocol",
21
+ "mcp-server",
22
+ "github",
23
+ "repository-search",
24
+ "repo-discovery",
25
+ "ranking",
26
+ "claude",
27
+ "ai-agent"
28
+ ],
29
+ "publishConfig": {
30
+ "access": "public"
31
+ },
32
+ "bin": {
33
+ "reposcout": "dist/server.js"
34
+ },
35
+ "files": [
36
+ "dist",
37
+ "README.md",
38
+ "LICENSE"
39
+ ],
40
+ "scripts": {
41
+ "build": "tsc -p tsconfig.json",
42
+ "dev": "tsx src/server.ts",
43
+ "start": "node dist/server.js",
44
+ "test": "node --import tsx --test tests/*.test.ts",
45
+ "typecheck": "tsc -p tsconfig.json --noEmit",
46
+ "lint": "biome check src tests",
47
+ "format": "biome format --write src tests"
48
+ },
49
+ "dependencies": {
50
+ "@modelcontextprotocol/sdk": "^1.12.0",
51
+ "octokit": "^4.1.0",
52
+ "zod": "^3.24.1"
53
+ },
54
+ "devDependencies": {
55
+ "@biomejs/biome": "^1.9.4",
56
+ "@types/node": "^24.0.0",
57
+ "tsx": "^4.19.2",
58
+ "typescript": "^5.7.2"
59
+ },
60
+ "engines": {
61
+ "node": ">=22"
62
+ }
63
+ }