Query npm, PyPI, crates.io, RubyGems, Packagist, and Arch Linux with one API. PURL-native, typed, cached.
If you need package metadata from multiple registries, you currently have two options: call each registry's REST API yourself (they all work differently), or depend on a third-party aggregation service.
There is no embeddable TypeScript library that normalizes across registries. The closest thing is git-pkgs/registries in Go, which covers 25 ecosystems but is Go-only. Aggregation APIs like ecosyste.ms and deps.dev exist, but they are external services you can't bundle into your own tool.
regxa fills that gap. One fetchPackage call, same response shape, regardless of whether the package lives on npm or Packagist. Uses PURL (ECMA-427) for addressing, so pkg:npm/lodash and pkg:cargo/serde resolve through the same code path. Storage-backed caching with a lockfile keeps things fast on repeated lookups.
- 🔍 Single API, six registries - npm, PyPI, crates.io, RubyGems, Packagist, Arch Linux (official + AUR)
- 📦 PURL-native - ECMA-427 identifiers as first-class input
- 🏷️ Normalized data model - same
Package,Version,Dependency,Maintainertypes everywhere - 💾 Storage-backed cache + lockfile - unstorage-native, sha256 integrity checks, configurable TTL
- ⌨️ CLI included -
regxa info npm/lodash,regxa versions cargo/serde,regxa deps pypi/flask@3.1.1 - 🔁 Retry + backoff - exponential backoff with jitter, rate limiter interface
- 🪶 ESM-only, zero CJS - built with obuild
pnpm add regxaFor the AI SDK tool (regxa/ai subpath), also install ai and zod:
pnpm add ai zodimport { fetchPackageFromPURL } from "regxa";
const pkg = await fetchPackageFromPURL("pkg:npm/lodash");
console.log(pkg.name); // "lodash"
console.log(pkg.latestVersion); // "4.17.23"
console.log(pkg.licenses); // "MIT"
console.log(pkg.repository); // "https://github.com/lodash/lodash"Works the same for any supported registry:
await fetchPackageFromPURL("pkg:cargo/serde");
await fetchPackageFromPURL("pkg:pypi/flask");
await fetchPackageFromPURL("pkg:gem/rails");
await fetchPackageFromPURL("pkg:composer/laravel/framework");
await fetchPackageFromPURL("pkg:alpm/arch/pacman");The pkg: prefix is optional in the CLI. npm/lodash works just as well:
regxa info npm/lodash
regxa versions cargo/serde
regxa deps pypi/flask@3.1.1
regxa maintainers gem/rails
regxa deps alpm/aur/paruAdd --json for machine-readable output, --no-cache to skip the cache.
regxa/ai exports a ready-made tool for AI SDK apps:
import { generateText } from "ai";
import { packageTool } from "regxa/ai";
const { text } = await generateText({
model: yourModel,
tools: { packageRegistry: packageTool },
prompt: "Show me the latest metadata for pkg:npm/lodash and then list its maintainers.",
});The tool supports these operations through one input schema:
// { operation: 'package', purl: 'pkg:npm/lodash' }
// { operation: 'versions', purl: 'pkg:cargo/serde' }
// { operation: 'dependencies', purl: 'pkg:pypi/flask@3.1.1' }
// { operation: 'maintainers', purl: 'pkg:gem/rails' }
// { operation: 'bulk-packages', purls: ['pkg:npm/lodash', 'pkg:cargo/serde'], concurrency?: number }| Ecosystem | PURL type | Registry |
|---|---|---|
| npm | pkg:npm/... |
registry.npmjs.org |
| Cargo | pkg:cargo/... |
crates.io |
| PyPI | pkg:pypi/... |
pypi.org |
| RubyGems | pkg:gem/... |
rubygems.org |
| Packagist | pkg:composer/... |
packagist.org |
| Arch Linux | pkg:alpm/... |
archlinux.org, aur.archlinux.org |
Scoped packages work as expected: pkg:npm/%40vue/core or npm/@vue/core in the CLI.
Arch Linux packages use a namespace: pkg:alpm/arch/pacman (or just pkg:alpm/pacman) for official repos, pkg:alpm/aur/paru for AUR. Official packages default to arch when the namespace is omitted; AUR requires the explicit aur namespace.
import {
fetchPackageFromPURL,
fetchVersionsFromPURL,
fetchDependenciesFromPURL,
fetchMaintainersFromPURL,
bulkFetchPackages,
} from "regxa";
// Single lookups
const pkg = await fetchPackageFromPURL("pkg:npm/lodash");
const versions = await fetchVersionsFromPURL("pkg:cargo/serde");
const deps = await fetchDependenciesFromPURL("pkg:pypi/flask@3.1.1");
const maintainers = await fetchMaintainersFromPURL("pkg:gem/rails");
// Bulk - fetches up to 15 packages concurrently
const packages = await bulkFetchPackages(["pkg:npm/lodash", "pkg:cargo/serde", "pkg:pypi/flask"]);For more control, create a registry instance directly:
import { create } from "regxa";
const npm = create("npm");
const pkg = await npm.fetchPackage("lodash");
const versions = await npm.fetchVersions("lodash");
const deps = await npm.fetchDependencies("lodash", "4.17.21");Wrap any registry with caching:
import { createCached } from "regxa";
const npm = createCached("npm");
// First call hits the network and writes to cache
const pkg = await npm.fetchPackage("lodash");
// Second call reads from cache (if TTL hasn't expired)
const same = await npm.fetchPackage("lodash");By default, regxa uses filesystem storage and follows platform cache conventions: ~/.cache/regxa on Linux (XDG), ~/Library/Caches/regxa on macOS, %LOCALAPPDATA%\regxa\cache on Windows. Override with REGXA_CACHE_DIR env var.
For edge/serverless runtimes, configure a custom unstorage driver (example: Cloudflare KV binding):
import { configureStorage, createCached } from "regxa";
import { createStorage } from "unstorage";
import cloudflareKVBindingDriver from "unstorage/drivers/cloudflare-kv-binding";
import "regxa/registries";
configureStorage(
createStorage({
driver: cloudflareKVBindingDriver({ binding: "REGXA_CACHE" }),
}),
);
const npm = createCached("npm");
const pkg = await npm.fetchPackage("lodash");import { parsePURL, buildPURL, fullName } from "regxa";
const parsed = parsePURL("pkg:npm/%40vue/core@3.5.0");
// { type: 'npm', namespace: '@vue', name: 'core', version: '3.5.0', qualifiers: {}, subpath: '' }
fullName(parsed); // "@vue/core"
buildPURL({ type: "cargo", name: "serde", version: "1.0.0" });
// "pkg:cargo/serde@1.0.0"import type { Package, Version, Dependency, Maintainer, Registry, ParsedPURL } from "regxa";regxa <command> [options]| Command | Description |
|---|---|
regxa info <purl> |
Package metadata (name, license, repo, latest version) |
regxa versions <purl> |
List all published versions |
regxa deps <purl> |
Dependencies for a specific version |
regxa maintainers <purl> |
Package maintainers / authors |
regxa cache status |
Show cache stats (entries, freshness) |
regxa cache path |
Print cache directory path |
regxa cache clear |
Remove all cached data |
regxa cache prune |
Remove stale entries |
| Flag | Description |
|---|---|
--json |
Output as JSON |
--no-cache |
Bypass cache, always fetch from registry |
regxa stores fetched data and freshness metadata in unstorage. Default TTLs:
| Data type | TTL |
|---|---|
| Package metadata | 1 hour |
| Version list | 30 minutes |
| Dependencies | 24 hours |
| Maintainers | 24 hours |
Each cached entry has a sha256 integrity hash. If the stored data doesn't match the hash, regxa refetches automatically.
Every registry returns the same normalized types:
interface Package {
name: string;
description: string;
homepage: string;
documentation: string; // docs URL (docs.rs, readthedocs, rubydoc, etc.)
repository: string;
licenses: string; // SPDX-normalized
keywords: string[];
namespace: string; // e.g. "@vue" for npm scoped packages
latestVersion: string;
metadata: Record<string, unknown>;
}
interface Version {
number: string;
publishedAt: Date | null;
licenses: string;
integrity: string;
status: "" | "yanked" | "deprecated" | "retracted";
metadata: Record<string, unknown>;
}
interface Dependency {
name: string;
requirements: string; // version constraint
scope: "runtime" | "development" | "test" | "build" | "optional";
optional: boolean;
}
interface Maintainer {
uuid: string;
login: string;
name: string;
email: string;
url: string;
role: string;
}