Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/offers-cli.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@rgstr/cli": patch
---

New: `rgstr offer <name> [--amount <usd>] [--why <text>] [-y]` — an offer on an unclaimed name you cannot claim, plan → confirm → file like `claim` (the floor, the amount, the why and the rule: the maintainer can claim the name for free at any time during the 30-day window, and that beats your offer; the card is saved at the checkout URL the filing returns and charged only if the offer wins). `rgstr offers` lists your organization's offers; `rgstr offer --withdraw <name>` ends one. `registered` is a state everywhere the CLI prints one: `whois` (`registered by Acme Labs · identity verified · routes to @acme/fastcache · no maintainer has claimed this name`, plus any open offer), `search` (`registered (Acme Labs)`), `check` (`registered (Acme Labs → @acme/fastcache)`, summary adds `and K registered`). API errors `offer_below_floor`, `offer_identity_required`, `offer_limit_reached`, `name_claimed`, `route_target_not_claimed` print their hint last like every other.
5 changes: 5 additions & 0 deletions .changeset/offers-mcp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@rgstr/mcp": patch
---

New: `rgstr_offer_plan(name, amount?, why?)` / `rgstr_offer_apply(plan_id)` — an offer on an unclaimed name under the same confirm-first rule (the plan is fingerprinted on name, amount, why, the active organization, the registry URL and the floor / window / open offer shown, re-read at apply; a change answers `plan_stale`), and `rgstr_offers` (read). The server instructions say an offer is a side effect: show the plan and confirm first. `rgstr_whois`, `rgstr_search` and `rgstr_check` carry the `registered` state.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

The terminal and agent front door for [rgstr](https://rgstr.org), the package
name registry over npm and PyPI: claim a name, prove it with the README
badge, route it, point your installs through it.
badge, route it, point your installs through it — or offer for an unclaimed
name you cannot claim (a maintainer's free claim always beats an offer).

| Package | What | Install |
| --- | --- | --- |
Expand Down
34 changes: 29 additions & 5 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
[rgstr](https://rgstr.org) is a package name registry over npm and PyPI: claim
the name of a package you maintain, prove it with the rgstr badge in your
README, and a person reviews the claim. Installs that go through rgstr resolve
claimed names to the real package; unclaimed names fall through unchanged.
claimed names to the real package; unclaimed names fall through unchanged. An
unclaimed name you cannot claim — a squat, a placeholder — can be offered for:
if no maintainer claims it during the window, it is *registered* to your
organization and routed to a name you claim (a claim always beats an offer).

`rgstr` is the CLI. The site is https://rgstr.org.

Expand All @@ -13,7 +16,8 @@ npm i -g @rgstr/cli # or install it: the command is `rgstr`
```

Nothing is filed or edited unless you say so: `claim` shows what it will file
and the README diff, asks, and files + writes only on yes. `setup` shows the
and the README diff, asks, and files + writes only on yes. `offer` shows the
floor, the amount, the why and the rule, then asks. `setup` shows the
diff and asks the same way. `--yes` skips the question; `--json` never asks and
does nothing without `--yes`. `setup` needs no sign-in.

Expand All @@ -22,11 +26,13 @@ does nothing without `--yes`. `setup` needs no sign-in.
| | |
| --- | --- |
| `rgstr claim [name]` | File the claim and put the proof in — after you confirm. In a package directory (package.json / pyproject.toml) it finds the package(s) and shows the plan: the claim it will file for your organization and the diff of the README rgstr reads — the folder the package declares on npm/PyPI, else the repository root — with the badge under the first heading; no README there means it says so and shows the file it would create. Then it asks `y/N`; on yes it files the claim and writes the badge (`--pr` then fetches, branches from `origin`'s default branch as it is now — never your local branch — puts the badge into that README, below any badge already there, commits, pushes and opens the PR with `gh`; your checkout is left where it was). `--yes` skips the question; without a terminal (CI, an agent) nothing happens until `--yes`. When `origin` is not the repository the package declares, nothing is written. At a workspace root it only lists the member packages; `--all` claims them all. `--here` puts the badge in the package's own README instead; rgstr reads it from the published package on the next publish. Without a package here (`rgstr claim <name>`) it files the claim once you confirm and prints the badge and an agent prompt. `rgstr claim --check` looks for the badge in the README rgstr reads and records it for the claim you filed — it files nothing new and writes no files, but it is not a read. A person then approves the claim. |
| `rgstr whois <name>` | Who claims a name, how much to trust it (unclaimed / claimed / verified — claimed plus paid identity verification), where it routes, and whether a claim is under review. No login needed. |
| `rgstr search <query>` | Names in rgstr's order, with trust, the claiming organization and weekly downloads. |
| `rgstr offer <name>` | For an unclaimed name you cannot claim (you do not maintain it): shows the floor for the name, the amount (`--amount <usd>`, whole dollars, default the floor), the why (`--why "<one line>"`, shown to the maintainer and the reviewer), any offer already open, and the rule — the maintainer can claim the name for free at any time during the 30-day window, and that beats your offer; your card is charged only if your offer wins, so their claim costs you nothing. Then it asks `y/N`; on yes it files the offer and prints the checkout URL where you save the card (charged only if the offer wins) and, if your organization's identity is not yet verified, the identity page; the 30-day window opens once both are done. `--yes` skips the question; without a terminal nothing is filed until `--yes`. `--withdraw` ends your open offer on the name, after the same question. |
| `rgstr offers` | Your organization's offers: name, amount, state (waiting for the card, or the card and identity verification / open / won — name registered / closed — the maintainer claimed the name; nothing was charged / lapsed / withdrawn) and when the window closes. |
| `rgstr whois <name>` | Who claims a name, how much to trust it (unclaimed / claimed / verified — claimed plus paid identity verification), where it routes, and whether a claim is under review. A registered name prints `registered by Acme Labs · identity verified · routes to @acme/fastcache · no maintainer has claimed this name`; an open offer prints ``offer $400 by Acme Labs, window closes Nov 20, 2026 — maintain this package? `rgstr claim <name>`, free``. No login needed. |
| `rgstr search <query>` | Names in rgstr's order, with trust (`registered (Acme Labs)` for a registered name), the claiming organization and weekly downloads. |
| `rgstr setup` | Point this project's installs at rgstr: shows the `.npmrc` (and yarn/bun/uv/poetry/pip) diff, asks, writes, then says what changed and how to undo it. No sign-in needed; signing in (`rgstr login`) is an optional follow-up to apply your organization's rules. `--yes` to skip the question, `--dry-run` to only look, `--off` removes exactly what it wrote. |
| `rgstr status` | Is this project pointed at rgstr, through which files, as which organization and login. |
| `rgstr check [name…]` | After `setup`: what resolving this project's dependencies through rgstr does. One line per direct dependency (package.json, pyproject.toml, requirements.txt) with its trust state, claimant and where it is served from, then `N of M names are claimed on rgstr; the rest fall through to npm / PyPI unchanged`. Informational — exit 0 whenever the check ran. |
| `rgstr check [name…]` | After `setup`: what resolving this project's dependencies through rgstr does. One line per direct dependency (package.json, pyproject.toml, requirements.txt) with its trust state, claimant and where it is served from — a registered name reads `registered (Acme Labs → @acme/fastcache)` and is served from the registrant's route — then `N of M names are claimed on rgstr; the rest fall through to npm / PyPI unchanged` (`and K registered` when there are any). Informational — exit 0 whenever the check ran. |
| `rgstr route <name> --to npm:other@^1` | Route a name you claim to the package that really ships it: shows the routes as they would be, asks, applies on yes (`--yes` to skip; `--rm <id>` removes one). Without `--to` / `--rm` it only lists them. |

Plus `rgstr login` / `logout` / `whoami` (device-code sign-in as the `rgstr-cli`
Expand All @@ -49,6 +55,23 @@ person at rgstr reviews. The name goes unclaimed → under review → claimed;
`rgstr whois <name>` shows where it is. "Verified" is something else: paid
identity verification of the organization, on top of a claim.

## Offers: a name you cannot claim

A name on npm can be a 0.0.1 placeholder while the project people mean by it
lives under another string. `rgstr offer <name>` (plan → `y/N` → file) puts an
amount on an unclaimed name; rgstr tells the maintainer, who can claim it for
free at any time in the 30-day window — and that beats the offer. Your card is
saved when you file and charged only if your offer wins, so a maintainer's
claim costs you nothing (there is nothing to refund). If nobody who maintains
the package claims it, the highest offer is
charged and the name is **registered** to the offering organization: public
(`whois`, `search`, `check` all say `registered`), routed only to a name that
organization already claims, kept with an annual fee, and still open to a
maintainer's claim as a dispute a reviewer decides. Offers need the
organization's identity verified, are capped at five open per organization,
and are not open on claimed names or scopes. Rules in full:
https://rgstr.org/registry/-/offers.

## Claim your name with an agent

In a checkout of the package's repository, `npx @rgstr/cli claim --yes --pr <name>`
Expand Down Expand Up @@ -86,6 +109,7 @@ URL. The CLI prints the hint as the last line; `--json` carries `code`,
```bash
RGSTR_TOKEN=<organization API key> RGSTR_ORG=<slug> rgstr claim --check --json
rgstr claim --yes --json # files the claim and writes the badge; without --yes it only reports the plan
rgstr offer <name> --amount 400 --yes --json # files the offer; without --yes it only reports the plan
rgstr setup --yes --json # --json never prompts: without --yes it reports the plan
```

Expand Down
14 changes: 14 additions & 0 deletions packages/cli/src/__tests__/check.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,20 @@ describe('rows and the summary line', () => {
expect(checkTableRows([disputed])[0][1]).toBe('claimed (disputed)');
});

it('a registered name routes to the registrant\'s claimed name and is not counted as claimed', () => {
const row = checkRow(dep('fastcache'), whois('fastcache', { state: 'registered', registrant: { name: 'Acme Labs', slug: 'acme' }, routesTo: '@acme/fastcache' }));
expect(row).toMatchObject({ held: false, holder: null, registered: true, registrant: 'Acme Labs', routesTo: '@acme/fastcache', servedFrom: "npm:@acme/fastcache (registrant's route)" });
expect(checkTableRows([row])[0]).toEqual(['fastcache', 'registered (Acme Labs → @acme/fastcache)', '—', "npm:@acme/fastcache (registrant's route)"]);
const routed = checkRow(dep('fastcache'), whois('fastcache', { state: 'registered', registrant: { name: 'Acme Labs', slug: 'acme' }, routesTo: '@acme/fastcache', routes: [route('@acme/fastcache', '^1')] }));
expect(routed.servedFrom).toBe("npm:@acme/fastcache@^1 (registrant's route)");
const held = checkRow(dep('computesdk'), whois('computesdk', { trust: 'claimed', state: 'held', holder: { name: 'ComputeSDK', slug: 'computesdk' } }));
const free = checkRow(dep('react'), whois('react'));
expect(summaryLine([held, row, free])).toBe('1 of 3 names are claimed on rgstr and 1 registered; the rest fall through to npm unchanged.');
expect(summaryLine([row, free])).toBe('0 of 2 names are claimed on rgstr and 1 registered; the rest fall through to npm unchanged.');
expect(summaryLine([held, row])).toBe('1 of 2 names are claimed on rgstr and 1 registered; every install here resolves where its claimant or registrant says.');
expect(summaryLine([row])).toBe('0 of 1 name is claimed on rgstr and 1 registered; every install here resolves where its claimant or registrant says.');
});

it('an unanswered name shows the reason and counts as not claimed', () => {
const row = unansweredRow(dep('Bad Name'), 'Invalid package name');
expect(row).toMatchObject({ held: false, trust: null, servedFrom: 'npm (unchanged)', error: 'Invalid package name' });
Expand Down
Loading
Loading