mcp-researchpowerpack 3.6.9
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 +635 -0
- package/dist/clients/reddit.d.ts +74 -0
- package/dist/clients/reddit.d.ts.map +1 -0
- package/dist/clients/reddit.js +305 -0
- package/dist/clients/reddit.js.map +1 -0
- package/dist/clients/research.d.ts +67 -0
- package/dist/clients/research.d.ts.map +1 -0
- package/dist/clients/research.js +252 -0
- package/dist/clients/research.js.map +1 -0
- package/dist/clients/scraper.d.ts +71 -0
- package/dist/clients/scraper.d.ts.map +1 -0
- package/dist/clients/scraper.js +321 -0
- package/dist/clients/scraper.js.map +1 -0
- package/dist/clients/search.d.ts +62 -0
- package/dist/clients/search.d.ts.map +1 -0
- package/dist/clients/search.js +219 -0
- package/dist/clients/search.js.map +1 -0
- package/dist/config/index.d.ts +62 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +142 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/loader.d.ts +40 -0
- package/dist/config/loader.d.ts.map +1 -0
- package/dist/config/loader.js +305 -0
- package/dist/config/loader.js.map +1 -0
- package/dist/config/types.d.ts +81 -0
- package/dist/config/types.d.ts.map +1 -0
- package/dist/config/types.js +6 -0
- package/dist/config/types.js.map +1 -0
- package/dist/config/yaml/tools.yaml +130 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +271 -0
- package/dist/index.js.map +1 -0
- package/dist/schemas/deep-research.d.ts +64 -0
- package/dist/schemas/deep-research.d.ts.map +1 -0
- package/dist/schemas/deep-research.js +224 -0
- package/dist/schemas/deep-research.js.map +1 -0
- package/dist/schemas/scrape-links.d.ts +32 -0
- package/dist/schemas/scrape-links.d.ts.map +1 -0
- package/dist/schemas/scrape-links.js +34 -0
- package/dist/schemas/scrape-links.js.map +1 -0
- package/dist/schemas/web-search.d.ts +22 -0
- package/dist/schemas/web-search.d.ts.map +1 -0
- package/dist/schemas/web-search.js +21 -0
- package/dist/schemas/web-search.js.map +1 -0
- package/dist/services/file-attachment.d.ts +30 -0
- package/dist/services/file-attachment.d.ts.map +1 -0
- package/dist/services/file-attachment.js +199 -0
- package/dist/services/file-attachment.js.map +1 -0
- package/dist/services/llm-processor.d.ts +27 -0
- package/dist/services/llm-processor.d.ts.map +1 -0
- package/dist/services/llm-processor.js +179 -0
- package/dist/services/llm-processor.js.map +1 -0
- package/dist/services/markdown-cleaner.d.ts +8 -0
- package/dist/services/markdown-cleaner.d.ts.map +1 -0
- package/dist/services/markdown-cleaner.js +44 -0
- package/dist/services/markdown-cleaner.js.map +1 -0
- package/dist/tools/definitions.d.ts +16 -0
- package/dist/tools/definitions.d.ts.map +1 -0
- package/dist/tools/definitions.js +17 -0
- package/dist/tools/definitions.js.map +1 -0
- package/dist/tools/reddit.d.ts +14 -0
- package/dist/tools/reddit.d.ts.map +1 -0
- package/dist/tools/reddit.js +213 -0
- package/dist/tools/reddit.js.map +1 -0
- package/dist/tools/registry.d.ts +71 -0
- package/dist/tools/registry.d.ts.map +1 -0
- package/dist/tools/registry.js +242 -0
- package/dist/tools/registry.js.map +1 -0
- package/dist/tools/research.d.ts +14 -0
- package/dist/tools/research.d.ts.map +1 -0
- package/dist/tools/research.js +194 -0
- package/dist/tools/research.js.map +1 -0
- package/dist/tools/scrape.d.ts +14 -0
- package/dist/tools/scrape.d.ts.map +1 -0
- package/dist/tools/scrape.js +201 -0
- package/dist/tools/scrape.js.map +1 -0
- package/dist/tools/search.d.ts +10 -0
- package/dist/tools/search.d.ts.map +1 -0
- package/dist/tools/search.js +137 -0
- package/dist/tools/search.js.map +1 -0
- package/dist/tools/utils.d.ts +105 -0
- package/dist/tools/utils.d.ts.map +1 -0
- package/dist/tools/utils.js +159 -0
- package/dist/tools/utils.js.map +1 -0
- package/dist/utils/concurrency.d.ts +29 -0
- package/dist/utils/concurrency.d.ts.map +1 -0
- package/dist/utils/concurrency.js +73 -0
- package/dist/utils/concurrency.js.map +1 -0
- package/dist/utils/errors.d.ts +77 -0
- package/dist/utils/errors.d.ts.map +1 -0
- package/dist/utils/errors.js +335 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/logger.d.ts +39 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +57 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/markdown-formatter.d.ts +5 -0
- package/dist/utils/markdown-formatter.d.ts.map +1 -0
- package/dist/utils/markdown-formatter.js +15 -0
- package/dist/utils/markdown-formatter.js.map +1 -0
- package/dist/utils/response.d.ts +88 -0
- package/dist/utils/response.d.ts.map +1 -0
- package/dist/utils/response.js +151 -0
- package/dist/utils/response.js.map +1 -0
- package/dist/utils/url-aggregator.d.ts +90 -0
- package/dist/utils/url-aggregator.d.ts.map +1 -0
- package/dist/utils/url-aggregator.js +502 -0
- package/dist/utils/url-aggregator.js.map +1 -0
- package/dist/version.d.ts +30 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +60 -0
- package/dist/version.js.map +1 -0
- package/dist/worker.d.ts +17 -0
- package/dist/worker.d.ts.map +1 -0
- package/dist/worker.js +53 -0
- package/dist/worker.js.map +1 -0
- package/package.json +73 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reddit.d.ts","sourceRoot":"","sources":["../../src/clients/reddit.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAcH,UAAU,IAAI;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,MAAM,CAAC;IACrB,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,IAAI,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,OAAO,CAAC;IAChB,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,OAAO;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,OAAO,CAAC;CACf;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,IAAI,CAAC;IACX,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,UAAU,eAAe;IACvB,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,GAAG,KAAK,CAAC,CAAC;IACzC,gBAAgB,EAAE,MAAM,CAAC;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;IACtB,iBAAiB,EAAE,iBAAiB,CAAC;CACtC;AAED,UAAU,iBAAiB;IACzB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,OAAO,CAAC;CACxB;AAED,wBAAgB,0BAA0B,CAAC,SAAS,EAAE,MAAM,GAAG,iBAAiB,CAK/E;AAcD,qBAAa,YAAY;IAKX,OAAO,CAAC,QAAQ;IAAU,OAAO,CAAC,YAAY;IAF1D,OAAO,CAAC,SAAS,CAA6D;gBAE1D,QAAQ,EAAE,MAAM,EAAU,YAAY,EAAE,MAAM;IAElE;;;;;OAKG;YACW,IAAI;IAqBlB;;OAEG;YACW,WAAW;IAuEzB,OAAO,CAAC,QAAQ;IAKhB;;;OAGG;IACG,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,WAAW,SAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IA2GlE,OAAO,CAAC,UAAU;IAOlB,OAAO,CAAC,eAAe;IA4BjB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,WAAW,SAAM,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,UAAU,GAAG,KAAK,CAAC,CAAC;IAarF,aAAa,CACjB,IAAI,EAAE,MAAM,EAAE,EACd,mBAAmB,CAAC,EAAE,MAAM,EAC5B,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;CAqD5B"}
|
|
@@ -0,0 +1,305 @@
|
|
|
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
|
+
import { REDDIT } from '../config/index.js';
|
|
7
|
+
import { USER_AGENT_VERSION } from '../version.js';
|
|
8
|
+
import { classifyError, fetchWithTimeout, sleep, ErrorCode, } from '../utils/errors.js';
|
|
9
|
+
import { pMap, pMapSettled } from '../utils/concurrency.js';
|
|
10
|
+
import { mcpLog } from '../utils/logger.js';
|
|
11
|
+
export function calculateCommentAllocation(postCount) {
|
|
12
|
+
const totalBudget = REDDIT.MAX_COMMENT_BUDGET;
|
|
13
|
+
const perPostBase = Math.floor(totalBudget / postCount);
|
|
14
|
+
const perPostCapped = Math.min(perPostBase, REDDIT.MAX_COMMENTS_PER_POST);
|
|
15
|
+
return { totalBudget, perPostBase, perPostCapped, redistributed: false };
|
|
16
|
+
}
|
|
17
|
+
// ============================================================================
|
|
18
|
+
// Module-Level Token Cache (shared across all RedditClient instances)
|
|
19
|
+
// ============================================================================
|
|
20
|
+
let cachedToken = null;
|
|
21
|
+
let cachedTokenExpiry = 0;
|
|
22
|
+
// Token cache logging only when DEBUG env is set
|
|
23
|
+
const DEBUG_TOKEN_CACHE = process.env.DEBUG_REDDIT === 'true';
|
|
24
|
+
// Pending auth promise for deduplicating concurrent auth calls
|
|
25
|
+
let pendingAuthPromise = null;
|
|
26
|
+
export class RedditClient {
|
|
27
|
+
clientId;
|
|
28
|
+
clientSecret;
|
|
29
|
+
// Instance-level references (now point to module cache)
|
|
30
|
+
// User agent uses centralized version from package.json - auto-synced!
|
|
31
|
+
userAgent = `script:${USER_AGENT_VERSION} (by /u/research-powerpack)`;
|
|
32
|
+
constructor(clientId, clientSecret) {
|
|
33
|
+
this.clientId = clientId;
|
|
34
|
+
this.clientSecret = clientSecret;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Authenticate with Reddit API with retry logic
|
|
38
|
+
* Uses module-level token cache and promise deduplication to prevent
|
|
39
|
+
* concurrent auth calls from firing multiple token requests
|
|
40
|
+
* Returns null on failure instead of throwing
|
|
41
|
+
*/
|
|
42
|
+
async auth() {
|
|
43
|
+
// Return module-cached token if still valid (with 60s buffer)
|
|
44
|
+
if (cachedToken && Date.now() < cachedTokenExpiry - 60000) {
|
|
45
|
+
if (DEBUG_TOKEN_CACHE)
|
|
46
|
+
console.error('[RedditClient] Token cache HIT');
|
|
47
|
+
return cachedToken;
|
|
48
|
+
}
|
|
49
|
+
// Deduplicate concurrent auth calls - if one is already in flight, await it
|
|
50
|
+
if (pendingAuthPromise) {
|
|
51
|
+
if (DEBUG_TOKEN_CACHE)
|
|
52
|
+
console.error('[RedditClient] Auth already in flight, awaiting...');
|
|
53
|
+
return pendingAuthPromise;
|
|
54
|
+
}
|
|
55
|
+
pendingAuthPromise = this.performAuth();
|
|
56
|
+
try {
|
|
57
|
+
return await pendingAuthPromise;
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
pendingAuthPromise = null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Internal auth implementation - called only once per cache miss
|
|
65
|
+
*/
|
|
66
|
+
async performAuth() {
|
|
67
|
+
if (DEBUG_TOKEN_CACHE)
|
|
68
|
+
console.error('[RedditClient] Token cache MISS - authenticating');
|
|
69
|
+
const credentials = Buffer.from(`${this.clientId}:${this.clientSecret}`).toString('base64');
|
|
70
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
71
|
+
try {
|
|
72
|
+
const res = await fetchWithTimeout('https://www.reddit.com/api/v1/access_token', {
|
|
73
|
+
method: 'POST',
|
|
74
|
+
headers: {
|
|
75
|
+
'Authorization': `Basic ${credentials}`,
|
|
76
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
77
|
+
'User-Agent': this.userAgent,
|
|
78
|
+
},
|
|
79
|
+
body: 'grant_type=client_credentials',
|
|
80
|
+
timeoutMs: 15000,
|
|
81
|
+
});
|
|
82
|
+
if (!res.ok) {
|
|
83
|
+
const text = await res.text().catch(() => '');
|
|
84
|
+
mcpLog('error', `Auth failed (${res.status}): ${text}`, 'reddit');
|
|
85
|
+
// 401/403 are not retryable - invalidate cache
|
|
86
|
+
if (res.status === 401 || res.status === 403) {
|
|
87
|
+
cachedToken = null;
|
|
88
|
+
cachedTokenExpiry = 0;
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
// Retry on server errors
|
|
92
|
+
if (res.status >= 500 && attempt < 2) {
|
|
93
|
+
await sleep(REDDIT.RETRY_DELAYS[attempt] || 2000);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
const data = await res.json();
|
|
99
|
+
if (!data.access_token) {
|
|
100
|
+
mcpLog('error', 'Auth response missing access_token', 'reddit');
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
// Update module-level cache (shared across all instances)
|
|
104
|
+
cachedToken = data.access_token;
|
|
105
|
+
cachedTokenExpiry = Date.now() + (data.expires_in || 3600) * 1000;
|
|
106
|
+
return cachedToken;
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
const err = classifyError(error);
|
|
110
|
+
mcpLog('error', `Auth error (attempt ${attempt + 1}): ${err.message}`, 'reddit');
|
|
111
|
+
// Invalidate cache on auth errors
|
|
112
|
+
if (err.code === ErrorCode.AUTH_ERROR) {
|
|
113
|
+
cachedToken = null;
|
|
114
|
+
cachedTokenExpiry = 0;
|
|
115
|
+
}
|
|
116
|
+
if (attempt < 2 && err.retryable) {
|
|
117
|
+
await sleep(REDDIT.RETRY_DELAYS[attempt] || 2000);
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
parseUrl(url) {
|
|
126
|
+
const m = url.match(/reddit\.com\/r\/([^\/]+)\/comments\/([a-z0-9]+)/i);
|
|
127
|
+
return m ? { sub: m[1], id: m[2] } : null;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Get a single Reddit post with comments
|
|
131
|
+
* Returns PostResult or throws Error (for use with Promise.allSettled)
|
|
132
|
+
*/
|
|
133
|
+
async getPost(url, maxComments = 100) {
|
|
134
|
+
const parsed = this.parseUrl(url);
|
|
135
|
+
if (!parsed) {
|
|
136
|
+
throw new Error(`Invalid Reddit URL format: ${url}`);
|
|
137
|
+
}
|
|
138
|
+
// Auth - returns null on failure
|
|
139
|
+
const token = await this.auth();
|
|
140
|
+
if (!token) {
|
|
141
|
+
throw new Error('Reddit authentication failed - check credentials');
|
|
142
|
+
}
|
|
143
|
+
const limit = Math.min(maxComments, 500);
|
|
144
|
+
let lastError = null;
|
|
145
|
+
for (let attempt = 0; attempt < REDDIT.RETRY_COUNT; attempt++) {
|
|
146
|
+
try {
|
|
147
|
+
const apiUrl = `https://oauth.reddit.com/r/${parsed.sub}/comments/${parsed.id}?sort=top&limit=${limit}&depth=10&raw_json=1`;
|
|
148
|
+
const res = await fetchWithTimeout(apiUrl, {
|
|
149
|
+
headers: {
|
|
150
|
+
'Authorization': `Bearer ${token}`,
|
|
151
|
+
'User-Agent': this.userAgent,
|
|
152
|
+
},
|
|
153
|
+
timeoutMs: 30000,
|
|
154
|
+
});
|
|
155
|
+
// Rate limited - always retry with backoff
|
|
156
|
+
if (res.status === 429) {
|
|
157
|
+
const delay = REDDIT.RETRY_DELAYS[attempt] || 32000;
|
|
158
|
+
mcpLog('warning', `Rate limited. Retry ${attempt + 1}/${REDDIT.RETRY_COUNT} after ${delay}ms`, 'reddit');
|
|
159
|
+
await sleep(delay);
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
// 404 - Post doesn't exist
|
|
163
|
+
if (res.status === 404) {
|
|
164
|
+
throw new Error(`Post not found: ${url}`);
|
|
165
|
+
}
|
|
166
|
+
// Other errors
|
|
167
|
+
if (!res.ok) {
|
|
168
|
+
lastError = classifyError({ status: res.status });
|
|
169
|
+
if (lastError.retryable && attempt < REDDIT.RETRY_COUNT - 1) {
|
|
170
|
+
const delay = REDDIT.RETRY_DELAYS[attempt] || 2000;
|
|
171
|
+
mcpLog('warning', `API error ${res.status}. Retry ${attempt + 1}/${REDDIT.RETRY_COUNT}`, 'reddit');
|
|
172
|
+
await sleep(delay);
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
throw new Error(`Reddit API error: ${res.status}`);
|
|
176
|
+
}
|
|
177
|
+
// Parse response safely
|
|
178
|
+
let data;
|
|
179
|
+
try {
|
|
180
|
+
data = await res.json();
|
|
181
|
+
}
|
|
182
|
+
catch (parseError) {
|
|
183
|
+
throw new Error('Failed to parse Reddit API response');
|
|
184
|
+
}
|
|
185
|
+
const [postListing, commentListing] = data;
|
|
186
|
+
const p = postListing?.data?.children?.[0]?.data;
|
|
187
|
+
if (!p) {
|
|
188
|
+
throw new Error(`Post data not found in response: ${url}`);
|
|
189
|
+
}
|
|
190
|
+
const post = {
|
|
191
|
+
title: p.title || 'Untitled',
|
|
192
|
+
author: p.author || '[deleted]',
|
|
193
|
+
subreddit: p.subreddit || parsed.sub,
|
|
194
|
+
body: this.formatBody(p),
|
|
195
|
+
score: p.score || 0,
|
|
196
|
+
commentCount: p.num_comments || 0,
|
|
197
|
+
url: `https://reddit.com${p.permalink || ''}`,
|
|
198
|
+
created: new Date((p.created_utc || 0) * 1000),
|
|
199
|
+
flair: p.link_flair_text || undefined,
|
|
200
|
+
isNsfw: p.over_18 || false,
|
|
201
|
+
isPinned: p.stickied || false,
|
|
202
|
+
};
|
|
203
|
+
const comments = this.extractComments(commentListing?.data?.children || [], maxComments, post.author);
|
|
204
|
+
return { post, comments, allocatedComments: maxComments, actualComments: post.commentCount };
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
lastError = classifyError(error);
|
|
208
|
+
// Don't retry non-retryable errors
|
|
209
|
+
if (!lastError.retryable) {
|
|
210
|
+
throw error instanceof Error ? error : new Error(lastError.message);
|
|
211
|
+
}
|
|
212
|
+
if (attempt < REDDIT.RETRY_COUNT - 1) {
|
|
213
|
+
const delay = REDDIT.RETRY_DELAYS[attempt] || 2000;
|
|
214
|
+
mcpLog('warning', `${lastError.code}: ${lastError.message}. Retry ${attempt + 1}/${REDDIT.RETRY_COUNT}`, 'reddit');
|
|
215
|
+
await sleep(delay);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
// All retries exhausted
|
|
220
|
+
throw new Error(lastError?.message || 'Failed to fetch Reddit post after retries');
|
|
221
|
+
}
|
|
222
|
+
formatBody(p) {
|
|
223
|
+
if (p.selftext?.trim())
|
|
224
|
+
return p.selftext;
|
|
225
|
+
if (p.is_self)
|
|
226
|
+
return '';
|
|
227
|
+
if (p.url)
|
|
228
|
+
return `**Link:** ${p.url}`;
|
|
229
|
+
return '';
|
|
230
|
+
}
|
|
231
|
+
extractComments(children, maxComments, opAuthor) {
|
|
232
|
+
const result = [];
|
|
233
|
+
const extract = (items, depth = 0) => {
|
|
234
|
+
const sorted = [...items].sort((a, b) => (b.data?.score || 0) - (a.data?.score || 0));
|
|
235
|
+
for (const c of sorted) {
|
|
236
|
+
if (result.length >= maxComments)
|
|
237
|
+
return;
|
|
238
|
+
if (c.kind !== 't1' || !c.data?.author || c.data.author === '[deleted]')
|
|
239
|
+
continue;
|
|
240
|
+
result.push({
|
|
241
|
+
author: c.data.author,
|
|
242
|
+
body: c.data.body || '',
|
|
243
|
+
score: c.data.score || 0,
|
|
244
|
+
depth,
|
|
245
|
+
isOP: c.data.author === opAuthor,
|
|
246
|
+
});
|
|
247
|
+
if (c.data.replies?.data?.children && result.length < maxComments) {
|
|
248
|
+
extract(c.data.replies.data.children, depth + 1);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
extract(children);
|
|
253
|
+
return result;
|
|
254
|
+
}
|
|
255
|
+
async getPosts(urls, maxComments = 100) {
|
|
256
|
+
if (urls.length <= REDDIT.BATCH_SIZE) {
|
|
257
|
+
// Limit to 5 concurrent Reddit API calls
|
|
258
|
+
const results = await pMap(urls, u => this.getPost(u, maxComments).catch(e => e), 5);
|
|
259
|
+
return new Map(urls.map((u, i) => [u, results[i]]));
|
|
260
|
+
}
|
|
261
|
+
return (await this.batchGetPosts(urls, maxComments)).results;
|
|
262
|
+
}
|
|
263
|
+
async batchGetPosts(urls, maxCommentsOverride, fetchComments = true, onBatchComplete) {
|
|
264
|
+
const totalBatches = Math.ceil(urls.length / REDDIT.BATCH_SIZE);
|
|
265
|
+
const allResults = new Map();
|
|
266
|
+
let rateLimitHits = 0;
|
|
267
|
+
const allocation = calculateCommentAllocation(urls.length);
|
|
268
|
+
const commentsPerPost = fetchComments ? (maxCommentsOverride || allocation.perPostCapped) : 0;
|
|
269
|
+
mcpLog('info', `Fetching ${urls.length} posts in ${totalBatches} batch(es), ${commentsPerPost} comments/post`, 'reddit');
|
|
270
|
+
for (let batchNum = 0; batchNum < totalBatches; batchNum++) {
|
|
271
|
+
const startIdx = batchNum * REDDIT.BATCH_SIZE;
|
|
272
|
+
const batchUrls = urls.slice(startIdx, startIdx + REDDIT.BATCH_SIZE);
|
|
273
|
+
mcpLog('info', `Batch ${batchNum + 1}/${totalBatches} (${batchUrls.length} posts)`, 'reddit');
|
|
274
|
+
// Limit to 5 concurrent Reddit API calls within each batch
|
|
275
|
+
const batchResults = await pMapSettled(batchUrls, url => this.getPost(url, commentsPerPost), 5);
|
|
276
|
+
for (let i = 0; i < batchResults.length; i++) {
|
|
277
|
+
const result = batchResults[i];
|
|
278
|
+
const url = batchUrls[i] || '';
|
|
279
|
+
if (result.status === 'fulfilled') {
|
|
280
|
+
allResults.set(url, result.value);
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
const errorMsg = result.reason?.message || String(result.reason);
|
|
284
|
+
if (errorMsg.includes('429') || errorMsg.includes('rate'))
|
|
285
|
+
rateLimitHits++;
|
|
286
|
+
allResults.set(url, new Error(errorMsg));
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
// Safe callback invocation
|
|
290
|
+
try {
|
|
291
|
+
onBatchComplete?.(batchNum + 1, totalBatches, allResults.size);
|
|
292
|
+
}
|
|
293
|
+
catch (callbackError) {
|
|
294
|
+
mcpLog('error', `onBatchComplete callback error: ${callbackError}`, 'reddit');
|
|
295
|
+
}
|
|
296
|
+
mcpLog('info', `Batch ${batchNum + 1} complete (${allResults.size}/${urls.length})`, 'reddit');
|
|
297
|
+
// Small delay between batches
|
|
298
|
+
if (batchNum < totalBatches - 1) {
|
|
299
|
+
await sleep(500);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return { results: allResults, batchesProcessed: totalBatches, totalPosts: urls.length, rateLimitHits, commentAllocation: allocation };
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
//# sourceMappingURL=reddit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reddit.js","sourceRoot":"","sources":["../../src/clients/reddit.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deep Research Client
|
|
3
|
+
* Handles research API requests with web search capabilities
|
|
4
|
+
* Implements robust retry logic and NEVER crashes the server
|
|
5
|
+
*/
|
|
6
|
+
import { type StructuredError } from '../utils/errors.js';
|
|
7
|
+
interface ResearchParams {
|
|
8
|
+
question: string;
|
|
9
|
+
systemPrompt?: string;
|
|
10
|
+
reasoningEffort?: 'low' | 'medium' | 'high';
|
|
11
|
+
maxSearchResults?: number;
|
|
12
|
+
maxTokens?: number;
|
|
13
|
+
temperature?: number;
|
|
14
|
+
responseFormat?: {
|
|
15
|
+
type: 'json_object' | 'text';
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export interface ResearchResponse {
|
|
19
|
+
id: string;
|
|
20
|
+
model: string;
|
|
21
|
+
created: number;
|
|
22
|
+
content: string;
|
|
23
|
+
finishReason?: string;
|
|
24
|
+
usage?: {
|
|
25
|
+
promptTokens: number;
|
|
26
|
+
completionTokens: number;
|
|
27
|
+
totalTokens: number;
|
|
28
|
+
sourcesUsed?: number;
|
|
29
|
+
};
|
|
30
|
+
annotations?: Array<{
|
|
31
|
+
type: 'url_citation';
|
|
32
|
+
url: string;
|
|
33
|
+
title: string;
|
|
34
|
+
startIndex: number;
|
|
35
|
+
endIndex: number;
|
|
36
|
+
}>;
|
|
37
|
+
error?: StructuredError;
|
|
38
|
+
}
|
|
39
|
+
export declare class ResearchClient {
|
|
40
|
+
private client;
|
|
41
|
+
constructor();
|
|
42
|
+
/**
|
|
43
|
+
* Check if an error is retryable for research requests
|
|
44
|
+
*/
|
|
45
|
+
private isRetryableError;
|
|
46
|
+
/**
|
|
47
|
+
* Calculate backoff for research retries
|
|
48
|
+
*/
|
|
49
|
+
private calculateBackoff;
|
|
50
|
+
/**
|
|
51
|
+
* Build request payload based on model type
|
|
52
|
+
* Gemini models use tools with google_search, others use search_parameters
|
|
53
|
+
*/
|
|
54
|
+
private buildRequestPayload;
|
|
55
|
+
/**
|
|
56
|
+
* Execute a single research request with a specific model
|
|
57
|
+
*/
|
|
58
|
+
private executeResearch;
|
|
59
|
+
/**
|
|
60
|
+
* Perform research with retry logic and fallback to secondary model
|
|
61
|
+
* Returns a ResearchResponse - may contain error field on failure
|
|
62
|
+
* NEVER throws - always returns a valid response object
|
|
63
|
+
*/
|
|
64
|
+
research(params: ResearchParams): Promise<ResearchResponse>;
|
|
65
|
+
}
|
|
66
|
+
export {};
|
|
67
|
+
//# sourceMappingURL=research.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"research.d.ts","sourceRoot":"","sources":["../../src/clients/research.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,EAIL,KAAK,eAAe,EACrB,MAAM,oBAAoB,CAAC;AAG5B,UAAU,cAAc;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,eAAe,CAAC,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;IAC5C,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,cAAc,CAAC,EAAE;QAAE,IAAI,EAAE,aAAa,GAAG,MAAM,CAAA;KAAE,CAAC;CACnD;AAED,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE;QACN,YAAY,EAAE,MAAM,CAAC;QACrB,gBAAgB,EAAE,MAAM,CAAC;QACzB,WAAW,EAAE,MAAM,CAAC;QACpB,WAAW,CAAC,EAAE,MAAM,CAAC;KACtB,CAAC;IACF,WAAW,CAAC,EAAE,KAAK,CAAC;QAClB,IAAI,EAAE,cAAc,CAAC;QACrB,GAAG,EAAE,MAAM,CAAC;QACZ,KAAK,EAAE,MAAM,CAAC;QACd,UAAU,EAAE,MAAM,CAAC;QACnB,QAAQ,EAAE,MAAM,CAAC;KAClB,CAAC,CAAC;IACH,KAAK,CAAC,EAAE,eAAe,CAAC;CACzB;AA4BD,qBAAa,cAAc;IACzB,OAAO,CAAC,MAAM,CAAS;;IAevB;;OAEG;IACH,OAAO,CAAC,gBAAgB;IA6BxB;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAMxB;;;OAGG;IACH,OAAO,CAAC,mBAAmB;IAqD3B;;OAEG;YACW,eAAe;IA+F7B;;;;OAIG;IACG,QAAQ,CAAC,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,gBAAgB,CAAC;CAkElE"}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deep Research Client
|
|
3
|
+
* Handles research API requests with web search capabilities
|
|
4
|
+
* Implements robust retry logic and NEVER crashes the server
|
|
5
|
+
*/
|
|
6
|
+
import OpenAI from 'openai';
|
|
7
|
+
import { RESEARCH } from '../config/index.js';
|
|
8
|
+
import { classifyError, sleep, ErrorCode, } from '../utils/errors.js';
|
|
9
|
+
import { mcpLog } from '../utils/logger.js';
|
|
10
|
+
// Research-specific retry configuration
|
|
11
|
+
// Research requests can be long-running, so we use longer delays
|
|
12
|
+
const RESEARCH_RETRY_CONFIG = {
|
|
13
|
+
maxRetries: 3, // 3 retries for robustness on transient failures
|
|
14
|
+
baseDelayMs: 5000, // Longer base delay
|
|
15
|
+
maxDelayMs: 60000,
|
|
16
|
+
};
|
|
17
|
+
// Retryable status codes for research API
|
|
18
|
+
const RETRYABLE_RESEARCH_CODES = new Set([429, 500, 502, 503, 504]);
|
|
19
|
+
// Models that use Gemini-style google_search tool instead of search_parameters
|
|
20
|
+
const GEMINI_STYLE_MODELS = new Set([
|
|
21
|
+
'google/gemini-2.5-flash',
|
|
22
|
+
'google/gemini-2.5-pro',
|
|
23
|
+
'google/gemini-2.0-flash',
|
|
24
|
+
'google/gemini-pro',
|
|
25
|
+
]);
|
|
26
|
+
/**
|
|
27
|
+
* Check if a model uses Gemini-style google_search tool
|
|
28
|
+
*/
|
|
29
|
+
function isGeminiStyleModel(model) {
|
|
30
|
+
return GEMINI_STYLE_MODELS.has(model) || model.startsWith('google/gemini');
|
|
31
|
+
}
|
|
32
|
+
export class ResearchClient {
|
|
33
|
+
client;
|
|
34
|
+
constructor() {
|
|
35
|
+
if (!RESEARCH.API_KEY) {
|
|
36
|
+
throw new Error('OPENROUTER_API_KEY is required for research');
|
|
37
|
+
}
|
|
38
|
+
this.client = new OpenAI({
|
|
39
|
+
baseURL: RESEARCH.BASE_URL,
|
|
40
|
+
apiKey: RESEARCH.API_KEY,
|
|
41
|
+
timeout: RESEARCH.TIMEOUT_MS,
|
|
42
|
+
maxRetries: 0, // We handle retries ourselves
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Check if an error is retryable for research requests
|
|
47
|
+
*/
|
|
48
|
+
isRetryableError(error) {
|
|
49
|
+
if (!error)
|
|
50
|
+
return false;
|
|
51
|
+
const err = error;
|
|
52
|
+
// Check HTTP status codes
|
|
53
|
+
if (err.status && RETRYABLE_RESEARCH_CODES.has(err.status)) {
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
// Check message patterns
|
|
57
|
+
const message = (err.message || '').toLowerCase();
|
|
58
|
+
if (message.includes('rate limit') ||
|
|
59
|
+
message.includes('timeout') ||
|
|
60
|
+
message.includes('timed out') ||
|
|
61
|
+
message.includes('service unavailable') ||
|
|
62
|
+
message.includes('connection')) {
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Calculate backoff for research retries
|
|
69
|
+
*/
|
|
70
|
+
calculateBackoff(attempt) {
|
|
71
|
+
const exponentialDelay = RESEARCH_RETRY_CONFIG.baseDelayMs * Math.pow(2, attempt);
|
|
72
|
+
const jitter = Math.random() * 0.3 * exponentialDelay;
|
|
73
|
+
return Math.min(exponentialDelay + jitter, RESEARCH_RETRY_CONFIG.maxDelayMs);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Build request payload based on model type
|
|
77
|
+
* Gemini models use tools with google_search, others use search_parameters
|
|
78
|
+
*/
|
|
79
|
+
buildRequestPayload(model, messages, options) {
|
|
80
|
+
const { temperature, reasoningEffort, maxTokens, maxSearchResults, responseFormat } = options;
|
|
81
|
+
if (isGeminiStyleModel(model)) {
|
|
82
|
+
// Gemini uses tools with google_search
|
|
83
|
+
const payload = {
|
|
84
|
+
model,
|
|
85
|
+
messages,
|
|
86
|
+
temperature,
|
|
87
|
+
max_tokens: maxTokens,
|
|
88
|
+
tools: [
|
|
89
|
+
{
|
|
90
|
+
type: 'google_search',
|
|
91
|
+
googleSearch: {},
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
};
|
|
95
|
+
if (responseFormat) {
|
|
96
|
+
payload.response_format = responseFormat;
|
|
97
|
+
}
|
|
98
|
+
return payload;
|
|
99
|
+
}
|
|
100
|
+
// Default: use search_parameters (for Grok, Perplexity, etc.)
|
|
101
|
+
const payload = {
|
|
102
|
+
model,
|
|
103
|
+
messages,
|
|
104
|
+
temperature,
|
|
105
|
+
reasoning_effort: reasoningEffort,
|
|
106
|
+
max_completion_tokens: maxTokens,
|
|
107
|
+
search_parameters: {
|
|
108
|
+
mode: 'on',
|
|
109
|
+
max_search_results: Math.min(maxSearchResults, 30),
|
|
110
|
+
return_citations: true,
|
|
111
|
+
sources: [{ type: 'web' }],
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
if (responseFormat) {
|
|
115
|
+
payload.response_format = responseFormat;
|
|
116
|
+
}
|
|
117
|
+
return payload;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Execute a single research request with a specific model
|
|
121
|
+
*/
|
|
122
|
+
async executeResearch(model, messages, options) {
|
|
123
|
+
const requestPayload = this.buildRequestPayload(model, messages, options);
|
|
124
|
+
let lastError;
|
|
125
|
+
// Retry loop for this model
|
|
126
|
+
for (let attempt = 0; attempt <= RESEARCH_RETRY_CONFIG.maxRetries; attempt++) {
|
|
127
|
+
try {
|
|
128
|
+
if (attempt > 0) {
|
|
129
|
+
mcpLog('warning', `Retry attempt ${attempt}/${RESEARCH_RETRY_CONFIG.maxRetries} for ${model}`, 'research');
|
|
130
|
+
}
|
|
131
|
+
const response = await this.client.chat.completions.create(requestPayload);
|
|
132
|
+
const choice = response.choices?.[0];
|
|
133
|
+
const message = choice?.message;
|
|
134
|
+
// Validate response
|
|
135
|
+
if (!message?.content && !choice) {
|
|
136
|
+
lastError = {
|
|
137
|
+
code: ErrorCode.INTERNAL_ERROR,
|
|
138
|
+
message: 'Research API returned empty response',
|
|
139
|
+
retryable: true,
|
|
140
|
+
};
|
|
141
|
+
if (attempt < RESEARCH_RETRY_CONFIG.maxRetries) {
|
|
142
|
+
const delayMs = this.calculateBackoff(attempt);
|
|
143
|
+
mcpLog('warning', `Empty response, retrying in ${delayMs}ms...`, 'research');
|
|
144
|
+
await sleep(delayMs);
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
id: response.id || '',
|
|
150
|
+
model: response.model || model,
|
|
151
|
+
created: response.created || Date.now(),
|
|
152
|
+
content: message?.content || '',
|
|
153
|
+
finishReason: choice?.finish_reason,
|
|
154
|
+
usage: response.usage ? {
|
|
155
|
+
promptTokens: response.usage.prompt_tokens,
|
|
156
|
+
completionTokens: response.usage.completion_tokens,
|
|
157
|
+
totalTokens: response.usage.total_tokens,
|
|
158
|
+
sourcesUsed: response.usage.num_sources_used,
|
|
159
|
+
} : undefined,
|
|
160
|
+
annotations: message?.annotations?.map((a) => ({
|
|
161
|
+
type: 'url_citation',
|
|
162
|
+
url: a.url_citation?.url || '',
|
|
163
|
+
title: a.url_citation?.title || '',
|
|
164
|
+
startIndex: a.url_citation?.start_index || 0,
|
|
165
|
+
endIndex: a.url_citation?.end_index || 0,
|
|
166
|
+
})),
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
lastError = classifyError(error);
|
|
171
|
+
const err = error;
|
|
172
|
+
mcpLog('error', `Error with ${model} (attempt ${attempt + 1}): ${lastError.message} (status: ${err.status})`, 'research');
|
|
173
|
+
// Check if we should retry
|
|
174
|
+
if (this.isRetryableError(error) && attempt < RESEARCH_RETRY_CONFIG.maxRetries) {
|
|
175
|
+
const delayMs = this.calculateBackoff(attempt);
|
|
176
|
+
mcpLog('warning', `Retrying in ${delayMs}ms...`, 'research');
|
|
177
|
+
await sleep(delayMs);
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
// Non-retryable or max retries reached
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
// Return error response
|
|
185
|
+
return {
|
|
186
|
+
id: '',
|
|
187
|
+
model,
|
|
188
|
+
created: Date.now(),
|
|
189
|
+
content: '',
|
|
190
|
+
error: lastError || {
|
|
191
|
+
code: ErrorCode.UNKNOWN_ERROR,
|
|
192
|
+
message: 'Unknown research error',
|
|
193
|
+
retryable: false,
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Perform research with retry logic and fallback to secondary model
|
|
199
|
+
* Returns a ResearchResponse - may contain error field on failure
|
|
200
|
+
* NEVER throws - always returns a valid response object
|
|
201
|
+
*/
|
|
202
|
+
async research(params) {
|
|
203
|
+
const { question, systemPrompt, reasoningEffort = RESEARCH.REASONING_EFFORT, maxSearchResults = RESEARCH.MAX_URLS, maxTokens = 32000, temperature = 0.3, responseFormat, } = params;
|
|
204
|
+
// Validate input
|
|
205
|
+
if (!question?.trim()) {
|
|
206
|
+
return {
|
|
207
|
+
id: '',
|
|
208
|
+
model: RESEARCH.MODEL,
|
|
209
|
+
created: Date.now(),
|
|
210
|
+
content: '',
|
|
211
|
+
error: {
|
|
212
|
+
code: ErrorCode.INVALID_INPUT,
|
|
213
|
+
message: 'Research question cannot be empty',
|
|
214
|
+
retryable: false,
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
const messages = [];
|
|
219
|
+
if (systemPrompt) {
|
|
220
|
+
messages.push({ role: 'system', content: systemPrompt });
|
|
221
|
+
}
|
|
222
|
+
messages.push({ role: 'user', content: question });
|
|
223
|
+
const options = { temperature, reasoningEffort, maxTokens, maxSearchResults, responseFormat };
|
|
224
|
+
// Try primary model first
|
|
225
|
+
mcpLog('info', `Trying primary model: ${RESEARCH.MODEL}`, 'research');
|
|
226
|
+
const primaryResult = await this.executeResearch(RESEARCH.MODEL, messages, options);
|
|
227
|
+
if (!primaryResult.error) {
|
|
228
|
+
return primaryResult;
|
|
229
|
+
}
|
|
230
|
+
// Primary failed - try fallback model if different
|
|
231
|
+
if (RESEARCH.FALLBACK_MODEL && RESEARCH.FALLBACK_MODEL !== RESEARCH.MODEL) {
|
|
232
|
+
mcpLog('warning', `Primary model failed, trying fallback: ${RESEARCH.FALLBACK_MODEL}`, 'research');
|
|
233
|
+
const fallbackResult = await this.executeResearch(RESEARCH.FALLBACK_MODEL, messages, options);
|
|
234
|
+
if (!fallbackResult.error) {
|
|
235
|
+
return fallbackResult;
|
|
236
|
+
}
|
|
237
|
+
// Both failed - return the fallback error (more recent)
|
|
238
|
+
mcpLog('error', `Both models failed. Primary: ${primaryResult.error?.message}, Fallback: ${fallbackResult.error?.message}`, 'research');
|
|
239
|
+
return {
|
|
240
|
+
...fallbackResult,
|
|
241
|
+
content: `Research failed with both models. Primary (${RESEARCH.MODEL}): ${primaryResult.error?.message}. Fallback (${RESEARCH.FALLBACK_MODEL}): ${fallbackResult.error?.message}`,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
// No fallback or same model - return primary error
|
|
245
|
+
mcpLog('error', `All attempts failed: ${primaryResult.error?.message}`, 'research');
|
|
246
|
+
return {
|
|
247
|
+
...primaryResult,
|
|
248
|
+
content: `Research failed: ${primaryResult.error?.message}`,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
//# sourceMappingURL=research.js.map
|