@primitivedotdev/cli 0.36.0 → 0.37.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 +92 -18
  2. package/package.json +1 -1
@@ -14464,8 +14464,11 @@ const RESERVED_FLAG_NAMES = new Set([
14464
14464
  "envelope",
14465
14465
  "output"
14466
14466
  ]);
14467
- function bodyFieldFlag(field) {
14468
- const common = { description: field.description || field.name };
14467
+ function bodyFieldFlag(field, aliases) {
14468
+ const common = {
14469
+ description: field.description || field.name,
14470
+ ...aliases && aliases.length > 0 ? { aliases } : {}
14471
+ };
14469
14472
  if (field.kind === "boolean") return Flags.boolean({
14470
14473
  ...common,
14471
14474
  allowNo: true
@@ -14507,12 +14510,13 @@ function buildFlags(operation) {
14507
14510
  flags["raw-body"] = Flags.string({ description: "Full request body as raw JSON. Escape hatch for nested or complex fields (e.g. arrays); prefer per-field flags (e.g. --to, --from, --body-text) when available." });
14508
14511
  flags["body-file"] = Flags.string({ description: "Path to a JSON file used as the request body. Same role as --raw-body for callers passing a saved payload." });
14509
14512
  const bodyFields = extractBodyFields(operation.requestSchema);
14513
+ const aliasesForOperation = OPERATION_FLAG_ALIASES[operation.sdkName];
14510
14514
  for (const field of bodyFields) {
14511
14515
  if (field.kind === "complex") continue;
14512
14516
  const name = flagName(field.name);
14513
14517
  if (RESERVED_FLAG_NAMES.has(name)) continue;
14514
14518
  if (flags[name] !== void 0) continue;
14515
- flags[name] = bodyFieldFlag(field);
14519
+ flags[name] = bodyFieldFlag(field, aliasesForOperation?.[field.name]);
14516
14520
  bodyFieldFlagToProperty.set(name, field.name);
14517
14521
  }
14518
14522
  }
@@ -14561,8 +14565,11 @@ const OPERATION_HINTS = {
14561
14565
  createFunction: "Tip: prefer `primitive functions deploy --name <name> --file <bundle>` for file-input ergonomics. This raw command exists for callers passing JSON.",
14562
14566
  updateFunction: "Tip: prefer `primitive functions redeploy --id <id> --file <bundle>` for file-input ergonomics. This raw command exists for callers passing JSON.",
14563
14567
  createFunctionSecret: "Tip: prefer `primitive functions set-secret --id <id> --key <KEY> --value <value> [--redeploy]` for secret writes that also push the binding live. This raw command exists for callers passing JSON.",
14564
- setFunctionSecret: "Tip: prefer `primitive functions set-secret --id <id> --key <KEY> --value <value> [--redeploy]` for secret writes that also push the binding live. This raw command exists for callers passing JSON."
14568
+ setFunctionSecret: "Tip: prefer `primitive functions set-secret --id <id> --key <KEY> --value <value> [--redeploy]` for secret writes that also push the binding live. This raw command exists for callers passing JSON.",
14569
+ startAgentSignup: "Tip: also pass --signup-code <code> (request from Primitive; invite-only during the agent beta) and --terms-accepted. Capture the signup_token from the response and feed it to `primitive agent verify-agent-signup --signup-token <token> --verification-code <6-digit-code>` (the verify flag accepts --code as an alias). The high-level `primitive signup <email>` command walks an interactive user through both steps with friendlier prompts.",
14570
+ verifyAgentSignup: "Tip: pass --verification-code <code> (or --code; both work). The response carries OAuth tokens but not your assigned inbox domain; run `primitive domains list` (or `primitive whoami`) after success to see the managed *.primitive.email address that routes to this account."
14565
14571
  };
14572
+ const OPERATION_FLAG_ALIASES = { verifyAgentSignup: { verification_code: ["code"] } };
14566
14573
  function createOperationCommand(operation) {
14567
14574
  const { flags, bodyFieldFlagToProperty } = buildFlags(operation);
14568
14575
  const baseDescription = operation.description !== null && operation.description !== void 0 ? canonicalizeCliReferences(operation.description) : `${operation.method} ${operation.path}`;
@@ -17677,8 +17684,8 @@ const PRIMITIVE_TEAM_AUTHOR = {
17677
17684
  name: "Primitive Team",
17678
17685
  url: "https://primitive.dev"
17679
17686
  };
17680
- const SDK_VERSION_RANGE = "^0.36.0";
17681
- const CLI_VERSION_RANGE = "^0.36.0";
17687
+ const SDK_VERSION_RANGE = "^0.37.0";
17688
+ const CLI_VERSION_RANGE = "^0.37.0";
17682
17689
  const ESBUILD_VERSION_RANGE = "^0.27.0";
17683
17690
  function renderHandler() {
17684
17691
  return `// env.PRIMITIVE_API_KEY, env.PRIMITIVE_WEBHOOK_SECRET, and
@@ -17753,22 +17760,50 @@ function inboundRecipientDomains(event: EmailReceivedEvent): Set<string> {
17753
17760
  // SMTP recipients instead.
17754
17761
  //
17755
17762
  // The default check skips:
17763
+ // - bounce notifications, which RFC 5321 requires to use an empty
17764
+ // SMTP envelope sender (MAIL FROM:<>). Replying to a null sender
17765
+ // is forbidden and would itself bounce, producing a
17766
+ // bounce-of-bounce chain. The body header on a bounce typically
17767
+ // reads "From: MAILER-DAEMON@..." which a naive From-only check
17768
+ // would treat as a normal sender, so we gate on the envelope here.
17756
17769
  // - direct self-mail where From equals one of the inbound recipients;
17757
- // - mailer-daemon/postmaster bounces from the same domain as the inbound;
17770
+ // - mailer-daemon/postmaster bounces from the same domain as the
17771
+ // inbound, as a backup for bounces that arrive with a non-empty
17772
+ // envelope sender;
17758
17773
  // - any address explicitly listed in EXTRA_SELF_ADDRESSES.
17759
17774
  //
17760
- // Extend this helper if you need stricter detection. Common additions:
17761
- // - Honor RFC 3834 auto-response headers: skip when
17762
- // event.email.headers["auto-submitted"] is anything other than "no",
17763
- // or when a List-Unsubscribe / Precedence: bulk header is present.
17775
+ // Anything with no identifiable sender at all (envelope + From both
17776
+ // empty) is treated as a loop terminator: better to drop one ambiguous
17777
+ // message than to reply blindly and loop on a bounce.
17778
+ //
17779
+ // Extend this helper if you need stricter detection. Common additions
17780
+ // not implemented here today:
17781
+ // - Honor RFC 3834 auto-response headers: skip when an
17782
+ // auto-submitted header is anything other than "no", or when a
17783
+ // list-unsubscribe / precedence: bulk header is present. The
17784
+ // EmailReceivedEvent.email.headers shape does not currently surface
17785
+ // these, so detection requires either a parsed-headers field on
17786
+ // the event or a parse of the raw RFC 822 body.
17764
17787
  // - Track Message-ID / In-Reply-To chains to break ping-pong loops
17765
17788
  // between two cooperating handlers on different domains.
17789
+ // - Rate-limit replies per sender per hour as a safety net.
17766
17790
  export function isLoop(event: EmailReceivedEvent): boolean {
17791
+ // RFC 5321: bounce notifications use the null MAIL FROM (envelope
17792
+ // sender = empty string). Some MTAs report this as "<>" verbatim.
17793
+ // Treat either as an unambiguous bounce signal.
17794
+ const envelopeSender = (event.email.smtp.mail_from || "").trim();
17795
+ if (envelopeSender === "" || envelopeSender === "<>") return true;
17796
+
17767
17797
  const fromAddresses = [
17768
17798
  ...extractEmailAddresses(event.email.headers.from),
17769
17799
  ...extractEmailAddresses(event.email.smtp.mail_from),
17770
17800
  ];
17771
- if (fromAddresses.length === 0) return false;
17801
+ // No identifiable sender across either envelope or header: treat as
17802
+ // a loop terminator. Was return false in the original template; that
17803
+ // returned mail with empty headers straight back into the handler
17804
+ // and let bounces with malformed bodies slip past the bounce guard
17805
+ // above.
17806
+ if (fromAddresses.length === 0) return true;
17772
17807
 
17773
17808
  const inboundAddresses = new Set(inboundRecipientAddresses(event));
17774
17809
  const inboundDomains = inboundRecipientDomains(event);
@@ -18244,6 +18279,25 @@ function emitLogRows(rows, jsonl) {
18244
18279
  process.stdout.write(`${line}\n`);
18245
18280
  }
18246
18281
  }
18282
+ async function readFunctionInvocations(client, id) {
18283
+ try {
18284
+ const result = await getFunction({
18285
+ client,
18286
+ path: { id },
18287
+ responseStyle: "fields",
18288
+ signal: AbortSignal.timeout(3e3)
18289
+ });
18290
+ if (result.error) return null;
18291
+ const fn = result.data?.data;
18292
+ if (!fn) return null;
18293
+ return {
18294
+ invocations_total: typeof fn.invocations_total === "number" ? fn.invocations_total : 0,
18295
+ invocations_24h: typeof fn.invocations_24h === "number" ? fn.invocations_24h : 0
18296
+ };
18297
+ } catch {
18298
+ return null;
18299
+ }
18300
+ }
18247
18301
  var FunctionsLogsCommand = class FunctionsLogsCommand extends Command {
18248
18302
  static description = "List or follow function execution logs. Defaults to compact text output; use --jsonl for one JSON object per log row.";
18249
18303
  static summary = "List or follow a function's execution logs";
@@ -18342,7 +18396,14 @@ var FunctionsLogsCommand = class FunctionsLogsCommand extends Command {
18342
18396
  cursor = page.next_cursor;
18343
18397
  }
18344
18398
  if (rows.length === 0 && !wroteEmptyHint) {
18345
- process.stderr.write(flags.follow ? hasObservedLogs ? "Waiting for new function logs...\n" : "No function logs yet. Waiting for new rows...\n" : "No function logs yet. Trigger the function, then run this command again.\n");
18399
+ let emptyHint;
18400
+ if (flags.follow) emptyHint = hasObservedLogs ? "Waiting for new function logs...\n" : "No function logs yet. Waiting for new rows...\n";
18401
+ else if (flags.cursor) emptyHint = "No more function logs after this cursor.\n";
18402
+ else {
18403
+ const fnInvocations = await readFunctionInvocations(apiClient.client, flags.id);
18404
+ emptyHint = fnInvocations && fnInvocations.invocations_total > 0 ? `No function logs yet, but this function has been invoked ${fnInvocations.invocations_total} time(s) (${fnInvocations.invocations_24h} in the last 24h). Your handler likely has no console.log/console.error calls on the path that fired. Add logging and redeploy to surface details.\n` : "No function logs yet. Trigger the function, then run this command again.\n";
18405
+ }
18406
+ process.stderr.write(emptyHint);
18346
18407
  wroteEmptyHint = true;
18347
18408
  }
18348
18409
  emitLogRows(rows, flags.jsonl);
@@ -21504,12 +21565,14 @@ var OtpResendCommand = class extends SigninOtpResendCommand {
21504
21565
  };
21505
21566
  //#endregion
21506
21567
  //#region src/oclif/commands/whoami.ts
21507
- function formatWhoamiSummary(account) {
21508
- return [
21568
+ function formatWhoamiSummary(account, managedInboxDomain) {
21569
+ const lines = [
21509
21570
  `Authenticated as ${account.email}`,
21510
21571
  `Account id: ${account.id}`,
21511
21572
  `Plan: ${account.plan}`
21512
- ].join("\n");
21573
+ ];
21574
+ if (managedInboxDomain) lines.push(`Managed inbox: any-local-part@${managedInboxDomain}`);
21575
+ return lines.join("\n");
21513
21576
  }
21514
21577
  var WhoamiCommand = class WhoamiCommand extends Command {
21515
21578
  static description = `Print the account currently authenticated by saved OAuth credentials or an explicit API key. Useful as a credentials smoke test: confirms auth is live and shows which account it belongs to.
@@ -21563,11 +21626,22 @@ var WhoamiCommand = class WhoamiCommand extends Command {
21563
21626
  process.stderr.write("Server returned an empty account body; this should not happen for a valid key.\n");
21564
21627
  throw new Errors.CLIError("unexpected empty response");
21565
21628
  }
21629
+ let managedInboxDomain = null;
21630
+ try {
21631
+ const domainsResult = await listDomains({
21632
+ client: apiClient.client,
21633
+ responseStyle: "fields"
21634
+ });
21635
+ if (!domainsResult.error) managedInboxDomain = (domainsResult.data?.data ?? []).find((row) => row.verified && row.managed_zone !== null)?.domain ?? null;
21636
+ } catch {}
21566
21637
  if (flags.json) {
21567
- this.log(JSON.stringify(account, null, 2));
21638
+ this.log(JSON.stringify({
21639
+ ...account,
21640
+ managed_inbox_domain: managedInboxDomain
21641
+ }, null, 2));
21568
21642
  return;
21569
21643
  }
21570
- this.log(formatWhoamiSummary(account));
21644
+ this.log(formatWhoamiSummary(account, managedInboxDomain));
21571
21645
  });
21572
21646
  }
21573
21647
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@primitivedotdev/cli",
3
- "version": "0.36.0",
3
+ "version": "0.37.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,