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.
@agntn/registries 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 -
registries info npm/lodash,registries versions cargo/serde,registries deps pypi/flask@3.1.1 - 🔁 Retry + backoff - exponential backoff with jitter, rate limiter interface
- 🪶 ESM-only, zero CJS - built with obuild
pnpm add @agntn/registriesFor the AI SDK tool (@agntn/registries/ai subpath), also install ai and zod:
pnpm add ai zodimport { fetchPackageFromPURL } from "@agntn/registries";
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:
registries info npm/lodash
registries versions cargo/serde
registries deps pypi/flask@3.1.1
registries maintainers gem/rails
registries deps alpm/aur/paruAdd --json for machine-readable output, --no-cache to skip the cache.
@agntn/registries/ai exports a ready-made tool for AI SDK apps:
import { generateText } from "ai";
import { packageTool } from "@agntn/registries/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 "@agntn/registries";
// 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, instantiate a concrete registry class:
import { Client } from "@agntn/registries";
import { NpmRegistry } from "@agntn/registries";
const npm = new NpmRegistry("https://registry.npmjs.org", new Client());
const pkg = await npm.fetchPackage("lodash");
const versions = await npm.fetchVersions("lodash");
const deps = await npm.fetchDependencies("lodash", "4.17.21");Use create("npm") when you prefer lookup through the registered ecosystem classes.
Wrap any registry with caching:
import { createCached } from "@agntn/registries";
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, @agntn/registries uses filesystem storage and follows platform cache conventions: ~/.cache/registries on Linux (XDG), ~/Library/Caches/registries on macOS, %LOCALAPPDATA%\registries\cache on Windows. Override with REGISTRIES_CACHE_DIR env var.
For edge/serverless runtimes, configure a custom unstorage driver (example: Cloudflare KV binding):
import { configureStorage, createCached } from "@agntn/registries";
import { createStorage } from "unstorage";
import cloudflareKVBindingDriver from "unstorage/drivers/cloudflare-kv-binding";
configureStorage(
createStorage({
driver: cloudflareKVBindingDriver({ binding: "REGISTRIES_CACHE" }),
}),
);
const npm = createCached("npm");
const pkg = await npm.fetchPackage("lodash");import { parsePURL, buildPURL, fullName } from "@agntn/registries";
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 "@agntn/registries";registries <command> [options]| Command | Description |
|---|---|
registries info <purl> |
Package metadata (name, license, repo, latest version) |
registries versions <purl> |
List all published versions |
registries deps <purl> |
Dependencies for a specific version |
registries maintainers <purl> |
Package maintainers / authors |
registries cache status |
Show cache stats (entries, freshness) |
registries cache path |
Print cache directory path |
registries cache clear |
Remove all cached data |
registries cache prune |
Remove stale entries |
| Flag | Description |
|---|---|
--json |
Output as JSON |
--no-cache |
Bypass cache, always fetch from registry |
@agntn/registries 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, @agntn/registries 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;
}