pub-mcp 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.
Files changed (63) hide show
  1. package/.dockerignore +13 -0
  2. package/.editorconfig +14 -0
  3. package/.env.example +15 -0
  4. package/.eslintrc.cjs +21 -0
  5. package/.github/workflows/ci.yml +63 -0
  6. package/.prettierrc +7 -0
  7. package/CHANGELOG.md +84 -0
  8. package/CODE_OF_CONDUCT.md +34 -0
  9. package/CONTRIBUTING.md +55 -0
  10. package/Dockerfile +13 -0
  11. package/LICENSE +21 -0
  12. package/README.md +287 -0
  13. package/dist/cache/cache.d.ts +27 -0
  14. package/dist/cache/cache.d.ts.map +1 -0
  15. package/dist/cache/cache.js +94 -0
  16. package/dist/cache/cache.js.map +1 -0
  17. package/dist/cli.d.ts +3 -0
  18. package/dist/cli.d.ts.map +1 -0
  19. package/dist/cli.js +286 -0
  20. package/dist/cli.js.map +1 -0
  21. package/dist/clients/githubClient.d.ts +33 -0
  22. package/dist/clients/githubClient.d.ts.map +1 -0
  23. package/dist/clients/githubClient.js +126 -0
  24. package/dist/clients/githubClient.js.map +1 -0
  25. package/dist/clients/pubClient.d.ts +28 -0
  26. package/dist/clients/pubClient.d.ts.map +1 -0
  27. package/dist/clients/pubClient.js +223 -0
  28. package/dist/clients/pubClient.js.map +1 -0
  29. package/dist/server-http.d.ts +7 -0
  30. package/dist/server-http.d.ts.map +1 -0
  31. package/dist/server-http.js +139 -0
  32. package/dist/server-http.js.map +1 -0
  33. package/dist/server.d.ts +2 -0
  34. package/dist/server.d.ts.map +1 -0
  35. package/dist/server.js +69 -0
  36. package/dist/server.js.map +1 -0
  37. package/dist/toolRegistry.d.ts +74 -0
  38. package/dist/toolRegistry.d.ts.map +1 -0
  39. package/dist/toolRegistry.js +80 -0
  40. package/dist/toolRegistry.js.map +1 -0
  41. package/dist/tools/index.d.ts +142 -0
  42. package/dist/tools/index.d.ts.map +1 -0
  43. package/dist/tools/index.js +214 -0
  44. package/dist/tools/index.js.map +1 -0
  45. package/dist/types/index.d.ts +55 -0
  46. package/dist/types/index.d.ts.map +1 -0
  47. package/dist/types/index.js +2 -0
  48. package/dist/types/index.js.map +1 -0
  49. package/dist/utils/rateLimiter.d.ts +13 -0
  50. package/dist/utils/rateLimiter.d.ts.map +1 -0
  51. package/dist/utils/rateLimiter.js +42 -0
  52. package/dist/utils/rateLimiter.js.map +1 -0
  53. package/package.json +72 -0
  54. package/skills-lock.json +25 -0
  55. package/src/cache/cache.ts +124 -0
  56. package/src/cli.ts +350 -0
  57. package/src/clients/githubClient.ts +169 -0
  58. package/src/clients/pubClient.ts +312 -0
  59. package/src/tools/index.ts +266 -0
  60. package/src/types/index.ts +61 -0
  61. package/src/utils/rateLimiter.ts +56 -0
  62. package/tsconfig.json +24 -0
  63. package/vitest.config.ts +14 -0
@@ -0,0 +1,312 @@
1
+ import type {
2
+ PackageInfo,
3
+ PackageVersion,
4
+ SearchResult,
5
+ PackageScore,
6
+ PackageChangelog,
7
+ } from '../types/index.js';
8
+ import { getPackageCache, getSearchCache } from '../cache/cache.js';
9
+
10
+ const DEFAULT_TIMEOUT = 10000;
11
+ const DEFAULT_RETRIES = 3;
12
+
13
+ export interface PubClientOptions {
14
+ baseUrl?: string;
15
+ timeout?: number;
16
+ retries?: number;
17
+ }
18
+
19
+ export class PubClientError extends Error {
20
+ constructor(
21
+ message: string,
22
+ public statusCode?: number,
23
+ public isRetryable: boolean = false
24
+ ) {
25
+ super(message);
26
+ this.name = 'PubClientError';
27
+ }
28
+ }
29
+
30
+ export class PubClient {
31
+ private baseUrl: string;
32
+ private timeout: number;
33
+ private retries: number;
34
+
35
+ constructor(options: PubClientOptions = {}) {
36
+ this.baseUrl = options.baseUrl || process.env.PUB_DEV_API_URL || 'https://pub.dev/api';
37
+ this.timeout = options.timeout || DEFAULT_TIMEOUT;
38
+ this.retries = options.retries || DEFAULT_RETRIES;
39
+ }
40
+
41
+ private async fetchWithTimeout(url: string, options: RequestInit = {}): Promise<Response> {
42
+ const controller = new AbortController();
43
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
44
+
45
+ try {
46
+ const response = await fetch(url, {
47
+ ...options,
48
+ signal: controller.signal,
49
+ });
50
+ return response;
51
+ } finally {
52
+ clearTimeout(timeoutId);
53
+ }
54
+ }
55
+
56
+ private async fetchWithRetry(url: string, options: RequestInit = {}): Promise<Response> {
57
+ let lastError: Error | null = null;
58
+
59
+ for (let attempt = 0; attempt < this.retries; attempt++) {
60
+ try {
61
+ const response = await this.fetchWithTimeout(url, options);
62
+
63
+ if (!response.ok) {
64
+ const isRetryable =
65
+ response.status === 429 || (response.status >= 500 && response.status < 600);
66
+ if (isRetryable && attempt < this.retries - 1) {
67
+ await this.delay(Math.pow(2, attempt) * 1000);
68
+ continue;
69
+ }
70
+
71
+ throw new PubClientError(
72
+ `HTTP error: ${response.status} ${response.statusText}`,
73
+ response.status,
74
+ isRetryable
75
+ );
76
+ }
77
+
78
+ return response;
79
+ } catch (error) {
80
+ lastError = error as Error;
81
+ if (attempt < this.retries - 1 && lastError.name === 'AbortError') {
82
+ await this.delay(Math.pow(2, attempt) * 1000);
83
+ }
84
+ }
85
+ }
86
+
87
+ throw lastError || new PubClientError('Failed to fetch after retries');
88
+ }
89
+
90
+ private delay(ms: number): Promise<void> {
91
+ return new Promise((resolve) => setTimeout(resolve, ms));
92
+ }
93
+
94
+ async searchPackages(query: string, limit: number = 10): Promise<SearchResult> {
95
+ const cacheKey = `search:${query}:${limit}`;
96
+ const cached = getSearchCache().get(cacheKey);
97
+ if (cached) {
98
+ return cached as SearchResult;
99
+ }
100
+
101
+ const url = `${this.baseUrl}/search?q=${encodeURIComponent(query)}&limit=${limit}`;
102
+ const response = await this.fetchWithRetry(url);
103
+
104
+ const data = (await response.json()) as {
105
+ packages?: { package: string; version: string; description?: string }[];
106
+ totalCount?: number;
107
+ };
108
+
109
+ const packages = (data.packages || []).map((p) => ({
110
+ name: p.package,
111
+ description: p.description || '',
112
+ latestVersion: p.version,
113
+ }));
114
+
115
+ const result = {
116
+ packages,
117
+ total: data.totalCount || packages.length,
118
+ };
119
+
120
+ getSearchCache().set(cacheKey, result);
121
+ return result;
122
+ }
123
+
124
+ async getPackage(name: string): Promise<PackageInfo> {
125
+ const cacheKey = `package:${name}`;
126
+ const cached = getPackageCache().get(cacheKey);
127
+ if (cached) {
128
+ return cached as PackageInfo;
129
+ }
130
+
131
+ const url = `${this.baseUrl}/packages/${name}`;
132
+ const response = await this.fetchWithRetry(url);
133
+
134
+ if (response.status === 404) {
135
+ throw new PubClientError(`Package not found: ${name}`, 404, false);
136
+ }
137
+
138
+ const data = (await response.json()) as {
139
+ name: string;
140
+ latest: { version: string; published: string; description?: string };
141
+ publisher?: { publisherName?: string };
142
+ urls?: { homepage?: string; repository?: string; issues?: string };
143
+ };
144
+
145
+ const result: PackageInfo = {
146
+ name: data.name,
147
+ description: data.latest.description || '',
148
+ latestVersion: data.latest.version,
149
+ published: data.latest.published,
150
+ updated: data.latest.published,
151
+ publisher: data.publisher?.publisherName,
152
+ homepage: data.urls?.homepage,
153
+ repository: data.urls?.repository,
154
+ issues: data.urls?.issues,
155
+ };
156
+
157
+ getPackageCache().set(cacheKey, result);
158
+ return result;
159
+ }
160
+
161
+ async getPackageVersions(name: string): Promise<PackageVersion[]> {
162
+ const cacheKey = `versions:${name}`;
163
+ const cached = getPackageCache().get(cacheKey);
164
+ if (cached) {
165
+ return cached as PackageVersion[];
166
+ }
167
+
168
+ const url = `${this.baseUrl}/packages/${name}`;
169
+ const response = await this.fetchWithRetry(url);
170
+
171
+ if (response.status === 404) {
172
+ throw new PubClientError(`Package not found: ${name}`, 404, false);
173
+ }
174
+
175
+ const data = (await response.json()) as {
176
+ versions: { version: string; published: string }[];
177
+ };
178
+
179
+ const result = data.versions.map((v) => ({
180
+ version: v.version,
181
+ published: v.published,
182
+ }));
183
+
184
+ getPackageCache().set(cacheKey, result);
185
+ return result;
186
+ }
187
+
188
+ async getReadme(name: string, version?: string): Promise<string> {
189
+ const cacheKey = `readme:${name}:${version || 'latest'}`;
190
+ const cached = getPackageCache().get(cacheKey);
191
+ if (cached) {
192
+ return cached as string;
193
+ }
194
+
195
+ const versionPart = version ? `/${version}` : '';
196
+ const url = `${this.baseUrl}/packages/${name}${versionPart}/readme`;
197
+
198
+ try {
199
+ const response = await this.fetchWithRetry(url);
200
+
201
+ if (response.status === 404) {
202
+ return '';
203
+ }
204
+
205
+ const content = await response.text();
206
+ getPackageCache().set(cacheKey, content);
207
+ return content;
208
+ } catch (error) {
209
+ if (error instanceof PubClientError && error.statusCode === 404) {
210
+ return '';
211
+ }
212
+ throw error;
213
+ }
214
+ }
215
+
216
+ async getDependencies(name: string, version?: string): Promise<Record<string, string>> {
217
+ const cacheKey = `deps:${name}:${version || 'latest'}`;
218
+ const cached = getPackageCache().get(cacheKey);
219
+ if (cached) {
220
+ return cached as Record<string, string>;
221
+ }
222
+
223
+ const versionPart = version ? `/${version}` : '';
224
+ const url = `${this.baseUrl}/packages/${name}${versionPart}`;
225
+ const response = await this.fetchWithRetry(url);
226
+
227
+ if (response.status === 404) {
228
+ throw new PubClientError(`Package not found: ${name}`, 404, false);
229
+ }
230
+
231
+ const data = (await response.json()) as {
232
+ latest?: { dependencies?: Record<string, string> };
233
+ };
234
+
235
+ const result = data.latest?.dependencies || {};
236
+ getPackageCache().set(cacheKey, result);
237
+ return result;
238
+ }
239
+
240
+ async getPackageScore(name: string): Promise<PackageScore> {
241
+ const cacheKey = `score:${name}`;
242
+ const cached = getPackageCache().get(cacheKey);
243
+ if (cached) {
244
+ return cached as PackageScore;
245
+ }
246
+
247
+ const url = `${this.baseUrl}/packages/${name}/score`;
248
+ const response = await this.fetchWithRetry(url);
249
+
250
+ if (response.status === 404) {
251
+ throw new PubClientError(`Package not found: ${name}`, 404, false);
252
+ }
253
+
254
+ const data = (await response.json()) as {
255
+ grantedPoints?: number;
256
+ maxPoints?: number;
257
+ likeCount?: number;
258
+ downloadCount30Days?: number;
259
+ tags?: string[];
260
+ };
261
+
262
+ const result: PackageScore = {
263
+ grantedPoints: data.grantedPoints ?? 0,
264
+ maxPoints: data.maxPoints ?? 0,
265
+ likeCount: data.likeCount ?? 0,
266
+ downloadCount30Days: data.downloadCount30Days ?? 0,
267
+ tags: data.tags ?? [],
268
+ };
269
+
270
+ getPackageCache().set(cacheKey, result);
271
+ return result;
272
+ }
273
+
274
+ async getChangelog(name: string, version?: string): Promise<PackageChangelog | null> {
275
+ const cacheKey = `changelog:${name}:${version || 'latest'}`;
276
+ const cached = getPackageCache().get(cacheKey);
277
+ if (cached) {
278
+ return cached as PackageChangelog;
279
+ }
280
+
281
+ const versionPart = version ? `/${version}` : '';
282
+ const url = `${this.baseUrl}/packages/${name}${versionPart}/changelog`;
283
+
284
+ try {
285
+ const response = await this.fetchWithRetry(url);
286
+
287
+ if (response.status === 404) {
288
+ return null;
289
+ }
290
+
291
+ const data = (await response.json()) as {
292
+ version: string;
293
+ published: string;
294
+ content?: string;
295
+ };
296
+
297
+ const result: PackageChangelog = {
298
+ version: data.version,
299
+ published: data.published,
300
+ content: data.content || '',
301
+ };
302
+
303
+ getPackageCache().set(cacheKey, result);
304
+ return result;
305
+ } catch (error) {
306
+ if (error instanceof PubClientError && error.statusCode === 404) {
307
+ return null;
308
+ }
309
+ throw error;
310
+ }
311
+ }
312
+ }
@@ -0,0 +1,266 @@
1
+ import { z } from 'zod';
2
+ import type { PubClient } from '../clients/pubClient.js';
3
+ import type { GitHubClient } from '../clients/githubClient.js';
4
+ import { parseGitHubUrl } from '../clients/githubClient.js';
5
+
6
+ export const searchPackagesSchema = z.object({
7
+ query: z.string().min(1),
8
+ limit: z.number().int().min(1).max(100).default(10),
9
+ });
10
+
11
+ export const getPackageInfoSchema = z.object({
12
+ name: z.string().min(1),
13
+ });
14
+
15
+ export const getPackageVersionsSchema = z.object({
16
+ name: z.string().min(1),
17
+ });
18
+
19
+ export const getReadmeSchema = z.object({
20
+ name: z.string().min(1),
21
+ version: z.string().optional(),
22
+ format: z.enum(['markdown', 'text', 'html']).default('markdown').optional(),
23
+ });
24
+
25
+ export const getDependenciesSchema = z.object({
26
+ name: z.string().min(1),
27
+ version: z.string().optional(),
28
+ });
29
+
30
+ export const getPackageScoreSchema = z.object({
31
+ name: z.string().min(1),
32
+ });
33
+
34
+ export const getChangelogSchema = z.object({
35
+ name: z.string().min(1),
36
+ version: z.string().optional(),
37
+ });
38
+
39
+ export const getPackageMetricsSchema = z.object({
40
+ name: z.string().min(1),
41
+ });
42
+
43
+ export type SearchPackagesInput = { query: string; limit?: number };
44
+ export type GetPackageInfoInput = { name: string };
45
+ export type GetPackageVersionsInput = { name: string };
46
+ export type GetReadmeInput = {
47
+ name: string;
48
+ version?: string;
49
+ format?: 'markdown' | 'text' | 'html';
50
+ };
51
+ export type GetDependenciesInput = { name: string; version?: string };
52
+ export type GetPackageScoreInput = { name: string };
53
+ export type GetChangelogInput = { name: string; version?: string };
54
+ export type GetPackageMetricsInput = { name: string };
55
+
56
+ export interface ToolDefinition {
57
+ name: string;
58
+ description: string;
59
+ inputSchema: {
60
+ type: 'object';
61
+ properties: Record<string, unknown>;
62
+ required?: string[];
63
+ };
64
+ }
65
+
66
+ export const tools: ToolDefinition[] = [
67
+ {
68
+ name: 'search_packages',
69
+ description: 'Search for packages on pub.dev by query',
70
+ inputSchema: {
71
+ type: 'object',
72
+ properties: {
73
+ query: { type: 'string', description: 'Search query string' },
74
+ limit: { type: 'number', description: 'Maximum number of results (default 10)' },
75
+ },
76
+ required: ['query'],
77
+ },
78
+ },
79
+ {
80
+ name: 'get_package_info',
81
+ description: 'Get detailed information about a specific package',
82
+ inputSchema: {
83
+ type: 'object',
84
+ properties: {
85
+ name: { type: 'string', description: 'Package name' },
86
+ },
87
+ required: ['name'],
88
+ },
89
+ },
90
+ {
91
+ name: 'get_package_versions',
92
+ description: 'Get all available versions of a package',
93
+ inputSchema: {
94
+ type: 'object',
95
+ properties: {
96
+ name: { type: 'string', description: 'Package name' },
97
+ },
98
+ required: ['name'],
99
+ },
100
+ },
101
+ {
102
+ name: 'get_readme',
103
+ description: 'Get the README content of a package',
104
+ inputSchema: {
105
+ type: 'object',
106
+ properties: {
107
+ name: { type: 'string', description: 'Package name' },
108
+ version: { type: 'string', description: 'Specific version (optional, defaults to latest)' },
109
+ format: {
110
+ type: 'string',
111
+ description: 'Output format: markdown, text, or html (default: markdown)',
112
+ enum: ['markdown', 'text', 'html'],
113
+ },
114
+ },
115
+ required: ['name'],
116
+ },
117
+ },
118
+ {
119
+ name: 'get_dependencies',
120
+ description: 'Get the dependencies of a package',
121
+ inputSchema: {
122
+ type: 'object',
123
+ properties: {
124
+ name: { type: 'string', description: 'Package name' },
125
+ version: { type: 'string', description: 'Specific version (optional, defaults to latest)' },
126
+ },
127
+ required: ['name'],
128
+ },
129
+ },
130
+ {
131
+ name: 'get_package_score',
132
+ description: 'Get package score and metrics (pana points, likes, downloads, tags)',
133
+ inputSchema: {
134
+ type: 'object',
135
+ properties: {
136
+ name: { type: 'string', description: 'Package name' },
137
+ },
138
+ required: ['name'],
139
+ },
140
+ },
141
+ {
142
+ name: 'get_changelog',
143
+ description: 'Get the changelog of a specific package version',
144
+ inputSchema: {
145
+ type: 'object',
146
+ properties: {
147
+ name: { type: 'string', description: 'Package name' },
148
+ version: { type: 'string', description: 'Specific version (optional, defaults to latest)' },
149
+ },
150
+ required: ['name'],
151
+ },
152
+ },
153
+ {
154
+ name: 'get_package_metrics',
155
+ description: 'Get comprehensive metrics for a package (info + score + versions)',
156
+ inputSchema: {
157
+ type: 'object',
158
+ properties: {
159
+ name: { type: 'string', description: 'Package name' },
160
+ },
161
+ required: ['name'],
162
+ },
163
+ },
164
+ ];
165
+
166
+ export async function handleSearchPackages(client: PubClient, input: SearchPackagesInput) {
167
+ const result = await client.searchPackages(input.query, input.limit);
168
+ return {
169
+ packages: result.packages.map((p) => ({
170
+ name: p.name,
171
+ description: p.description,
172
+ latestVersion: p.latestVersion,
173
+ })),
174
+ total: result.total,
175
+ };
176
+ }
177
+
178
+ export async function handleGetPackageInfo(client: PubClient, input: GetPackageInfoInput) {
179
+ const pkg = await client.getPackage(input.name);
180
+ return pkg;
181
+ }
182
+
183
+ export async function handleGetPackageVersions(client: PubClient, input: GetPackageVersionsInput) {
184
+ const versions = await client.getPackageVersions(input.name);
185
+ return { versions };
186
+ }
187
+
188
+ export async function handleGetReadme(
189
+ client: PubClient,
190
+ githubClient: GitHubClient,
191
+ input: GetReadmeInput
192
+ ) {
193
+ const readme = await client.getReadme(input.name, input.version);
194
+
195
+ let finalReadme = readme;
196
+ let source = 'pub.dev';
197
+
198
+ if (!readme && input.name) {
199
+ try {
200
+ const pkg = await client.getPackage(input.name);
201
+ if (pkg.repository) {
202
+ const parsed = parseGitHubUrl(pkg.repository);
203
+ if (parsed) {
204
+ const githubReadme = await githubClient.getRepositoryReadme(parsed.owner, parsed.repo);
205
+ if (githubReadme) {
206
+ finalReadme = githubReadme;
207
+ source = 'github';
208
+ }
209
+ }
210
+ }
211
+ } catch {
212
+ // ignore errors and return no readme
213
+ }
214
+ }
215
+
216
+ const content = finalReadme || 'No README available';
217
+
218
+ if (input.format === 'text') {
219
+ return {
220
+ readme: content.replace(/[#*`_~\[\]]/g, '').trim(),
221
+ source,
222
+ format: 'text',
223
+ };
224
+ }
225
+
226
+ return { readme: content, source, format: input.format || 'markdown' };
227
+ }
228
+
229
+ export async function handleGetDependencies(client: PubClient, input: GetDependenciesInput) {
230
+ const dependencies = await client.getDependencies(input.name, input.version);
231
+ return { dependencies };
232
+ }
233
+
234
+ export async function handleGetPackageScore(client: PubClient, input: GetPackageScoreInput) {
235
+ const score = await client.getPackageScore(input.name);
236
+ return score;
237
+ }
238
+
239
+ export async function handleGetChangelog(client: PubClient, input: GetChangelogInput) {
240
+ const changelog = await client.getChangelog(input.name, input.version);
241
+
242
+ if (!changelog) {
243
+ return {
244
+ version: input.version || 'latest',
245
+ content: 'No changelog available',
246
+ published: '',
247
+ };
248
+ }
249
+
250
+ return changelog;
251
+ }
252
+
253
+ export async function handleGetPackageMetrics(client: PubClient, input: GetPackageMetricsInput) {
254
+ const [info, score, versions] = await Promise.all([
255
+ client.getPackage(input.name),
256
+ client.getPackageScore(input.name),
257
+ client.getPackageVersions(input.name),
258
+ ]);
259
+
260
+ return {
261
+ info,
262
+ score,
263
+ versionCount: versions.length,
264
+ latestVersion: versions[0]?.version,
265
+ };
266
+ }
@@ -0,0 +1,61 @@
1
+ export interface PackageInfo {
2
+ name: string;
3
+ description: string;
4
+ latestVersion: string;
5
+ published: string;
6
+ updated: string;
7
+ publisher?: string;
8
+ homepage?: string;
9
+ repository?: string;
10
+ issues?: string;
11
+ }
12
+
13
+ export interface PackageVersion {
14
+ version: string;
15
+ published: string;
16
+ }
17
+
18
+ export interface PackageScore {
19
+ grantedPoints: number;
20
+ maxPoints: number;
21
+ likeCount: number;
22
+ downloadCount30Days: number;
23
+ tags: string[];
24
+ }
25
+
26
+ export interface PackageChangelog {
27
+ version: string;
28
+ published: string;
29
+ content: string;
30
+ }
31
+
32
+ export interface PackageDependencies {
33
+ direct: Record<string, string>;
34
+ dev: Record<string, string>;
35
+ transitive: Record<string, string>;
36
+ }
37
+
38
+ export interface SearchResult {
39
+ packages: {
40
+ name: string;
41
+ description: string;
42
+ latestVersion: string;
43
+ }[];
44
+ total: number;
45
+ }
46
+
47
+ export interface ToolInput {
48
+ query?: string;
49
+ limit?: number;
50
+ name?: string;
51
+ version?: string;
52
+ }
53
+
54
+ export interface ToolOutput {
55
+ packages?: PackageInfo[];
56
+ versions?: PackageVersion[];
57
+ readme?: string;
58
+ dependencies?: PackageDependencies;
59
+ searchResults?: SearchResult;
60
+ [key: string]: unknown;
61
+ }
@@ -0,0 +1,56 @@
1
+ import pLimit from 'p-limit';
2
+
3
+ const MAX_CONCURRENT = parseInt(process.env.MAX_CONCURRENT_REQUESTS || '5', 10);
4
+ const RETRY_ATTEMPTS = parseInt(process.env.RETRY_ATTEMPTS || '3', 10);
5
+
6
+ export class RateLimiter {
7
+ private limit: ReturnType<typeof pLimit>;
8
+
9
+ constructor(concurrency: number = MAX_CONCURRENT) {
10
+ this.limit = pLimit(concurrency);
11
+ }
12
+
13
+ async run<T>(fn: () => Promise<T>): Promise<T> {
14
+ return this.limit(fn);
15
+ }
16
+
17
+ setConcurrency(concurrency: number): void {
18
+ this.limit = pLimit(concurrency);
19
+ }
20
+ }
21
+
22
+ export const rateLimiter = new RateLimiter();
23
+
24
+ export async function withRetry<T>(
25
+ fn: () => Promise<T>,
26
+ options: { attempts?: number; backoff?: number } = {}
27
+ ): Promise<T> {
28
+ const attempts = options.attempts || RETRY_ATTEMPTS;
29
+ const backoff = options.backoff || 1000;
30
+
31
+ let lastError: Error | null = null;
32
+
33
+ for (let i = 0; i < attempts; i++) {
34
+ try {
35
+ return await fn();
36
+ } catch (error) {
37
+ lastError = error as Error;
38
+ const isRetryable = error instanceof Error &&
39
+ ('statusCode' in error
40
+ ? (error as { statusCode?: number }).statusCode === 429
41
+ : error.message.includes('ECONNRESET') || error.message.includes('ETIMEDOUT'));
42
+
43
+ if (!isRetryable || i === attempts - 1) {
44
+ throw lastError;
45
+ }
46
+
47
+ await new Promise((resolve) => setTimeout(resolve, backoff * Math.pow(2, i)));
48
+ }
49
+ }
50
+
51
+ throw lastError;
52
+ }
53
+
54
+ export async function withRateLimit<T>(fn: () => Promise<T>): Promise<T> {
55
+ return rateLimiter.run(fn);
56
+ }