@x12i/pagx
v2.0.1
Published
An npm-style CLI and SDK for scoped agentic packages
Readme
pagx
An npm-style CLI and Node.js SDK for public agentic packages: versioned,
scoped bundles of agent definitions, prompts, and assets. Publish them to the
hosted pagx registry (or one you host on Cloudflare), and install them into any
project's .pagx/ folder.
- The npm package is
@x12i/pagx; the command it installs ispagx. - Agent package scopes (for example
@x12i/helper) live in your pagx registry and are unrelated to the CLI's npm scope. - The default registry is
https://registry.pagenti.dev. Reading and installing need no configuration; publishing needs a publisher token. - The Cloudflare Worker owns authentication, metadata, and storage. Clients never receive Cloudflare, D1, R2, or database credentials.
Requirements
- Node.js 22 or later
Upgrading to 2.0
- Access is set per version. Publishing one version as
restrictedkeeps the package's public versions installable, andlatestfor other readers stays on the last public version. - Tokens have a role:
admin,read-write, orread.read-writeandreadtokens can be limited to named packages. Tokens issued by 1.x stayadminfor their scope. verifyPublisherTokenreturns{ username, role, packages }. Replaceconst username = await verifyPublisherToken(...)withconst { username } = await verifyPublisherToken(...).- 1.x clients keep installing, viewing, searching, and publishing public packages against the 2.0 registry.
Install
npm install -g @x12i/pagx
pagx --versionUpgrade that global command later with:
pagx upgradepagx upgrade installs the latest @x12i/pagx with the package manager that
installed the pagx command: npm install -g, pnpm add -g, yarn global add,
bun add -g, or volta install.
Or run it without a global install:
npx @x12i/pagx --helpTo use the SDK from code, add it to your project:
npm install @x12i/pagxQuick start
# 1. Install a package into ./.pagx/@x12i/helper (uses the default registry)
pagx install @x12i/helper
# 2. Or install a package folder you are authoring, without any registry
pagx install ./agents/my-agent # -> ./.pagx/@x12i/my-agent
# 3. Publish. Set PAGX_TOKEN to the token the registry operator gave you.
# Clients do not mint tokens. PowerShell: $env:PAGX_TOKEN = "..."
export PAGX_TOKEN=...
pagx init agents/my-agent --scope @x12i -y
pagx publish agents/my-agentpagx ls, pagx check, pagx init, pagx pack, pagx uninstall, and local-path
installs never contact a registry.
Commands
If you know npm, use the same words. pagx install, pagx i, pagx add,
pagx isntall, and the dashed forms pagx -i and pagx --install are the same
command. A unique prefix works too, so pagx inst and pagx pub are enough.
pagx --help lists every command.
| npm command | pagx | What it does here |
| --- | --- | --- |
| install, i, add, in, ins, inst, … | same, plus -i and --install | Install into .pagx/ from the registry, a folder, or a .tgz. No arguments installs dependencies from pagx.json. |
| uninstall, remove, rm, r, un, unlink | same | Remove the package from .pagx/ and from pagx.json. |
| update, up | same | Reinstall the newest version that matches the saved range. |
| — | upgrade | Install the latest version of the pagx command itself. --dry-run prints the package-manager command. |
| ci | same | Reinstall the exact versions in pagx-lock.json. Fails if the lock is missing or out of date. |
| ls, list, la, ll | same | List what is installed. A package with agent.type includes that type. la and ll include the path. |
| outdated | same | Compare installed versions with the registry. Exits with code 1 when something is behind. |
| — | check | Compare installed agent.type values with acceptedTypes in pagx.json. Exits with code 1 on a mismatch. Does not change .pagx/. |
| publish | same, plus pub and --publish | Pack the current directory (or a folder) and upload it. |
| pack | same | Write a .tgz without uploading. |
| init, create, innit | same | Write pagx.json. -y skips prompts (there are none; defaults are always used). |
| view, info, show, v | same | Metadata. Try version, versions, readme, or dist-tags. |
| search, find, s, se | same | Filter package names. --scope limits the list. --type keeps packages whose latest version has that agent.type. |
| config, get, set, c | same | set, get, list, and delete for registry, scope, _authToken, and //<registry>/:@<scope>:_authToken. |
| whoami | same | The username, role, and package list for the selected token. Pass --scope when several tokens are stored. |
| ping | same | GET /health on the registry. |
| version | same | Print or bump pagx.json (patch, minor, major, or an exact version). |
| dist-tag, dist-tags | ls | List tags. Change one with pagx publish --tag <tag>. |
| owner ls | same | Publishers of a package. The scope is the owner. |
| prefix, root | same | Project directory, and the .pagx folder packages install into. |
| logout | same | Delete publisher tokens from ~/.pagxrc. |
| login | use token | There is no password login. pagx token signs a publisher token. |
Flags you already use:
| Flag | Meaning |
| --- | --- |
| -g, --global | Install, update, remove, or list in ~/.pagx instead of ./.pagx. Does not edit pagx.json. |
| --save / --no-save | Update pagx.json and pagx-lock.json (the default), or skip both. |
| -E, --save-exact | Save 1.2.3 instead of ^1.2.3. |
| --dry-run | Resolve or pack without writing or uploading. |
| --ignore-scripts | Accepted. pagx never runs lifecycle scripts. |
| --tag <tag> | Dist-tag for publish (latest by default). |
| --access | public or restricted for publish. Omitted defaults to restricted when pagx.json contains "private": true, and to public otherwise. |
| --type <type> | With search, keep packages whose latest version declares this agent.type. |
| --registry, --scope, --userconfig, --json | Override configuration. --json works on any command. |
| -v, --version | Print the CLI version. pagx version is the separate bump command. |
-D / --save-dev, --save-optional, and --save-peer are rejected: a pagx
project has one dependencies list. pagx run, pagx test, pagx start,
pagx unpublish, and pagx link are rejected with the reason, instead of
failing as unknown commands.
These commands are pagx itself, not npm. Clients of the hosted registry do not run them. The registry operator issues a token from this repository:
npm run token:issue -- clientname # writes .keys/clientname-token.txtpagx keygen and pagx token remain for a registry you host yourself:
pagx keygen --out .keys
pagx token --key .keys/private-key.json --username x12i --expires-in 3600 --out .keys/publisher-token.txt
pagx token --key .keys/private-key.json --username x12i --role read-write --package @x12i/helper --out .keys/helper-token.txt--role is admin (the default), read-write, or read. Repeat --package
to limit a read-write or read token to named packages in its own scope.
An admin token always covers the whole scope. Tokens issued before roles
existed keep full access to their scope.
Package specs follow npm syntax: @scope/name, @scope/[email protected],
"@scope/name@^1.0.0", or @scope/name@<dist-tag>.
Configuration
Nothing is required to read or install a public package from the default
registry. To publish, to read a restricted package, or to use a registry you
host, create a user (~/.pagxrc) or project (./.pagxrc) config file:
registry=https://registry.pagenti.dev
scope=@x12i
//registry.pagenti.dev/:_authToken=${PAGX_TOKEN}Public installs need no token. A restricted version is visible only to a token
that may read that package. Install, view, search, and ping send a token
when .pagxrc or PAGX_TOKEN has one. For a package read, a scope line in
.pagxrc is used when that package's scope has one. Set PAGX_TOKEN to the
publisher token the registry operator issued for your username. A bare
_authToken=... also works for the registry configured in the same file, but
URL-scoped tokens are recommended. Keep .pagxrc out of source control.
One registry token is enough when you publish a single scope. To publish more than one scope from the same machine, store a token for each scope. Publish reads the scope from the package name and sends that token:
registry=https://registry.pagenti.dev
scope=@cursor
//registry.pagenti.dev/:@cursor:_authToken=${CURSOR_TOKEN}
//registry.pagenti.dev/:@claude:_authToken=${CLAUDE_TOKEN}
//registry.pagenti.dev/:@openai:_authToken=${OPENAI_TOKEN}
//registry.pagenti.dev/:@x12i:_authToken=${X12I_TOKEN}scope= is the default for pagx init, pagx search, and pagx whoami.
pagx whoami --scope @claude uses the @claude token. Publishing
@x12i/funcx uses the @x12i token even when scope= is @cursor. If that
scope has no line, publish stops instead of sending another scope's token.
When any :@<scope>:_authToken line matches the active registry, those lines
are the only file tokens used. PAGX_TOKEN, or loadConfig({ token }), still
overrides the file and is sent for every scope.
Precedence, highest first:
- Command-line options
- Environment variables:
PAGX_REGISTRY,PAGX_TOKEN,PAGX_SCOPE,PAGX_USERCONFIG - The nearest project
.pagxrc ~/.pagxrc- The built-in default,
https://registry.pagenti.dev
An explicitly changed registry never receives an unrelated token, and
a bare _authToken is never sent to the built-in default; use PAGX_TOKEN or
a URL-scoped token for it. A token stored for one scope is never sent for
another scope. Production endpoints require HTTPS; HTTP is allowed
only on localhost.
Package format
Every package has a pagx.json (or package.json) with a scoped name and a
canonical SemVer version. Other manifest fields and files can hold any agent
metadata and assets. agent.type is an optional label for the consumer that
knows how to read those files. Install extracts the archive into .pagx/ and
stops. pagx does not copy or symlink files into .cursor/, .claude/, or any
other directory.
{
"name": "@x12i/react-best-practices",
"version": "1.0.0",
"description": "Enforces modern React and Tailwind patterns",
"agent": {
"type": "cursor-rules",
"typeVersion": 1,
"entrypoint": "rules/"
}
}agent.type, when present, is lowercase words separated by single hyphens, at
most 64 characters: cursor-rules, not Cursor Rules or cursor_rules. The
registry does not decide which values exist. cursor-rules, claude-skill,
and memorix-pack are conventions a harness may recognize. cursor-rule and
cursor-rules are different labels.
agent.typeVersion is an optional integer from 1 to 999. It tells the harness
which shape that type used, separately from the package's SemVer. It is
rejected without agent.type. A missing generation means the harness uses
whatever default it has for that type.
agent.entrypoint, when present, is a relative path inside the package. A
trailing slash means a directory. Publish fails when that path is not a file or
a directory prefix in the archive. pagx does not check that the files match
the type.
acceptedTypes is not a package field. It belongs on the project pagx.json.
Installed and locally authored packages live under .pagx/ in your project:
.pagx/
@x12i/
helper/
pagx.json
README.md
AGENT.md
assets/See examples/helper for a complete package.
Publish an agentic package
pagx init .pagx/@x12i/helper --scope @x12i -y
pagx publish .pagx/@x12i/helper
pagx publish .pagx/@x12i/helper --tag next
pagx publish .pagx/@x12i/helper --dry-runInside the package directory, pagx publish works without a path, just like
npm publish.
- Names are globally unique as the full
@username/namepair; different scopes may reuse a short name. - A username has exactly one scope, equal to that username.
adminandread-writetokens publish only there. Areadtoken cannot publish. - Versions are immutable: bump the version before republishing.
- Publishing updates the chosen dist-tag (
latestby default, even when an older version is published). Use--tag nextfor prereleases. - SemVer build metadata (
1.0.0+build) is rejected so equivalent versions can't produce ambiguous releases. "private": truein the package manifest publishes that version asrestrictedunless--access publicis set. Any other--accessvalue is rejected. The setting applies to the version being uploaded.- A scope must be allowed to host private packages, or a
restrictedupload is rejected. Public versions of the same package stay installable.latestfor everyone else stays on the last public version.
Archives are gzip tarballs. Publishing excludes .backup, .pagx-install.json,
.git, .keys, .pagxrc, .env*, and node_modules. No lifecycle scripts or
agent code run during packing or installation. README.md (or README) is
optional and is exposed by the registry APIs.
Default limits: 20 MiB compressed, 32 MiB of file data, 10,000 entries, 64 KiB of manifest JSON, and 512 KiB of README. Links, special files, traversal paths, duplicate paths, and nonportable Windows names are refused.
Install and back up
pagx install @x12i/helper
pagx i @x12i/[email protected]
pagx install "@x12i/helper@^1.0.0"
pagx install @x12i/helper@next
pagx update
pagx outdated
pagx ls
pagx uninstall @x12i/helperInstall a local folder or tarball
Any spec that isn't a scoped name is a local path, as with npm install ./dir:
pagx install ./agents/neo-networks # a package folder with pagx.json
pagx install ./x12i-neo-networks-1.0.0.tgz # a file from pagx pack
pagx install file:../shared/agent # npm's file: formThe folder is packed with the same rules as pagx publish (so .env*, .git,
.keys, and node_modules are left out), validated, and installed into
.pagx/@scope/name from its manifest name, with the same backup and receipt
handling as registry installs. No registry or token is needed. With --save
(the default) pagx.json records "@scope/name": "file:agents/neo-networks",
and pagx install, update, and ci reinstall from that path. outdated
skips file: dependencies.
Installation downloads compressed bytes, checks SHA-512 integrity and archive
identity, and extracts into a staging directory. The destination is
<working-directory>/.pagx/@scope/name, or ~/.pagx/@scope/name with --global.
Before replacement, different existing contents are compressed into
.pagx/@scope/name/.backup/<content-sha256>.tgz. The content hash covers file
paths, bytes, and empty directories, and ignores timestamps, modes, backups, and
the pagx receipt.
- Each historical version of the contents is backed up once, and all prior backups survive upgrades and downgrades.
- Local extra files (including hidden files excluded from publication) are included in backups.
- Identical contents are neither replaced nor backed up.
- A failure before the directory swap preserves the old package. If a swap rollback fails, pagx keeps a recovery directory and reports its location.
- Per-package locks prevent competing local installs. After a crash, inspect
.pagx/.install-*before clearing the reported stale lock.
Dependencies and lock file
Explicit installs save dependency selectors in the working directory's
pagx.json and record resolved versions and integrity in pagx-lock.json
(unless --no-save is given). pagx install with no arguments installs every
entry in dependencies, reusing matching lock entries. Explicit package specs
always resolve the selector again. This is an npm-style workflow without an npm
dependency graph or lifecycle scripts.
{
"dependencies": { "@x12i/helper": "^1.0.0" },
"acceptedTypes": ["cursor-rules", "claude-skill"]
}acceptedTypes is optional project policy. pagx check compares it with the
agent.type of each installed package. Install still extracts packages that
are not on the list. Omit the key to skip the check. An empty array accepts
nothing, including packages with no type. The check matches the type string
only, not typeVersion. It exits with code 1 when something does not match,
and it does not change .pagx/.
Inspect packages
pagx view @x12i/helper
pagx view @x12i/helper versions
pagx view @x12i/helper readme
pagx search --scope @x12i
pagx search react --type cursor-rules
pagx pack .pagx/@x12i/helperSDK
@x12i/pagx is the library behind the CLI. Pagx.open() reads the same
.pagxrc and environment variables, then the methods match the commands:
install, update, uninstall, ci, list, outdated, search, view,
publish, pack, whoami, and ping.
import { Pagx } from '@x12i/pagx';
const pagx = await Pagx.open(); // cwd, .pagxrc, PAGX_REGISTRY, PAGX_TOKEN
await pagx.install(['@x12i/helper']); // writes .pagx/ and pagx.json
await pagx.update(); // within the saved ranges
console.log(await pagx.list());
console.log(await pagx.view('@x12i/helper', 'version'));
await pagx.uninstall(['@x12i/helper']);The same functions are available without the class, which is what the CLI calls:
import { loadConfig, installPackages, publishPackage, RegistryClient } from '@x12i/pagx';
const config = await loadConfig({ cwd: process.cwd() });
await installPackages(config, ['@x12i/helper@latest']);
await publishPackage(config, '.pagx/@x12i/helper');
const catalog = await new RegistryClient(config).json('/v1/packages?limit=50');| Export | CLI equivalent |
| --- | --- |
| Pagx.open(options) | Reads configuration, then exposes the methods below |
| installPackages / pagx.install | pagx install (updates pagx.json and pagx-lock.json unless save: false) |
| updatePackages / pagx.update | pagx update |
| removePackages / pagx.uninstall | pagx uninstall |
| ciPackages / pagx.ci | pagx ci |
| listInstalled / pagx.list | pagx ls |
| outdatedPackages / pagx.outdated | pagx outdated |
| searchPackages / pagx.search | pagx search (type is --type) |
| checkPackages / pagx.check | pagx check |
| viewPackage / pagx.view | pagx view |
| packageOwners / pagx.owners | pagx owner ls |
| publishPackage / pagx.publish | pagx publish |
| packDirectory / pagx.pack | pagx pack |
| initPackage / pagx.init | pagx init |
| bumpManifestVersion / pagx.version | pagx version |
| installPackage(config, spec) | Install one spec only. Does not edit pagx.json |
| installLocal(cwd, path) | Install a folder or .tgz into <cwd>/.pagx. No registry |
| DEFAULT_REGISTRY | The registry used when nothing else is configured |
| uninstallPackage(cwd, spec) | Delete one install directory only |
| loadConfig / writeConfig | .pagxrc. writeConfig with a scope stores //<registry>/:@<scope>:_authToken |
| tokenForScope(config, scope) | Token publish sends for that scope |
| RegistryClient | whoami(), ping(), metadata(name), download(path), json(path) |
| parseName, parseSpec, resolveVersion, validateManifest, validateAgentType, readAcceptedTypes | Spec, manifest, and project-policy checks |
| integrity, inspectArchive | SHA-512 and archive checks |
| generatePublisherKeys, createPublisherToken, verifyPublisherToken | pagx keygen and pagx token |
| PagxError | Error with an HTTP-style status |
Key helpers are also exported from @x12i/pagx/auth. The package is ESM
(import, not require) and requires Node.js 22 or later.
import { generatePublisherKeys, createPublisherToken } from '@x12i/pagx';
const keys = await generatePublisherKeys();
const token = await createPublisherToken(keys.privateKey, 'x12i', { expiresIn: 3600 });Cloud credentials stay entirely server-side. The SDK never receives D1, R2, or Cloudflare tokens.
Server and authentication
The registry server (in server/) uses @x12i/hybrid-docstore with
Cloudflare D1 and R2. Version documents offload archives and READMEs to R2;
metadata, dist-tags, and public verification keys live in D1. The hybrid
interceptor rolls back uploaded objects when SQL fails, and a custom D1 provider
commits immutable version creation and tag updates in one D1 batch. Local
Wrangler development provides SQLite-backed D1 and local R2 storage.
Publisher tokens use Ed25519 signatures (EdDSA JWT). The trusted issuer holds the private signing key; the registry verifies with public keys. HTTPS encrypts transport; JWT claims are signed, not encrypted. Private signing keys are never stored as ordinary documents. Optional server-side issuance reads its private key from a Cloudflare secret. Key-generation APIs return private material once, over an administrator-authenticated connection.
pagx keygen --out .keys
pagx token --key .keys/private-key.json --username x12i --expires-in 3600 --out .keys/publisher-token.txtMore detail:
Development
git clone [email protected]:x12i/pagx.git
cd pagx
npm ci
npm run check # typecheck, tests, build
npm run dry-run --workspace server # validate the Worker bundle
npm pack --dry-run # inspect the npm tarballDuring development, run node dist/client/cli.js in place of pagx. On
Windows, the tests' local Worker runtime requires the
Microsoft Visual C++ Redistributable.
Useful scripts: npm run setup:local, npm run dev:server,
npm run db:migrate:local, npm run db:migrate:remote, and
npm run deploy:server.
Releasing
Nothing publishes or deploys automatically.
Registry. devops-agent runs
deploy/registry.workflow.json: it creates or
reuses the R2 bucket and D1 database, writes server/wrangler.deploy.json with
the real D1 ID, applies server/migrations (tracked in d1_migrations, like
Wrangler), deploys the Worker, sets the PUBLIC_KEYS_JSON and ADMIN_TOKEN
secrets, and waits for /health. The pagx-specific steps live in the
deploy/pagx-devops.mjs plugin.
# CLOUDFLARE_ACCOUNT_ID and CLOUDFLARE_API_TOKEN in the environment or .env
npm run release:registry:plan # validate, no cloud calls
npm run release:registry # idempotent; safe to rerun
npx devops-agent history # encrypted action journal in .safe-actions/The script reads .keys/public-keys.json and keeps the admin secret in
.keys/admin-token.txt (created on first run). It fails if
https://registry.pagenti.dev/health does not answer. The signing private key stays in
.keys/ and is not uploaded. Issue a client token with:
npm run token:issue -- clientnameThe workflow can also run directly:
npx devops-agent workflow run deploy/registry.workflow.json --plugin deploy/pagx-devops.mjs
with PAGX_PUBLIC_KEYS_JSON and PAGX_ADMIN_TOKEN set.
npm package. Publishing requires membership in the @x12i npm scope.
npm run check
npm version <major|minor|patch>
npm publish --access public
git push --follow-tags