API
Core functions
Section titled “Core functions”import { stats, calc, createCache } from '@mcptoolshop/registry-stats';stats()
Section titled “stats()”Query a single registry:
const npm = await stats('npm', 'express');const pypi = await stats('pypi', 'requests');const nuget = await stats('nuget', 'Newtonsoft.Json');const vscode = await stats('vscode', 'esbenp.prettier-vscode');const docker = await stats('docker', 'library/node');stats.all()
Section titled “stats.all()”Query all registries at once. A bad package name throws RegistryError. A registry that fails is recorded on .errors and does not reject the call. Pass options.registries to query a subset. An explicit array is the allowlist, including an empty array, and an empty array queries nothing. Omit the field to walk every built-in provider. GitHub Releases is included only when the package name is owner/repo. An explicit list that names github stays strict.
const all = await stats.all('express');stats.bulk()
Section titled “stats.bulk()”Fetch multiple packages from one registry, concurrency-limited. One thrown name keeps the other rows and is listed on .errors. The call rejects only when every name threw. A missing package stays null and is not an error. JSON.stringify drops .errors.
const bulk = await stats.bulk('npm', ['express', 'koa', 'fastify']);stats.range()
Section titled “stats.range()”Daily download counts (npm + PyPI only):
const daily = await stats.range('npm', 'express', '2025-01-01', '2025-06-30');stats.compare()
Section titled “stats.compare()”Compare across registries:
const cmp = await stats.compare('express');await stats.compare('express', ['npm', 'pypi']);stats.mine()
Section titled “stats.mine()”Discover all npm packages by a maintainer, then fetch stats for each:
const results = await stats.mine('mikefrilot', { onProgress(done, total, pkg) { console.log(`${done}/${total}: ${pkg}`); },});// Returns PackageStats[] sorted by monthly downloads (descending)Uses the npm search API for discovery and the smart bulk API for stats (single HTTP call for unscoped packages, throttled sequential for scoped). The bulk .errors channel rides on the returned array. registry-stats --mine --json prints that array, and the failures go to stderr.
Calculations
Section titled “Calculations”calc.total(daily); // sum of all downloadscalc.avg(daily); // daily averagecalc.groupTotals(calc.monthly(daily)); // { '2025-01': 134982, ... }calc.trend(daily); // { direction: 'up', changePercent: 8.3 }calc.movingAvg(daily, 7); // 7-day moving averagecalc.popularity(daily); // 0-100 log-scale scorecalc.toCSV(daily); // CSV stringcalc.toChartData(daily, 'express'); // Chart.js-compatibleCaching
Section titled “Caching”const cache = createCache();await stats('npm', 'express', { cache }); // fetchesawait stats('npm', 'express', { cache }); // cache hit (5 min TTL)The StatsCache interface is pluggable — bring your own Redis or file backend.
AI Inference
Section titled “AI Inference”Zero-dependency, pure-math inference — no ML runtime, no external APIs.
import { forecast, detectAnomalies, segmentTrends, detectSeasonality, computeMomentum, generateRecommendations, computeHealthScore, generateActionableAdvice, computeYearlyProgress, inferPortfolio,} from '@mcptoolshop/registry-stats';
// 7-day forecast with 80% confidence intervalsconst predictions = forecast(dailySeries, 7);
// Anomaly detection (adaptive rolling z-score, 14-day window)const anomalies = detectAnomalies(dailySeries);
// Piecewise trend segmentationconst segments = segmentTrends(dailySeries);
// Composite momentum score (-100 to +100)const momentum = computeMomentum(dailySeries);
// Package health score (0-100 with A-F grade)const health = computeHealthScore('my-pkg', 'npm', dailySeries, momentum);// → { score: 72, grade: 'B', components: { activity, consistency, growth, stability } }
// Actionable advice with severity/urgency// Takes: (packageInferences[], healthScores[], opts)const advice = generateActionableAdvice(packages, healthScores, { gini: 0.6, npmPct: 85 });// → [{ type, severity, urgency, title, detail, action, packages }]
// Full portfolio analysis (the main entry point)const result = inferPortfolio(leaderboard, { gini: 0.6, npmPct: 85 });// → { packages, forecastTotal7, riskScore, diversityTrend, portfolioMomentum, recommendations, healthScores, actionableAdvice }// diversityTrend is 'improving' | 'stable' | 'declining'REST server
Section titled “REST server”serve() binds 127.0.0.1 unless host is set. createHandler() sends Access-Control-Allow-Origin: * unless corsOrigin is set. createServer(handler).listen(port) with no host binds every interface, so the sample below pins loopback.
serve() and createHandler() take rateLimitMax (default 60), rateLimitWindowSeconds (default 60), requestTimeoutMs (default 30000), and trustProxy (default false). When trustProxy is false, the limiter uses the socket address. When it is true, the client is the rightmost trimmed X-Forwarded-For hop. When requestTimeoutMs elapses, the handler returns 504 Gateway timeout and aborts the upstream work. The CLI serve subcommand does not expose flags for these four.
import { createHandler, serve } from '@mcptoolshop/registry-stats';
// Loopback only. Set host to '0.0.0.0' only when you mean to publish it.serve({ port: 3000, host: '127.0.0.1' });
// Bring your own server. listen(3000) with no host binds every interface.import { createServer } from 'node:http';const handler = createHandler();createServer(handler).listen(3000, '127.0.0.1');Endpoints
Section titled “Endpoints”| Method | Path | Description |
|---|---|---|
GET |
/ |
Health check (lists available endpoints) |
GET |
/stats/:package |
All registries. Failures go in X-Registry-Errors, not the JSON array. |
GET |
/stats/:registry/:package |
Single registry |
GET |
/compare/:package?registries=npm,pypi |
Cross-registry comparison |
GET |
/range/:registry/:package?start=...&end=...&format=json|csv|chart |
Time series data |
All endpoints return JSON by default. The /range endpoint supports format=csv (returns CSV with Content-Disposition header) and format=chart (returns Chart.js-compatible JSON). createHandler defaults Access-Control-Allow-Origin to *. Pass corsOrigin to narrow it. serve() stays on 127.0.0.1 unless host is set.
GET /stats/:package returns a JSON array. An empty array plus the X-Registry-Errors header is an outage. An empty array without that header means no registry returned the package. GET /compare returns an object, so a failure on that route stays in the JSON body.