primcli 1.6.0 → 1.7.0

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.
Files changed (2) hide show
  1. package/dist/oclif/index.js +417 -3
  2. package/package.json +1 -1
@@ -7,7 +7,7 @@ import process$1 from "node:process";
7
7
  import { createInterface } from "node:readline/promises";
8
8
  import { hostname } from "node:os";
9
9
  import { spawn } from "node:child_process";
10
- import { X402Error, createX402Client } from "@primitivedotdev/sdk/x402";
10
+ import { concat, getAddress, hexToBytes, keccak256, stringToBytes } from "viem";
11
11
  import { privateKeyToAccount } from "viem/accounts";
12
12
  //#region \0rolldown/runtime.js
13
13
  var __defProp = Object.defineProperty;
@@ -21020,8 +21020,8 @@ const PRIMITIVE_TEAM_AUTHOR = {
21020
21020
  name: "Primitive Team",
21021
21021
  url: "https://primitive.dev"
21022
21022
  };
21023
- const SDK_VERSION_RANGE = "^1.6.0";
21024
- const CLI_VERSION_RANGE = "^1.6.0";
21023
+ const SDK_VERSION_RANGE = "^1.7.0";
21024
+ const CLI_VERSION_RANGE = "^1.7.0";
21025
21025
  const ESBUILD_VERSION_RANGE = "^0.27.0";
21026
21026
  function renderHandler() {
21027
21027
  return `// env.PRIMITIVE_API_KEY, env.PRIMITIVE_WEBHOOK_SECRET, and
@@ -24411,6 +24411,420 @@ var OrgSecretsSetCommand = class OrgSecretsSetCommand extends Command {
24411
24411
  }
24412
24412
  };
24413
24413
  //#endregion
24414
+ //#region ../sdk-node/dist/x402/index.js
24415
+ /**
24416
+ * x402 client-side signing.
24417
+ *
24418
+ * The payer signs an EIP-3009 `transferWithAuthorization` over the customer's
24419
+ * own key; the key never leaves them. This module derives the interaction-bound
24420
+ * nonce and assembles the EIP-712 typed data and the wire payload. The byte
24421
+ * layout here MUST match the platform verifier exactly; a normative test vector
24422
+ * (see sign.test.ts) locks the nonce derivation to the same value the server
24423
+ * recomputes.
24424
+ */
24425
+ /** A challenge nonce is 32 bytes rendered as 64 lowercase hex chars, no 0x. */
24426
+ const CHALLENGE_NONCE_RE = /^[0-9a-f]{64}$/;
24427
+ /** Single-byte domain separator between the variable-length string fields. */
24428
+ const FIELD_SEPARATOR = new Uint8Array([0]);
24429
+ /**
24430
+ * Derive the EIP-3009 nonce bound to a specific interaction step:
24431
+ *
24432
+ * keccak256( utf8(lower(interaction_id)) || 0x00
24433
+ * || utf8(lower(challenge_step_id)) || 0x00
24434
+ * || hexdecode(challenge_nonce) )
24435
+ *
24436
+ * The `0x00` separators pin the field boundaries (undelimited concatenation of
24437
+ * variable-length strings is collision-ambiguous), and the challenge nonce is
24438
+ * decoded to its 32 raw bytes before hashing. The platform recomputes this and
24439
+ * rejects a mismatch.
24440
+ */
24441
+ function deriveEip3009Nonce(input) {
24442
+ if (!CHALLENGE_NONCE_RE.test(input.challengeNonce)) throw new Error("challengeNonce must be exactly 64 lowercase hex chars (32 bytes), no 0x prefix");
24443
+ return keccak256(concat([
24444
+ stringToBytes(input.interactionId.toLowerCase()),
24445
+ FIELD_SEPARATOR,
24446
+ stringToBytes(input.challengeStepId.toLowerCase()),
24447
+ FIELD_SEPARATOR,
24448
+ hexToBytes(`0x${input.challengeNonce}`)
24449
+ ]));
24450
+ }
24451
+ /**
24452
+ * The EIP-3009 `TransferWithAuthorization` EIP-712 type. The field order and
24453
+ * types are part of the on-chain contract and MUST NOT change.
24454
+ */
24455
+ const TRANSFER_WITH_AUTHORIZATION_TYPES = { TransferWithAuthorization: [
24456
+ {
24457
+ name: "from",
24458
+ type: "address"
24459
+ },
24460
+ {
24461
+ name: "to",
24462
+ type: "address"
24463
+ },
24464
+ {
24465
+ name: "value",
24466
+ type: "uint256"
24467
+ },
24468
+ {
24469
+ name: "validAfter",
24470
+ type: "uint256"
24471
+ },
24472
+ {
24473
+ name: "validBefore",
24474
+ type: "uint256"
24475
+ },
24476
+ {
24477
+ name: "nonce",
24478
+ type: "bytes32"
24479
+ }
24480
+ ] };
24481
+ function transferWithAuthorizationTypedData(domain, auth) {
24482
+ return {
24483
+ domain: {
24484
+ name: domain.name,
24485
+ version: domain.version,
24486
+ chainId: domain.chainId,
24487
+ verifyingContract: domain.verifyingContract
24488
+ },
24489
+ types: TRANSFER_WITH_AUTHORIZATION_TYPES,
24490
+ primaryType: "TransferWithAuthorization",
24491
+ message: auth
24492
+ };
24493
+ }
24494
+ /**
24495
+ * Build the payout-address ownership message. This MUST be byte-identical to the
24496
+ * platform's `buildPayoutRegistrationMessage`, or registration fails the
24497
+ * ownership proof. The org id is in the signed bytes, so a captured signature
24498
+ * can never register the address under a different org.
24499
+ */
24500
+ function buildPayoutRegistrationMessage(input) {
24501
+ return [
24502
+ "Primitive x402 payout address authorization",
24503
+ "",
24504
+ "I authorize this address as a payout destination for my Primitive organization.",
24505
+ "",
24506
+ `org: ${input.org}`,
24507
+ `address: ${input.address.toLowerCase()}`,
24508
+ `network: ${input.network}`,
24509
+ `issued: ${input.issuedAt}`
24510
+ ].join("\n");
24511
+ }
24512
+ /** Assemble the wire payload from a signed authorization. */
24513
+ function toPaymentPayload(network, auth, signature) {
24514
+ return {
24515
+ x402Version: 1,
24516
+ scheme: "exact",
24517
+ network,
24518
+ payload: {
24519
+ signature,
24520
+ authorization: {
24521
+ from: auth.from,
24522
+ to: auth.to,
24523
+ value: auth.value.toString(),
24524
+ validAfter: auth.validAfter.toString(),
24525
+ validBefore: auth.validBefore.toString(),
24526
+ nonce: auth.nonce
24527
+ }
24528
+ }
24529
+ };
24530
+ }
24531
+ /**
24532
+ * Compute the EIP-3009 validity window for a payment. `validBefore` is the
24533
+ * value that governs on-chain validity, so it MUST cover the challenge's
24534
+ * `expires_at` plus a settlement margin; `validAfter` is set generously in the
24535
+ * past for clock skew. The total window is hard-capped at `maxWindowSec` so
24536
+ * neither a far-future `challengeExpiresAtSec` nor a widened margin can produce
24537
+ * a window the platform verifier would later reject.
24538
+ */
24539
+ function computePaymentValidityWindow(params) {
24540
+ const margin = params.settlementMarginSec ?? 300;
24541
+ const skew = params.clockSkewSec ?? 300;
24542
+ const validBefore = BigInt(params.challengeExpiresAtSec + margin);
24543
+ const validAfter = BigInt(params.nowSec - skew);
24544
+ if (validBefore <= validAfter) throw new Error("invalid validity window: validBefore must be after validAfter (challenge already expired?)");
24545
+ const maxWindow = BigInt(params.maxWindowSec ?? 86400);
24546
+ if (validBefore - validAfter > maxWindow) throw new Error(`invalid validity window: total window exceeds the ${maxWindow}s cap (challenge expiry too far out?)`);
24547
+ return {
24548
+ validAfter,
24549
+ validBefore
24550
+ };
24551
+ }
24552
+ /**
24553
+ * The interaction-aware signer: derive the bound nonce, assemble the
24554
+ * authorization, and sign it. This is the one piece a stock x402 signer cannot
24555
+ * do (it generates the nonce internally with no injection point), so the payer
24556
+ * side needs this Primitive-provided helper. The key never leaves the caller.
24557
+ */
24558
+ async function signInteractionPayment(params) {
24559
+ const authorization = {
24560
+ from: getAddress(params.payer),
24561
+ to: getAddress(params.payTo),
24562
+ value: params.amount,
24563
+ validAfter: params.validAfter,
24564
+ validBefore: params.validBefore,
24565
+ nonce: deriveEip3009Nonce(params.nonceBinding)
24566
+ };
24567
+ return {
24568
+ authorization,
24569
+ signature: await params.sign(transferWithAuthorizationTypedData(params.domain, authorization))
24570
+ };
24571
+ }
24572
+ /** The authorization nonce is 32 bytes rendered as a 0x-prefixed 64-char hex string. */
24573
+ const NONCE_HEX_RE = /^0x[0-9a-fA-F]{64}$/;
24574
+ /** A shape-valid EIP signature is 65 bytes (r,s,v) rendered as 130 hex chars. */
24575
+ const SIGNATURE_HEX_RE = /^0x[0-9a-fA-F]{130}$/;
24576
+ /**
24577
+ * Assemble (and validate) the exact-EVM x402 wire payload from an
24578
+ * interaction-bound, locally-signed authorization. The numeric authorization
24579
+ * fields are decimal strings in the wire schema, so the bigints are stringified
24580
+ * here; the nonce passes through as hex. Validation rejects a malformed nonce or
24581
+ * signature loudly rather than emitting a payload the platform will reject.
24582
+ */
24583
+ function buildExactEvmPaymentPayload(params) {
24584
+ if (params.network !== "base" && params.network !== "base-sepolia") throw new Error(`buildExactEvmPaymentPayload: unsupported network ${params.network}`);
24585
+ if (!SIGNATURE_HEX_RE.test(params.signature)) throw new Error("buildExactEvmPaymentPayload: signature must be a 0x-prefixed 65-byte (130 hex char) EIP signature");
24586
+ if (!NONCE_HEX_RE.test(params.authorization.nonce)) throw new Error("buildExactEvmPaymentPayload: authorization.nonce must be a 0x-prefixed 32-byte (64 hex char) value");
24587
+ return toPaymentPayload(params.network, params.authorization, params.signature);
24588
+ }
24589
+ const CHAIN_IDS = {
24590
+ "base-sepolia": 84532,
24591
+ base: 8453
24592
+ };
24593
+ const DEFAULT_BASE_URL = "https://api.primitive.dev";
24594
+ const CHARGE_INPUT_KEYS = {
24595
+ amount: true,
24596
+ amountUsdc: true,
24597
+ network: true,
24598
+ payerOrg: true,
24599
+ description: true,
24600
+ resource: true,
24601
+ expiresIn: true,
24602
+ idempotencyKey: true
24603
+ };
24604
+ function usdcToBaseUnits$1(human) {
24605
+ const trimmed = human.trim();
24606
+ if (!/^\d+(\.\d+)?$/.test(trimmed)) return null;
24607
+ const [whole, frac = ""] = trimmed.split(".");
24608
+ if (frac.length > 6) return null;
24609
+ const base = BigInt(whole) * 1000000n + BigInt(frac.padEnd(6, "0"));
24610
+ return base > 0n ? base.toString() : null;
24611
+ }
24612
+ var X402Error = class extends Error {
24613
+ /** HTTP status, or 0 for a client-side / transport error that never reached the server. */
24614
+ status;
24615
+ body;
24616
+ /** The `Retry-After` response header, if the server sent one. */
24617
+ retryAfter;
24618
+ constructor(message, status, body, options) {
24619
+ super(message, options?.cause !== void 0 ? { cause: options.cause } : void 0);
24620
+ this.name = "X402Error";
24621
+ this.status = status;
24622
+ this.body = body;
24623
+ this.retryAfter = options?.retryAfter ?? null;
24624
+ }
24625
+ };
24626
+ /**
24627
+ * Assert a challenge is fully hydrated before signing, so a missing field fails
24628
+ * with a named X402Error instead of an opaque viem/BigInt error mid-sign.
24629
+ */
24630
+ function validateChallenge(c) {
24631
+ const bad = (field) => {
24632
+ throw new X402Error(`challenge is missing or malformed: ${field}`, 0);
24633
+ };
24634
+ if (!c || typeof c !== "object") bad("challenge");
24635
+ if (!c.id) bad("id");
24636
+ if (!c.network) bad("network");
24637
+ if (!c.expires_at) bad("expires_at");
24638
+ const nb = c.nonce_binding;
24639
+ if (!nb?.interaction_id || !nb.challenge_step_id || !nb.challenge_nonce) bad("nonce_binding");
24640
+ const pr = c.payment_requirements;
24641
+ if (!pr) bad("payment_requirements");
24642
+ if (!/^[1-9][0-9]{0,38}$/.test(pr.maxAmountRequired ?? "")) bad("payment_requirements.maxAmountRequired (expected a positive integer string in token base units)");
24643
+ if (!/^0x[0-9a-fA-F]{40}$/.test(pr.payTo ?? "")) bad("payment_requirements.payTo (expected a 0x address)");
24644
+ if (!/^0x[0-9a-fA-F]{40}$/.test(pr.asset ?? "")) bad("payment_requirements.asset (expected a 0x address)");
24645
+ if (!pr.extra?.name || !pr.extra.version) bad("payment_requirements.extra (name/version)");
24646
+ }
24647
+ var X402Client = class {
24648
+ #apiKey;
24649
+ #baseUrl;
24650
+ #fetch;
24651
+ #timeoutMs;
24652
+ constructor(options = {}) {
24653
+ this.#apiKey = options.apiKey ?? process.env.PRIMITIVE_API_KEY ?? "";
24654
+ this.#baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
24655
+ this.#fetch = options.fetch ?? fetch;
24656
+ this.#timeoutMs = options.timeoutMs ?? 3e4;
24657
+ }
24658
+ async #request(method, path, body, init) {
24659
+ if (!this.#apiKey) throw new X402Error("no API key configured; set PRIMITIVE_API_KEY or pass { apiKey } to the client", 0);
24660
+ const timeout = AbortSignal.timeout(this.#timeoutMs);
24661
+ const signal = init?.signal ? AbortSignal.any([timeout, init.signal]) : timeout;
24662
+ let res;
24663
+ try {
24664
+ res = await this.#fetch(`${this.#baseUrl}${path}`, {
24665
+ method,
24666
+ headers: {
24667
+ authorization: `Bearer ${this.#apiKey}`,
24668
+ "content-type": "application/json",
24669
+ ...init?.headers
24670
+ },
24671
+ body: body === void 0 ? void 0 : JSON.stringify(body),
24672
+ signal
24673
+ });
24674
+ } catch (cause) {
24675
+ throw new X402Error(cause instanceof Error && cause.name === "TimeoutError" ? `request to ${path} timed out after ${this.#timeoutMs}ms` : `request to ${path} failed: ${cause instanceof Error ? cause.message : String(cause)}`, 0, void 0, { cause });
24676
+ }
24677
+ const retryAfter = res.headers.get("retry-after");
24678
+ const text = await res.text().catch(() => "");
24679
+ let json;
24680
+ if (text) try {
24681
+ json = JSON.parse(text);
24682
+ } catch {
24683
+ throw new X402Error(`non-JSON response (${res.status}) from ${path}: ${text.slice(0, 200)}`, res.status, text.slice(0, 500), { retryAfter });
24684
+ }
24685
+ if (!res.ok || json?.success === false) throw new X402Error(json?.error?.message ?? `request failed with ${res.status}`, res.status, json ?? text.slice(0, 500), { retryAfter });
24686
+ if (!json || json.success !== true || json.data === void 0) throw new X402Error(`unexpected response shape (${res.status}) from ${path}: missing success/data envelope`, res.status, json ?? text.slice(0, 500), { retryAfter });
24687
+ return json.data;
24688
+ }
24689
+ /** Request a payment (payee side). Returns the challenge to hand to the payer. */
24690
+ async charge(input) {
24691
+ for (const key of Object.keys(input)) if (!(key in CHARGE_INPUT_KEYS)) throw new X402Error(`unknown charge() option "${key}"; expected one of: ${Object.keys(CHARGE_INPUT_KEYS).join(", ")}`, 0);
24692
+ if (input.amount !== void 0 && input.amountUsdc !== void 0) throw new X402Error("charge() takes exactly one of `amount` (base units) or `amountUsdc` (human USDC), not both", 0);
24693
+ const amount = input.amountUsdc !== void 0 ? usdcToBaseUnits$1(input.amountUsdc) : input.amount ?? null;
24694
+ if (!amount || !/^[1-9][0-9]{0,38}$/.test(amount)) throw new X402Error("charge() requires `amount` as a positive integer string in token base units (e.g. \"10000\"), or `amountUsdc` as a positive USDC amount with at most 6 decimals (e.g. \"0.01\")", 0);
24695
+ const body = {
24696
+ amount,
24697
+ network: input.network ?? "base-sepolia"
24698
+ };
24699
+ if (input.payerOrg) body.payer_org = input.payerOrg;
24700
+ if (input.description) body.description = input.description;
24701
+ if (input.resource) body.resource = input.resource;
24702
+ if (input.expiresIn !== void 0) body.expires_in = input.expiresIn;
24703
+ return this.#request("POST", "/v1/x402/challenges", body, { headers: input.idempotencyKey ? { "idempotency-key": input.idempotencyKey } : void 0 });
24704
+ }
24705
+ /**
24706
+ * Pay a challenge (payer side). Derives the interaction-bound authorization,
24707
+ * signs it locally with the caller's key, and submits it for settlement.
24708
+ */
24709
+ async pay(challenge, options) {
24710
+ if (!options?.signer?.address || typeof options.signer.signTypedData !== "function") throw new X402Error("pay() requires options.signer with { address, signTypedData } (e.g. a viem LocalAccount)", 0);
24711
+ validateChallenge(challenge);
24712
+ const chainId = CHAIN_IDS[challenge.network];
24713
+ if (chainId === void 0) throw new X402Error(`unsupported network: ${challenge.network}`, 0);
24714
+ const pr = challenge.payment_requirements;
24715
+ if (pr.network !== challenge.network) throw new X402Error(`challenge network mismatch: ${challenge.network} vs payment_requirements ${pr.network}`, 0);
24716
+ if (pr.scheme !== "exact") throw new X402Error(`unsupported payment scheme: ${pr.scheme}`, 0);
24717
+ const nowSec = Math.floor(Date.now() / 1e3);
24718
+ const expiresAtMs = Date.parse(challenge.expires_at);
24719
+ if (Number.isNaN(expiresAtMs)) throw new X402Error(`challenge has an invalid expires_at: ${challenge.expires_at}`, 0);
24720
+ const expiresAtSec = Math.floor(expiresAtMs / 1e3);
24721
+ if (expiresAtSec <= nowSec) throw new X402Error(`challenge has already expired (expires_at ${challenge.expires_at}); not signing`, 0);
24722
+ let validAfter;
24723
+ let validBefore;
24724
+ try {
24725
+ ({validAfter, validBefore} = computePaymentValidityWindow({
24726
+ challengeExpiresAtSec: expiresAtSec,
24727
+ nowSec
24728
+ }));
24729
+ } catch (cause) {
24730
+ throw new X402Error(cause instanceof Error ? cause.message : String(cause), 0, void 0, { cause });
24731
+ }
24732
+ const { authorization, signature } = await signInteractionPayment({
24733
+ sign: (typedData) => options.signer.signTypedData(typedData),
24734
+ payer: options.signer.address,
24735
+ domain: {
24736
+ name: pr.extra.name,
24737
+ version: pr.extra.version,
24738
+ chainId,
24739
+ verifyingContract: pr.asset
24740
+ },
24741
+ payTo: pr.payTo,
24742
+ amount: BigInt(pr.maxAmountRequired),
24743
+ nonceBinding: {
24744
+ interactionId: challenge.nonce_binding.interaction_id,
24745
+ challengeStepId: challenge.nonce_binding.challenge_step_id,
24746
+ challengeNonce: challenge.nonce_binding.challenge_nonce
24747
+ },
24748
+ validAfter,
24749
+ validBefore
24750
+ });
24751
+ return this.#request("POST", `/v1/x402/challenges/${challenge.id}/pay`, { payment: buildExactEvmPaymentPayload({
24752
+ network: challenge.network,
24753
+ authorization,
24754
+ signature
24755
+ }) });
24756
+ }
24757
+ /** Fetch a challenge by id (scoped to the challenger org that created it). */
24758
+ async getChallenge(id) {
24759
+ if (!id) throw new X402Error("getChallenge() requires a challenge id", 0);
24760
+ return this.#request("GET", `/v1/x402/challenges/${encodeURIComponent(id)}`);
24761
+ }
24762
+ /** Resolve the caller's own organization id from the account endpoint. */
24763
+ async #resolveOrgId() {
24764
+ const account = await this.#request("GET", "/v1/account");
24765
+ if (!account?.id) throw new X402Error("could not resolve your organization id from /v1/account; pass { org } explicitly", 0);
24766
+ return account.id;
24767
+ }
24768
+ /**
24769
+ * Register a payout address for your org (payee side). The signer proves
24770
+ * control of its own address with an org-bound `personal_sign`; the proven
24771
+ * address becomes (or updates to) the default payout destination for the
24772
+ * network. `charge()` resolves its `pay_to` from this directory, so a payee
24773
+ * must register before requesting payments.
24774
+ *
24775
+ * `org` is optional: when omitted it is resolved from your authenticated
24776
+ * account, so most callers never need to supply it.
24777
+ */
24778
+ async registerPayoutAddress(input, options) {
24779
+ if (typeof options?.signer?.signMessage !== "function") throw new X402Error("registerPayoutAddress() requires a signer with signMessage (e.g. a viem LocalAccount)", 0);
24780
+ const org = input.org ?? await this.#resolveOrgId();
24781
+ const network = input.network ?? "base-sepolia";
24782
+ const issuedAt = input.issuedAt ?? (/* @__PURE__ */ new Date()).toISOString();
24783
+ const address = options.signer.address;
24784
+ const message = buildPayoutRegistrationMessage({
24785
+ org,
24786
+ address,
24787
+ network,
24788
+ issuedAt
24789
+ });
24790
+ const signature = await options.signer.signMessage({ message });
24791
+ return this.#request("POST", "/v1/x402/payout-addresses", {
24792
+ address,
24793
+ network,
24794
+ signature,
24795
+ issued_at: issuedAt,
24796
+ ...input.label !== void 0 ? { label: input.label } : {}
24797
+ });
24798
+ }
24799
+ /** List your org's registered payout addresses. */
24800
+ async listPayoutAddresses() {
24801
+ return this.#request("GET", "/v1/x402/payout-addresses");
24802
+ }
24803
+ /**
24804
+ * List the most recent payments your org's spend policy declined (newest
24805
+ * first). Use this to see why an outbound payment was refused.
24806
+ */
24807
+ async listDeclinedPayments() {
24808
+ return this.#request("GET", "/v1/x402/declined-payments");
24809
+ }
24810
+ /** Read your org's spend policy (kill-switch + caps + allowlist). */
24811
+ async getSpendPolicy() {
24812
+ return this.#request("GET", "/v1/x402/spend-policy");
24813
+ }
24814
+ /**
24815
+ * Update your org's spend policy. The endpoint is a PUT, but the server
24816
+ * applies it as a merge: only the fields you include are changed and omitted
24817
+ * fields keep their current value, so a partial update can't silently reset
24818
+ * the kill-switch. Pass `null` to clear a cap.
24819
+ */
24820
+ async setSpendPolicy(update) {
24821
+ return this.#request("PUT", "/v1/x402/spend-policy", update);
24822
+ }
24823
+ };
24824
+ function createX402Client(options = {}) {
24825
+ return new X402Client(options);
24826
+ }
24827
+ //#endregion
24414
24828
  //#region src/oclif/commands/payments-shared.ts
24415
24829
  const PRIVATE_KEY_ENV = "PRIMITIVE_X402_PRIVATE_KEY";
24416
24830
  const PRIVATE_KEY_FLAG_DESCRIPTION = `Hex private key (0x-prefixed) of the wallet that signs. Prefer the ${PRIVATE_KEY_ENV} environment variable so the key is not exposed in shell history or the process list. The key is used locally to sign and never sent to Primitive.`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "primcli",
3
- "version": "1.6.0",
3
+ "version": "1.7.0",
4
4
  "description": "Official Primitive CLI: deploy Primitive Functions, send and inspect mail, manage endpoints, all from the terminal. Wraps the @primitivedotdev/sdk runtime client with one-shot commands.",
5
5
  "type": "module",
6
6
  "sideEffects": false,