mcp-researchpowerpack-http 3.10.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/README.md +124 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +227 -0
- package/dist/index.js.map +7 -0
- package/dist/mcp-use.json +7 -0
- package/dist/src/clients/github.d.ts +83 -0
- package/dist/src/clients/github.d.ts.map +1 -0
- package/dist/src/clients/github.js +370 -0
- package/dist/src/clients/github.js.map +7 -0
- package/dist/src/clients/reddit.d.ts +60 -0
- package/dist/src/clients/reddit.d.ts.map +1 -0
- package/dist/src/clients/reddit.js +287 -0
- package/dist/src/clients/reddit.js.map +7 -0
- package/dist/src/clients/research.d.ts +67 -0
- package/dist/src/clients/research.d.ts.map +1 -0
- package/dist/src/clients/research.js +282 -0
- package/dist/src/clients/research.js.map +7 -0
- package/dist/src/clients/scraper.d.ts +72 -0
- package/dist/src/clients/scraper.d.ts.map +1 -0
- package/dist/src/clients/scraper.js +327 -0
- package/dist/src/clients/scraper.js.map +7 -0
- package/dist/src/clients/search.d.ts +57 -0
- package/dist/src/clients/search.d.ts.map +1 -0
- package/dist/src/clients/search.js +218 -0
- package/dist/src/clients/search.js.map +7 -0
- package/dist/src/config/index.d.ts +93 -0
- package/dist/src/config/index.d.ts.map +1 -0
- package/dist/src/config/index.js +218 -0
- package/dist/src/config/index.js.map +7 -0
- package/dist/src/schemas/deep-research.d.ts +40 -0
- package/dist/src/schemas/deep-research.d.ts.map +1 -0
- package/dist/src/schemas/deep-research.js +216 -0
- package/dist/src/schemas/deep-research.js.map +7 -0
- package/dist/src/schemas/github-score.d.ts +50 -0
- package/dist/src/schemas/github-score.d.ts.map +1 -0
- package/dist/src/schemas/github-score.js +58 -0
- package/dist/src/schemas/github-score.js.map +7 -0
- package/dist/src/schemas/scrape-links.d.ts +23 -0
- package/dist/src/schemas/scrape-links.d.ts.map +1 -0
- package/dist/src/schemas/scrape-links.js +32 -0
- package/dist/src/schemas/scrape-links.js.map +7 -0
- package/dist/src/schemas/web-search.d.ts +18 -0
- package/dist/src/schemas/web-search.d.ts.map +1 -0
- package/dist/src/schemas/web-search.js +28 -0
- package/dist/src/schemas/web-search.js.map +7 -0
- package/dist/src/scoring/github-quality.d.ts +142 -0
- package/dist/src/scoring/github-quality.d.ts.map +1 -0
- package/dist/src/scoring/github-quality.js +202 -0
- package/dist/src/scoring/github-quality.js.map +7 -0
- package/dist/src/services/file-attachment.d.ts +30 -0
- package/dist/src/services/file-attachment.d.ts.map +1 -0
- package/dist/src/services/file-attachment.js +205 -0
- package/dist/src/services/file-attachment.js.map +7 -0
- package/dist/src/services/llm-processor.d.ts +29 -0
- package/dist/src/services/llm-processor.d.ts.map +1 -0
- package/dist/src/services/llm-processor.js +206 -0
- package/dist/src/services/llm-processor.js.map +7 -0
- package/dist/src/services/markdown-cleaner.d.ts +8 -0
- package/dist/src/services/markdown-cleaner.d.ts.map +1 -0
- package/dist/src/services/markdown-cleaner.js +63 -0
- package/dist/src/services/markdown-cleaner.js.map +7 -0
- package/dist/src/tools/github-score.d.ts +12 -0
- package/dist/src/tools/github-score.d.ts.map +1 -0
- package/dist/src/tools/github-score.js +306 -0
- package/dist/src/tools/github-score.js.map +7 -0
- package/dist/src/tools/mcp-helpers.d.ts +27 -0
- package/dist/src/tools/mcp-helpers.d.ts.map +1 -0
- package/dist/src/tools/mcp-helpers.js +47 -0
- package/dist/src/tools/mcp-helpers.js.map +7 -0
- package/dist/src/tools/reddit.d.ts +54 -0
- package/dist/src/tools/reddit.d.ts.map +1 -0
- package/dist/src/tools/reddit.js +498 -0
- package/dist/src/tools/reddit.js.map +7 -0
- package/dist/src/tools/registry.d.ts +3 -0
- package/dist/src/tools/registry.d.ts.map +1 -0
- package/dist/src/tools/registry.js +17 -0
- package/dist/src/tools/registry.js.map +7 -0
- package/dist/src/tools/research.d.ts +14 -0
- package/dist/src/tools/research.d.ts.map +1 -0
- package/dist/src/tools/research.js +250 -0
- package/dist/src/tools/research.js.map +7 -0
- package/dist/src/tools/scrape.d.ts +14 -0
- package/dist/src/tools/scrape.d.ts.map +1 -0
- package/dist/src/tools/scrape.js +290 -0
- package/dist/src/tools/scrape.js.map +7 -0
- package/dist/src/tools/search.d.ts +10 -0
- package/dist/src/tools/search.d.ts.map +1 -0
- package/dist/src/tools/search.js +197 -0
- package/dist/src/tools/search.js.map +7 -0
- package/dist/src/tools/utils.d.ts +105 -0
- package/dist/src/tools/utils.d.ts.map +1 -0
- package/dist/src/tools/utils.js +96 -0
- package/dist/src/tools/utils.js.map +7 -0
- package/dist/src/utils/concurrency.d.ts +28 -0
- package/dist/src/utils/concurrency.d.ts.map +1 -0
- package/dist/src/utils/concurrency.js +62 -0
- package/dist/src/utils/concurrency.js.map +7 -0
- package/dist/src/utils/errors.d.ts +95 -0
- package/dist/src/utils/errors.d.ts.map +1 -0
- package/dist/src/utils/errors.js +289 -0
- package/dist/src/utils/errors.js.map +7 -0
- package/dist/src/utils/logger.d.ts +33 -0
- package/dist/src/utils/logger.d.ts.map +1 -0
- package/dist/src/utils/logger.js +41 -0
- package/dist/src/utils/logger.js.map +7 -0
- package/dist/src/utils/markdown-formatter.d.ts +5 -0
- package/dist/src/utils/markdown-formatter.d.ts.map +1 -0
- package/dist/src/utils/markdown-formatter.js +15 -0
- package/dist/src/utils/markdown-formatter.js.map +7 -0
- package/dist/src/utils/response.d.ts +83 -0
- package/dist/src/utils/response.d.ts.map +1 -0
- package/dist/src/utils/response.js +109 -0
- package/dist/src/utils/response.js.map +7 -0
- package/dist/src/utils/retry.d.ts +43 -0
- package/dist/src/utils/retry.d.ts.map +1 -0
- package/dist/src/utils/retry.js +37 -0
- package/dist/src/utils/retry.js.map +7 -0
- package/dist/src/utils/url-aggregator.d.ts +92 -0
- package/dist/src/utils/url-aggregator.d.ts.map +1 -0
- package/dist/src/utils/url-aggregator.js +357 -0
- package/dist/src/utils/url-aggregator.js.map +7 -0
- package/dist/src/version.d.ts +28 -0
- package/dist/src/version.d.ts.map +1 -0
- package/dist/src/version.js +32 -0
- package/dist/src/version.js.map +7 -0
- package/package.json +73 -0
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
import { GITHUB, parseEnv } from "../config/index.js";
|
|
2
|
+
import {
|
|
3
|
+
classifyError,
|
|
4
|
+
fetchWithTimeout,
|
|
5
|
+
sleep,
|
|
6
|
+
ErrorCode
|
|
7
|
+
} from "../utils/errors.js";
|
|
8
|
+
import { calculateBackoff } from "../utils/retry.js";
|
|
9
|
+
import { pMapSettled } from "../utils/concurrency.js";
|
|
10
|
+
import { mcpLog } from "../utils/logger.js";
|
|
11
|
+
const REPO_DETAILS_QUERY = `
|
|
12
|
+
query RepoDetails($owner: String!, $name: String!) {
|
|
13
|
+
repository(owner: $owner, name: $name) {
|
|
14
|
+
stargazerCount
|
|
15
|
+
forkCount
|
|
16
|
+
watchers { totalCount }
|
|
17
|
+
openIssues: issues(states: OPEN) { totalCount }
|
|
18
|
+
closedIssues: issues(states: CLOSED) { totalCount }
|
|
19
|
+
pullRequests { totalCount }
|
|
20
|
+
releases { totalCount }
|
|
21
|
+
defaultBranchRef {
|
|
22
|
+
target {
|
|
23
|
+
... on Commit {
|
|
24
|
+
history { totalCount }
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
licenseInfo { spdxId }
|
|
29
|
+
primaryLanguage { name }
|
|
30
|
+
repositoryTopics(first: 5) {
|
|
31
|
+
nodes { topic { name } }
|
|
32
|
+
}
|
|
33
|
+
isArchived
|
|
34
|
+
pushedAt
|
|
35
|
+
createdAt
|
|
36
|
+
diskUsage
|
|
37
|
+
description
|
|
38
|
+
homepageUrl
|
|
39
|
+
codeOfConduct { name }
|
|
40
|
+
ciCheck: object(expression: "HEAD:.github/workflows") {
|
|
41
|
+
... on Tree { entries { name } }
|
|
42
|
+
}
|
|
43
|
+
contributingGuide: object(expression: "HEAD:CONTRIBUTING.md") {
|
|
44
|
+
... on Blob { byteSize }
|
|
45
|
+
}
|
|
46
|
+
issueTemplate: object(expression: "HEAD:.github/ISSUE_TEMPLATE") {
|
|
47
|
+
... on Tree { entries { name } }
|
|
48
|
+
}
|
|
49
|
+
prTemplate: object(expression: "HEAD:.github/PULL_REQUEST_TEMPLATE.md") {
|
|
50
|
+
... on Blob { byteSize }
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
`;
|
|
55
|
+
const RETRYABLE_CODES = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
|
|
56
|
+
function getGhCliToken() {
|
|
57
|
+
try {
|
|
58
|
+
const { execSync } = require("node:child_process");
|
|
59
|
+
const token = execSync("gh auth token", {
|
|
60
|
+
timeout: 3e3,
|
|
61
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
62
|
+
encoding: "utf-8"
|
|
63
|
+
}).trim();
|
|
64
|
+
if (token.length > 0) {
|
|
65
|
+
mcpLog("info", "Using GitHub token from `gh auth token`", "github");
|
|
66
|
+
}
|
|
67
|
+
return token;
|
|
68
|
+
} catch {
|
|
69
|
+
return "";
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
class GitHubClient {
|
|
73
|
+
token;
|
|
74
|
+
apiCallCount = 0;
|
|
75
|
+
constructor(token) {
|
|
76
|
+
const env = parseEnv();
|
|
77
|
+
this.token = token || env.GITHUB_TOKEN || getGhCliToken();
|
|
78
|
+
if (!this.token) {
|
|
79
|
+
throw new Error(
|
|
80
|
+
"GitHub capability is not configured. Set GITHUB_TOKEN or log in with `gh auth login`."
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
get apiCalls() {
|
|
85
|
+
return this.apiCallCount;
|
|
86
|
+
}
|
|
87
|
+
// --------------------------------------------------------------------------
|
|
88
|
+
// Search
|
|
89
|
+
// --------------------------------------------------------------------------
|
|
90
|
+
async searchRepos(query, sort = "stars", perPage = 20) {
|
|
91
|
+
const url = `${GITHUB.REST_BASE_URL}/search/repositories?q=${encodeURIComponent(query)}&sort=${sort}&order=desc&per_page=${perPage}`;
|
|
92
|
+
try {
|
|
93
|
+
const response = await this.restGet(url);
|
|
94
|
+
if ("error" in response) {
|
|
95
|
+
return { items: [], totalCount: 0, error: response.error };
|
|
96
|
+
}
|
|
97
|
+
const data = response.data;
|
|
98
|
+
const items = (data.items ?? []).map((item) => ({
|
|
99
|
+
fullName: String(item.full_name ?? ""),
|
|
100
|
+
owner: item.owner?.login ? String(item.owner.login) : "",
|
|
101
|
+
name: String(item.name ?? ""),
|
|
102
|
+
description: item.description ? String(item.description) : null,
|
|
103
|
+
stars: Number(item.stargazers_count ?? 0),
|
|
104
|
+
forks: Number(item.forks_count ?? 0),
|
|
105
|
+
language: item.language ? String(item.language) : null,
|
|
106
|
+
pushedAt: String(item.pushed_at ?? ""),
|
|
107
|
+
url: String(item.html_url ?? "")
|
|
108
|
+
}));
|
|
109
|
+
return { items, totalCount: Number(data.total_count ?? 0) };
|
|
110
|
+
} catch (error) {
|
|
111
|
+
return { items: [], totalCount: 0, error: classifyError(error) };
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// --------------------------------------------------------------------------
|
|
115
|
+
// Fetch Full Repo Details (GraphQL + 2 REST)
|
|
116
|
+
// --------------------------------------------------------------------------
|
|
117
|
+
async fetchRepoDetails(owner, name) {
|
|
118
|
+
const graphqlResult = await this.graphqlQuery(REPO_DETAILS_QUERY, { owner, name });
|
|
119
|
+
if (graphqlResult.error || !graphqlResult.data) {
|
|
120
|
+
return { error: graphqlResult.error ?? { code: ErrorCode.UNKNOWN_ERROR, message: "GraphQL returned no data", retryable: false } };
|
|
121
|
+
}
|
|
122
|
+
const repo = graphqlResult.data?.repository;
|
|
123
|
+
if (!repo) {
|
|
124
|
+
return { error: { code: ErrorCode.NOT_FOUND, message: `Repository ${owner}/${name} not found`, retryable: false } };
|
|
125
|
+
}
|
|
126
|
+
const graphql = parseGraphQLResponse(repo);
|
|
127
|
+
const participation = await this.fetchParticipation(owner, name);
|
|
128
|
+
const contributors = await this.fetchContributors(owner, name);
|
|
129
|
+
return {
|
|
130
|
+
data: {
|
|
131
|
+
graphql,
|
|
132
|
+
participation,
|
|
133
|
+
contributors
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
// --------------------------------------------------------------------------
|
|
138
|
+
// Batch fetch with concurrency
|
|
139
|
+
// --------------------------------------------------------------------------
|
|
140
|
+
async fetchMultipleRepoDetails(repos) {
|
|
141
|
+
const results = await pMapSettled(
|
|
142
|
+
repos,
|
|
143
|
+
async (repo) => {
|
|
144
|
+
const result = await this.fetchRepoDetails(repo.owner, repo.name);
|
|
145
|
+
return { ...repo, ...result };
|
|
146
|
+
},
|
|
147
|
+
GITHUB.MAX_CONCURRENT_REPOS
|
|
148
|
+
);
|
|
149
|
+
return results.map((settled, i) => {
|
|
150
|
+
const repo = repos[i];
|
|
151
|
+
if (settled.status === "fulfilled") {
|
|
152
|
+
return settled.value;
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
owner: repo.owner,
|
|
156
|
+
name: repo.name,
|
|
157
|
+
error: classifyError(settled.reason)
|
|
158
|
+
};
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
// --------------------------------------------------------------------------
|
|
162
|
+
// GraphQL
|
|
163
|
+
// --------------------------------------------------------------------------
|
|
164
|
+
async graphqlQuery(query, variables) {
|
|
165
|
+
this.apiCallCount++;
|
|
166
|
+
for (let attempt = 0; attempt <= GITHUB.RETRY_COUNT; attempt++) {
|
|
167
|
+
try {
|
|
168
|
+
if (attempt > 0) {
|
|
169
|
+
mcpLog("warning", `GraphQL retry ${attempt}/${GITHUB.RETRY_COUNT}`, "github");
|
|
170
|
+
}
|
|
171
|
+
const response = await fetchWithTimeout(GITHUB.GRAPHQL_URL, {
|
|
172
|
+
method: "POST",
|
|
173
|
+
headers: {
|
|
174
|
+
Authorization: `Bearer ${this.token}`,
|
|
175
|
+
"Content-Type": "application/json",
|
|
176
|
+
"User-Agent": "mcp-researchpowerpack"
|
|
177
|
+
},
|
|
178
|
+
body: JSON.stringify({ query, variables }),
|
|
179
|
+
timeoutMs: GITHUB.TIMEOUT_MS
|
|
180
|
+
});
|
|
181
|
+
if (!response.ok) {
|
|
182
|
+
if (RETRYABLE_CODES.has(response.status) && attempt < GITHUB.RETRY_COUNT) {
|
|
183
|
+
const delay = calculateBackoff(attempt);
|
|
184
|
+
await sleep(delay);
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
const text = await response.text().catch(() => "");
|
|
188
|
+
return { error: classifyError({ status: response.status, message: text }) };
|
|
189
|
+
}
|
|
190
|
+
const json = await response.json();
|
|
191
|
+
if (json.errors && json.errors.length > 0) {
|
|
192
|
+
const msg = json.errors.map((e) => e.message).join("; ");
|
|
193
|
+
if (json.data) {
|
|
194
|
+
mcpLog("warning", `GraphQL partial error: ${msg}`, "github");
|
|
195
|
+
return { data: json.data };
|
|
196
|
+
}
|
|
197
|
+
return { error: { code: ErrorCode.INTERNAL_ERROR, message: msg, retryable: false } };
|
|
198
|
+
}
|
|
199
|
+
return { data: json.data };
|
|
200
|
+
} catch (error) {
|
|
201
|
+
const structured = classifyError(error);
|
|
202
|
+
if (structured.retryable && attempt < GITHUB.RETRY_COUNT) {
|
|
203
|
+
const delay = calculateBackoff(attempt);
|
|
204
|
+
await sleep(delay);
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
return { error: structured };
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return { error: { code: ErrorCode.UNKNOWN_ERROR, message: "GraphQL failed after retries", retryable: false } };
|
|
211
|
+
}
|
|
212
|
+
// --------------------------------------------------------------------------
|
|
213
|
+
// REST: Participation (handles 202 lazy computation)
|
|
214
|
+
// --------------------------------------------------------------------------
|
|
215
|
+
async fetchParticipation(owner, name) {
|
|
216
|
+
const url = `${GITHUB.REST_BASE_URL}/repos/${owner}/${name}/stats/participation`;
|
|
217
|
+
const emptyResult = { all: [], owner: [] };
|
|
218
|
+
for (let attempt = 0; attempt < GITHUB.PARTICIPATION_MAX_RETRIES; attempt++) {
|
|
219
|
+
try {
|
|
220
|
+
this.apiCallCount++;
|
|
221
|
+
const response = await fetchWithTimeout(url, {
|
|
222
|
+
headers: {
|
|
223
|
+
Authorization: `Bearer ${this.token}`,
|
|
224
|
+
"User-Agent": "mcp-researchpowerpack"
|
|
225
|
+
},
|
|
226
|
+
timeoutMs: GITHUB.TIMEOUT_MS
|
|
227
|
+
});
|
|
228
|
+
if (response.status === 202) {
|
|
229
|
+
mcpLog("info", `Participation stats computing for ${owner}/${name}, retry ${attempt + 1}`, "github");
|
|
230
|
+
await sleep(GITHUB.PARTICIPATION_RETRY_DELAY_MS);
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
if (!response.ok) {
|
|
234
|
+
mcpLog("warning", `Participation fetch failed ${response.status} for ${owner}/${name}`, "github");
|
|
235
|
+
return emptyResult;
|
|
236
|
+
}
|
|
237
|
+
const data = await response.json();
|
|
238
|
+
if (!Array.isArray(data.all)) return emptyResult;
|
|
239
|
+
return {
|
|
240
|
+
all: data.all ?? [],
|
|
241
|
+
owner: data.owner ?? []
|
|
242
|
+
};
|
|
243
|
+
} catch (error) {
|
|
244
|
+
mcpLog("warning", `Participation error for ${owner}/${name}: ${classifyError(error).message}`, "github");
|
|
245
|
+
return emptyResult;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
mcpLog("warning", `Participation still 202 after retries for ${owner}/${name}`, "github");
|
|
249
|
+
return emptyResult;
|
|
250
|
+
}
|
|
251
|
+
// --------------------------------------------------------------------------
|
|
252
|
+
// REST: Contributors
|
|
253
|
+
// --------------------------------------------------------------------------
|
|
254
|
+
async fetchContributors(owner, name) {
|
|
255
|
+
const url = `${GITHUB.REST_BASE_URL}/repos/${owner}/${name}/contributors?per_page=100&anon=true`;
|
|
256
|
+
try {
|
|
257
|
+
this.apiCallCount++;
|
|
258
|
+
const response = await fetchWithTimeout(url, {
|
|
259
|
+
headers: {
|
|
260
|
+
Authorization: `Bearer ${this.token}`,
|
|
261
|
+
"User-Agent": "mcp-researchpowerpack"
|
|
262
|
+
},
|
|
263
|
+
timeoutMs: GITHUB.TIMEOUT_MS
|
|
264
|
+
});
|
|
265
|
+
if (response.status === 202) {
|
|
266
|
+
return [];
|
|
267
|
+
}
|
|
268
|
+
if (!response.ok) {
|
|
269
|
+
mcpLog("warning", `Contributors fetch failed ${response.status} for ${owner}/${name}`, "github");
|
|
270
|
+
return [];
|
|
271
|
+
}
|
|
272
|
+
const data = await response.json();
|
|
273
|
+
if (!Array.isArray(data)) return [];
|
|
274
|
+
return data.map((c) => ({
|
|
275
|
+
login: String(c.login ?? "anonymous"),
|
|
276
|
+
contributions: Number(c.contributions ?? 0)
|
|
277
|
+
}));
|
|
278
|
+
} catch (error) {
|
|
279
|
+
mcpLog("warning", `Contributors error for ${owner}/${name}: ${classifyError(error).message}`, "github");
|
|
280
|
+
return [];
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
// --------------------------------------------------------------------------
|
|
284
|
+
// REST helper
|
|
285
|
+
// --------------------------------------------------------------------------
|
|
286
|
+
async restGet(url) {
|
|
287
|
+
this.apiCallCount++;
|
|
288
|
+
for (let attempt = 0; attempt <= GITHUB.RETRY_COUNT; attempt++) {
|
|
289
|
+
try {
|
|
290
|
+
const response = await fetchWithTimeout(url, {
|
|
291
|
+
headers: {
|
|
292
|
+
Authorization: `Bearer ${this.token}`,
|
|
293
|
+
Accept: "application/vnd.github+json",
|
|
294
|
+
"User-Agent": "mcp-researchpowerpack"
|
|
295
|
+
},
|
|
296
|
+
timeoutMs: GITHUB.TIMEOUT_MS
|
|
297
|
+
});
|
|
298
|
+
if (!response.ok) {
|
|
299
|
+
if (RETRYABLE_CODES.has(response.status) && attempt < GITHUB.RETRY_COUNT) {
|
|
300
|
+
const delay = calculateBackoff(attempt);
|
|
301
|
+
await sleep(delay);
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
const text = await response.text().catch(() => "");
|
|
305
|
+
return { error: classifyError({ status: response.status, message: text }) };
|
|
306
|
+
}
|
|
307
|
+
return { data: await response.json() };
|
|
308
|
+
} catch (error) {
|
|
309
|
+
const structured = classifyError(error);
|
|
310
|
+
if (structured.retryable && attempt < GITHUB.RETRY_COUNT) {
|
|
311
|
+
const delay = calculateBackoff(attempt);
|
|
312
|
+
await sleep(delay);
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
return { error: structured };
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return { error: { code: ErrorCode.UNKNOWN_ERROR, message: "REST request failed after retries", retryable: false } };
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
function parseGraphQLResponse(repo) {
|
|
322
|
+
const defaultBranch = repo.defaultBranchRef;
|
|
323
|
+
const target = defaultBranch?.target;
|
|
324
|
+
const history = target?.history;
|
|
325
|
+
const watchers = repo.watchers;
|
|
326
|
+
const openIssues = repo.openIssues;
|
|
327
|
+
const closedIssues = repo.closedIssues;
|
|
328
|
+
const pullRequests = repo.pullRequests;
|
|
329
|
+
const releases = repo.releases;
|
|
330
|
+
const licenseInfo = repo.licenseInfo;
|
|
331
|
+
const primaryLanguage = repo.primaryLanguage;
|
|
332
|
+
const topics = repo.repositoryTopics;
|
|
333
|
+
const codeOfConduct = repo.codeOfConduct;
|
|
334
|
+
const ciCheck = repo.ciCheck;
|
|
335
|
+
const hasCI = Array.isArray(ciCheck?.entries) && ciCheck.entries.length > 0;
|
|
336
|
+
const contributingGuide = repo.contributingGuide;
|
|
337
|
+
const hasContributing = (contributingGuide?.byteSize ?? 0) > 0;
|
|
338
|
+
const issueTemplate = repo.issueTemplate;
|
|
339
|
+
const hasIssueTemplate = Array.isArray(issueTemplate?.entries) && issueTemplate.entries.length > 0;
|
|
340
|
+
const prTemplate = repo.prTemplate;
|
|
341
|
+
const hasPrTemplate = (prTemplate?.byteSize ?? 0) > 0;
|
|
342
|
+
return {
|
|
343
|
+
stars: Number(repo.stargazerCount ?? 0),
|
|
344
|
+
forks: Number(repo.forkCount ?? 0),
|
|
345
|
+
watchers: watchers?.totalCount ?? 0,
|
|
346
|
+
openIssues: openIssues?.totalCount ?? 0,
|
|
347
|
+
closedIssues: closedIssues?.totalCount ?? 0,
|
|
348
|
+
totalCommits: history?.totalCount ?? 0,
|
|
349
|
+
totalReleases: releases?.totalCount ?? 0,
|
|
350
|
+
totalPRs: pullRequests?.totalCount ?? 0,
|
|
351
|
+
sizeKb: Number(repo.diskUsage ?? 0),
|
|
352
|
+
language: primaryLanguage?.name ?? null,
|
|
353
|
+
license: licenseInfo?.spdxId ?? null,
|
|
354
|
+
archived: Boolean(repo.isArchived),
|
|
355
|
+
createdAt: String(repo.createdAt ?? ""),
|
|
356
|
+
pushedAt: String(repo.pushedAt ?? ""),
|
|
357
|
+
description: repo.description ? String(repo.description) : null,
|
|
358
|
+
homepage: repo.homepageUrl ? String(repo.homepageUrl) : null,
|
|
359
|
+
hasCI,
|
|
360
|
+
hasContributing,
|
|
361
|
+
hasIssueTemplate,
|
|
362
|
+
hasPrTemplate,
|
|
363
|
+
hasCodeOfConduct: (codeOfConduct?.name ?? "").length > 0,
|
|
364
|
+
hasTopics: (topics?.nodes?.length ?? 0) > 0
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
export {
|
|
368
|
+
GitHubClient
|
|
369
|
+
};
|
|
370
|
+
//# sourceMappingURL=github.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/clients/github.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * GitHub API Client\n * GraphQL + REST client for fetching repository data for quality scoring.\n * Implements robust error handling that NEVER crashes.\n */\n\nimport { GITHUB, parseEnv } from '../config/index.js';\nimport {\n classifyError,\n fetchWithTimeout,\n sleep,\n ErrorCode,\n type StructuredError,\n} from '../utils/errors.js';\nimport { calculateBackoff } from '../utils/retry.js';\nimport { pMapSettled } from '../utils/concurrency.js';\nimport { mcpLog } from '../utils/logger.js';\n\n// ============================================================================\n// Data Interfaces\n// ============================================================================\n\nexport interface GitHubSearchItem {\n readonly fullName: string;\n readonly owner: string;\n readonly name: string;\n readonly description: string | null;\n readonly stars: number;\n readonly forks: number;\n readonly language: string | null;\n readonly pushedAt: string;\n readonly url: string;\n}\n\nexport interface ParticipationData {\n readonly all: readonly number[];\n readonly owner: readonly number[];\n}\n\nexport interface ContributorEntry {\n readonly login: string;\n readonly contributions: number;\n}\n\nexport interface RepoGraphQLData {\n readonly stars: number;\n readonly forks: number;\n readonly watchers: number;\n readonly openIssues: number;\n readonly closedIssues: number;\n readonly totalCommits: number;\n readonly totalReleases: number;\n readonly totalPRs: number;\n readonly sizeKb: number;\n readonly language: string | null;\n readonly license: string | null;\n readonly archived: boolean;\n readonly createdAt: string;\n readonly pushedAt: string;\n readonly description: string | null;\n readonly homepage: string | null;\n readonly hasCI: boolean;\n readonly hasContributing: boolean;\n readonly hasIssueTemplate: boolean;\n readonly hasPrTemplate: boolean;\n readonly hasCodeOfConduct: boolean;\n readonly hasTopics: boolean;\n}\n\nexport interface RepoFullData {\n readonly graphql: RepoGraphQLData;\n readonly participation: ParticipationData;\n readonly contributors: readonly ContributorEntry[];\n}\n\n// ============================================================================\n// GraphQL Query\n// ============================================================================\n\nconst REPO_DETAILS_QUERY = `\nquery RepoDetails($owner: String!, $name: String!) {\n repository(owner: $owner, name: $name) {\n stargazerCount\n forkCount\n watchers { totalCount }\n openIssues: issues(states: OPEN) { totalCount }\n closedIssues: issues(states: CLOSED) { totalCount }\n pullRequests { totalCount }\n releases { totalCount }\n defaultBranchRef {\n target {\n ... on Commit {\n history { totalCount }\n }\n }\n }\n licenseInfo { spdxId }\n primaryLanguage { name }\n repositoryTopics(first: 5) {\n nodes { topic { name } }\n }\n isArchived\n pushedAt\n createdAt\n diskUsage\n description\n homepageUrl\n codeOfConduct { name }\n ciCheck: object(expression: \"HEAD:.github/workflows\") {\n ... on Tree { entries { name } }\n }\n contributingGuide: object(expression: \"HEAD:CONTRIBUTING.md\") {\n ... on Blob { byteSize }\n }\n issueTemplate: object(expression: \"HEAD:.github/ISSUE_TEMPLATE\") {\n ... on Tree { entries { name } }\n }\n prTemplate: object(expression: \"HEAD:.github/PULL_REQUEST_TEMPLATE.md\") {\n ... on Blob { byteSize }\n }\n }\n}\n`;\n\n// ============================================================================\n// Constants\n// ============================================================================\n\nconst RETRYABLE_CODES = new Set([429, 500, 502, 503, 504]);\n\n/**\n * Try to get the GitHub token from `gh auth token` (GitHub CLI).\n * Returns empty string on failure \u2014 never throws.\n */\nfunction getGhCliToken(): string {\n try {\n const { execSync } = require('node:child_process') as typeof import('node:child_process');\n const token = execSync('gh auth token', {\n timeout: 3000,\n stdio: ['ignore', 'pipe', 'ignore'],\n encoding: 'utf-8',\n }).trim();\n if (token.length > 0) {\n mcpLog('info', 'Using GitHub token from `gh auth token`', 'github');\n }\n return token;\n } catch {\n return '';\n }\n}\n\n// ============================================================================\n// Client\n// ============================================================================\n\nexport class GitHubClient {\n private token: string;\n private apiCallCount = 0;\n\n constructor(token?: string) {\n const env = parseEnv();\n this.token = token || env.GITHUB_TOKEN || getGhCliToken();\n\n if (!this.token) {\n throw new Error(\n 'GitHub capability is not configured. Set GITHUB_TOKEN or log in with `gh auth login`.',\n );\n }\n }\n\n get apiCalls(): number {\n return this.apiCallCount;\n }\n\n // --------------------------------------------------------------------------\n // Search\n // --------------------------------------------------------------------------\n\n async searchRepos(\n query: string,\n sort: string = 'stars',\n perPage: number = 20,\n ): Promise<{ items: GitHubSearchItem[]; totalCount: number; error?: StructuredError }> {\n const url = `${GITHUB.REST_BASE_URL}/search/repositories?q=${encodeURIComponent(query)}&sort=${sort}&order=desc&per_page=${perPage}`;\n\n try {\n const response = await this.restGet(url);\n if ('error' in response) {\n return { items: [], totalCount: 0, error: response.error };\n }\n\n const data = response.data as {\n total_count?: number;\n items?: Array<Record<string, unknown>>;\n };\n\n const items: GitHubSearchItem[] = (data.items ?? []).map((item) => ({\n fullName: String(item.full_name ?? ''),\n owner: (item.owner as Record<string, unknown>)?.login\n ? String((item.owner as Record<string, unknown>).login)\n : '',\n name: String(item.name ?? ''),\n description: item.description ? String(item.description) : null,\n stars: Number(item.stargazers_count ?? 0),\n forks: Number(item.forks_count ?? 0),\n language: item.language ? String(item.language) : null,\n pushedAt: String(item.pushed_at ?? ''),\n url: String(item.html_url ?? ''),\n }));\n\n return { items, totalCount: Number(data.total_count ?? 0) };\n } catch (error) {\n return { items: [], totalCount: 0, error: classifyError(error) };\n }\n }\n\n // --------------------------------------------------------------------------\n // Fetch Full Repo Details (GraphQL + 2 REST)\n // --------------------------------------------------------------------------\n\n async fetchRepoDetails(\n owner: string,\n name: string,\n ): Promise<{ data?: RepoFullData; error?: StructuredError }> {\n // 1. GraphQL for bulk metadata\n const graphqlResult = await this.graphqlQuery(REPO_DETAILS_QUERY, { owner, name });\n if (graphqlResult.error || !graphqlResult.data) {\n return { error: graphqlResult.error ?? { code: ErrorCode.UNKNOWN_ERROR, message: 'GraphQL returned no data', retryable: false } };\n }\n\n const repo = (graphqlResult.data as { repository?: Record<string, unknown> })?.repository;\n if (!repo) {\n return { error: { code: ErrorCode.NOT_FOUND, message: `Repository ${owner}/${name} not found`, retryable: false } };\n }\n\n const graphql = parseGraphQLResponse(repo);\n\n // 2. REST: stats/participation (handles 202 retry)\n const participation = await this.fetchParticipation(owner, name);\n\n // 3. REST: contributors\n const contributors = await this.fetchContributors(owner, name);\n\n return {\n data: {\n graphql,\n participation,\n contributors,\n },\n };\n }\n\n // --------------------------------------------------------------------------\n // Batch fetch with concurrency\n // --------------------------------------------------------------------------\n\n async fetchMultipleRepoDetails(\n repos: Array<{ owner: string; name: string }>,\n ): Promise<Array<{ owner: string; name: string; data?: RepoFullData; error?: StructuredError }>> {\n const results = await pMapSettled(\n repos,\n async (repo) => {\n const result = await this.fetchRepoDetails(repo.owner, repo.name);\n return { ...repo, ...result };\n },\n GITHUB.MAX_CONCURRENT_REPOS,\n );\n\n return results.map((settled, i) => {\n const repo = repos[i]!;\n if (settled.status === 'fulfilled') {\n return settled.value;\n }\n return {\n owner: repo.owner,\n name: repo.name,\n error: classifyError(settled.reason),\n };\n });\n }\n\n // --------------------------------------------------------------------------\n // GraphQL\n // --------------------------------------------------------------------------\n\n private async graphqlQuery(\n query: string,\n variables: Record<string, unknown>,\n ): Promise<{ data?: unknown; error?: StructuredError }> {\n this.apiCallCount++;\n\n for (let attempt = 0; attempt <= GITHUB.RETRY_COUNT; attempt++) {\n try {\n if (attempt > 0) {\n mcpLog('warning', `GraphQL retry ${attempt}/${GITHUB.RETRY_COUNT}`, 'github');\n }\n\n const response = await fetchWithTimeout(GITHUB.GRAPHQL_URL, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${this.token}`,\n 'Content-Type': 'application/json',\n 'User-Agent': 'mcp-researchpowerpack',\n },\n body: JSON.stringify({ query, variables }),\n timeoutMs: GITHUB.TIMEOUT_MS,\n });\n\n if (!response.ok) {\n if (RETRYABLE_CODES.has(response.status) && attempt < GITHUB.RETRY_COUNT) {\n const delay = calculateBackoff(attempt);\n await sleep(delay);\n continue;\n }\n const text = await response.text().catch(() => '');\n return { error: classifyError({ status: response.status, message: text }) };\n }\n\n const json = (await response.json()) as { data?: unknown; errors?: Array<{ message: string }> };\n\n if (json.errors && json.errors.length > 0) {\n const msg = json.errors.map((e) => e.message).join('; ');\n // GraphQL can return partial data with errors \u2014 use data if available\n if (json.data) {\n mcpLog('warning', `GraphQL partial error: ${msg}`, 'github');\n return { data: json.data };\n }\n return { error: { code: ErrorCode.INTERNAL_ERROR, message: msg, retryable: false } };\n }\n\n return { data: json.data };\n } catch (error) {\n const structured = classifyError(error);\n if (structured.retryable && attempt < GITHUB.RETRY_COUNT) {\n const delay = calculateBackoff(attempt);\n await sleep(delay);\n continue;\n }\n return { error: structured };\n }\n }\n\n return { error: { code: ErrorCode.UNKNOWN_ERROR, message: 'GraphQL failed after retries', retryable: false } };\n }\n\n // --------------------------------------------------------------------------\n // REST: Participation (handles 202 lazy computation)\n // --------------------------------------------------------------------------\n\n private async fetchParticipation(\n owner: string,\n name: string,\n ): Promise<ParticipationData> {\n const url = `${GITHUB.REST_BASE_URL}/repos/${owner}/${name}/stats/participation`;\n const emptyResult: ParticipationData = { all: [], owner: [] };\n\n for (let attempt = 0; attempt < GITHUB.PARTICIPATION_MAX_RETRIES; attempt++) {\n try {\n this.apiCallCount++;\n const response = await fetchWithTimeout(url, {\n headers: {\n Authorization: `Bearer ${this.token}`,\n 'User-Agent': 'mcp-researchpowerpack',\n },\n timeoutMs: GITHUB.TIMEOUT_MS,\n });\n\n if (response.status === 202) {\n // GitHub is computing stats \u2014 retry after delay\n mcpLog('info', `Participation stats computing for ${owner}/${name}, retry ${attempt + 1}`, 'github');\n await sleep(GITHUB.PARTICIPATION_RETRY_DELAY_MS);\n continue;\n }\n\n if (!response.ok) {\n mcpLog('warning', `Participation fetch failed ${response.status} for ${owner}/${name}`, 'github');\n return emptyResult;\n }\n\n const data = (await response.json()) as { all?: number[]; owner?: number[] };\n if (!Array.isArray(data.all)) return emptyResult;\n\n return {\n all: data.all ?? [],\n owner: data.owner ?? [],\n };\n } catch (error) {\n mcpLog('warning', `Participation error for ${owner}/${name}: ${classifyError(error).message}`, 'github');\n return emptyResult;\n }\n }\n\n mcpLog('warning', `Participation still 202 after retries for ${owner}/${name}`, 'github');\n return emptyResult;\n }\n\n // --------------------------------------------------------------------------\n // REST: Contributors\n // --------------------------------------------------------------------------\n\n private async fetchContributors(\n owner: string,\n name: string,\n ): Promise<readonly ContributorEntry[]> {\n const url = `${GITHUB.REST_BASE_URL}/repos/${owner}/${name}/contributors?per_page=100&anon=true`;\n\n try {\n this.apiCallCount++;\n const response = await fetchWithTimeout(url, {\n headers: {\n Authorization: `Bearer ${this.token}`,\n 'User-Agent': 'mcp-researchpowerpack',\n },\n timeoutMs: GITHUB.TIMEOUT_MS,\n });\n\n if (response.status === 202) {\n // Stats being computed \u2014 return empty, will degrade gracefully\n return [];\n }\n\n if (!response.ok) {\n mcpLog('warning', `Contributors fetch failed ${response.status} for ${owner}/${name}`, 'github');\n return [];\n }\n\n const data = (await response.json()) as Array<{ login?: string; contributions?: number }>;\n if (!Array.isArray(data)) return [];\n\n return data.map((c) => ({\n login: String(c.login ?? 'anonymous'),\n contributions: Number(c.contributions ?? 0),\n }));\n } catch (error) {\n mcpLog('warning', `Contributors error for ${owner}/${name}: ${classifyError(error).message}`, 'github');\n return [];\n }\n }\n\n // --------------------------------------------------------------------------\n // REST helper\n // --------------------------------------------------------------------------\n\n private async restGet(\n url: string,\n ): Promise<{ data: unknown } | { error: StructuredError }> {\n this.apiCallCount++;\n\n for (let attempt = 0; attempt <= GITHUB.RETRY_COUNT; attempt++) {\n try {\n const response = await fetchWithTimeout(url, {\n headers: {\n Authorization: `Bearer ${this.token}`,\n Accept: 'application/vnd.github+json',\n 'User-Agent': 'mcp-researchpowerpack',\n },\n timeoutMs: GITHUB.TIMEOUT_MS,\n });\n\n if (!response.ok) {\n if (RETRYABLE_CODES.has(response.status) && attempt < GITHUB.RETRY_COUNT) {\n const delay = calculateBackoff(attempt);\n await sleep(delay);\n continue;\n }\n const text = await response.text().catch(() => '');\n return { error: classifyError({ status: response.status, message: text }) };\n }\n\n return { data: await response.json() };\n } catch (error) {\n const structured = classifyError(error);\n if (structured.retryable && attempt < GITHUB.RETRY_COUNT) {\n const delay = calculateBackoff(attempt);\n await sleep(delay);\n continue;\n }\n return { error: structured };\n }\n }\n\n return { error: { code: ErrorCode.UNKNOWN_ERROR, message: 'REST request failed after retries', retryable: false } };\n }\n}\n\n// ============================================================================\n// GraphQL Response Parser\n// ============================================================================\n\nfunction parseGraphQLResponse(repo: Record<string, unknown>): RepoGraphQLData {\n const defaultBranch = repo.defaultBranchRef as Record<string, unknown> | null;\n const target = defaultBranch?.target as Record<string, unknown> | null;\n const history = target?.history as { totalCount?: number } | null;\n\n const watchers = repo.watchers as { totalCount?: number } | null;\n const openIssues = repo.openIssues as { totalCount?: number } | null;\n const closedIssues = repo.closedIssues as { totalCount?: number } | null;\n const pullRequests = repo.pullRequests as { totalCount?: number } | null;\n const releases = repo.releases as { totalCount?: number } | null;\n const licenseInfo = repo.licenseInfo as { spdxId?: string } | null;\n const primaryLanguage = repo.primaryLanguage as { name?: string } | null;\n const topics = repo.repositoryTopics as { nodes?: Array<unknown> } | null;\n const codeOfConduct = repo.codeOfConduct as { name?: string } | null;\n\n // CI detection: check if .github/workflows directory has entries\n const ciCheck = repo.ciCheck as { entries?: Array<unknown> } | null;\n const hasCI = Array.isArray(ciCheck?.entries) && ciCheck.entries.length > 0;\n\n // Contributing guide\n const contributingGuide = repo.contributingGuide as { byteSize?: number } | null;\n const hasContributing = (contributingGuide?.byteSize ?? 0) > 0;\n\n // Issue template\n const issueTemplate = repo.issueTemplate as { entries?: Array<unknown> } | null;\n const hasIssueTemplate = Array.isArray(issueTemplate?.entries) && issueTemplate.entries.length > 0;\n\n // PR template\n const prTemplate = repo.prTemplate as { byteSize?: number } | null;\n const hasPrTemplate = (prTemplate?.byteSize ?? 0) > 0;\n\n return {\n stars: Number(repo.stargazerCount ?? 0),\n forks: Number(repo.forkCount ?? 0),\n watchers: watchers?.totalCount ?? 0,\n openIssues: openIssues?.totalCount ?? 0,\n closedIssues: closedIssues?.totalCount ?? 0,\n totalCommits: history?.totalCount ?? 0,\n totalReleases: releases?.totalCount ?? 0,\n totalPRs: pullRequests?.totalCount ?? 0,\n sizeKb: Number(repo.diskUsage ?? 0),\n language: primaryLanguage?.name ?? null,\n license: licenseInfo?.spdxId ?? null,\n archived: Boolean(repo.isArchived),\n createdAt: String(repo.createdAt ?? ''),\n pushedAt: String(repo.pushedAt ?? ''),\n description: repo.description ? String(repo.description) : null,\n homepage: repo.homepageUrl ? String(repo.homepageUrl) : null,\n hasCI,\n hasContributing,\n hasIssueTemplate,\n hasPrTemplate,\n hasCodeOfConduct: (codeOfConduct?.name ?? '').length > 0,\n hasTopics: (topics?.nodes?.length ?? 0) > 0,\n };\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reddit OAuth API Client
|
|
3
|
+
* Fetches posts and comments sorted by score (most upvoted first)
|
|
4
|
+
* Implements robust error handling that NEVER crashes
|
|
5
|
+
*/
|
|
6
|
+
interface Post {
|
|
7
|
+
readonly title: string;
|
|
8
|
+
readonly author: string;
|
|
9
|
+
readonly subreddit: string;
|
|
10
|
+
readonly body: string;
|
|
11
|
+
readonly score: number;
|
|
12
|
+
readonly commentCount: number;
|
|
13
|
+
readonly url: string;
|
|
14
|
+
readonly created: Date;
|
|
15
|
+
readonly flair?: string;
|
|
16
|
+
readonly isNsfw: boolean;
|
|
17
|
+
readonly isPinned: boolean;
|
|
18
|
+
}
|
|
19
|
+
export interface Comment {
|
|
20
|
+
readonly author: string;
|
|
21
|
+
readonly body: string;
|
|
22
|
+
readonly score: number;
|
|
23
|
+
readonly depth: number;
|
|
24
|
+
readonly isOP: boolean;
|
|
25
|
+
}
|
|
26
|
+
export interface PostResult {
|
|
27
|
+
readonly post: Post;
|
|
28
|
+
readonly comments: Comment[];
|
|
29
|
+
readonly actualComments: number;
|
|
30
|
+
}
|
|
31
|
+
interface BatchPostResult {
|
|
32
|
+
readonly results: Map<string, PostResult | Error>;
|
|
33
|
+
readonly batchesProcessed: number;
|
|
34
|
+
readonly totalPosts: number;
|
|
35
|
+
readonly rateLimitHits: number;
|
|
36
|
+
}
|
|
37
|
+
export declare class RedditClient {
|
|
38
|
+
private clientId;
|
|
39
|
+
private clientSecret;
|
|
40
|
+
private userAgent;
|
|
41
|
+
constructor(clientId: string, clientSecret: string);
|
|
42
|
+
/**
|
|
43
|
+
* Authenticate with Reddit API with retry logic
|
|
44
|
+
* Uses module-level token cache and promise deduplication to prevent
|
|
45
|
+
* concurrent auth calls from firing multiple token requests
|
|
46
|
+
* Returns null on failure instead of throwing
|
|
47
|
+
*/
|
|
48
|
+
private auth;
|
|
49
|
+
private performAuth;
|
|
50
|
+
private parseUrl;
|
|
51
|
+
/**
|
|
52
|
+
* Get a single Reddit post with comments
|
|
53
|
+
* Returns PostResult or throws Error (for use with Promise.allSettled)
|
|
54
|
+
*/
|
|
55
|
+
getPost(url: string): Promise<PostResult>;
|
|
56
|
+
getPosts(urls: string[]): Promise<Map<string, PostResult | Error>>;
|
|
57
|
+
batchGetPosts(urls: string[], fetchComments?: boolean, onBatchComplete?: (batchNum: number, totalBatches: number, processed: number) => void): Promise<BatchPostResult>;
|
|
58
|
+
}
|
|
59
|
+
export {};
|
|
60
|
+
//# sourceMappingURL=reddit.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reddit.d.ts","sourceRoot":"","sources":["../../../src/clients/reddit.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAuBH,UAAU,IAAI;IACZ,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,OAAO,EAAE,IAAI,CAAC;IACvB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;CAC5B;AAED,MAAM,WAAW,OAAO;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC;IACpB,QAAQ,CAAC,QAAQ,EAAE,OAAO,EAAE,CAAC;IAC7B,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;CACjC;AAED,UAAU,eAAe;IACvB,QAAQ,CAAC,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,GAAG,KAAK,CAAC,CAAC;IAClD,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;CAChC;AAoND,qBAAa,YAAY;IAGX,OAAO,CAAC,QAAQ;IAAU,OAAO,CAAC,YAAY;IAF1D,OAAO,CAAC,SAAS,CAA6D;gBAE1D,QAAQ,EAAE,MAAM,EAAU,YAAY,EAAE,MAAM;IAElE;;;;;OAKG;YACW,IAAI;YAmBJ,WAAW;IAmEzB,OAAO,CAAC,QAAQ;IAKhB;;;OAGG;IACG,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IAkDzC,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,UAAU,GAAG,KAAK,CAAC,CAAC;IAYlE,aAAa,CACjB,IAAI,EAAE,MAAM,EAAE,EACd,aAAa,UAAO,EACpB,eAAe,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,KAAK,IAAI,GACpF,OAAO,CAAC,eAAe,CAAC;CAkC5B"}
|