@primitivedotdev/cli 0.31.6 → 0.31.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/run.js CHANGED
@@ -7,5 +7,10 @@ import { restartWithProxyEnvIfNeeded } from "../dist/oclif/proxy-auto-detect.js"
7
7
  // process.env inside this process is too late for built-in fetch.
8
8
  restartWithProxyEnvIfNeeded();
9
9
 
10
+ const { writeLoggedOutSignupHintIfNeeded } = await import(
11
+ "../dist/oclif/root-signup-hint.js"
12
+ );
13
+ writeLoggedOutSignupHintIfNeeded();
14
+
10
15
  const { execute } = await import("@oclif/core");
11
16
  await execute({ dir: import.meta.url });
@@ -655,6 +655,7 @@ var sdk_gen_exports = /* @__PURE__ */ __exportAll({
655
655
  getSendPermissions: () => getSendPermissions,
656
656
  getSentEmail: () => getSentEmail,
657
657
  getStorageStats: () => getStorageStats,
658
+ getThread: () => getThread,
658
659
  getWebhookSecret: () => getWebhookSecret,
659
660
  listDeliveries: () => listDeliveries,
660
661
  listDomains: () => listDomains,
@@ -1579,6 +1580,33 @@ const getSentEmail = (options) => (options.client ?? client).get({
1579
1580
  ...options
1580
1581
  });
1581
1582
  /**
1583
+ * Get a conversation thread by id
1584
+ *
1585
+ * Returns a conversation thread: its metadata plus the inbound
1586
+ * and outbound messages that belong to it, interleaved in time
1587
+ * order (oldest first). A thread spans both received emails and
1588
+ * your sends, so an agent can reconstruct an entire back-and-forth
1589
+ * from one call instead of walking reply headers.
1590
+ *
1591
+ * Each message carries a `direction` (`inbound` | `outbound`) and
1592
+ * an `id`; fetch the full message via `/emails/{id}` or
1593
+ * `/sent-emails/{id}` accordingly. Bodies are omitted here to keep
1594
+ * the thread view lightweight.
1595
+ *
1596
+ * Discover a thread id from the `thread_id` field on any email or
1597
+ * sent-email (list or detail). The message list is capped; compare
1598
+ * `message_count` against `messages.length` to detect truncation.
1599
+ *
1600
+ */
1601
+ const getThread = (options) => (options.client ?? client).get({
1602
+ security: [{
1603
+ scheme: "bearer",
1604
+ type: "http"
1605
+ }],
1606
+ url: "/threads/{id}",
1607
+ ...options
1608
+ });
1609
+ /**
1582
1610
  * List functions
1583
1611
  *
1584
1612
  * Returns every active (non-deleted) function in the org, newest
@@ -1929,6 +1957,10 @@ const openapiDocument = {
1929
1957
  "name": "Sending",
1930
1958
  "description": "Send outbound emails through the Primitive API"
1931
1959
  },
1960
+ {
1961
+ "name": "Threads",
1962
+ "description": "Conversation threads spanning received and sent emails"
1963
+ },
1932
1964
  {
1933
1965
  "name": "Endpoints",
1934
1966
  "description": "Manage webhook endpoints that receive email events"
@@ -3424,6 +3456,27 @@ const openapiDocument = {
3424
3456
  "404": { "$ref": "#/components/responses/NotFound" }
3425
3457
  }
3426
3458
  } },
3459
+ "/threads/{id}": {
3460
+ "parameters": [{ "$ref": "#/components/parameters/ResourceId" }],
3461
+ "get": {
3462
+ "operationId": "getThread",
3463
+ "summary": "Get a conversation thread by id",
3464
+ "description": "Returns a conversation thread: its metadata plus the inbound\nand outbound messages that belong to it, interleaved in time\norder (oldest first). A thread spans both received emails and\nyour sends, so an agent can reconstruct an entire back-and-forth\nfrom one call instead of walking reply headers.\n\nEach message carries a `direction` (`inbound` | `outbound`) and\nan `id`; fetch the full message via `/emails/{id}` or\n`/sent-emails/{id}` accordingly. Bodies are omitted here to keep\nthe thread view lightweight.\n\nDiscover a thread id from the `thread_id` field on any email or\nsent-email (list or detail). The message list is capped; compare\n`message_count` against `messages.length` to detect truncation.\n",
3465
+ "tags": ["Threads"],
3466
+ "responses": {
3467
+ "200": {
3468
+ "description": "Thread detail",
3469
+ "content": { "application/json": { "schema": { "allOf": [{ "$ref": "#/components/schemas/SuccessEnvelope" }, {
3470
+ "type": "object",
3471
+ "properties": { "data": { "$ref": "#/components/schemas/Thread" } }
3472
+ }] } } }
3473
+ },
3474
+ "400": { "$ref": "#/components/responses/ValidationError" },
3475
+ "401": { "$ref": "#/components/responses/Unauthorized" },
3476
+ "404": { "$ref": "#/components/responses/NotFound" }
3477
+ }
3478
+ }
3479
+ },
3427
3480
  "/functions": {
3428
3481
  "get": {
3429
3482
  "operationId": "listFunctions",
@@ -5291,7 +5344,12 @@ const openapiDocument = {
5291
5344
  },
5292
5345
  "raw_size_bytes": { "type": ["integer", "null"] },
5293
5346
  "webhook_status": { "$ref": "#/components/schemas/EmailWebhookStatus" },
5294
- "webhook_attempt_count": { "type": "integer" }
5347
+ "webhook_attempt_count": { "type": "integer" },
5348
+ "thread_id": {
5349
+ "type": ["string", "null"],
5350
+ "format": "uuid",
5351
+ "description": "Conversation thread this message belongs to. Fetch\n`/threads/{thread_id}` for the full ordered thread. NULL on\nmessages received before threading was enabled.\n"
5352
+ }
5295
5353
  },
5296
5354
  "required": [
5297
5355
  "id",
@@ -5510,6 +5568,19 @@ const openapiDocument = {
5510
5568
  "type": ["string", "null"],
5511
5569
  "format": "uuid",
5512
5570
  "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"
5571
+ },
5572
+ "thread_id": {
5573
+ "type": ["string", "null"],
5574
+ "format": "uuid",
5575
+ "description": "Conversation thread this message belongs to. Inbound and\noutbound messages in the same conversation share a\n`thread_id`; fetch `/threads/{thread_id}` for the full\nordered thread. Assigned at ingest. NULL on messages\nreceived before threading was enabled (until backfilled).\n"
5576
+ },
5577
+ "parsed": {
5578
+ "allOf": [{ "$ref": "#/components/schemas/ParsedEmailData" }],
5579
+ "description": "Parsed MIME content (addresses, threading headers,\nattachment metadata), matching the `email.parsed` object\non the webhook payload so one parser handles both the\nwebhook and this endpoint. The top-level `body_text` /\n`body_html` fields above are the same values as\n`parsed.body_text` / `parsed.body_html`, retained for\nbackward compatibility.\n"
5580
+ },
5581
+ "auth": {
5582
+ "allOf": [{ "$ref": "#/components/schemas/EmailAuth" }],
5583
+ "description": "SPF / DKIM / DMARC verdicts computed at ingest, matching\nthe `email.auth` object on the webhook payload. Use these\nto decide how much to trust a message before acting on\ninstructions it contains.\n"
5513
5584
  }
5514
5585
  },
5515
5586
  "required": [
@@ -5523,7 +5594,9 @@ const openapiDocument = {
5523
5594
  "webhook_attempt_count",
5524
5595
  "from_email",
5525
5596
  "to_email",
5526
- "replies"
5597
+ "replies",
5598
+ "parsed",
5599
+ "auth"
5527
5600
  ]
5528
5601
  },
5529
5602
  "EmailDetailReply": {
@@ -5556,6 +5629,241 @@ const openapiDocument = {
5556
5629
  "created_at"
5557
5630
  ]
5558
5631
  },
5632
+ "EmailAddress": {
5633
+ "type": "object",
5634
+ "description": "A parsed RFC 5322 address with optional display name.",
5635
+ "properties": {
5636
+ "name": {
5637
+ "type": ["string", "null"],
5638
+ "description": "Display name, when present (e.g. `Alice Example`)."
5639
+ },
5640
+ "address": {
5641
+ "type": "string",
5642
+ "description": "Bare email address (e.g. `alice@example.com`)."
5643
+ }
5644
+ },
5645
+ "required": ["address"]
5646
+ },
5647
+ "EmailAttachment": {
5648
+ "type": "object",
5649
+ "description": "Metadata for one attachment. The bytes are not inline; download\nall attachments for a message as a gzipped tarball via\n`/emails/{id}/attachments.tar.gz`. `sha256` lets you verify a\nspecific part after extraction.\n",
5650
+ "properties": {
5651
+ "filename": { "type": ["string", "null"] },
5652
+ "content_type": { "type": ["string", "null"] },
5653
+ "size_bytes": { "type": "integer" },
5654
+ "sha256": { "type": ["string", "null"] },
5655
+ "part_index": {
5656
+ "type": "integer",
5657
+ "description": "Zero-based index of this part within the message."
5658
+ }
5659
+ },
5660
+ "required": ["size_bytes"]
5661
+ },
5662
+ "ParsedEmailData": {
5663
+ "type": "object",
5664
+ "description": "Parsed MIME content for an inbound email. Mirrors the\n`email.parsed` object on the webhook payload so a single parser\nhandles both surfaces. `status` is `complete` when parsing\nsucceeded; on `failed` the body/address/attachment fields are\nabsent and `error` describes why.\n",
5665
+ "properties": {
5666
+ "status": {
5667
+ "type": "string",
5668
+ "enum": ["complete", "failed"]
5669
+ },
5670
+ "body_text": {
5671
+ "type": ["string", "null"],
5672
+ "description": "Plain-text body. Present when `status` is `complete`."
5673
+ },
5674
+ "body_html": {
5675
+ "type": ["string", "null"],
5676
+ "description": "HTML body. Present when `status` is `complete`."
5677
+ },
5678
+ "reply_to": {
5679
+ "type": ["array", "null"],
5680
+ "items": { "$ref": "#/components/schemas/EmailAddress" },
5681
+ "description": "Parsed `Reply-To` header addresses."
5682
+ },
5683
+ "cc": {
5684
+ "type": ["array", "null"],
5685
+ "items": { "$ref": "#/components/schemas/EmailAddress" },
5686
+ "description": "Parsed `Cc` header addresses."
5687
+ },
5688
+ "bcc": {
5689
+ "type": ["array", "null"],
5690
+ "items": { "$ref": "#/components/schemas/EmailAddress" },
5691
+ "description": "Parsed `Bcc` header addresses (rarely present on inbound)."
5692
+ },
5693
+ "to_addresses": {
5694
+ "type": ["array", "null"],
5695
+ "items": { "$ref": "#/components/schemas/EmailAddress" },
5696
+ "description": "Parsed `To` header addresses."
5697
+ },
5698
+ "in_reply_to": {
5699
+ "type": ["array", "null"],
5700
+ "items": { "type": "string" },
5701
+ "description": "Message-IDs from the `In-Reply-To` header."
5702
+ },
5703
+ "references": {
5704
+ "type": ["array", "null"],
5705
+ "items": { "type": "string" },
5706
+ "description": "Message-IDs from the `References` header."
5707
+ },
5708
+ "attachments": {
5709
+ "type": "array",
5710
+ "items": { "$ref": "#/components/schemas/EmailAttachment" },
5711
+ "description": "Attachment metadata. Empty array when none."
5712
+ },
5713
+ "error": {
5714
+ "type": ["object", "null"],
5715
+ "description": "Present (non-null) only when `status` is `failed`. When\npresent, all three fields are populated, so a consumer can\nbranch on `code` without defensive null checks.\n",
5716
+ "properties": {
5717
+ "code": {
5718
+ "type": "string",
5719
+ "description": "Stable failure code (e.g. `PARSE_FAILED`)."
5720
+ },
5721
+ "message": { "type": "string" },
5722
+ "retryable": { "type": "boolean" }
5723
+ },
5724
+ "required": [
5725
+ "code",
5726
+ "message",
5727
+ "retryable"
5728
+ ]
5729
+ }
5730
+ },
5731
+ "required": ["status"]
5732
+ },
5733
+ "DkimSignature": {
5734
+ "type": "object",
5735
+ "description": "One DKIM signature found on the message, with its verdict.",
5736
+ "properties": {
5737
+ "domain": { "type": "string" },
5738
+ "selector": { "type": "string" },
5739
+ "result": {
5740
+ "type": "string",
5741
+ "description": "Verification result (e.g. `pass`, `fail`, `none`)."
5742
+ },
5743
+ "aligned": {
5744
+ "type": "boolean",
5745
+ "description": "Whether the signing domain aligns with the From domain (for DMARC)."
5746
+ },
5747
+ "keyBits": { "type": ["integer", "null"] },
5748
+ "algo": { "type": ["string", "null"] }
5749
+ },
5750
+ "required": [
5751
+ "domain",
5752
+ "selector",
5753
+ "result",
5754
+ "aligned"
5755
+ ]
5756
+ },
5757
+ "EmailAuth": {
5758
+ "type": "object",
5759
+ "description": "SPF / DKIM / DMARC verdicts computed at ingest. Mirrors the\n`email.auth` object on the webhook payload. Field names are\ncamelCase to match that payload exactly. For messages received\nbefore auth was recorded, the verdicts default to `none`.\n",
5760
+ "properties": {
5761
+ "spf": {
5762
+ "type": "string",
5763
+ "description": "SPF result (e.g. `pass`, `fail`, `softfail`, `none`)."
5764
+ },
5765
+ "dmarc": {
5766
+ "type": "string",
5767
+ "description": "DMARC result (e.g. `pass`, `fail`, `none`)."
5768
+ },
5769
+ "dmarcPolicy": {
5770
+ "type": ["string", "null"],
5771
+ "description": "Published DMARC policy (`none`, `quarantine`, `reject`)."
5772
+ },
5773
+ "dmarcFromDomain": {
5774
+ "type": ["string", "null"],
5775
+ "description": "The From-header domain DMARC was evaluated against."
5776
+ },
5777
+ "dmarcSpfAligned": { "type": "boolean" },
5778
+ "dmarcDkimAligned": { "type": "boolean" },
5779
+ "dmarcSpfStrict": { "type": ["boolean", "null"] },
5780
+ "dmarcDkimStrict": { "type": ["boolean", "null"] },
5781
+ "dkimSignatures": {
5782
+ "type": "array",
5783
+ "items": { "$ref": "#/components/schemas/DkimSignature" }
5784
+ }
5785
+ },
5786
+ "required": [
5787
+ "spf",
5788
+ "dmarc",
5789
+ "dmarcSpfAligned",
5790
+ "dmarcDkimAligned",
5791
+ "dkimSignatures"
5792
+ ]
5793
+ },
5794
+ "Thread": {
5795
+ "type": "object",
5796
+ "description": "A conversation thread: its metadata plus the inbound and\noutbound messages that belong to it, interleaved oldest-first.\nMembership is the stored `thread_id` on each message. Bodies are\nomitted here to keep the thread view lightweight; fetch\n`/emails/{id}` or `/sent-emails/{id}` for a single message's\nfull content.\n",
5797
+ "properties": {
5798
+ "id": {
5799
+ "type": "string",
5800
+ "format": "uuid"
5801
+ },
5802
+ "subject": {
5803
+ "type": ["string", "null"],
5804
+ "description": "Normalized subject of the thread (Re/Fwd prefixes stripped)."
5805
+ },
5806
+ "root_message_id": {
5807
+ "type": ["string", "null"],
5808
+ "description": "Message-ID of the conversation root, when known."
5809
+ },
5810
+ "message_count": {
5811
+ "type": "integer",
5812
+ "description": "Total messages in the thread. `messages` is capped (most\nrecent first, then re-sorted oldest-first), so\n`message_count > messages.length` signals truncation.\n"
5813
+ },
5814
+ "first_message_at": {
5815
+ "type": ["string", "null"],
5816
+ "format": "date-time"
5817
+ },
5818
+ "last_message_at": {
5819
+ "type": ["string", "null"],
5820
+ "format": "date-time"
5821
+ },
5822
+ "created_at": {
5823
+ "type": "string",
5824
+ "format": "date-time"
5825
+ },
5826
+ "messages": {
5827
+ "type": "array",
5828
+ "items": { "$ref": "#/components/schemas/ThreadMessage" }
5829
+ }
5830
+ },
5831
+ "required": [
5832
+ "id",
5833
+ "message_count",
5834
+ "created_at",
5835
+ "messages"
5836
+ ]
5837
+ },
5838
+ "ThreadMessage": {
5839
+ "type": "object",
5840
+ "description": "One message in a thread (inbound or outbound).",
5841
+ "properties": {
5842
+ "direction": {
5843
+ "type": "string",
5844
+ "enum": ["inbound", "outbound"],
5845
+ "description": "`inbound` for a received email (`/emails/{id}`), `outbound`\nfor a send (`/sent-emails/{id}`). Use it with `id` to fetch\nfull content from the right endpoint.\n"
5846
+ },
5847
+ "id": {
5848
+ "type": "string",
5849
+ "format": "uuid"
5850
+ },
5851
+ "message_id": { "type": ["string", "null"] },
5852
+ "from": { "type": ["string", "null"] },
5853
+ "to": { "type": ["string", "null"] },
5854
+ "subject": { "type": ["string", "null"] },
5855
+ "status": {
5856
+ "type": ["string", "null"],
5857
+ "description": "Lifecycle status (an EmailStatus or SentEmailStatus value, per `direction`)."
5858
+ },
5859
+ "timestamp": {
5860
+ "type": ["string", "null"],
5861
+ "format": "date-time",
5862
+ "description": "received_at for inbound, created_at for outbound."
5863
+ }
5864
+ },
5865
+ "required": ["direction", "id"]
5866
+ },
5559
5867
  "SendMailAttachment": {
5560
5868
  "type": "object",
5561
5869
  "additionalProperties": false,
@@ -5772,6 +6080,11 @@ const openapiDocument = {
5772
6080
  "format": "uuid",
5773
6081
  "description": "Reference to the inbound `emails.id` that this send\nreplied to, when known. Populated when the caller used\n/emails/{id}/reply or when /send-mail's `in_reply_to`\nmatched a stored inbound message_id in the same org.\n"
5774
6082
  },
6083
+ "thread_id": {
6084
+ "type": ["string", "null"],
6085
+ "format": "uuid",
6086
+ "description": "Conversation thread this send belongs to. A reply inherits\nthe thread of the inbound it answers; a fresh send starts a\nnew thread. Fetch `/threads/{thread_id}` for the full\nordered thread (inbound + outbound interleaved). NULL on\ngate-denied sends and on sends created before threading was\nenabled.\n"
6087
+ },
5775
6088
  "queue_id": {
5776
6089
  "type": ["string", "null"],
5777
6090
  "description": "Message identifier assigned by Primitive's outbound\nrelay once the agent accepts the message. Null on\nqueued, gate_denied, and agent_failed rows.\n"
@@ -8873,6 +9186,218 @@ const operationManifest = [
8873
9186
  "type": ["string", "null"],
8874
9187
  "format": "uuid",
8875
9188
  "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"
9189
+ },
9190
+ "thread_id": {
9191
+ "type": ["string", "null"],
9192
+ "format": "uuid",
9193
+ "description": "Conversation thread this message belongs to. Inbound and\noutbound messages in the same conversation share a\n`thread_id`; fetch `/threads/{thread_id}` for the full\nordered thread. Assigned at ingest. NULL on messages\nreceived before threading was enabled (until backfilled).\n"
9194
+ },
9195
+ "parsed": {
9196
+ "allOf": [{
9197
+ "type": "object",
9198
+ "description": "Parsed MIME content for an inbound email. Mirrors the\n`email.parsed` object on the webhook payload so a single parser\nhandles both surfaces. `status` is `complete` when parsing\nsucceeded; on `failed` the body/address/attachment fields are\nabsent and `error` describes why.\n",
9199
+ "properties": {
9200
+ "status": {
9201
+ "type": "string",
9202
+ "enum": ["complete", "failed"]
9203
+ },
9204
+ "body_text": {
9205
+ "type": ["string", "null"],
9206
+ "description": "Plain-text body. Present when `status` is `complete`."
9207
+ },
9208
+ "body_html": {
9209
+ "type": ["string", "null"],
9210
+ "description": "HTML body. Present when `status` is `complete`."
9211
+ },
9212
+ "reply_to": {
9213
+ "type": ["array", "null"],
9214
+ "items": {
9215
+ "type": "object",
9216
+ "description": "A parsed RFC 5322 address with optional display name.",
9217
+ "properties": {
9218
+ "name": {
9219
+ "type": ["string", "null"],
9220
+ "description": "Display name, when present (e.g. `Alice Example`)."
9221
+ },
9222
+ "address": {
9223
+ "type": "string",
9224
+ "description": "Bare email address (e.g. `alice@example.com`)."
9225
+ }
9226
+ },
9227
+ "required": ["address"]
9228
+ },
9229
+ "description": "Parsed `Reply-To` header addresses."
9230
+ },
9231
+ "cc": {
9232
+ "type": ["array", "null"],
9233
+ "items": {
9234
+ "type": "object",
9235
+ "description": "A parsed RFC 5322 address with optional display name.",
9236
+ "properties": {
9237
+ "name": {
9238
+ "type": ["string", "null"],
9239
+ "description": "Display name, when present (e.g. `Alice Example`)."
9240
+ },
9241
+ "address": {
9242
+ "type": "string",
9243
+ "description": "Bare email address (e.g. `alice@example.com`)."
9244
+ }
9245
+ },
9246
+ "required": ["address"]
9247
+ },
9248
+ "description": "Parsed `Cc` header addresses."
9249
+ },
9250
+ "bcc": {
9251
+ "type": ["array", "null"],
9252
+ "items": {
9253
+ "type": "object",
9254
+ "description": "A parsed RFC 5322 address with optional display name.",
9255
+ "properties": {
9256
+ "name": {
9257
+ "type": ["string", "null"],
9258
+ "description": "Display name, when present (e.g. `Alice Example`)."
9259
+ },
9260
+ "address": {
9261
+ "type": "string",
9262
+ "description": "Bare email address (e.g. `alice@example.com`)."
9263
+ }
9264
+ },
9265
+ "required": ["address"]
9266
+ },
9267
+ "description": "Parsed `Bcc` header addresses (rarely present on inbound)."
9268
+ },
9269
+ "to_addresses": {
9270
+ "type": ["array", "null"],
9271
+ "items": {
9272
+ "type": "object",
9273
+ "description": "A parsed RFC 5322 address with optional display name.",
9274
+ "properties": {
9275
+ "name": {
9276
+ "type": ["string", "null"],
9277
+ "description": "Display name, when present (e.g. `Alice Example`)."
9278
+ },
9279
+ "address": {
9280
+ "type": "string",
9281
+ "description": "Bare email address (e.g. `alice@example.com`)."
9282
+ }
9283
+ },
9284
+ "required": ["address"]
9285
+ },
9286
+ "description": "Parsed `To` header addresses."
9287
+ },
9288
+ "in_reply_to": {
9289
+ "type": ["array", "null"],
9290
+ "items": { "type": "string" },
9291
+ "description": "Message-IDs from the `In-Reply-To` header."
9292
+ },
9293
+ "references": {
9294
+ "type": ["array", "null"],
9295
+ "items": { "type": "string" },
9296
+ "description": "Message-IDs from the `References` header."
9297
+ },
9298
+ "attachments": {
9299
+ "type": "array",
9300
+ "items": {
9301
+ "type": "object",
9302
+ "description": "Metadata for one attachment. The bytes are not inline; download\nall attachments for a message as a gzipped tarball via\n`/emails/{id}/attachments.tar.gz`. `sha256` lets you verify a\nspecific part after extraction.\n",
9303
+ "properties": {
9304
+ "filename": { "type": ["string", "null"] },
9305
+ "content_type": { "type": ["string", "null"] },
9306
+ "size_bytes": { "type": "integer" },
9307
+ "sha256": { "type": ["string", "null"] },
9308
+ "part_index": {
9309
+ "type": "integer",
9310
+ "description": "Zero-based index of this part within the message."
9311
+ }
9312
+ },
9313
+ "required": ["size_bytes"]
9314
+ },
9315
+ "description": "Attachment metadata. Empty array when none."
9316
+ },
9317
+ "error": {
9318
+ "type": ["object", "null"],
9319
+ "description": "Present (non-null) only when `status` is `failed`. When\npresent, all three fields are populated, so a consumer can\nbranch on `code` without defensive null checks.\n",
9320
+ "properties": {
9321
+ "code": {
9322
+ "type": "string",
9323
+ "description": "Stable failure code (e.g. `PARSE_FAILED`)."
9324
+ },
9325
+ "message": { "type": "string" },
9326
+ "retryable": { "type": "boolean" }
9327
+ },
9328
+ "required": [
9329
+ "code",
9330
+ "message",
9331
+ "retryable"
9332
+ ]
9333
+ }
9334
+ },
9335
+ "required": ["status"]
9336
+ }],
9337
+ "description": "Parsed MIME content (addresses, threading headers,\nattachment metadata), matching the `email.parsed` object\non the webhook payload so one parser handles both the\nwebhook and this endpoint. The top-level `body_text` /\n`body_html` fields above are the same values as\n`parsed.body_text` / `parsed.body_html`, retained for\nbackward compatibility.\n"
9338
+ },
9339
+ "auth": {
9340
+ "allOf": [{
9341
+ "type": "object",
9342
+ "description": "SPF / DKIM / DMARC verdicts computed at ingest. Mirrors the\n`email.auth` object on the webhook payload. Field names are\ncamelCase to match that payload exactly. For messages received\nbefore auth was recorded, the verdicts default to `none`.\n",
9343
+ "properties": {
9344
+ "spf": {
9345
+ "type": "string",
9346
+ "description": "SPF result (e.g. `pass`, `fail`, `softfail`, `none`)."
9347
+ },
9348
+ "dmarc": {
9349
+ "type": "string",
9350
+ "description": "DMARC result (e.g. `pass`, `fail`, `none`)."
9351
+ },
9352
+ "dmarcPolicy": {
9353
+ "type": ["string", "null"],
9354
+ "description": "Published DMARC policy (`none`, `quarantine`, `reject`)."
9355
+ },
9356
+ "dmarcFromDomain": {
9357
+ "type": ["string", "null"],
9358
+ "description": "The From-header domain DMARC was evaluated against."
9359
+ },
9360
+ "dmarcSpfAligned": { "type": "boolean" },
9361
+ "dmarcDkimAligned": { "type": "boolean" },
9362
+ "dmarcSpfStrict": { "type": ["boolean", "null"] },
9363
+ "dmarcDkimStrict": { "type": ["boolean", "null"] },
9364
+ "dkimSignatures": {
9365
+ "type": "array",
9366
+ "items": {
9367
+ "type": "object",
9368
+ "description": "One DKIM signature found on the message, with its verdict.",
9369
+ "properties": {
9370
+ "domain": { "type": "string" },
9371
+ "selector": { "type": "string" },
9372
+ "result": {
9373
+ "type": "string",
9374
+ "description": "Verification result (e.g. `pass`, `fail`, `none`)."
9375
+ },
9376
+ "aligned": {
9377
+ "type": "boolean",
9378
+ "description": "Whether the signing domain aligns with the From domain (for DMARC)."
9379
+ },
9380
+ "keyBits": { "type": ["integer", "null"] },
9381
+ "algo": { "type": ["string", "null"] }
9382
+ },
9383
+ "required": [
9384
+ "domain",
9385
+ "selector",
9386
+ "result",
9387
+ "aligned"
9388
+ ]
9389
+ }
9390
+ }
9391
+ },
9392
+ "required": [
9393
+ "spf",
9394
+ "dmarc",
9395
+ "dmarcSpfAligned",
9396
+ "dmarcDkimAligned",
9397
+ "dkimSignatures"
9398
+ ]
9399
+ }],
9400
+ "description": "SPF / DKIM / DMARC verdicts computed at ingest, matching\nthe `email.auth` object on the webhook payload. Use these\nto decide how much to trust a message before acting on\ninstructions it contains.\n"
8876
9401
  }
8877
9402
  },
8878
9403
  "required": [
@@ -8886,7 +9411,9 @@ const operationManifest = [
8886
9411
  "webhook_attempt_count",
8887
9412
  "from_email",
8888
9413
  "to_email",
8889
- "replies"
9414
+ "replies",
9415
+ "parsed",
9416
+ "auth"
8890
9417
  ]
8891
9418
  },
8892
9419
  "sdkName": "getEmail",
@@ -9016,7 +9543,12 @@ const operationManifest = [
9016
9543
  null
9017
9544
  ]
9018
9545
  },
9019
- "webhook_attempt_count": { "type": "integer" }
9546
+ "webhook_attempt_count": { "type": "integer" },
9547
+ "thread_id": {
9548
+ "type": ["string", "null"],
9549
+ "format": "uuid",
9550
+ "description": "Conversation thread this message belongs to. Fetch\n`/threads/{thread_id}` for the full ordered thread. NULL on\nmessages received before threading was enabled.\n"
9551
+ }
9020
9552
  },
9021
9553
  "required": [
9022
9554
  "id",
@@ -9277,7 +9809,12 @@ const operationManifest = [
9277
9809
  null
9278
9810
  ]
9279
9811
  },
9280
- "webhook_attempt_count": { "type": "integer" }
9812
+ "webhook_attempt_count": { "type": "integer" },
9813
+ "thread_id": {
9814
+ "type": ["string", "null"],
9815
+ "format": "uuid",
9816
+ "description": "Conversation thread this message belongs to. Fetch\n`/threads/{thread_id}` for the full ordered thread. NULL on\nmessages received before threading was enabled.\n"
9817
+ }
9281
9818
  },
9282
9819
  "required": [
9283
9820
  "id",
@@ -11523,6 +12060,11 @@ const operationManifest = [
11523
12060
  "format": "uuid",
11524
12061
  "description": "Reference to the inbound `emails.id` that this send\nreplied to, when known. Populated when the caller used\n/emails/{id}/reply or when /send-mail's `in_reply_to`\nmatched a stored inbound message_id in the same org.\n"
11525
12062
  },
12063
+ "thread_id": {
12064
+ "type": ["string", "null"],
12065
+ "format": "uuid",
12066
+ "description": "Conversation thread this send belongs to. A reply inherits\nthe thread of the inbound it answers; a fresh send starts a\nnew thread. Fetch `/threads/{thread_id}` for the full\nordered thread (inbound + outbound interleaved). NULL on\ngate-denied sends and on sends created before threading was\nenabled.\n"
12067
+ },
11526
12068
  "queue_id": {
11527
12069
  "type": ["string", "null"],
11528
12070
  "description": "Message identifier assigned by Primitive's outbound\nrelay once the agent accepts the message. Null on\nqueued, gate_denied, and agent_failed rows.\n"
@@ -11807,6 +12349,11 @@ const operationManifest = [
11807
12349
  "format": "uuid",
11808
12350
  "description": "Reference to the inbound `emails.id` that this send\nreplied to, when known. Populated when the caller used\n/emails/{id}/reply or when /send-mail's `in_reply_to`\nmatched a stored inbound message_id in the same org.\n"
11809
12351
  },
12352
+ "thread_id": {
12353
+ "type": ["string", "null"],
12354
+ "format": "uuid",
12355
+ "description": "Conversation thread this send belongs to. A reply inherits\nthe thread of the inbound it answers; a fresh send starts a\nnew thread. Fetch `/threads/{thread_id}` for the full\nordered thread (inbound + outbound interleaved). NULL on\ngate-denied sends and on sends created before threading was\nenabled.\n"
12356
+ },
11810
12357
  "queue_id": {
11811
12358
  "type": ["string", "null"],
11812
12359
  "description": "Message identifier assigned by Primitive's outbound\nrelay once the agent accepts the message. Null on\nqueued, gate_denied, and agent_failed rows.\n"
@@ -12259,6 +12806,101 @@ const operationManifest = [
12259
12806
  "tag": "Sending",
12260
12807
  "tagCommand": "sending"
12261
12808
  },
12809
+ {
12810
+ "binaryResponse": false,
12811
+ "bodyRequired": false,
12812
+ "command": "get-thread",
12813
+ "description": "Returns a conversation thread: its metadata plus the inbound\nand outbound messages that belong to it, interleaved in time\norder (oldest first). A thread spans both received emails and\nyour sends, so an agent can reconstruct an entire back-and-forth\nfrom one call instead of walking reply headers.\n\nEach message carries a `direction` (`inbound` | `outbound`) and\nan `id`; fetch the full message via `/emails/{id}` or\n`/sent-emails/{id}` accordingly. Bodies are omitted here to keep\nthe thread view lightweight.\n\nDiscover a thread id from the `thread_id` field on any email or\nsent-email (list or detail). The message list is capped; compare\n`message_count` against `messages.length` to detect truncation.\n",
12814
+ "hasJsonBody": false,
12815
+ "method": "GET",
12816
+ "operationId": "getThread",
12817
+ "path": "/threads/{id}",
12818
+ "pathParams": [{
12819
+ "description": "Resource UUID",
12820
+ "enum": null,
12821
+ "name": "id",
12822
+ "required": true,
12823
+ "type": "string"
12824
+ }],
12825
+ "queryParams": [],
12826
+ "requestSchema": null,
12827
+ "responseSchema": {
12828
+ "type": "object",
12829
+ "description": "A conversation thread: its metadata plus the inbound and\noutbound messages that belong to it, interleaved oldest-first.\nMembership is the stored `thread_id` on each message. Bodies are\nomitted here to keep the thread view lightweight; fetch\n`/emails/{id}` or `/sent-emails/{id}` for a single message's\nfull content.\n",
12830
+ "properties": {
12831
+ "id": {
12832
+ "type": "string",
12833
+ "format": "uuid"
12834
+ },
12835
+ "subject": {
12836
+ "type": ["string", "null"],
12837
+ "description": "Normalized subject of the thread (Re/Fwd prefixes stripped)."
12838
+ },
12839
+ "root_message_id": {
12840
+ "type": ["string", "null"],
12841
+ "description": "Message-ID of the conversation root, when known."
12842
+ },
12843
+ "message_count": {
12844
+ "type": "integer",
12845
+ "description": "Total messages in the thread. `messages` is capped (most\nrecent first, then re-sorted oldest-first), so\n`message_count > messages.length` signals truncation.\n"
12846
+ },
12847
+ "first_message_at": {
12848
+ "type": ["string", "null"],
12849
+ "format": "date-time"
12850
+ },
12851
+ "last_message_at": {
12852
+ "type": ["string", "null"],
12853
+ "format": "date-time"
12854
+ },
12855
+ "created_at": {
12856
+ "type": "string",
12857
+ "format": "date-time"
12858
+ },
12859
+ "messages": {
12860
+ "type": "array",
12861
+ "items": {
12862
+ "type": "object",
12863
+ "description": "One message in a thread (inbound or outbound).",
12864
+ "properties": {
12865
+ "direction": {
12866
+ "type": "string",
12867
+ "enum": ["inbound", "outbound"],
12868
+ "description": "`inbound` for a received email (`/emails/{id}`), `outbound`\nfor a send (`/sent-emails/{id}`). Use it with `id` to fetch\nfull content from the right endpoint.\n"
12869
+ },
12870
+ "id": {
12871
+ "type": "string",
12872
+ "format": "uuid"
12873
+ },
12874
+ "message_id": { "type": ["string", "null"] },
12875
+ "from": { "type": ["string", "null"] },
12876
+ "to": { "type": ["string", "null"] },
12877
+ "subject": { "type": ["string", "null"] },
12878
+ "status": {
12879
+ "type": ["string", "null"],
12880
+ "description": "Lifecycle status (an EmailStatus or SentEmailStatus value, per `direction`)."
12881
+ },
12882
+ "timestamp": {
12883
+ "type": ["string", "null"],
12884
+ "format": "date-time",
12885
+ "description": "received_at for inbound, created_at for outbound."
12886
+ }
12887
+ },
12888
+ "required": ["direction", "id"]
12889
+ }
12890
+ }
12891
+ },
12892
+ "required": [
12893
+ "id",
12894
+ "message_count",
12895
+ "created_at",
12896
+ "messages"
12897
+ ]
12898
+ },
12899
+ "sdkName": "getThread",
12900
+ "summary": "Get a conversation thread by id",
12901
+ "tag": "Threads",
12902
+ "tagCommand": "threads"
12903
+ },
12262
12904
  {
12263
12905
  "binaryResponse": false,
12264
12906
  "bodyRequired": false,
@@ -13887,6 +14529,21 @@ function resolveChatResponseBody(reply) {
13887
14529
  function matchDescription(strategy) {
13888
14530
  return strategy === "strict" ? "strict, matched by reply_to_sent_email_id" : "fallback, matched by sender/time window";
13889
14531
  }
14532
+ function normalizeEmailAddress(value) {
14533
+ return value.trim().toLowerCase();
14534
+ }
14535
+ function derivedReplySubject(parent) {
14536
+ const subject = parent.subject?.trim();
14537
+ if (!subject) return "Re: (no subject)";
14538
+ return /^re:/i.test(subject) ? subject : `Re: ${subject}`;
14539
+ }
14540
+ function assertParentMatchesRecipient(parent, recipient) {
14541
+ if (normalizeEmailAddress(parent.from_email) === normalizeEmailAddress(recipient)) return;
14542
+ throw cliError$6(`Inbound email ${parent.id} is from ${parent.from_email}, not ${recipient}. Use \`primitive chat ${parent.from_email} --reply <message> --reply-to-email-id ${parent.id}\` or omit --reply-to-email-id to continue the latest inbound from ${recipient}.`);
14543
+ }
14544
+ function emailDetailFromEnvelope(envelope) {
14545
+ return envelope?.data ?? envelope ?? null;
14546
+ }
13890
14547
  function buildCommand(kind, description, argv, options = {}) {
13891
14548
  const requiresMessage = options.requiresMessage ?? false;
13892
14549
  return {
@@ -13907,15 +14564,15 @@ function buildChatFollowUpCommands(context) {
13907
14564
  "primitive",
13908
14565
  "chat",
13909
14566
  context.recipient,
14567
+ "--reply",
13910
14568
  "<message>",
13911
14569
  "--from",
13912
14570
  context.from,
13913
- "--subject",
13914
- context.subject,
14571
+ "--reply-to-email-id",
14572
+ context.reply.id,
13915
14573
  "--timeout",
13916
14574
  String(context.timeoutSeconds)
13917
14575
  ];
13918
- if (context.reply.message_id) continueParts.push("--in-reply-to", context.reply.message_id);
13919
14576
  if (context.json) continueParts.push("--json");
13920
14577
  if (context.quiet) continueParts.push("--quiet");
13921
14578
  if (context.strictOnly) continueParts.push("--strict-only");
@@ -14049,6 +14706,55 @@ function formatChatRecoveryContext(context) {
14049
14706
  for (const { description, command } of buildChatRecoveryCommands(context)) lines.push(` ${description}:`, ` ${command}`);
14050
14707
  return lines.join("\n");
14051
14708
  }
14709
+ async function loadInboundEmailDetail(params) {
14710
+ const result = await getEmail({
14711
+ client: params.apiClient.client,
14712
+ path: { id: params.id },
14713
+ responseStyle: "fields"
14714
+ });
14715
+ if (result.error) {
14716
+ const payload = extractErrorPayload(result.error);
14717
+ writeErrorWithHints(payload);
14718
+ surfaceUnauthorizedHint({
14719
+ ...params.authFailureContext,
14720
+ payload
14721
+ });
14722
+ throw new Errors.CLIError(`Could not load inbound email ${params.id}.`, { exit: 1 });
14723
+ }
14724
+ const detail = emailDetailFromEnvelope(result.data);
14725
+ if (!detail) throw new Errors.CLIError(`Could not load inbound email ${params.id}: the API returned no email body.`, { exit: 1 });
14726
+ return detail;
14727
+ }
14728
+ async function findLatestInboundFromRecipient(params) {
14729
+ const result = await searchEmails({
14730
+ client: params.apiClient.client,
14731
+ query: {
14732
+ from: params.recipient,
14733
+ to: params.from,
14734
+ include_facets: "false",
14735
+ limit: params.pageSize,
14736
+ snippet: "false",
14737
+ sort: "received_at_desc"
14738
+ },
14739
+ responseStyle: "fields"
14740
+ });
14741
+ if (result.error) {
14742
+ const payload = extractErrorPayload(result.error);
14743
+ writeErrorWithHints(payload);
14744
+ surfaceUnauthorizedHint({
14745
+ ...params.authFailureContext,
14746
+ payload
14747
+ });
14748
+ throw new Errors.CLIError("Could not find a prior chat reply.", { exit: 1 });
14749
+ }
14750
+ const row = (result.data?.data ?? []).find((email) => email.status === "accepted" || email.status === "completed");
14751
+ if (!row) return null;
14752
+ return loadInboundEmailDetail({
14753
+ apiClient: params.apiClient,
14754
+ authFailureContext: params.authFailureContext,
14755
+ id: row.id
14756
+ });
14757
+ }
14052
14758
  var ChatCommand = class ChatCommand extends Command {
14053
14759
  static description = `Send a message to an address and wait for the reply.
14054
14760
 
@@ -14062,6 +14768,13 @@ var ChatCommand = class ChatCommand extends Command {
14062
14768
  follow-up commands as templates. The default transcript is for humans;
14063
14769
  agents and scripts should pass --json for parse-safe output.
14064
14770
 
14771
+ To continue an existing chat, pass --reply '<message>'. By default,
14772
+ the CLI replies to the latest inbound email from the recipient to
14773
+ your sender address. For exact continuation, pass
14774
+ --reply-to-email-id <inbound-email-id>. Reply mode uses Primitive's
14775
+ reply endpoint, so the reply subject and threading headers are
14776
+ derived from the inbound email instead of copied into CLI flags.
14777
+
14065
14778
  --json emits a structured envelope with both sides of the exchange,
14066
14779
  a direct response_body field, match details, and follow-up command
14067
14780
  metadata such as kind, argv, placeholders, and requires_message.
@@ -14079,6 +14792,8 @@ var ChatCommand = class ChatCommand extends Command {
14079
14792
  static examples = [
14080
14793
  "<%= config.bin %> chat help@agent.acme.dev 'how do I rotate my API key?'",
14081
14794
  "cat error.log | <%= config.bin %> chat help@agent.acme.dev --subject 'webhook 401s'",
14795
+ "<%= config.bin %> chat help@agent.acme.dev --reply 'one more thing'",
14796
+ "<%= config.bin %> chat help@agent.acme.dev --reply 'one more thing' --reply-to-email-id <inbound-email-id>",
14082
14797
  "<%= config.bin %> chat help@agent.acme.dev 'follow up question' --json",
14083
14798
  "<%= config.bin %> chat help@agent.acme.dev 'one more thing' --timeout 300"
14084
14799
  ];
@@ -14106,7 +14821,9 @@ var ChatCommand = class ChatCommand extends Command {
14106
14821
  }),
14107
14822
  from: Flags.string({ description: "Sender address. Defaults to agent@<your-first-verified-outbound-domain>." }),
14108
14823
  subject: Flags.string({ description: "Subject line. Defaults to the first line of the message when omitted." }),
14109
- "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>`." }),
14824
+ reply: Flags.string({ description: "Reply body. Continues the latest inbound email from the recipient to your sender address; pass --reply-to-email-id for an exact thread." }),
14825
+ "reply-to-email-id": Flags.string({ description: "Inbound email id to continue exactly. Uses Primitive's reply endpoint, so recipient, subject, and threading headers are derived from the inbound email." }),
14826
+ "in-reply-to": Flags.string({ description: "Raw Message-Id of the parent email to thread a new send against. Prefer --reply-to-email-id with --reply when continuing an inbound email stored by Primitive." }),
14110
14827
  json: Flags.boolean({ description: "Emit a structured JSON envelope { sent, reply, response_body, response_body_format, match, follow_up_commands } on stdout instead of the human-readable transcript." }),
14111
14828
  quiet: Flags.boolean({ description: "Suppress stderr progress updates while sending and waiting. Errors and recovery commands are still written to stderr." }),
14112
14829
  timeout: Flags.integer({
@@ -14136,8 +14853,12 @@ var ChatCommand = class ChatCommand extends Command {
14136
14853
  };
14137
14854
  async run() {
14138
14855
  const { args, flags } = await this.parse(ChatCommand);
14139
- const message = args.message !== void 0 && args.message !== "" ? args.message : await readStdinToString();
14140
- if (!message.trim()) throw cliError$6("Message body is empty.");
14856
+ const replyMode = flags.reply !== void 0 || flags["reply-to-email-id"] !== void 0;
14857
+ if (flags.reply !== void 0 && args.message !== void 0 && args.message !== "") throw cliError$6("Pass the reply body either as --reply or as the positional message, not both.");
14858
+ if (replyMode && flags.subject !== void 0) throw cliError$6("--subject is not used with --reply. Primitive derives the reply subject from the inbound email.");
14859
+ if (replyMode && flags["in-reply-to"] !== void 0) throw cliError$6("Use --reply-to-email-id with --reply instead of raw --in-reply-to.");
14860
+ const message = flags.reply !== void 0 ? flags.reply : args.message !== void 0 && args.message !== "" ? args.message : await readStdinToString();
14861
+ if (!message.trim()) throw cliError$6(replyMode ? "Reply body is empty." : "Message body is empty.");
14141
14862
  await runWithTiming(flags.time, async () => {
14142
14863
  const { apiClient, auth, baseUrlOverridden } = await createAuthenticatedCliApiClient({
14143
14864
  apiKey: flags["api-key"],
@@ -14150,12 +14871,71 @@ var ChatCommand = class ChatCommand extends Command {
14150
14871
  baseUrlOverridden,
14151
14872
  configDir: this.config.configDir
14152
14873
  };
14153
- const from = flags.from ?? await pickDefaultFromAddress(apiClient, authFailureContext);
14154
- const subject = flags.subject ?? deriveSubject(message);
14155
- const sentAtIso = (/* @__PURE__ */ new Date()).toISOString();
14156
14874
  const progress = flags.quiet ? null : new ChatProgressIndicator(process.stderr);
14157
- progress?.start(`Sending message to ${args.recipient}`);
14158
- const sendResult = await sendEmail({
14875
+ let from;
14876
+ let parentReply;
14877
+ let subject;
14878
+ if (replyMode) {
14879
+ const replyContext = await (async () => {
14880
+ let replyContextFailureMessage = "Could not load reply context.";
14881
+ try {
14882
+ if (flags["reply-to-email-id"] !== void 0) {
14883
+ progress?.start(`Loading reply context for ${flags["reply-to-email-id"]}`);
14884
+ const exactParentReply = await loadInboundEmailDetail({
14885
+ apiClient,
14886
+ authFailureContext,
14887
+ id: flags["reply-to-email-id"]
14888
+ });
14889
+ replyContextFailureMessage = `Inbound email ${flags["reply-to-email-id"]} does not match recipient ${args.recipient}.`;
14890
+ assertParentMatchesRecipient(exactParentReply, args.recipient);
14891
+ return {
14892
+ from: flags.from ?? exactParentReply.to_email,
14893
+ parentReply: exactParentReply
14894
+ };
14895
+ }
14896
+ const replyFrom = flags.from ?? await pickDefaultFromAddress(apiClient, authFailureContext);
14897
+ progress?.start(`Finding latest inbound email from ${args.recipient}`);
14898
+ const latestParentReply = await findLatestInboundFromRecipient({
14899
+ apiClient,
14900
+ authFailureContext,
14901
+ from: replyFrom,
14902
+ pageSize: flags["page-size"],
14903
+ recipient: args.recipient
14904
+ });
14905
+ if (!latestParentReply) {
14906
+ replyContextFailureMessage = "No prior inbound email found.";
14907
+ throw cliError$6(`No prior inbound email from ${args.recipient} to ${replyFrom}. Start a new chat with \`primitive chat ${args.recipient} <message>\`, pass --from, or pass --reply-to-email-id <inbound-email-id>.`);
14908
+ }
14909
+ replyContextFailureMessage = `Inbound email ${latestParentReply.id} does not match recipient ${args.recipient}.`;
14910
+ assertParentMatchesRecipient(latestParentReply, args.recipient);
14911
+ return {
14912
+ from: replyFrom,
14913
+ parentReply: latestParentReply
14914
+ };
14915
+ } catch (error) {
14916
+ progress?.fail(replyContextFailureMessage);
14917
+ throw error;
14918
+ }
14919
+ })();
14920
+ from = replyContext.from;
14921
+ parentReply = replyContext.parentReply;
14922
+ subject = derivedReplySubject(replyContext.parentReply);
14923
+ } else {
14924
+ from = flags.from ?? await pickDefaultFromAddress(apiClient, authFailureContext);
14925
+ subject = flags.subject ?? deriveSubject(message);
14926
+ }
14927
+ const sentAtIso = (/* @__PURE__ */ new Date()).toISOString();
14928
+ if (replyMode) progress?.update(`Sending reply to ${args.recipient}`);
14929
+ else progress?.start(`Sending message to ${args.recipient}`);
14930
+ const sendResult = parentReply !== void 0 ? await replyToEmail({
14931
+ body: {
14932
+ body_text: message,
14933
+ from
14934
+ },
14935
+ client: apiClient.client,
14936
+ path: { id: parentReply.id },
14937
+ responseStyle: "fields"
14938
+ }) : await sendEmail({
14159
14939
  body: {
14160
14940
  from,
14161
14941
  to: args.recipient,
@@ -14167,7 +14947,7 @@ var ChatCommand = class ChatCommand extends Command {
14167
14947
  responseStyle: "fields"
14168
14948
  });
14169
14949
  if (sendResult.error) {
14170
- progress?.fail("Message send failed.");
14950
+ progress?.fail(replyMode ? "Reply send failed." : "Message send failed.");
14171
14951
  const errorPayload = extractErrorPayload(sendResult.error);
14172
14952
  writeErrorWithHints(errorPayload);
14173
14953
  surfaceUnauthorizedHint({
@@ -14182,13 +14962,15 @@ var ChatCommand = class ChatCommand extends Command {
14182
14962
  progress?.fail("Send succeeded but the API returned no data.");
14183
14963
  throw cliError$6("Send succeeded but the API returned no data.");
14184
14964
  }
14185
- progress?.update(`Message sent; waiting for reply from ${args.recipient}`, {
14965
+ const replyAddress = sent.from || from;
14966
+ progress?.update(`${replyMode ? "Reply" : "Message"} sent; waiting for reply from ${args.recipient}`, {
14186
14967
  heartbeatMs: 15e3,
14187
14968
  timeoutSeconds: flags.timeout
14188
14969
  });
14189
14970
  const baseContext = {
14190
- from,
14971
+ from: replyAddress,
14191
14972
  json: flags.json,
14973
+ parentReply,
14192
14974
  quiet: flags.quiet,
14193
14975
  recipient: args.recipient,
14194
14976
  sent,
@@ -14203,7 +14985,7 @@ var ChatCommand = class ChatCommand extends Command {
14203
14985
  replyResult = await waitForReply({
14204
14986
  apiClient,
14205
14987
  authFailureContext,
14206
- from,
14988
+ from: replyAddress,
14207
14989
  interval: flags.interval,
14208
14990
  notice: (message) => {
14209
14991
  if (progress) {
@@ -0,0 +1,31 @@
1
+ import { existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { homedir } from "node:os";
4
+ //#region src/oclif/root-signup-hint.ts
5
+ const CREDENTIALS_FILE = "credentials.json";
6
+ function activeConfigDir(env, home) {
7
+ if (env.PRIMITIVE_CONFIG_DIR) return env.PRIMITIVE_CONFIG_DIR;
8
+ return join(env.XDG_CONFIG_HOME || join(home, ".config"), "primitive");
9
+ }
10
+ function shouldShowLoggedOutSignupHint(options = {}) {
11
+ if ((options.argv ?? process.argv.slice(2)).length > 0) return false;
12
+ const env = options.env ?? process.env;
13
+ if (env.PRIMITIVE_HIDE_SIGNUP_HINT === "1") return false;
14
+ if (env.PRIMITIVE_API_KEY?.trim()) return false;
15
+ return !existsSync(join(activeConfigDir(env, options.home ?? homedir()), CREDENTIALS_FILE));
16
+ }
17
+ function loggedOutSignupHint() {
18
+ return [
19
+ "New to Primitive?",
20
+ " You or your user don't have an account yet?",
21
+ " Run `primitive signup <email> --signup-code <invite-code> --accept-terms`",
22
+ " to create an account, get your own domain, and get started now.",
23
+ ""
24
+ ].join("\n");
25
+ }
26
+ function writeLoggedOutSignupHintIfNeeded(options = {}) {
27
+ if (!shouldShowLoggedOutSignupHint(options)) return;
28
+ (options.write ?? ((message) => process.stdout.write(message)))(loggedOutSignupHint());
29
+ }
30
+ //#endregion
31
+ export { loggedOutSignupHint, shouldShowLoggedOutSignupHint, writeLoggedOutSignupHintIfNeeded };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@primitivedotdev/cli",
3
- "version": "0.31.6",
3
+ "version": "0.31.8",
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,
@@ -25,8 +25,15 @@
25
25
  "dirname": "primitive",
26
26
  "plugins": [
27
27
  "@oclif/plugin-help",
28
- "@oclif/plugin-autocomplete"
28
+ "@oclif/plugin-autocomplete",
29
+ "@oclif/plugin-warn-if-update-available"
29
30
  ],
31
+ "warn-if-update-available": {
32
+ "timeoutInDays": 1,
33
+ "frequency": 1,
34
+ "frequencyUnit": "days",
35
+ "message": "Primitive CLI update available from <%= chalk.greenBright(config.version) %> to <%= chalk.greenBright(latest) %>. Run `npm install -g @primitivedotdev/cli@latest` to update."
36
+ },
30
37
  "topics": {
31
38
  "cli": {
32
39
  "description": "CLI authentication"
@@ -59,6 +66,9 @@
59
66
  "sent": {
60
67
  "description": "Short aliases for outbound sent-email history: `primitive sent list` and `primitive sent get`."
61
68
  },
69
+ "threads": {
70
+ "description": "Inspect conversation threads spanning received and sent emails: `primitive threads get --id <thread-id>`."
71
+ },
62
72
  "endpoints": {
63
73
  "description": "Manage webhook endpoints that receive email events"
64
74
  },
@@ -112,7 +122,8 @@
112
122
  "dependencies": {
113
123
  "@oclif/core": "^4.10.5",
114
124
  "@oclif/plugin-autocomplete": "^3.2.45",
115
- "@oclif/plugin-help": "^6.2.44"
125
+ "@oclif/plugin-help": "^6.2.44",
126
+ "@oclif/plugin-warn-if-update-available": "^3.1.65"
116
127
  },
117
128
  "devDependencies": {
118
129
  "@biomejs/biome": "^2.4.10",