@primitivedotdev/cli 0.30.3 → 0.31.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 +458 -175
  2. package/package.json +1 -1
@@ -2413,6 +2413,15 @@ const openapiDocument = {
2413
2413
  },
2414
2414
  "description": "Filter by domain ID."
2415
2415
  },
2416
+ {
2417
+ "name": "reply_to_sent_email_id",
2418
+ "in": "query",
2419
+ "schema": {
2420
+ "type": "string",
2421
+ "format": "uuid"
2422
+ },
2423
+ "description": "Filter to inbound emails that are replies to a specific\noutbound send. The value is a `sent_emails.id` (UUID). At\ninbound ingest, Primitive matches the parsed In-Reply-To\nheader (or References as a fallback) against\n`sent_emails.message_id` in the same org and records the\nresolved id on `emails.reply_to_sent_email_id`. This filter\nis the strict-threading lookup behind `primitive chat` and\nany UI that wants to show the inbound reply to a given\nsend. NULL on inbound that isn't a threaded reply to one\nof your sends, so existing emails received before this\ningestion landed will not match.\n"
2424
+ },
2416
2425
  {
2417
2426
  "name": "status",
2418
2427
  "in": "query",
@@ -4666,6 +4675,11 @@ const openapiDocument = {
4666
4675
  "type": "array",
4667
4676
  "description": "Sent emails recorded as replies to this inbound, in send\norder (ascending). Populated when a customer's send-mail\nrequest carries an `in_reply_to` Message-ID that matches\nthis inbound's `message_id` in the same org. Includes\nattempts that were gate-denied, so the array reflects every\nrecorded reply attempt regardless of outcome.\n",
4668
4677
  "items": { "$ref": "#/components/schemas/EmailDetailReply" }
4678
+ },
4679
+ "reply_to_sent_email_id": {
4680
+ "type": ["string", "null"],
4681
+ "format": "uuid",
4682
+ "description": "The `sent_emails.id` of the outbound this inbound was a\nreply to, when resolvable. Set at inbound ingest by\nmatching the parsed In-Reply-To (or References, as a\nfallback) against `sent_emails.message_id` in the same\norg. The mirror of `sent_emails.in_reply_to_email_id` for\nthe inbound side of a thread. NULL when the inbound is\nnot a threaded reply to one of your sends, when neither\nheader survived the path through intermediate MTAs, or on\ninbound received before this auto-link landed.\n"
4669
4683
  }
4670
4684
  },
4671
4685
  "required": [
@@ -7336,6 +7350,11 @@ const operationManifest = [
7336
7350
  "created_at"
7337
7351
  ]
7338
7352
  }
7353
+ },
7354
+ "reply_to_sent_email_id": {
7355
+ "type": ["string", "null"],
7356
+ "format": "uuid",
7357
+ "description": "The `sent_emails.id` of the outbound this inbound was a\nreply to, when resolvable. Set at inbound ingest by\nmatching the parsed In-Reply-To (or References, as a\nfallback) against `sent_emails.message_id` in the same\norg. The mirror of `sent_emails.in_reply_to_email_id` for\nthe inbound side of a thread. NULL when the inbound is\nnot a threaded reply to one of your sends, when neither\nheader survived the path through intermediate MTAs, or on\ninbound received before this auto-link landed.\n"
7339
7358
  }
7340
7359
  },
7341
7360
  "required": [
@@ -7588,6 +7607,13 @@ const operationManifest = [
7588
7607
  "required": false,
7589
7608
  "type": "string"
7590
7609
  },
7610
+ {
7611
+ "description": "Filter to inbound emails that are replies to a specific\noutbound send. The value is a `sent_emails.id` (UUID). At\ninbound ingest, Primitive matches the parsed In-Reply-To\nheader (or References as a fallback) against\n`sent_emails.message_id` in the same org and records the\nresolved id on `emails.reply_to_sent_email_id`. This filter\nis the strict-threading lookup behind `primitive chat` and\nany UI that wants to show the inbound reply to a given\nsend. NULL on inbound that isn't a threaded reply to one\nof your sends, so existing emails received before this\ningestion landed will not match.\n",
7612
+ "enum": null,
7613
+ "name": "reply_to_sent_email_id",
7614
+ "required": false,
7615
+ "type": "string"
7616
+ },
7591
7617
  {
7592
7618
  "description": "Filter by inbound email lifecycle status.",
7593
7619
  "enum": null,
@@ -11062,7 +11088,7 @@ function resolveConfigEnvironment(config) {
11062
11088
  } : null;
11063
11089
  }
11064
11090
  function upsertCliEnvironment(params) {
11065
- const name = normalizeCliEnvironmentName(params.environmentName ?? DEFAULT_ENVIRONMENT);
11091
+ const name = normalizeCliEnvironmentName(params.environmentName ?? "default");
11066
11092
  const existing = params.config.environments[name] ?? {};
11067
11093
  const nextHeaders = { ...existing.headers ?? {} };
11068
11094
  for (const assignment of params.headers ?? []) {
@@ -11139,6 +11165,7 @@ function resolveCliApiRequestConfig(params) {
11139
11165
  const currentEnvironment = resolveConfigEnvironment(loadCliConfig(params.configDir));
11140
11166
  const configuredApiBaseUrl1 = currentEnvironment?.config.api_base_url_1;
11141
11167
  const configuredApiBaseUrl2 = currentEnvironment?.config.api_base_url_2;
11168
+ if (currentEnvironment !== null && currentEnvironment.name !== "default" && params.apiBaseUrl1 === void 0 && configuredApiBaseUrl1 === void 0) throw new Errors.CLIError(`The active Primitive CLI environment \`${currentEnvironment.name}\` does not specify an api_base_url_1. Set one with \`primitive config set --environment ${currentEnvironment.name} --api-base-url-1 https://...\`, or switch to a different environment with \`primitive config use <name>\`. Refusing to fall back to the production default for a non-default environment.`, { exit: 1 });
11142
11169
  const apiBaseUrl1 = params.apiBaseUrl1 !== void 0 ? normalizeApiBaseUrl1(params.apiBaseUrl1) : configuredApiBaseUrl1;
11143
11170
  const apiBaseUrl2 = params.apiBaseUrl2 !== void 0 ? normalizeApiBaseUrl2(params.apiBaseUrl2) : configuredApiBaseUrl2;
11144
11171
  return {
@@ -11410,26 +11437,26 @@ function coerceParameterValue(parameter, value) {
11410
11437
  if (typeof value === "boolean" || typeof value === "number" || typeof value === "string") return value;
11411
11438
  throw new Errors.CLIError(`Unsupported flag value for --${parameter.name}`);
11412
11439
  }
11413
- function cliError$5(message) {
11440
+ function cliError$6(message) {
11414
11441
  return new Errors.CLIError(message, { exit: 1 });
11415
11442
  }
11416
11443
  function parseJson(source, flagLabel) {
11417
11444
  try {
11418
11445
  return JSON.parse(source);
11419
11446
  } catch (error) {
11420
- throw cliError$5(`${flagLabel} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
11447
+ throw cliError$6(`${flagLabel} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
11421
11448
  }
11422
11449
  }
11423
11450
  function readJsonBody(flags) {
11424
11451
  const bodyFile = flags["body-file"];
11425
11452
  const rawBody = flags["raw-body"];
11426
- if (bodyFile && rawBody) throw cliError$5("Use either --raw-body or --body-file, not both");
11453
+ if (bodyFile && rawBody) throw cliError$6("Use either --raw-body or --body-file, not both");
11427
11454
  if (typeof bodyFile === "string") {
11428
11455
  let contents;
11429
11456
  try {
11430
11457
  contents = readFileSync(bodyFile, "utf8");
11431
11458
  } catch (error) {
11432
- throw cliError$5(`Could not read --body-file ${bodyFile}: ${error instanceof Error ? error.message : String(error)}`);
11459
+ throw cliError$6(`Could not read --body-file ${bodyFile}: ${error instanceof Error ? error.message : String(error)}`);
11433
11460
  }
11434
11461
  return parseJson(contents, `--body-file ${bodyFile}`);
11435
11462
  }
@@ -11439,7 +11466,7 @@ function readTextFileFlag(path, flagLabel) {
11439
11466
  try {
11440
11467
  return readFileSync(path, "utf8");
11441
11468
  } catch (error) {
11442
- throw cliError$5(`Could not read ${flagLabel} ${path}: ${error instanceof Error ? error.message : String(error)}`);
11469
+ throw cliError$6(`Could not read ${flagLabel} ${path}: ${error instanceof Error ? error.message : String(error)}`);
11443
11470
  }
11444
11471
  }
11445
11472
  function extractErrorPayload(raw) {
@@ -11504,15 +11531,27 @@ function writeErrorWithHints(payload) {
11504
11531
  }
11505
11532
  if (code in NETWORK_ERROR_HINTS) process.stderr.write(`${NETWORK_ERROR_HINTS[code]}\n`);
11506
11533
  }
11507
- function removeStaleSavedCredentialOnUnauthorized(params) {
11508
- if (extractErrorCode(params.payload) !== API_ERROR_CODES.unauthorized || params.auth.source !== "stored") return false;
11534
+ /**
11535
+ * Surface a user-facing hint when a request comes back unauthorized.
11536
+ *
11537
+ * Deliberately does NOT mutate the saved credentials.json. Auto-
11538
+ * deleting on any 401 made transient rejections look like permanent
11539
+ * credential failures and forced unnecessary re-login cycles; we
11540
+ * surface a hint and let the user decide whether to re-authenticate.
11541
+ *
11542
+ * The one legitimate auto-delete case lives in `primitive login`'s
11543
+ * `checkExistingLogin`: the user has explicitly asked to log in,
11544
+ * existing credentials are probed, and if they fail we clean up
11545
+ * before minting a new key. That path calls `deleteCliCredentials`
11546
+ * directly rather than going through this function.
11547
+ */
11548
+ function surfaceUnauthorizedHint(params) {
11549
+ if (extractErrorCode(params.payload) !== API_ERROR_CODES.unauthorized || params.auth.source !== "stored") return;
11509
11550
  if (params.baseUrlOverridden && params.auth.credentials !== null && params.auth.apiBaseUrl1 !== params.auth.credentials.api_base_url_1) {
11510
- process.stderr.write("Saved Primitive CLI credentials were rejected by the overridden API base URL. The local credential was not removed; unset PRIMITIVE_API_BASE_URL_1, run `primitive config reset` to clear configured URL overrides, or run `primitive logout` to remove the stored credential.\n");
11511
- return false;
11551
+ process.stderr.write("Saved Primitive CLI credentials were rejected by the overridden API base URL. The saved credential is preserved; unset PRIMITIVE_API_BASE_URL_1, run `primitive config reset` to clear configured URL overrides, or run `primitive logout` to remove the stored credential.\n");
11552
+ return;
11512
11553
  }
11513
- deleteCliCredentials(params.configDir);
11514
- process.stderr.write("Removed saved Primitive CLI credentials because the backing API key is no longer valid. Run `primitive login` to create a new one.\n");
11515
- return true;
11554
+ process.stderr.write("Your saved Primitive CLI credential was rejected. If the command was working a moment ago, please retry; brief retries often clear transient rejections. If it keeps failing, run `primitive logout && primitive login` to mint a fresh credential.\n");
11516
11555
  }
11517
11556
  function formatElapsed(ms) {
11518
11557
  const seconds = ms / 1e3;
@@ -11676,7 +11715,7 @@ function createOperationCommand(operation) {
11676
11715
  if (result.error) {
11677
11716
  const errorPayload = extractErrorPayload(result.error);
11678
11717
  writeErrorWithHints(errorPayload);
11679
- removeStaleSavedCredentialOnUnauthorized({
11718
+ surfaceUnauthorizedHint({
11680
11719
  auth,
11681
11720
  baseUrlOverridden,
11682
11721
  configDir: this.config.configDir,
@@ -11737,6 +11776,384 @@ function canonicalizeCliReferences(description) {
11737
11776
  return description.replaceAll("`primitive emails:latest`", "`primitive emails latest`").replaceAll("`primitive describe emails:get-email | jq '.responseSchema.properties'`", "`primitive describe emails:get | jq '.responseSchema.properties'`");
11738
11777
  }
11739
11778
  //#endregion
11779
+ //#region src/oclif/outbound-defaults.ts
11780
+ const SUBJECT_MAX_LENGTH = 200;
11781
+ function deriveSubject(body) {
11782
+ for (const line of body.split("\n")) {
11783
+ const trimmed = line.trim();
11784
+ if (!trimmed) continue;
11785
+ return trimmed.length > SUBJECT_MAX_LENGTH ? `${trimmed.slice(0, SUBJECT_MAX_LENGTH - 3)}...` : trimmed;
11786
+ }
11787
+ return "Message";
11788
+ }
11789
+ function isVerifiedDomain(domain) {
11790
+ return domain.is_active === true;
11791
+ }
11792
+ async function pickDefaultFromAddress(apiClient, authFailureContext) {
11793
+ const result = await listDomains({
11794
+ client: apiClient.client,
11795
+ responseStyle: "fields"
11796
+ });
11797
+ if (result.error) {
11798
+ const errorPayload = extractErrorPayload(result.error);
11799
+ if (extractErrorCode(errorPayload) === API_ERROR_CODES.unauthorized) {
11800
+ writeErrorWithHints(errorPayload);
11801
+ surfaceUnauthorizedHint({
11802
+ ...authFailureContext,
11803
+ payload: errorPayload
11804
+ });
11805
+ throw new Errors.CLIError("Cannot send: API key is missing or invalid (see hint above).", { exit: 1 });
11806
+ }
11807
+ throw new Errors.CLIError(`Could not look up your verified domains to default --from. Pass --from explicitly. Underlying error: ${formatErrorPayload(errorPayload)}`);
11808
+ }
11809
+ const first = result.data?.data?.find(isVerifiedDomain);
11810
+ if (!first) throw new Errors.CLIError("No active verified outbound domain found on this account; pass --from explicitly. To set up outbound, claim a domain via `primitive domains add` and verify it.");
11811
+ return `agent@${first.domain}`;
11812
+ }
11813
+ //#endregion
11814
+ //#region src/oclif/commands/emails-poll.ts
11815
+ function quoteDslValue(value) {
11816
+ if (/^[^\s"]+$/.test(value)) return value;
11817
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
11818
+ }
11819
+ function combineQ(q, domain) {
11820
+ const parts = [q?.trim(), domain ? `domain:${quoteDslValue(domain.trim())}` : void 0].filter((part) => Boolean(part));
11821
+ return parts.length > 0 ? parts.join(" ") : void 0;
11822
+ }
11823
+ function normalizeIsoDate(value, label) {
11824
+ const parsed = new Date(value);
11825
+ if (Number.isNaN(parsed.getTime())) throw new Error(`${label} must be a valid date or ISO-8601 timestamp.`);
11826
+ return parsed.toISOString();
11827
+ }
11828
+ function filtersFromFlags(flags) {
11829
+ return {
11830
+ body: flags.body,
11831
+ domain: flags.domain,
11832
+ domainId: flags["domain-id"],
11833
+ from: flags.from,
11834
+ hasAttachment: flags["has-attachment"],
11835
+ q: flags.q,
11836
+ replyToSentEmailId: flags["reply-to-sent-email-id"],
11837
+ spamScoreGte: flags["spam-score-gte"],
11838
+ spamScoreLt: flags["spam-score-lt"],
11839
+ subject: flags.subject,
11840
+ to: flags.to
11841
+ };
11842
+ }
11843
+ function sinceFromFlags(flags) {
11844
+ if (flags.since) return normalizeIsoDate(flags.since, "--since");
11845
+ return flags["include-existing"] ? void 0 : (/* @__PURE__ */ new Date()).toISOString();
11846
+ }
11847
+ function buildEmailSearchQuery(params) {
11848
+ const query = {
11849
+ include_facets: "false",
11850
+ limit: params.pageSize,
11851
+ snippet: "false",
11852
+ sort: "received_at_asc"
11853
+ };
11854
+ const q = combineQ(params.filters.q, params.filters.domain);
11855
+ if (q) query.q = q;
11856
+ if (params.filters.body) query.body = params.filters.body;
11857
+ if (params.filters.domainId) query.domain_id = params.filters.domainId;
11858
+ if (params.filters.from) query.from = params.filters.from;
11859
+ if (params.filters.hasAttachment !== void 0) query.has_attachment = params.filters.hasAttachment ? "true" : "false";
11860
+ if (params.filters.spamScoreGte !== void 0) query.spam_score_gte = params.filters.spamScoreGte;
11861
+ if (params.filters.spamScoreLt !== void 0) query.spam_score_lt = params.filters.spamScoreLt;
11862
+ if (params.filters.replyToSentEmailId) query.reply_to_sent_email_id = params.filters.replyToSentEmailId;
11863
+ if (params.filters.subject) query.subject = params.filters.subject;
11864
+ if (params.filters.to) query.to = params.filters.to;
11865
+ if (params.since) query.date_from = params.since;
11866
+ if (params.cursor) query.cursor = params.cursor;
11867
+ return query;
11868
+ }
11869
+ function encodeReceivedAtSearchCursor(email) {
11870
+ const raw = `r|${new Date(email.received_at).toISOString()}|${email.id}`;
11871
+ return Buffer.from(raw, "utf8").toString("base64url");
11872
+ }
11873
+ function cursorFromRows(rows) {
11874
+ const last = rows.at(-1);
11875
+ return last ? encodeReceivedAtSearchCursor(last) : null;
11876
+ }
11877
+ function collectNewAcceptedEmails(rows, seenIds) {
11878
+ const fresh = [];
11879
+ for (const row of rows) {
11880
+ if (row.status !== "accepted" && row.status !== "completed") continue;
11881
+ if (seenIds.has(row.id)) continue;
11882
+ seenIds.add(row.id);
11883
+ fresh.push(row);
11884
+ }
11885
+ return fresh;
11886
+ }
11887
+ async function fetchEmailSearchPage(params) {
11888
+ const result = await searchEmails({
11889
+ client: params.apiClient.client,
11890
+ query: buildEmailSearchQuery({
11891
+ cursor: params.cursor,
11892
+ filters: params.filters,
11893
+ pageSize: params.pageSize,
11894
+ since: params.since
11895
+ }),
11896
+ responseStyle: "fields"
11897
+ });
11898
+ if (result.error) return {
11899
+ ok: false,
11900
+ error: result.error
11901
+ };
11902
+ const envelope = result.data;
11903
+ const rows = envelope?.data ?? [];
11904
+ return {
11905
+ ok: true,
11906
+ cursor: envelope?.meta.cursor ?? cursorFromRows(rows),
11907
+ rows
11908
+ };
11909
+ }
11910
+ function sleep$1(ms) {
11911
+ return new Promise((resolve) => setTimeout(resolve, ms));
11912
+ }
11913
+ //#endregion
11914
+ //#region src/oclif/commands/chat.ts
11915
+ const DEFAULT_CHAT_TIMEOUT_SECONDS = 120;
11916
+ const DEFAULT_STRICT_PHASE_SECONDS = 60;
11917
+ function cliError$5(message) {
11918
+ return new Errors.CLIError(message, { exit: 1 });
11919
+ }
11920
+ async function readStdinToString() {
11921
+ if (process.stdin.isTTY) throw cliError$5("No message provided. Pass the message as the second positional argument or pipe it via stdin.");
11922
+ const chunks = [];
11923
+ for await (const chunk of process.stdin) chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
11924
+ return Buffer.concat(chunks).toString("utf8");
11925
+ }
11926
+ var ChatCommand = class ChatCommand extends Command {
11927
+ static description = `Send a message to an address and wait for the reply.
11928
+
11929
+ This is the first-party verb for talking to agents that live behind
11930
+ email addresses. \`primitive send\` is transport (fire-and-forget);
11931
+ \`primitive chat\` is semantic (send + wait for the threaded reply).
11932
+
11933
+ The message body can be given as the second positional argument or
11934
+ piped via stdin. The reply body is written to stdout; --json emits a
11935
+ structured envelope with both sides of the exchange.
11936
+
11937
+ Matching the reply: the wait phase polls inbound mail filtered by
11938
+ the recipient as sender and the send time as a lower bound. The
11939
+ first match is taken; the full inbound row is then fetched for the
11940
+ body. Exits non-zero on timeout.`;
11941
+ static summary = "Chat with an agent over email (send and wait for the reply)";
11942
+ static examples = [
11943
+ "<%= config.bin %> chat help@agent.acme.dev 'how do I rotate my API key?'",
11944
+ "cat error.log | <%= config.bin %> chat help@agent.acme.dev --subject 'webhook 401s'",
11945
+ "<%= config.bin %> chat help@agent.acme.dev 'follow up question' --json",
11946
+ "<%= config.bin %> chat help@agent.acme.dev 'one more thing' --timeout 300"
11947
+ ];
11948
+ static args = {
11949
+ recipient: Args.string({
11950
+ description: "Address to chat with (e.g. help@agent.acme.dev).",
11951
+ required: true
11952
+ }),
11953
+ message: Args.string({ description: "Message body. If omitted, read from stdin." })
11954
+ };
11955
+ static flags = {
11956
+ "api-key": Flags.string({
11957
+ description: "Primitive API key (defaults to PRIMITIVE_API_KEY or saved `primitive login` credentials)",
11958
+ env: "PRIMITIVE_API_KEY"
11959
+ }),
11960
+ "api-base-url-1": Flags.string({
11961
+ description: "Override the primary API base URL. Internal testing only; not documented to customers.",
11962
+ env: "PRIMITIVE_API_BASE_URL_1",
11963
+ hidden: true
11964
+ }),
11965
+ "api-base-url-2": Flags.string({
11966
+ description: "Override the attachments-supporting send host base URL. Internal testing only; not documented to customers.",
11967
+ env: "PRIMITIVE_API_BASE_URL_2",
11968
+ hidden: true
11969
+ }),
11970
+ from: Flags.string({ description: "Sender address. Defaults to agent@<your-first-verified-outbound-domain>." }),
11971
+ subject: Flags.string({ description: "Subject line. Defaults to the first line of the message when omitted." }),
11972
+ "in-reply-to": Flags.string({ description: "Message-Id of the parent email to thread this against. Use when continuing a prior conversation from outside the CLI; for an inbound you received via Primitive, prefer `primitive reply --id <inbound-id>`." }),
11973
+ json: Flags.boolean({ description: "Emit a structured JSON envelope { sent, reply } on stdout instead of just the reply body." }),
11974
+ timeout: Flags.integer({
11975
+ default: DEFAULT_CHAT_TIMEOUT_SECONDS,
11976
+ description: "Seconds to wait for a reply before exiting non-zero; 0 waits forever.",
11977
+ min: 0
11978
+ }),
11979
+ "strict-phase-seconds": Flags.integer({
11980
+ default: DEFAULT_STRICT_PHASE_SECONDS,
11981
+ description: "Seconds to wait in strict-threading mode (filter by reply_to_sent_email_id) before falling back to time-window matching. Set to the full --timeout to disable the fallback; --strict-only is the explicit way to do that.",
11982
+ min: 1
11983
+ }),
11984
+ "strict-only": Flags.boolean({ description: "Disable the time-window fallback. Only accept inbounds whose threading headers (In-Reply-To / References) resolve to this send. Recommended when correctness matters more than success rate (e.g. agents talking to agents)." }),
11985
+ interval: Flags.integer({
11986
+ default: 2,
11987
+ description: "Seconds between polls while waiting for the reply.",
11988
+ min: 1
11989
+ }),
11990
+ "page-size": Flags.integer({
11991
+ default: 50,
11992
+ description: "Inbound emails to fetch per poll while waiting (1-100). Internal tuning knob.",
11993
+ max: 100,
11994
+ min: 1,
11995
+ hidden: true
11996
+ }),
11997
+ time: Flags.boolean({ description: TIME_FLAG_DESCRIPTION })
11998
+ };
11999
+ async run() {
12000
+ const { args, flags } = await this.parse(ChatCommand);
12001
+ const message = args.message !== void 0 && args.message !== "" ? args.message : await readStdinToString();
12002
+ if (!message.trim()) throw cliError$5("Message body is empty.");
12003
+ await runWithTiming(flags.time, async () => {
12004
+ const { apiClient, auth, baseUrlOverridden } = createAuthenticatedCliApiClient({
12005
+ apiKey: flags["api-key"],
12006
+ apiBaseUrl1: flags["api-base-url-1"],
12007
+ apiBaseUrl2: flags["api-base-url-2"],
12008
+ configDir: this.config.configDir
12009
+ });
12010
+ const authFailureContext = {
12011
+ auth,
12012
+ baseUrlOverridden,
12013
+ configDir: this.config.configDir
12014
+ };
12015
+ const from = flags.from ?? await pickDefaultFromAddress(apiClient, authFailureContext);
12016
+ const subject = flags.subject ?? deriveSubject(message);
12017
+ const sentAtIso = (/* @__PURE__ */ new Date()).toISOString();
12018
+ const sendResult = await sendEmail({
12019
+ body: {
12020
+ from,
12021
+ to: args.recipient,
12022
+ subject,
12023
+ body_text: message,
12024
+ ...flags["in-reply-to"] !== void 0 ? { in_reply_to: flags["in-reply-to"] } : {}
12025
+ },
12026
+ client: apiClient._sendClient,
12027
+ responseStyle: "fields"
12028
+ });
12029
+ if (sendResult.error) {
12030
+ const errorPayload = extractErrorPayload(sendResult.error);
12031
+ writeErrorWithHints(errorPayload);
12032
+ surfaceUnauthorizedHint({
12033
+ ...authFailureContext,
12034
+ payload: errorPayload
12035
+ });
12036
+ process.exitCode = 1;
12037
+ return;
12038
+ }
12039
+ const sent = sendResult.data?.data;
12040
+ if (!sent) throw cliError$5("Send succeeded but the API returned no data.");
12041
+ const reply = await waitForReply({
12042
+ apiClient,
12043
+ authFailureContext,
12044
+ from,
12045
+ interval: flags.interval,
12046
+ pageSize: flags["page-size"],
12047
+ recipient: args.recipient,
12048
+ sentAtIso,
12049
+ sentId: sent.id,
12050
+ strictOnly: flags["strict-only"],
12051
+ strictPhaseSeconds: flags["strict-phase-seconds"],
12052
+ timeoutSeconds: flags.timeout
12053
+ });
12054
+ if (reply === null) {
12055
+ process.stderr.write(`Timed out after ${flags.timeout}s waiting for a reply from ${args.recipient}.\n`);
12056
+ process.exitCode = 1;
12057
+ return;
12058
+ }
12059
+ if (flags.json) {
12060
+ const envelope = {
12061
+ sent,
12062
+ reply
12063
+ };
12064
+ this.log(JSON.stringify(envelope, null, 2));
12065
+ } else {
12066
+ const body = reply.body_text ?? reply.body_html ?? "";
12067
+ this.log(body);
12068
+ }
12069
+ });
12070
+ }
12071
+ };
12072
+ async function waitForReply(params) {
12073
+ const totalDeadline = params.timeoutSeconds === 0 ? null : Date.now() + params.timeoutSeconds * 1e3;
12074
+ const strictDeadlineFromBudget = Date.now() + params.strictPhaseSeconds * 1e3;
12075
+ const strictDeadline = params.strictOnly ? totalDeadline : totalDeadline === null ? strictDeadlineFromBudget : Math.min(strictDeadlineFromBudget, totalDeadline);
12076
+ const phases = [{
12077
+ label: "strict",
12078
+ filters: { replyToSentEmailId: params.sentId },
12079
+ deadline: strictDeadline
12080
+ }];
12081
+ if (!params.strictOnly) phases.push({
12082
+ label: "fallback",
12083
+ filters: {
12084
+ from: params.recipient,
12085
+ to: params.from
12086
+ },
12087
+ deadline: totalDeadline
12088
+ });
12089
+ let strictFilterUnsupported = false;
12090
+ for (const phase of phases) {
12091
+ if (phase.label === "strict" && strictFilterUnsupported) continue;
12092
+ const seenIds = /* @__PURE__ */ new Set();
12093
+ let cursor = null;
12094
+ while (true) {
12095
+ if (phase.deadline !== null && Date.now() >= phase.deadline) break;
12096
+ const page = await fetchEmailSearchPage({
12097
+ apiClient: params.apiClient,
12098
+ cursor,
12099
+ filters: phase.filters,
12100
+ pageSize: params.pageSize,
12101
+ since: params.sentAtIso
12102
+ });
12103
+ if (!page.ok) {
12104
+ const payload = extractErrorPayload(page.error);
12105
+ writeErrorWithHints(payload);
12106
+ surfaceUnauthorizedHint({
12107
+ ...params.authFailureContext,
12108
+ payload
12109
+ });
12110
+ throw new Errors.CLIError("Failed to poll for reply.", { exit: 1 });
12111
+ }
12112
+ let lastAccepted;
12113
+ for (let i = page.rows.length - 1; i >= 0; i--) {
12114
+ const row = page.rows[i];
12115
+ if (row.status === "accepted" || row.status === "completed") {
12116
+ lastAccepted = row;
12117
+ break;
12118
+ }
12119
+ }
12120
+ if (lastAccepted) cursor = encodeReceivedAtSearchCursor(lastAccepted);
12121
+ const matches = collectNewAcceptedEmails(page.rows, seenIds);
12122
+ for (const match of matches) {
12123
+ const full = await getEmail({
12124
+ client: params.apiClient.client,
12125
+ path: { id: match.id },
12126
+ responseStyle: "fields"
12127
+ });
12128
+ if (full.error) {
12129
+ const payload = extractErrorPayload(full.error);
12130
+ writeErrorWithHints(payload);
12131
+ surfaceUnauthorizedHint({
12132
+ ...params.authFailureContext,
12133
+ payload
12134
+ });
12135
+ throw new Errors.CLIError(`Reply landed but fetching the full body failed (id=${match.id}).`, { exit: 1 });
12136
+ }
12137
+ const envelope = full.data;
12138
+ const detail = envelope?.data ?? envelope ?? null;
12139
+ if (!detail) throw new Errors.CLIError(`Reply landed but the email body could not be loaded (id=${match.id}).`, { exit: 1 });
12140
+ if (phase.label === "strict" && detail.reply_to_sent_email_id !== params.sentId) {
12141
+ if (!strictFilterUnsupported) process.stderr.write(params.strictOnly ? "Strict-phase reply matching is not supported by this Primitive API host; --strict-only requires server support so the command will exit without a match.\n" : "Strict-phase reply matching is not supported by this Primitive API host; falling back to time-window matching.\n");
12142
+ strictFilterUnsupported = true;
12143
+ continue;
12144
+ }
12145
+ return detail;
12146
+ }
12147
+ if (strictFilterUnsupported && phase.label === "strict") break;
12148
+ if (lastAccepted !== void 0) continue;
12149
+ if (phase.deadline !== null && Date.now() >= phase.deadline) break;
12150
+ if (totalDeadline !== null && Date.now() >= totalDeadline) return null;
12151
+ await sleep$1(params.interval * 1e3);
12152
+ }
12153
+ }
12154
+ return null;
12155
+ }
12156
+ //#endregion
11740
12157
  //#region src/oclif/commands/config.ts
11741
12158
  function loadOrCreateConfig(configDir) {
11742
12159
  return loadCliConfig(configDir) ?? emptyCliConfig();
@@ -12197,7 +12614,7 @@ var EmailsLatestCommand = class EmailsLatestCommand extends Command {
12197
12614
  if (result.error) {
12198
12615
  const errorPayload = extractErrorPayload(result.error);
12199
12616
  writeErrorWithHints(errorPayload);
12200
- removeStaleSavedCredentialOnUnauthorized({
12617
+ surfaceUnauthorizedHint({
12201
12618
  auth,
12202
12619
  baseUrlOverridden,
12203
12620
  configDir: this.config.configDir,
@@ -12223,104 +12640,6 @@ var EmailsLatestCommand = class EmailsLatestCommand extends Command {
12223
12640
  }
12224
12641
  };
12225
12642
  //#endregion
12226
- //#region src/oclif/commands/emails-poll.ts
12227
- function quoteDslValue(value) {
12228
- if (/^[^\s"]+$/.test(value)) return value;
12229
- return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
12230
- }
12231
- function combineQ(q, domain) {
12232
- const parts = [q?.trim(), domain ? `domain:${quoteDslValue(domain.trim())}` : void 0].filter((part) => Boolean(part));
12233
- return parts.length > 0 ? parts.join(" ") : void 0;
12234
- }
12235
- function normalizeIsoDate(value, label) {
12236
- const parsed = new Date(value);
12237
- if (Number.isNaN(parsed.getTime())) throw new Error(`${label} must be a valid date or ISO-8601 timestamp.`);
12238
- return parsed.toISOString();
12239
- }
12240
- function filtersFromFlags(flags) {
12241
- return {
12242
- body: flags.body,
12243
- domain: flags.domain,
12244
- domainId: flags["domain-id"],
12245
- from: flags.from,
12246
- hasAttachment: flags["has-attachment"],
12247
- q: flags.q,
12248
- spamScoreGte: flags["spam-score-gte"],
12249
- spamScoreLt: flags["spam-score-lt"],
12250
- subject: flags.subject,
12251
- to: flags.to
12252
- };
12253
- }
12254
- function sinceFromFlags(flags) {
12255
- if (flags.since) return normalizeIsoDate(flags.since, "--since");
12256
- return flags["include-existing"] ? void 0 : (/* @__PURE__ */ new Date()).toISOString();
12257
- }
12258
- function buildEmailSearchQuery(params) {
12259
- const query = {
12260
- include_facets: "false",
12261
- limit: params.pageSize,
12262
- snippet: "false",
12263
- sort: "received_at_asc"
12264
- };
12265
- const q = combineQ(params.filters.q, params.filters.domain);
12266
- if (q) query.q = q;
12267
- if (params.filters.body) query.body = params.filters.body;
12268
- if (params.filters.domainId) query.domain_id = params.filters.domainId;
12269
- if (params.filters.from) query.from = params.filters.from;
12270
- if (params.filters.hasAttachment !== void 0) query.has_attachment = params.filters.hasAttachment ? "true" : "false";
12271
- if (params.filters.spamScoreGte !== void 0) query.spam_score_gte = params.filters.spamScoreGte;
12272
- if (params.filters.spamScoreLt !== void 0) query.spam_score_lt = params.filters.spamScoreLt;
12273
- if (params.filters.subject) query.subject = params.filters.subject;
12274
- if (params.filters.to) query.to = params.filters.to;
12275
- if (params.since) query.date_from = params.since;
12276
- if (params.cursor) query.cursor = params.cursor;
12277
- return query;
12278
- }
12279
- function encodeReceivedAtSearchCursor(email) {
12280
- const raw = `r|${new Date(email.received_at).toISOString()}|${email.id}`;
12281
- return Buffer.from(raw, "utf8").toString("base64url");
12282
- }
12283
- function cursorFromRows(rows) {
12284
- const last = rows.at(-1);
12285
- return last ? encodeReceivedAtSearchCursor(last) : null;
12286
- }
12287
- function collectNewAcceptedEmails(rows, seenIds) {
12288
- const fresh = [];
12289
- for (const row of rows) {
12290
- if (row.status !== "accepted" && row.status !== "completed") continue;
12291
- if (seenIds.has(row.id)) continue;
12292
- seenIds.add(row.id);
12293
- fresh.push(row);
12294
- }
12295
- return fresh;
12296
- }
12297
- async function fetchEmailSearchPage(params) {
12298
- const result = await searchEmails({
12299
- client: params.apiClient.client,
12300
- query: buildEmailSearchQuery({
12301
- cursor: params.cursor,
12302
- filters: params.filters,
12303
- pageSize: params.pageSize,
12304
- since: params.since
12305
- }),
12306
- responseStyle: "fields"
12307
- });
12308
- if (result.error) return {
12309
- ok: false,
12310
- error: result.error
12311
- };
12312
- const envelope = result.data;
12313
- const rows = envelope?.data ?? [];
12314
- return {
12315
- ok: true,
12316
- cursor: envelope?.meta.cursor ?? cursorFromRows(rows),
12317
- rows
12318
- };
12319
- }
12320
- function sleep$1(ms) {
12321
- return new Promise((resolve) => setTimeout(resolve, ms));
12322
- }
12323
- //#endregion
12324
12643
  //#region src/oclif/commands/emails-wait.ts
12325
12644
  const DEFAULT_WAIT_TIMEOUT_SECONDS$1 = 300;
12326
12645
  function cliError$4(message) {
@@ -12373,6 +12692,7 @@ var EmailsWaitCommand = class EmailsWaitCommand extends Command {
12373
12692
  min: 1
12374
12693
  }),
12375
12694
  q: Flags.string({ description: "Full-text search DSL query" }),
12695
+ "reply-to-sent-email-id": Flags.string({ description: "Filter to inbound emails that are threaded replies to a specific outbound send (UUID from a /v1/send-mail response). Combine with --to and --since for the strictest version of the wait-for-reply pattern." }),
12376
12696
  since: Flags.string({ description: "Only match emails received on or after this date/time" }),
12377
12697
  "spam-score-gte": Flags.integer({ description: "Only match emails with spam score greater than or equal to this value" }),
12378
12698
  "spam-score-lt": Flags.integer({ description: "Only match emails with spam score below this value" }),
@@ -12417,7 +12737,7 @@ var EmailsWaitCommand = class EmailsWaitCommand extends Command {
12417
12737
  if (!page.ok) {
12418
12738
  const payload = extractErrorPayload(page.error);
12419
12739
  writeErrorWithHints(payload);
12420
- removeStaleSavedCredentialOnUnauthorized({
12740
+ surfaceUnauthorizedHint({
12421
12741
  auth,
12422
12742
  baseUrlOverridden,
12423
12743
  configDir: this.config.configDir,
@@ -12539,7 +12859,7 @@ var EmailsWatchCommand = class EmailsWatchCommand extends Command {
12539
12859
  if (!page.ok) {
12540
12860
  const payload = extractErrorPayload(page.error);
12541
12861
  writeErrorWithHints(payload);
12542
- removeStaleSavedCredentialOnUnauthorized({
12862
+ surfaceUnauthorizedHint({
12543
12863
  auth,
12544
12864
  baseUrlOverridden,
12545
12865
  configDir: this.config.configDir,
@@ -13261,7 +13581,7 @@ var FunctionsDeployCommand = class FunctionsDeployCommand extends Command {
13261
13581
  process.stderr.write(`Function ${outcome.created.name} (${outcome.created.id}) was created and secrets [${succeeded}] were written, but the final redeploy failed; the new bindings are NOT yet live. Re-run \`primitive functions redeploy --id ${outcome.created.id} --file <bundle>\` once the cause is fixed.\n`);
13262
13582
  }
13263
13583
  writeErrorWithHints(outcome.payload);
13264
- removeStaleSavedCredentialOnUnauthorized({
13584
+ surfaceUnauthorizedHint({
13265
13585
  ...authFailureContext,
13266
13586
  payload: outcome.payload
13267
13587
  });
@@ -13284,7 +13604,7 @@ var FunctionsDeployCommand = class FunctionsDeployCommand extends Command {
13284
13604
  });
13285
13605
  if (waitResult.kind === "error") {
13286
13606
  writeErrorWithHints(waitResult.payload);
13287
- removeStaleSavedCredentialOnUnauthorized({
13607
+ surfaceUnauthorizedHint({
13288
13608
  ...authFailureContext,
13289
13609
  payload: waitResult.payload
13290
13610
  });
@@ -13317,8 +13637,8 @@ const PRIMITIVE_TEAM_AUTHOR = {
13317
13637
  name: "Primitive Team",
13318
13638
  url: "https://primitive.dev"
13319
13639
  };
13320
- const SDK_VERSION_RANGE = "^0.30.3";
13321
- const CLI_VERSION_RANGE = "^0.30.3";
13640
+ const SDK_VERSION_RANGE = "^0.31.0";
13641
+ const CLI_VERSION_RANGE = "^0.31.0";
13322
13642
  const ESBUILD_VERSION_RANGE = "^0.27.0";
13323
13643
  function renderHandler() {
13324
13644
  return `// env.PRIMITIVE_API_KEY is auto-injected by the Primitive Functions runtime.
@@ -13828,7 +14148,7 @@ var FunctionsLogsCommand = class FunctionsLogsCommand extends Command {
13828
14148
  if (result.error) {
13829
14149
  const errorPayload = extractErrorPayload(result.error);
13830
14150
  writeErrorWithHints(errorPayload);
13831
- removeStaleSavedCredentialOnUnauthorized({
14151
+ surfaceUnauthorizedHint({
13832
14152
  auth,
13833
14153
  baseUrlOverridden,
13834
14154
  configDir: this.config.configDir,
@@ -14084,7 +14404,7 @@ var FunctionsRedeployCommand = class FunctionsRedeployCommand extends Command {
14084
14404
  process.stderr.write(`Secrets [${succeeded}] were written, but the redeploy step failed; the new bindings are NOT yet live. Re-run \`primitive functions redeploy --id ${flags.id} --file <bundle>\` once the cause is fixed.\n`);
14085
14405
  }
14086
14406
  writeErrorWithHints(outcome.payload);
14087
- removeStaleSavedCredentialOnUnauthorized({
14407
+ surfaceUnauthorizedHint({
14088
14408
  ...authFailureContext,
14089
14409
  payload: outcome.payload
14090
14410
  });
@@ -14106,7 +14426,7 @@ var FunctionsRedeployCommand = class FunctionsRedeployCommand extends Command {
14106
14426
  });
14107
14427
  if (waitResult.kind === "error") {
14108
14428
  writeErrorWithHints(waitResult.payload);
14109
- removeStaleSavedCredentialOnUnauthorized({
14429
+ surfaceUnauthorizedHint({
14110
14430
  ...authFailureContext,
14111
14431
  payload: waitResult.payload
14112
14432
  });
@@ -14307,7 +14627,7 @@ var FunctionsSetSecretCommand = class FunctionsSetSecretCommand extends Command
14307
14627
  if (outcome.stage === "get-function") process.stderr.write("Secret was written, but reading current function code for redeploy failed; the secret is NOT yet live. Re-run with --redeploy, or call `primitive functions redeploy --id <id> --file <bundle>` once you have the bundle.\n");
14308
14628
  else if (outcome.stage === "redeploy") process.stderr.write("Secret was written, but the redeploy step failed; the secret is NOT yet live. Inspect the function's deploy_error and re-run `primitive functions redeploy --id <id> --file <bundle>` once the cause is fixed.\n");
14309
14629
  writeErrorWithHints(outcome.payload);
14310
- removeStaleSavedCredentialOnUnauthorized({
14630
+ surfaceUnauthorizedHint({
14311
14631
  ...authFailureContext,
14312
14632
  payload: outcome.payload
14313
14633
  });
@@ -14495,7 +14815,7 @@ var FunctionsTestFunctionCommand = class FunctionsTestFunctionCommand extends Co
14495
14815
  if (triggerResult.error) {
14496
14816
  const payload = extractErrorPayload(triggerResult.error);
14497
14817
  writeErrorWithHints(payload);
14498
- removeStaleSavedCredentialOnUnauthorized({
14818
+ surfaceUnauthorizedHint({
14499
14819
  auth,
14500
14820
  baseUrlOverridden,
14501
14821
  configDir: this.config.configDir,
@@ -14533,7 +14853,7 @@ var FunctionsTestFunctionCommand = class FunctionsTestFunctionCommand extends Co
14533
14853
  if (!page.ok) {
14534
14854
  const payload = extractErrorPayload(page.error);
14535
14855
  writeErrorWithHints(payload);
14536
- removeStaleSavedCredentialOnUnauthorized({
14856
+ surfaceUnauthorizedHint({
14537
14857
  auth,
14538
14858
  baseUrlOverridden,
14539
14859
  configDir: this.config.configDir,
@@ -14561,7 +14881,7 @@ var FunctionsTestFunctionCommand = class FunctionsTestFunctionCommand extends Co
14561
14881
  if (result.error) {
14562
14882
  const payload = extractErrorPayload(result.error);
14563
14883
  writeErrorWithHints(payload);
14564
- removeStaleSavedCredentialOnUnauthorized({
14884
+ surfaceUnauthorizedHint({
14565
14885
  auth,
14566
14886
  baseUrlOverridden,
14567
14887
  configDir: this.config.configDir,
@@ -14641,22 +14961,17 @@ async function checkExistingLogin(params) {
14641
14961
  })))(apiClient);
14642
14962
  if (!result.error) return { status: "valid" };
14643
14963
  const payload = extractErrorPayload(result.error);
14644
- if (removeStaleSavedCredentialOnUnauthorized({
14645
- auth: {
14646
- apiKey: params.credentials.api_key,
14647
- apiBaseUrl1: probeApiBaseUrl1,
14648
- apiBaseUrl2: normalizeApiBaseUrl2(void 0),
14649
- credentials: params.credentials,
14650
- source: "stored"
14651
- },
14652
- baseUrlOverridden: requestConfig.baseUrlOverridden,
14653
- configDir: params.configDir,
14654
- payload
14655
- })) return { status: "removed_stale" };
14964
+ const code = extractErrorCode(payload);
14965
+ const baseUrlDiffersFromSaved = requestConfig.baseUrlOverridden && requestConfig.apiBaseUrl1 !== params.credentials.api_base_url_1;
14966
+ if (code === API_ERROR_CODES.unauthorized && !baseUrlDiffersFromSaved) {
14967
+ deleteCliCredentials(params.configDir);
14968
+ process.stderr.write("Removed saved Primitive CLI credentials because the existing key was rejected during login. Continuing with a fresh login.\n");
14969
+ return { status: "removed_stale" };
14970
+ }
14656
14971
  return {
14657
14972
  status: "blocked",
14658
14973
  payload,
14659
- message: extractErrorCode(payload) === API_ERROR_CODES.unauthorized ? "Saved Primitive CLI credentials were rejected. Run `primitive logout` to remove them before logging in again." : "A saved Primitive CLI login exists, but the CLI could not verify whether it is still valid. Run `primitive logout` before logging in again."
14974
+ message: code === API_ERROR_CODES.unauthorized ? "Saved Primitive CLI credentials were rejected by an API URL different from the one they were saved with. Run `primitive logout` to remove them, or switch back to the original environment before logging in again." : "A saved Primitive CLI login exists, but the CLI could not verify whether it is still valid. Run `primitive logout` before logging in again."
14660
14975
  };
14661
14976
  }
14662
14977
  var LoginCommand = class LoginCommand extends Command {
@@ -15041,7 +15356,7 @@ var ReplyCommand = class ReplyCommand extends Command {
15041
15356
  if (result.error) {
15042
15357
  const errorPayload = extractErrorPayload(result.error);
15043
15358
  writeErrorWithHints(errorPayload);
15044
- removeStaleSavedCredentialOnUnauthorized({
15359
+ surfaceUnauthorizedHint({
15045
15360
  auth,
15046
15361
  baseUrlOverridden,
15047
15362
  configDir: this.config.configDir,
@@ -15060,39 +15375,6 @@ var ReplyCommand = class ReplyCommand extends Command {
15060
15375
  };
15061
15376
  //#endregion
15062
15377
  //#region src/oclif/commands/send.ts
15063
- const SUBJECT_MAX_LENGTH = 200;
15064
- function deriveSubject(body) {
15065
- for (const line of body.split("\n")) {
15066
- const trimmed = line.trim();
15067
- if (!trimmed) continue;
15068
- return trimmed.length > SUBJECT_MAX_LENGTH ? `${trimmed.slice(0, SUBJECT_MAX_LENGTH - 3)}...` : trimmed;
15069
- }
15070
- return "Message";
15071
- }
15072
- function isVerifiedDomain(domain) {
15073
- return domain.is_active === true;
15074
- }
15075
- async function pickDefaultFromAddress(apiClient, authFailureContext) {
15076
- const result = await listDomains({
15077
- client: apiClient.client,
15078
- responseStyle: "fields"
15079
- });
15080
- if (result.error) {
15081
- const errorPayload = extractErrorPayload(result.error);
15082
- if (extractErrorCode(errorPayload) === API_ERROR_CODES.unauthorized) {
15083
- writeErrorWithHints(errorPayload);
15084
- removeStaleSavedCredentialOnUnauthorized({
15085
- ...authFailureContext,
15086
- payload: errorPayload
15087
- });
15088
- throw new Errors.CLIError("Cannot send: API key is missing or invalid (see hint above).", { exit: 1 });
15089
- }
15090
- throw new Errors.CLIError(`Could not look up your verified domains to default --from. Pass --from explicitly. Underlying error: ${formatErrorPayload(errorPayload)}`);
15091
- }
15092
- const first = result.data?.data?.find(isVerifiedDomain);
15093
- if (!first) throw new Errors.CLIError("No active verified outbound domain found on this account; pass --from explicitly. To set up outbound, claim a domain via `primitive domains add` and verify it.");
15094
- return `agent@${first.domain}`;
15095
- }
15096
15378
  var SendCommand = class SendCommand extends Command {
15097
15379
  static description = `Send an outbound email. Agent-grade shortcut for \`sending send\` with sensible defaults.
15098
15380
 
@@ -15184,7 +15466,7 @@ var SendCommand = class SendCommand extends Command {
15184
15466
  if (result.error) {
15185
15467
  const errorPayload = extractErrorPayload(result.error);
15186
15468
  writeErrorWithHints(errorPayload);
15187
- removeStaleSavedCredentialOnUnauthorized({
15469
+ surfaceUnauthorizedHint({
15188
15470
  ...authFailureContext,
15189
15471
  payload: errorPayload
15190
15472
  });
@@ -15629,7 +15911,7 @@ var WhoamiCommand = class WhoamiCommand extends Command {
15629
15911
  if (result.error) {
15630
15912
  const errorPayload = extractErrorPayload(result.error);
15631
15913
  writeErrorWithHints(errorPayload);
15632
- removeStaleSavedCredentialOnUnauthorized({
15914
+ surfaceUnauthorizedHint({
15633
15915
  auth,
15634
15916
  baseUrlOverridden,
15635
15917
  configDir: this.config.configDir,
@@ -15860,6 +16142,7 @@ const COMMANDS = {
15860
16142
  describe: DescribeCommand,
15861
16143
  send: SendCommand,
15862
16144
  reply: ReplyCommand,
16145
+ chat: ChatCommand,
15863
16146
  login: LoginCommand,
15864
16147
  signup: SignupCommand,
15865
16148
  logout: LogoutCommand,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@primitivedotdev/cli",
3
- "version": "0.30.3",
3
+ "version": "0.31.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,