primcli 1.75.0 → 3.0.0-preview.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -150,9 +150,9 @@ enter it, wait for the email and take what you need from it:
150
150
  ```bash
151
151
  # Sign up with your address, then:
152
152
  primitive wait --from github.com --code # prints 482913
153
- primitive wait --from vercel.com --link # prints the sign-in link
153
+ primitive wait --from accounts.example.com --link # prints the sign-in link
154
154
  primitive wait --subject "verify" --timeout 90s
155
- primitive wait --from stripe.com --code --json
155
+ primitive wait --from billing.example.com --code --json
156
156
  ```
157
157
 
158
158
  `primitive wait` waits for the next email to the identity the CLI resolves (a
@@ -224,8 +224,55 @@ Use `primitive signin <email> --accept-terms`, then `primitive signin confirm <e
224
224
 
225
225
  Use `primitive logout --force` to remove local CLI credentials, pending email-code auth state, and stale credential locks without contacting Primitive. This is the recovery command when an interrupted auth command leaves the CLI saying another credential operation is already in progress.
226
226
 
227
+ Use `primitive org leave` to leave the organization you are signed in to. It asks for confirmation first; pass `--yes` to skip the prompt in scripts. Leaving is final. Only a person signed in with `primitive login` can leave; API keys are refused. The only owner of an organization cannot leave it until someone else is made an owner. If the server asks you to confirm in the dashboard, the command prints the confirmation page and, in a terminal, asks for the token it shows; in a script, pass the token with `--step-up-token`. Refusals from the server are printed as returned, and the command exits 1.
228
+
227
229
  Use `primitive signup <email>` for new account creation, then `primitive signup confirm <email> <code>` with the emailed verification code. Non-interactive signup is available with `--accept-terms`.
228
230
 
231
+ ### Switching organizations
232
+
233
+ Where the server supports it, browser login (`primitive login` with no email) logs you in as yourself rather than into one organization. The organization you choose in the browser becomes the current one, and you can switch without logging in again:
234
+
235
+ ```bash
236
+ primitive orgs # your organizations, your role, and whether the CLI may be used there
237
+ primitive org use acme # switch by name or id
238
+ primitive emails list --org beta # act in another organization for one command
239
+ ```
240
+
241
+ `--org` works on every command (set `PRIMITIVE_CLI_ORG` for the same effect; `primitive memories` keeps its own `--org` flag for organization-scoped memories). Each organization decides who may use the CLI there; when it refuses, `primitive orgs` and `primitive org use` show the reason (`not_a_member`, `cli_access_disabled_by_org`, `role_insufficient` or `cli_access_revoked`). The CLI keeps a short-lived access token per organization and gets a new one when it expires or is rejected. `primitive logout` signs you out of every organization.
242
+
243
+ Against a server without organization switching, and for logins saved by earlier CLI versions, the login stays bound to one organization and works as before. `--org` naming a different organization is refused for such a login, for an API key and for a connected agent profile.
244
+
245
+ `primitive login --permission <permission>` asks for a narrowed login. A server that does not offer narrowed CLI logins refuses it, and the CLI stops rather than logging in with a wider grant.
246
+
247
+ ### Grants and keys
248
+
249
+ `primitive whoami` shows your organization, role and grant when the server reports them. `primitive whoami --check mail:send` asks whether the current credential may use a permission, prints the decision, and exits 1 when it may not; nothing is enforced by asking. A request refused because the credential's grant lacks a permission fails with `grant_forbidden`, and the CLI names the permission.
250
+
251
+ ```bash
252
+ primitive keys list
253
+ primitive keys get <id> --json
254
+ primitive keys create --name orders --permission mail:read --rule <rule-id>
255
+ primitive keys update <id> --add mail:send,mail:reply
256
+ primitive keys rotate <id>
257
+ primitive keys revoke <id>
258
+ ```
259
+
260
+ `keys list` shows each key's id, prefix, name, permissions, the mail it reaches, expiry and last use, and `keys get` shows one key in full. Neither shows a secret. You see your own keys, and every key if your role may read them all.
261
+
262
+ `keys create` needs you to say what the key can do: a `--preset`, or `--permission` with the mail it reaches (`--address`, `--domain`, `--rule`, each repeatable, or `--all-mailboxes`). There is no default, and a super admin key needs `--preset super_admin`. A key never holds more than you do, and your organization's credential policy applies. `keys update` adds or removes permissions (`--add`, `--remove`) or replaces the key's email rules (`--rule`); the key keeps its id and secret. A request already held open with that key, such as a long poll or a wait, ends once when its permissions change.
263
+
264
+ Creating, rotating and changing a key need your approval in the browser. The command prints the approval link, opens it where it can, and waits for the one-time token the page shows. In a script, approve first and pass the token with `--step-up-token`. The new secret is printed once, alone on stdout (or in the JSON with `--json`), and the CLI never saves it, so store it right away. `keys revoke` asks for confirmation; pass `--yes` in scripts.
265
+
266
+ `primitive roles list`, `primitive roles groups`, `primitive roles mailbox-access` and `primitive roles access-mode` read roles, groups, mailbox access and the access mode. Where the server has not enabled them, they print `feature_disabled`. The built-in roles are `owner`, `admin`, `operator` and `member`. `operator` has every permission `admin` has except managing members, changing roles, sending invitations and billing. By default, owners, admins and operators may use the CLI. An organization can open it to every member by setting `cli_access` to `all_members` in its credential policy.
267
+
268
+ ### Trying the preview
269
+
270
+ Install the 3.0.0 preview with `npm install -g primitive@preview`. `npm install -g primitive` stays on the latest stable version.
271
+
272
+ ### Downgrading after signing in as yourself
273
+
274
+ A login as yourself is saved with `auth_method: "user_grant"`, which CLI 1.75.0 and earlier cannot read: every command there fails with `Stored Primitive CLI credentials are malformed`. Run `primitive logout` with this CLI before installing an older one. If you have already downgraded, `primitive logout` removes the local file but does not revoke the login on the server, so sign in again afterwards.
275
+
229
276
  ## Reply state and the agent loop
230
277
 
231
278
  Every inbound email carries reply state: `awaiting` is `you` when the latest
@@ -315,6 +362,7 @@ primitive emails get --id <inbound-email-id> --compact
315
362
  primitive sent list
316
363
  primitive sent delete --id <sent-email-id>
317
364
  primitive domains list
365
+ primitive billing
318
366
  primitive functions templates
319
367
  primitive functions init my-fn --template email-reply
320
368
  primitive functions logs --id <function-id>
@@ -427,8 +475,8 @@ signal or interaction, so two agents cannot keep acknowledging each other.
427
475
  acknowledgement for a message identified by its Message-Id.
428
476
 
429
477
  A plain `primitive reply` to an email the server classifies as an interaction
430
- the reply does not complete (a payment or contact request, or a kind this CLI
431
- cannot answer), or to informational mail or a status signal, is still sent.
478
+ the reply does not complete (a contact request, or a kind this CLI cannot
479
+ answer), or to informational mail or a status signal, is still sent.
432
480
  Without `--json` one `Warning:` line on stderr names the command that answers
433
481
  it; with `--json` the envelope carries `interaction_warning` (`code`,
434
482
  `email_id`, `kind`, `category`, `message`, `expected`), null otherwise.
@@ -629,65 +677,79 @@ primitive routes remove <route-id>
629
677
  Recipient routing is gated by an organization entitlement; routes are inert
630
678
  until it is enabled.
631
679
 
632
- ## x402 payments
633
-
634
- The `primitive payments` command group drives non-custodial x402 USDC payments. One agent registers a payout address and requests a payment; the paying agent signs locally with its own wallet key and settles. The key never leaves your machine. Networks are `base` and `base-sepolia`. Amounts take a human USDC value (`--amount-usdc 0.01`) or token base units (`--amount 10000`, since USDC has 6 decimals). Your org is resolved automatically from your API key, so payout registration takes no org flag.
635
-
636
- ```bash
637
- # Payee, one time: register the default address your org is paid at. Signs an
638
- # ownership message locally with your wallet key. Org is auto-resolved.
639
- primitive payments register-payout-address --network base-sepolia --label treasury
640
-
641
- # Payee: request a payment with a human USDC amount. Prints the challenge JSON
642
- # to stdout; a one-line summary goes to stderr.
643
- primitive payments charge --network base-sepolia --amount-usdc 0.01
644
- # Capture the challenge JSON to hand to the payer.
645
- primitive payments charge --network base-sepolia --amount-usdc 0.01 > challenge.json
646
-
647
- # Payer: sign and settle the challenge locally. Reads the challenge inline,
648
- # from a file, or piped on stdin.
649
- primitive payments pay --challenge-file challenge.json
650
- cat challenge.json | primitive payments pay
651
-
652
- # Email-native flow: the payee issues a challenge over an email thread.
653
- # Note: create-email-challenge takes --amount in token base units only; unlike
654
- # `charge` it has no --amount-usdc. USDC has 6 decimals, so multiply by
655
- # 1,000,000: 0.01 USDC is --amount 10000.
656
- primitive payments create-email-challenge --from payee@your-domain.example \
657
- --to payer@their-domain.example --amount 10000 --network base-sepolia
658
- # Payer (recommended): pay the email challenge in one step. Signs the challenge
659
- # locally with your wallet key AND sends the signed interaction.json, so you
660
- # skip the manual sign-then-send dance. --in-reply-to is the inbound challenge
661
- # email you received; it is fetched to address the payment to the payee, with
662
- # From defaulting to the payer it was sent to. The send is not threaded under
663
- # the challenge (the payment associates by interaction_id). The message carries
664
- # a short default note alongside the attachment; pass --body to customize it.
665
- primitive payments pay-email --challenge-file challenge.json \
666
- --in-reply-to <inbound-challenge-email-id> --wait
667
-
668
- # Advanced: sign only, without sending. Emits the portable interaction.json
669
- # artifact you can attach yourself (e.g. with `primitive send --attachment`).
670
- primitive payments pay-email-step --challenge-file challenge.json > interaction.json
671
-
672
- # Inspect a challenge by id, or list your registered payout addresses.
673
- primitive payments get-challenge --id <challenge-id>
674
- primitive payments list-payout-addresses
675
-
676
- # Read and update the org spend policy (kill-switch, per-payment and daily caps,
677
- # payee allowlist). The update merges: omitted fields keep their value.
678
- primitive payments get-spend-policy
679
- primitive payments update-spend-policy --max-per-payment 5000000
680
- ```
681
-
682
- `charge` is the friendly verb that matches the SDK `charge` and accepts `--amount-usdc`; `create-challenge` is the lower-level command that takes base-unit `--amount`. Either creates a challenge. All four signing commands accept `--json`. `register-payout-address` and `pay` print a human-readable summary by default and raw JSON with `--json`; `pay-email` and `pay-email-step` print JSON by default (the send result and the `interaction.json` bytes, respectively), and `--json` switches them to a fuller envelope object.
683
-
684
- The signing commands (`register-payout-address`, `pay`, `pay-email`, and `pay-email-step`) need your wallet key. `pay-email` is the recommended payer path for the email-native flow: it signs and sends in one step. `pay-email-step` signs only and emits the `interaction.json` artifact for advanced use where you want to deliver it yourself. Set the key in `PRIMITIVE_X402_PRIVATE_KEY` (a `0x`-prefixed hex private key) so it never lands in shell history or the process list:
685
-
686
- ```bash
687
- export PRIMITIVE_X402_PRIVATE_KEY=0x...
680
+ ## Supported receiver API
681
+
682
+ A receiver in a coding harness that the CLI does not support must decide
683
+ which received emails may wake its session. `primitive/receiver` exposes the
684
+ CLI's own contact admission policy for that decision. It is the only
685
+ supported library import from this package.
686
+
687
+ ```ts
688
+ import { createWakePolicy, WakePolicyError } from "primitive/receiver";
689
+
690
+ const policy = createWakePolicy({
691
+ recipient: "agent@example.com",
692
+ contactRequests: false,
693
+ readContactPolicy: (signal) => readPolicy(signal),
694
+ readContactPage: (cursor, signal) => readContacts(cursor, signal),
695
+ readNetworkAdmission: (emailId, sender, signal) =>
696
+ checkNetworkAdmission(emailId, sender, signal),
697
+ });
698
+
699
+ const decision = await policy.admit({ emailId, sender, receivedAt }, signal);
700
+ if (decision.kind !== "denied") {
701
+ const confirm = await policy.recheck(decision, signal);
702
+ confirm(); // immediately before the wake
703
+ wake(decision.kind === "request" ? "contact request" : "mail");
704
+ }
688
705
  ```
689
706
 
690
- A `--private-key` flag is available as an escape hatch for scripted use, but the environment variable is preferred. The non-signing commands (`charge`, `get-challenge`, `list-payout-addresses`, `get-spend-policy`, `update-spend-policy`) need only your Primitive API key. Run `primitive payments <command> --help` for the full flag list of any command.
707
+ The three readers call the API as the connected address. Each returns the
708
+ `data` of one operation:
709
+
710
+ | Reader | API operation | CLI command |
711
+ | --- | --- | --- |
712
+ | `readContactPolicy` | `getAgentContactPolicy` | `primitive contacts get-agent-contact-policy` |
713
+ | `readContactPage` | `listAgentContacts`, 100 rows per page, with the next `cursor` or `null` | `primitive agent contacts list` |
714
+ | `readNetworkAdmission` | `checkDefaultNetworkContactAdmission` | `primitive agent-networks check-default-network-contact-admission` |
715
+
716
+ `admit` returns one of three decisions for an email:
717
+
718
+ - `allowed`: the email may wake the session.
719
+ - `request`: a first-contact request from an unknown sender. It is returned
720
+ only when `contactRequests` is true and the owner allows contact requests.
721
+ Show it as a request, never as a task. Allow at most one per sender.
722
+ - `denied`: the email must not wake the session.
723
+
724
+ An admission carries the email ID, sender and received time, normalized to
725
+ lower case and ISO 8601. Pass the sender only after you check that it is
726
+ authenticated.
727
+
728
+ Permission can change between admission and wake. Call `recheck` with the
729
+ admission that `admit` returned, then call the function it returns
730
+ synchronously right before the wake. That function throws when the
731
+ permission was refreshed since, or when the policy read behind it is older
732
+ than 30 seconds. Admissions are bound to the policy that issued them, so
733
+ `recheck` rejects any other object with a `TypeError`.
734
+
735
+ Failures are `WakePolicyError` values. Branch on `code`:
736
+
737
+ | `code` | `retryable` | Meaning |
738
+ | --- | --- | --- |
739
+ | `policy_read_failed` | true | A policy read failed temporarily. The policy paces repeated reads itself. |
740
+ | `network_admission_pending` | true | The server has not settled network admission for this email. Retry in a few seconds. |
741
+ | `permission_changed` | true | Permission changed or expired before the wake. Admit the email again. |
742
+ | `policy_unavailable` | false | The policy is invalid or access was lost. Retrying will not help until access is restored. |
743
+
744
+ A reader reports a temporary failure, such as a rate limit, a 5xx response
745
+ or a dropped connection, by throwing `new WakePolicyError("policy_read_failed")`.
746
+ Any other error from a reader makes the policy unavailable. An aborted signal
747
+ rejects with the signal's reason.
748
+
749
+ The package also resolves `primitive/bin/*`, `primitive/dist/*` and
750
+ `primitive/man/*` so existing deep imports keep working. Those paths are not
751
+ supported. File names under `dist/` other than `dist/receiver.js` change
752
+ between releases, and the paths may stop resolving in a later major version.
691
753
 
692
754
  ## Migrating from `@primitivedotdev/sdk` CLI
693
755
 
@@ -1357,7 +1419,6 @@ untrusted content names the command:
1357
1419
 
1358
1420
  | Server classification | `how to answer` |
1359
1421
  |---|---|
1360
- | `x402.payment/1` | `primitive payments challenge-from-email --id <id>` to review, `primitive payments pay-email --in-reply-to <id>` to pay |
1361
1422
  | `primitive.contact/1` | `primitive contacts accept --id <id>` |
1362
1423
  | `repeat.tick/1` | reply if needed; `primitive repeat stop --id <id>` once its goal is met |
1363
1424
  | `repeat.stop/1`, status signals, `fyi` | no reply needed |
@@ -1370,7 +1431,7 @@ When the server classifies the email as an interaction, its `interaction.json`
1370
1431
  part is left out of the attachment list. `primitive inbox next` carries the
1371
1432
  same `interaction` and `next_actions` in its JSON envelope, prints the same
1372
1433
  line above the conversation, and ends with matching instructions: the answering
1373
- command first for payment and contact requests (a reply only as optional), "No
1434
+ command first for contact requests (a reply only as optional), "No
1374
1435
  reply needed." for fyi mail, signals and repeat-stopped notices, and the stop
1375
1436
  command for a repeat only when the recipient may stop it. `inbox next` skips
1376
1437
  mail that needs no reply (fyi mail, status signals, repeat-stopped notices):
@@ -200,7 +200,7 @@ export const WAKE_SENTENCES = {
200
200
  export function wakeSentence(label) {
201
201
  if (label === "fyi" || label === "repeat.stop/1")
202
202
  return WAKE_SENTENCES.no_reply;
203
- if (label === "x402.payment/1" || label === "primitive.contact/1")
203
+ if (label === "primitive.contact/1")
204
204
  return WAKE_SENTENCES.answer_with_command;
205
205
  if (label === "repeat.tick/1") return WAKE_SENTENCES.repeat;
206
206
  return WAKE_SENTENCES.unsupported;
package/bin/run.js CHANGED
@@ -72,5 +72,19 @@ const { writeRootAuthContextIfNeeded } = await import(
72
72
  );
73
73
  await writeRootAuthContextIfNeeded();
74
74
 
75
+ // `--org <org>` works on every command: take it out of argv before oclif
76
+ // parses the command, and pass it on as PRIMITIVE_CLI_ORG. Applied last, so
77
+ // the detached receiver and signal workers started above never inherit it.
78
+ {
79
+ const { applyGlobalOrgFlag } = await import(
80
+ "../dist/oclif/global-org-flag.js"
81
+ );
82
+ const orgFlagError = applyGlobalOrgFlag();
83
+ if (orgFlagError) {
84
+ process.stderr.write(`${orgFlagError}\n`);
85
+ process.exit(2);
86
+ }
87
+ }
88
+
75
89
  const { execute } = await import("@oclif/core");
76
90
  await execute({ dir: import.meta.url });