@primitivedotdev/cli 0.30.2 → 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 +468 -184
  2. package/package.json +5 -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();
@@ -11748,8 +12165,7 @@ function redactConfig(config) {
11748
12165
  };
11749
12166
  }
11750
12167
  var ConfigSetCommand = class ConfigSetCommand extends Command {
11751
- static hidden = true;
11752
- static summary = "Set hidden Primitive CLI request config";
12168
+ static summary = "Set a Primitive CLI request environment";
11753
12169
  static flags = {
11754
12170
  environment: Flags.string({
11755
12171
  char: "e",
@@ -11783,8 +12199,7 @@ var ConfigSetCommand = class ConfigSetCommand extends Command {
11783
12199
  }
11784
12200
  };
11785
12201
  var ConfigUseCommand = class ConfigUseCommand extends Command {
11786
- static hidden = true;
11787
- static summary = "Switch active Primitive CLI request config";
12202
+ static summary = "Switch the active Primitive CLI request environment";
11788
12203
  static args = { environment: Args.string({
11789
12204
  description: "Environment name to use",
11790
12205
  required: true
@@ -11802,8 +12217,7 @@ var ConfigUseCommand = class ConfigUseCommand extends Command {
11802
12217
  }
11803
12218
  };
11804
12219
  var ConfigListCommand = class ConfigListCommand extends Command {
11805
- static hidden = true;
11806
- static summary = "List hidden Primitive CLI request configs";
12220
+ static summary = "List Primitive CLI request environments";
11807
12221
  static flags = {
11808
12222
  json: Flags.boolean({ description: "Print JSON" }),
11809
12223
  "show-secrets": Flags.boolean({ description: "Show header values instead of redacting them" })
@@ -11833,8 +12247,7 @@ var ConfigListCommand = class ConfigListCommand extends Command {
11833
12247
  }
11834
12248
  };
11835
12249
  var ConfigResetCommand = class ConfigResetCommand extends Command {
11836
- static hidden = true;
11837
- static summary = "Reset hidden Primitive CLI request config";
12250
+ static summary = "Reset Primitive CLI request environments";
11838
12251
  static flags = { environment: Flags.string({
11839
12252
  char: "e",
11840
12253
  description: "Only remove one environment"
@@ -11858,6 +12271,11 @@ var ConfigResetCommand = class ConfigResetCommand extends Command {
11858
12271
  process.stderr.write(`Primitive CLI environment ${environment} removed.\n`);
11859
12272
  }
11860
12273
  };
12274
+ var ConfigCommand = class extends ConfigListCommand {
12275
+ static hidden = true;
12276
+ static summary = "Manage Primitive CLI request environments";
12277
+ static description = "Manage local Primitive CLI request environments for API endpoint overrides and request headers.";
12278
+ };
11861
12279
  //#endregion
11862
12280
  //#region src/oclif/commands/doctor.ts
11863
12281
  const MIN_NODE_MAJOR = 22;
@@ -12196,7 +12614,7 @@ var EmailsLatestCommand = class EmailsLatestCommand extends Command {
12196
12614
  if (result.error) {
12197
12615
  const errorPayload = extractErrorPayload(result.error);
12198
12616
  writeErrorWithHints(errorPayload);
12199
- removeStaleSavedCredentialOnUnauthorized({
12617
+ surfaceUnauthorizedHint({
12200
12618
  auth,
12201
12619
  baseUrlOverridden,
12202
12620
  configDir: this.config.configDir,
@@ -12222,104 +12640,6 @@ var EmailsLatestCommand = class EmailsLatestCommand extends Command {
12222
12640
  }
12223
12641
  };
12224
12642
  //#endregion
12225
- //#region src/oclif/commands/emails-poll.ts
12226
- function quoteDslValue(value) {
12227
- if (/^[^\s"]+$/.test(value)) return value;
12228
- return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
12229
- }
12230
- function combineQ(q, domain) {
12231
- const parts = [q?.trim(), domain ? `domain:${quoteDslValue(domain.trim())}` : void 0].filter((part) => Boolean(part));
12232
- return parts.length > 0 ? parts.join(" ") : void 0;
12233
- }
12234
- function normalizeIsoDate(value, label) {
12235
- const parsed = new Date(value);
12236
- if (Number.isNaN(parsed.getTime())) throw new Error(`${label} must be a valid date or ISO-8601 timestamp.`);
12237
- return parsed.toISOString();
12238
- }
12239
- function filtersFromFlags(flags) {
12240
- return {
12241
- body: flags.body,
12242
- domain: flags.domain,
12243
- domainId: flags["domain-id"],
12244
- from: flags.from,
12245
- hasAttachment: flags["has-attachment"],
12246
- q: flags.q,
12247
- spamScoreGte: flags["spam-score-gte"],
12248
- spamScoreLt: flags["spam-score-lt"],
12249
- subject: flags.subject,
12250
- to: flags.to
12251
- };
12252
- }
12253
- function sinceFromFlags(flags) {
12254
- if (flags.since) return normalizeIsoDate(flags.since, "--since");
12255
- return flags["include-existing"] ? void 0 : (/* @__PURE__ */ new Date()).toISOString();
12256
- }
12257
- function buildEmailSearchQuery(params) {
12258
- const query = {
12259
- include_facets: "false",
12260
- limit: params.pageSize,
12261
- snippet: "false",
12262
- sort: "received_at_asc"
12263
- };
12264
- const q = combineQ(params.filters.q, params.filters.domain);
12265
- if (q) query.q = q;
12266
- if (params.filters.body) query.body = params.filters.body;
12267
- if (params.filters.domainId) query.domain_id = params.filters.domainId;
12268
- if (params.filters.from) query.from = params.filters.from;
12269
- if (params.filters.hasAttachment !== void 0) query.has_attachment = params.filters.hasAttachment ? "true" : "false";
12270
- if (params.filters.spamScoreGte !== void 0) query.spam_score_gte = params.filters.spamScoreGte;
12271
- if (params.filters.spamScoreLt !== void 0) query.spam_score_lt = params.filters.spamScoreLt;
12272
- if (params.filters.subject) query.subject = params.filters.subject;
12273
- if (params.filters.to) query.to = params.filters.to;
12274
- if (params.since) query.date_from = params.since;
12275
- if (params.cursor) query.cursor = params.cursor;
12276
- return query;
12277
- }
12278
- function encodeReceivedAtSearchCursor(email) {
12279
- const raw = `r|${new Date(email.received_at).toISOString()}|${email.id}`;
12280
- return Buffer.from(raw, "utf8").toString("base64url");
12281
- }
12282
- function cursorFromRows(rows) {
12283
- const last = rows.at(-1);
12284
- return last ? encodeReceivedAtSearchCursor(last) : null;
12285
- }
12286
- function collectNewAcceptedEmails(rows, seenIds) {
12287
- const fresh = [];
12288
- for (const row of rows) {
12289
- if (row.status !== "accepted" && row.status !== "completed") continue;
12290
- if (seenIds.has(row.id)) continue;
12291
- seenIds.add(row.id);
12292
- fresh.push(row);
12293
- }
12294
- return fresh;
12295
- }
12296
- async function fetchEmailSearchPage(params) {
12297
- const result = await searchEmails({
12298
- client: params.apiClient.client,
12299
- query: buildEmailSearchQuery({
12300
- cursor: params.cursor,
12301
- filters: params.filters,
12302
- pageSize: params.pageSize,
12303
- since: params.since
12304
- }),
12305
- responseStyle: "fields"
12306
- });
12307
- if (result.error) return {
12308
- ok: false,
12309
- error: result.error
12310
- };
12311
- const envelope = result.data;
12312
- const rows = envelope?.data ?? [];
12313
- return {
12314
- ok: true,
12315
- cursor: envelope?.meta.cursor ?? cursorFromRows(rows),
12316
- rows
12317
- };
12318
- }
12319
- function sleep$1(ms) {
12320
- return new Promise((resolve) => setTimeout(resolve, ms));
12321
- }
12322
- //#endregion
12323
12643
  //#region src/oclif/commands/emails-wait.ts
12324
12644
  const DEFAULT_WAIT_TIMEOUT_SECONDS$1 = 300;
12325
12645
  function cliError$4(message) {
@@ -12372,6 +12692,7 @@ var EmailsWaitCommand = class EmailsWaitCommand extends Command {
12372
12692
  min: 1
12373
12693
  }),
12374
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." }),
12375
12696
  since: Flags.string({ description: "Only match emails received on or after this date/time" }),
12376
12697
  "spam-score-gte": Flags.integer({ description: "Only match emails with spam score greater than or equal to this value" }),
12377
12698
  "spam-score-lt": Flags.integer({ description: "Only match emails with spam score below this value" }),
@@ -12416,7 +12737,7 @@ var EmailsWaitCommand = class EmailsWaitCommand extends Command {
12416
12737
  if (!page.ok) {
12417
12738
  const payload = extractErrorPayload(page.error);
12418
12739
  writeErrorWithHints(payload);
12419
- removeStaleSavedCredentialOnUnauthorized({
12740
+ surfaceUnauthorizedHint({
12420
12741
  auth,
12421
12742
  baseUrlOverridden,
12422
12743
  configDir: this.config.configDir,
@@ -12538,7 +12859,7 @@ var EmailsWatchCommand = class EmailsWatchCommand extends Command {
12538
12859
  if (!page.ok) {
12539
12860
  const payload = extractErrorPayload(page.error);
12540
12861
  writeErrorWithHints(payload);
12541
- removeStaleSavedCredentialOnUnauthorized({
12862
+ surfaceUnauthorizedHint({
12542
12863
  auth,
12543
12864
  baseUrlOverridden,
12544
12865
  configDir: this.config.configDir,
@@ -13260,7 +13581,7 @@ var FunctionsDeployCommand = class FunctionsDeployCommand extends Command {
13260
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`);
13261
13582
  }
13262
13583
  writeErrorWithHints(outcome.payload);
13263
- removeStaleSavedCredentialOnUnauthorized({
13584
+ surfaceUnauthorizedHint({
13264
13585
  ...authFailureContext,
13265
13586
  payload: outcome.payload
13266
13587
  });
@@ -13283,7 +13604,7 @@ var FunctionsDeployCommand = class FunctionsDeployCommand extends Command {
13283
13604
  });
13284
13605
  if (waitResult.kind === "error") {
13285
13606
  writeErrorWithHints(waitResult.payload);
13286
- removeStaleSavedCredentialOnUnauthorized({
13607
+ surfaceUnauthorizedHint({
13287
13608
  ...authFailureContext,
13288
13609
  payload: waitResult.payload
13289
13610
  });
@@ -13316,8 +13637,8 @@ const PRIMITIVE_TEAM_AUTHOR = {
13316
13637
  name: "Primitive Team",
13317
13638
  url: "https://primitive.dev"
13318
13639
  };
13319
- const SDK_VERSION_RANGE = "^0.30.2";
13320
- const CLI_VERSION_RANGE = "^0.30.2";
13640
+ const SDK_VERSION_RANGE = "^0.31.0";
13641
+ const CLI_VERSION_RANGE = "^0.31.0";
13321
13642
  const ESBUILD_VERSION_RANGE = "^0.27.0";
13322
13643
  function renderHandler() {
13323
13644
  return `// env.PRIMITIVE_API_KEY is auto-injected by the Primitive Functions runtime.
@@ -13827,7 +14148,7 @@ var FunctionsLogsCommand = class FunctionsLogsCommand extends Command {
13827
14148
  if (result.error) {
13828
14149
  const errorPayload = extractErrorPayload(result.error);
13829
14150
  writeErrorWithHints(errorPayload);
13830
- removeStaleSavedCredentialOnUnauthorized({
14151
+ surfaceUnauthorizedHint({
13831
14152
  auth,
13832
14153
  baseUrlOverridden,
13833
14154
  configDir: this.config.configDir,
@@ -14083,7 +14404,7 @@ var FunctionsRedeployCommand = class FunctionsRedeployCommand extends Command {
14083
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`);
14084
14405
  }
14085
14406
  writeErrorWithHints(outcome.payload);
14086
- removeStaleSavedCredentialOnUnauthorized({
14407
+ surfaceUnauthorizedHint({
14087
14408
  ...authFailureContext,
14088
14409
  payload: outcome.payload
14089
14410
  });
@@ -14105,7 +14426,7 @@ var FunctionsRedeployCommand = class FunctionsRedeployCommand extends Command {
14105
14426
  });
14106
14427
  if (waitResult.kind === "error") {
14107
14428
  writeErrorWithHints(waitResult.payload);
14108
- removeStaleSavedCredentialOnUnauthorized({
14429
+ surfaceUnauthorizedHint({
14109
14430
  ...authFailureContext,
14110
14431
  payload: waitResult.payload
14111
14432
  });
@@ -14306,7 +14627,7 @@ var FunctionsSetSecretCommand = class FunctionsSetSecretCommand extends Command
14306
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");
14307
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");
14308
14629
  writeErrorWithHints(outcome.payload);
14309
- removeStaleSavedCredentialOnUnauthorized({
14630
+ surfaceUnauthorizedHint({
14310
14631
  ...authFailureContext,
14311
14632
  payload: outcome.payload
14312
14633
  });
@@ -14494,7 +14815,7 @@ var FunctionsTestFunctionCommand = class FunctionsTestFunctionCommand extends Co
14494
14815
  if (triggerResult.error) {
14495
14816
  const payload = extractErrorPayload(triggerResult.error);
14496
14817
  writeErrorWithHints(payload);
14497
- removeStaleSavedCredentialOnUnauthorized({
14818
+ surfaceUnauthorizedHint({
14498
14819
  auth,
14499
14820
  baseUrlOverridden,
14500
14821
  configDir: this.config.configDir,
@@ -14532,7 +14853,7 @@ var FunctionsTestFunctionCommand = class FunctionsTestFunctionCommand extends Co
14532
14853
  if (!page.ok) {
14533
14854
  const payload = extractErrorPayload(page.error);
14534
14855
  writeErrorWithHints(payload);
14535
- removeStaleSavedCredentialOnUnauthorized({
14856
+ surfaceUnauthorizedHint({
14536
14857
  auth,
14537
14858
  baseUrlOverridden,
14538
14859
  configDir: this.config.configDir,
@@ -14560,7 +14881,7 @@ var FunctionsTestFunctionCommand = class FunctionsTestFunctionCommand extends Co
14560
14881
  if (result.error) {
14561
14882
  const payload = extractErrorPayload(result.error);
14562
14883
  writeErrorWithHints(payload);
14563
- removeStaleSavedCredentialOnUnauthorized({
14884
+ surfaceUnauthorizedHint({
14564
14885
  auth,
14565
14886
  baseUrlOverridden,
14566
14887
  configDir: this.config.configDir,
@@ -14640,22 +14961,17 @@ async function checkExistingLogin(params) {
14640
14961
  })))(apiClient);
14641
14962
  if (!result.error) return { status: "valid" };
14642
14963
  const payload = extractErrorPayload(result.error);
14643
- if (removeStaleSavedCredentialOnUnauthorized({
14644
- auth: {
14645
- apiKey: params.credentials.api_key,
14646
- apiBaseUrl1: probeApiBaseUrl1,
14647
- apiBaseUrl2: normalizeApiBaseUrl2(void 0),
14648
- credentials: params.credentials,
14649
- source: "stored"
14650
- },
14651
- baseUrlOverridden: requestConfig.baseUrlOverridden,
14652
- configDir: params.configDir,
14653
- payload
14654
- })) 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
+ }
14655
14971
  return {
14656
14972
  status: "blocked",
14657
14973
  payload,
14658
- 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."
14659
14975
  };
14660
14976
  }
14661
14977
  var LoginCommand = class LoginCommand extends Command {
@@ -15040,7 +15356,7 @@ var ReplyCommand = class ReplyCommand extends Command {
15040
15356
  if (result.error) {
15041
15357
  const errorPayload = extractErrorPayload(result.error);
15042
15358
  writeErrorWithHints(errorPayload);
15043
- removeStaleSavedCredentialOnUnauthorized({
15359
+ surfaceUnauthorizedHint({
15044
15360
  auth,
15045
15361
  baseUrlOverridden,
15046
15362
  configDir: this.config.configDir,
@@ -15059,39 +15375,6 @@ var ReplyCommand = class ReplyCommand extends Command {
15059
15375
  };
15060
15376
  //#endregion
15061
15377
  //#region src/oclif/commands/send.ts
15062
- const SUBJECT_MAX_LENGTH = 200;
15063
- function deriveSubject(body) {
15064
- for (const line of body.split("\n")) {
15065
- const trimmed = line.trim();
15066
- if (!trimmed) continue;
15067
- return trimmed.length > SUBJECT_MAX_LENGTH ? `${trimmed.slice(0, SUBJECT_MAX_LENGTH - 3)}...` : trimmed;
15068
- }
15069
- return "Message";
15070
- }
15071
- function isVerifiedDomain(domain) {
15072
- return domain.is_active === true;
15073
- }
15074
- async function pickDefaultFromAddress(apiClient, authFailureContext) {
15075
- const result = await listDomains({
15076
- client: apiClient.client,
15077
- responseStyle: "fields"
15078
- });
15079
- if (result.error) {
15080
- const errorPayload = extractErrorPayload(result.error);
15081
- if (extractErrorCode(errorPayload) === API_ERROR_CODES.unauthorized) {
15082
- writeErrorWithHints(errorPayload);
15083
- removeStaleSavedCredentialOnUnauthorized({
15084
- ...authFailureContext,
15085
- payload: errorPayload
15086
- });
15087
- throw new Errors.CLIError("Cannot send: API key is missing or invalid (see hint above).", { exit: 1 });
15088
- }
15089
- throw new Errors.CLIError(`Could not look up your verified domains to default --from. Pass --from explicitly. Underlying error: ${formatErrorPayload(errorPayload)}`);
15090
- }
15091
- const first = result.data?.data?.find(isVerifiedDomain);
15092
- 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.");
15093
- return `agent@${first.domain}`;
15094
- }
15095
15378
  var SendCommand = class SendCommand extends Command {
15096
15379
  static description = `Send an outbound email. Agent-grade shortcut for \`sending send\` with sensible defaults.
15097
15380
 
@@ -15183,7 +15466,7 @@ var SendCommand = class SendCommand extends Command {
15183
15466
  if (result.error) {
15184
15467
  const errorPayload = extractErrorPayload(result.error);
15185
15468
  writeErrorWithHints(errorPayload);
15186
- removeStaleSavedCredentialOnUnauthorized({
15469
+ surfaceUnauthorizedHint({
15187
15470
  ...authFailureContext,
15188
15471
  payload: errorPayload
15189
15472
  });
@@ -15628,7 +15911,7 @@ var WhoamiCommand = class WhoamiCommand extends Command {
15628
15911
  if (result.error) {
15629
15912
  const errorPayload = extractErrorPayload(result.error);
15630
15913
  writeErrorWithHints(errorPayload);
15631
- removeStaleSavedCredentialOnUnauthorized({
15914
+ surfaceUnauthorizedHint({
15632
15915
  auth,
15633
15916
  baseUrlOverridden,
15634
15917
  configDir: this.config.configDir,
@@ -15851,7 +16134,7 @@ const generatedCommands = Object.fromEntries(operationManifest.filter((operation
15851
16134
  const COMMANDS = {
15852
16135
  completion: CompletionCommand,
15853
16136
  "list-operations": ListOperationsCommand,
15854
- config: ConfigListCommand,
16137
+ config: ConfigCommand,
15855
16138
  "config:list": ConfigListCommand,
15856
16139
  "config:reset": ConfigResetCommand,
15857
16140
  "config:set": ConfigSetCommand,
@@ -15859,6 +16142,7 @@ const COMMANDS = {
15859
16142
  describe: DescribeCommand,
15860
16143
  send: SendCommand,
15861
16144
  reply: ReplyCommand,
16145
+ chat: ChatCommand,
15862
16146
  login: LoginCommand,
15863
16147
  signup: SignupCommand,
15864
16148
  logout: LogoutCommand,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@primitivedotdev/cli",
3
- "version": "0.30.2",
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,
@@ -27,6 +27,10 @@
27
27
  "cli": {
28
28
  "description": "CLI authentication"
29
29
  },
30
+ "config": {
31
+ "description": "Manage local Primitive CLI request environments",
32
+ "hidden": true
33
+ },
30
34
  "account": {
31
35
  "description": "Manage your account settings, storage, and webhook secret"
32
36
  },