ID@agntn/registriesv0.5.0
One PURL. Every registry.
One TypeScript interface over npm, PyPI, crates.io, RubyGems, Packagist and Arch Linux. Package, versions, dependencies and maintainers come back in the same shape, from a CLI, a library call or an AI SDK tool. The registries keep their quirks to themselves.
- Registries
- 6
- 7 API hosts
- Lookups
- 4
- one shape each
- No key
- 6 registries
- public APIs only
$ pnpm add @agntn/registriesPURL in, package out
A package URL names the ecosystem, the namespace, the name and the version. The library parses it once, picks the adapter, asks the registry and hands back one Package. This panel walks through 6 packages. Each one starts as a recorded sample and gets swapped for the docs worker's live answer the moment it lands.
- pkg:npm/lodash, pkg:cargo/serde, pkg:alpm/aur/paru: one addressing scheme, ECMA-427
- License normalized to SPDX, repository URL canonicalized, versions dated
- A missing package is a typed NotFoundError, never an empty object
lodash 4.18.1
Lodash modular utilities.
- Licenses
- MIT
- Repository
- github.com/lodash/lodash
- Versions
- 117 · newest 2026-04-01
- Keywords
- modules, stdlib, util
Same calls, every adapter
Every registry is a class with the same four methods and a URL builder. Swap the PURL and the rest of the code stays. Upstream quirks, from npm's dist-tags to the AUR's RPC, stay inside the adapter and never leak through the public types.
- fetchPackage, fetchVersions, fetchDependencies, fetchMaintainers on every registry
- create('npm') imports one adapter on first use, nothing runs at import, no switch statement
- Bring your own Client for retries, timeouts, rate limiting and a User-Agent
Read
import { createFromPURL } from "@agntn/registries";// NpmRegistry, imported on the first create()const [registry, name] = await createFromPURL("pkg:npm/lodash");const pkg = await registry.fetchPackage(name);const versions = await registry.fetchVersions(name);const deps = await registry.fetchDependencies(name, pkg.latestVersion);const people = await registry.fetchMaintainers(name);// "4.18.1", 117 versions, 0 deps, 2 maintainersA terminal client with a lockfile
The same lookups from a shell. Answers land in the platform cache directory with a lockfile that records when each entry was fetched and how long it stays fresh. The second call for the same package never leaves the machine.
- registries info, versions, deps, maintainers, with --json for scripts
- Cache on unstorage with sha256 integrity and a TTL per data type
- Filesystem by default, any unstorage driver on the edge
- 4.18.12026-04-01ok
- 4.18.02026-03-31deprecated
- 4.17.232026-01-21ok
- 4.17.212021-02-20ok
Ask again for the same PURL and the answer comes out of the cache, not the registry, until the lockfile says its TTL ran out.
6 adapters, one shape
npm hides deprecation in a version field, crates.io yanks, Packagist marks nothing at all, and Arch splits into the official repos and the AUR. Each adapter maps its API onto the shared types and keeps the rest to itself. Adding one means writing that mapping, not the HTTP client, the retry policy or the cache.
Import@agntn/registries/registries/<name>
| PURL | How it reads | ||
|---|---|---|---|
| npm | pkg:npm/@scope/name | One package document from registry.npmjs.org. Scopes in the namespace, deprecated as a status. | deprecated |
| crates.io | pkg:cargo/name | Crate, versions, dependencies and owners across four endpoints. yanked as a status, docs.rs for docs. | yanked |
| PyPI | pkg:pypi/name | JSON API for metadata, Simple API for versions. Names normalized per PEP 503, extras become scopes. | yanked |
| RubyGems | pkg:gem/name | Gem, versions, dependencies of each version and owners. yanked as a status, rubydoc for docs. | yanked |
| Packagist | pkg:composer/vendor/name | One document per vendor/name with every version inside. php and ext-* requirements skipped. | nothing |
| Arch Linux | pkg:alpm/arch/name · pkg:alpm/aur/name | Official repos and the AUR behind one type, routed by namespace. One version per package, flagged out of date as deprecated. | deprecated |
One tool call, four hosts
Each tool is written once. The AI SDK, the MCP server and the Pi and OMP extensions all get that one definition, so a model asking about a PURL gets the same answer wherever it sits. Even the refusals match. A description that says "ignore previous instructions" is still just a description.
- Six AI SDK tools on the /ai subpath, packageTool through ecosystemsTool, one schema each
- registries mcp serves the same six over stdio, and the Pi and OMP extensions register them in a terminal
- Bulk lookups skip a failed package instead of failing the batch
npm
The agent gets the normalized Package as JSON. No prose, no guessing what npm meant.
- name
- "lodash"
- latestVersion
- "4.18.1"
- licenses
- "MIT"
Start with one command
One install gives you the library, the registries CLI and the MCP server. Hand it a PURL and read what the registry said.
- PinPre-1.0, so pin exact versions.
- KeysNo API keys. Every registry here answers the public.
- DataDescriptions and maintainer names are what the owner typed. Treat them as data.
First lookup
$ pnpm add @agntn/registriesimport { fetchPackageFromPURL } from "@agntn/registries";const pkg = await fetchPackageFromPURL("pkg:npm/lodash");pkg.latestVersion; // "4.18.1"pkg.licenses; // "MIT"