@primitivedotdev/cli 0.31.3 → 0.31.5

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.
@@ -1,7 +1,7 @@
1
1
  import { Args, Command, Errors, Flags } from "@oclif/core";
2
2
  import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
3
3
  import { randomUUID } from "node:crypto";
4
- import { dirname, join, resolve } from "node:path";
4
+ import { basename, dirname, join, resolve } from "node:path";
5
5
  import { spawn } from "node:child_process";
6
6
  import { hostname } from "node:os";
7
7
  import process$1 from "node:process";
@@ -645,11 +645,13 @@ var sdk_gen_exports = /* @__PURE__ */ __exportAll({
645
645
  deleteFunctionSecret: () => deleteFunctionSecret,
646
646
  discardEmailContent: () => discardEmailContent,
647
647
  downloadAttachments: () => downloadAttachments,
648
+ downloadDomainZoneFile: () => downloadDomainZoneFile,
648
649
  downloadRawEmail: () => downloadRawEmail,
649
650
  getAccount: () => getAccount,
650
651
  getEmail: () => getEmail,
651
652
  getFunction: () => getFunction,
652
653
  getFunctionTestRunTrace: () => getFunctionTestRunTrace,
654
+ getInboxStatus: () => getInboxStatus,
653
655
  getSendPermissions: () => getSendPermissions,
654
656
  getSentEmail: () => getSentEmail,
655
657
  getStorageStats: () => getStorageStats,
@@ -941,9 +943,11 @@ const listDomains = (options) => (options?.client ?? client).get({
941
943
  /**
942
944
  * Claim a new domain
943
945
  *
944
- * Creates an unverified domain claim. You will receive a
945
- * `verification_token` to add as a DNS TXT record before
946
- * calling the verify endpoint.
946
+ * Creates an unverified domain claim and returns the exact
947
+ * DNS records to publish in `dns_records`. Publish those
948
+ * records before calling the verify endpoint. To give users
949
+ * an importable DNS file, call `downloadDomainZoneFile` or run
950
+ * `primitive domains zone-file --id <domain-id>`.
947
951
  *
948
952
  */
949
953
  const addDomain = (options) => (options.client ?? client).post({
@@ -993,9 +997,15 @@ const updateDomain = (options) => (options.client ?? client).patch({
993
997
  /**
994
998
  * Verify domain ownership
995
999
  *
996
- * Checks DNS records (MX and TXT) to verify domain ownership.
1000
+ * Checks DNS records required for inbound routing, ownership,
1001
+ * and outbound authentication: MX, ownership TXT, SPF, DKIM,
1002
+ * DMARC, and TLS-RPT.
997
1003
  * On success, the domain is promoted from unverified to verified.
998
- * On failure, returns which checks passed and which failed.
1004
+ * On failure, returns which checks passed and which failed,
1005
+ * plus the exact DNS records still expected. To give users
1006
+ * an importable DNS file for missing records, call
1007
+ * `downloadDomainZoneFile` or run
1008
+ * `primitive domains zone-file --id <domain-id>`.
999
1009
  *
1000
1010
  */
1001
1011
  const verifyDomain = (options) => (options.client ?? client).post({
@@ -1007,6 +1017,45 @@ const verifyDomain = (options) => (options.client ?? client).post({
1007
1017
  ...options
1008
1018
  });
1009
1019
  /**
1020
+ * Download domain DNS zone file
1021
+ *
1022
+ * Downloads a BIND-format DNS zone file containing the DNS records
1023
+ * required for a domain claim. Agents should offer this after
1024
+ * `addDomain` when users want to import DNS records instead of
1025
+ * copying each record manually.
1026
+ *
1027
+ */
1028
+ const downloadDomainZoneFile = (options) => (options.client ?? client).get({
1029
+ security: [{
1030
+ scheme: "bearer",
1031
+ type: "http"
1032
+ }],
1033
+ url: "/domains/{id}/zone-file",
1034
+ ...options
1035
+ });
1036
+ /**
1037
+ * Get inbound inbox readiness
1038
+ *
1039
+ * Returns one consolidated view of inbound domain readiness,
1040
+ * webhook/function processing routes, deployed Functions, and
1041
+ * recent inbound email activity.
1042
+ *
1043
+ * Agents should call this before guiding a user through inbound
1044
+ * setup. It answers the practical questions "can I receive mail",
1045
+ * "will anything process that mail", and "what should I do next"
1046
+ * without forcing clients to stitch together domains, endpoints,
1047
+ * functions, and emails manually.
1048
+ *
1049
+ */
1050
+ const getInboxStatus = (options) => (options?.client ?? client).get({
1051
+ security: [{
1052
+ scheme: "bearer",
1053
+ type: "http"
1054
+ }],
1055
+ url: "/inbox/status",
1056
+ ...options
1057
+ });
1058
+ /**
1010
1059
  * List inbound emails
1011
1060
  *
1012
1061
  * Returns a paginated list of INBOUND emails received at your
@@ -1868,6 +1917,10 @@ const openapiDocument = {
1868
1917
  "name": "Domains",
1869
1918
  "description": "Claim, verify, and manage email domains"
1870
1919
  },
1920
+ {
1921
+ "name": "Inbox",
1922
+ "description": "Check inbound email setup and processing readiness"
1923
+ },
1871
1924
  {
1872
1925
  "name": "Emails",
1873
1926
  "description": "List, inspect, and manage received emails"
@@ -2333,7 +2386,7 @@ const openapiDocument = {
2333
2386
  "post": {
2334
2387
  "operationId": "addDomain",
2335
2388
  "summary": "Claim a new domain",
2336
- "description": "Creates an unverified domain claim. You will receive a\n`verification_token` to add as a DNS TXT record before\ncalling the verify endpoint.\n",
2389
+ "description": "Creates an unverified domain claim and returns the exact\nDNS records to publish in `dns_records`. Publish those\nrecords before calling the verify endpoint. To give users\nan importable DNS file, call `downloadDomainZoneFile` or run\n`primitive domains zone-file --id <domain-id>`.\n",
2337
2390
  "tags": ["Domains"],
2338
2391
  "requestBody": {
2339
2392
  "required": true,
@@ -2426,7 +2479,7 @@ const openapiDocument = {
2426
2479
  "post": {
2427
2480
  "operationId": "verifyDomain",
2428
2481
  "summary": "Verify domain ownership",
2429
- "description": "Checks DNS records (MX and TXT) to verify domain ownership.\nOn success, the domain is promoted from unverified to verified.\nOn failure, returns which checks passed and which failed.\n",
2482
+ "description": "Checks DNS records required for inbound routing, ownership,\nand outbound authentication: MX, ownership TXT, SPF, DKIM,\nDMARC, and TLS-RPT.\nOn success, the domain is promoted from unverified to verified.\nOn failure, returns which checks passed and which failed,\nplus the exact DNS records still expected. To give users\nan importable DNS file for missing records, call\n`downloadDomainZoneFile` or run\n`primitive domains zone-file --id <domain-id>`.\n",
2430
2483
  "tags": ["Domains"],
2431
2484
  "responses": {
2432
2485
  "200": {
@@ -2442,6 +2495,55 @@ const openapiDocument = {
2442
2495
  }
2443
2496
  }
2444
2497
  },
2498
+ "/domains/{id}/zone-file": {
2499
+ "parameters": [{ "$ref": "#/components/parameters/ResourceId" }],
2500
+ "get": {
2501
+ "operationId": "downloadDomainZoneFile",
2502
+ "summary": "Download domain DNS zone file",
2503
+ "description": "Downloads a BIND-format DNS zone file containing the DNS records\nrequired for a domain claim. Agents should offer this after\n`addDomain` when users want to import DNS records instead of\ncopying each record manually.\n",
2504
+ "tags": ["Domains"],
2505
+ "parameters": [{
2506
+ "name": "outbound_only",
2507
+ "in": "query",
2508
+ "schema": { "type": "boolean" },
2509
+ "description": "When true, include only outbound DNS records. Verified domains\ndefault to outbound-only; pending claims default to all required\nrecords.\n"
2510
+ }],
2511
+ "responses": {
2512
+ "200": {
2513
+ "description": "BIND-format zone file",
2514
+ "content": { "text/plain": { "schema": {
2515
+ "type": "string",
2516
+ "format": "binary"
2517
+ } } },
2518
+ "headers": { "Content-Disposition": { "schema": {
2519
+ "type": "string",
2520
+ "example": "attachment; filename=\"example.com.zone\""
2521
+ } } }
2522
+ },
2523
+ "400": { "$ref": "#/components/responses/ValidationError" },
2524
+ "401": { "$ref": "#/components/responses/Unauthorized" },
2525
+ "404": { "$ref": "#/components/responses/NotFound" },
2526
+ "429": { "$ref": "#/components/responses/RateLimited" }
2527
+ }
2528
+ }
2529
+ },
2530
+ "/inbox/status": { "get": {
2531
+ "operationId": "getInboxStatus",
2532
+ "summary": "Get inbound inbox readiness",
2533
+ "description": "Returns one consolidated view of inbound domain readiness,\nwebhook/function processing routes, deployed Functions, and\nrecent inbound email activity.\n\nAgents should call this before guiding a user through inbound\nsetup. It answers the practical questions \"can I receive mail\",\n\"will anything process that mail\", and \"what should I do next\"\nwithout forcing clients to stitch together domains, endpoints,\nfunctions, and emails manually.\n",
2534
+ "tags": ["Inbox"],
2535
+ "responses": {
2536
+ "200": {
2537
+ "description": "Consolidated inbox readiness status",
2538
+ "content": { "application/json": { "schema": { "allOf": [{ "$ref": "#/components/schemas/SuccessEnvelope" }, {
2539
+ "type": "object",
2540
+ "properties": { "data": { "$ref": "#/components/schemas/InboxStatus" } }
2541
+ }] } } }
2542
+ },
2543
+ "401": { "$ref": "#/components/responses/Unauthorized" },
2544
+ "429": { "$ref": "#/components/responses/RateLimited" }
2545
+ }
2546
+ } },
2445
2547
  "/emails": { "get": {
2446
2548
  "operationId": "listEmails",
2447
2549
  "summary": "List inbound emails",
@@ -4730,8 +4832,183 @@ const openapiDocument = {
4730
4832
  } },
4731
4833
  "required": ["secret"]
4732
4834
  },
4835
+ "InboxStatus": {
4836
+ "type": "object",
4837
+ "additionalProperties": false,
4838
+ "properties": {
4839
+ "ready": {
4840
+ "type": "boolean",
4841
+ "description": "True when at least one active inbound domain has an enabled processing route."
4842
+ },
4843
+ "receiving_ready": {
4844
+ "type": "boolean",
4845
+ "description": "True when at least one active verified or managed domain can receive mail."
4846
+ },
4847
+ "processing_ready": {
4848
+ "type": "boolean",
4849
+ "description": "True when at least one receiving-ready domain has an enabled webhook or function route."
4850
+ },
4851
+ "summary": {
4852
+ "type": "string",
4853
+ "description": "Short human-readable status summary."
4854
+ },
4855
+ "next_actions": {
4856
+ "type": "array",
4857
+ "items": { "$ref": "#/components/schemas/InboxStatusNextAction" }
4858
+ },
4859
+ "domains": {
4860
+ "type": "array",
4861
+ "items": { "$ref": "#/components/schemas/InboxStatusDomain" }
4862
+ },
4863
+ "endpoints": { "$ref": "#/components/schemas/InboxStatusEndpointSummary" },
4864
+ "functions": { "$ref": "#/components/schemas/InboxStatusFunctionSummary" },
4865
+ "recent_emails": { "$ref": "#/components/schemas/InboxStatusRecentEmailSummary" }
4866
+ },
4867
+ "required": [
4868
+ "ready",
4869
+ "receiving_ready",
4870
+ "processing_ready",
4871
+ "summary",
4872
+ "next_actions",
4873
+ "domains",
4874
+ "endpoints",
4875
+ "functions",
4876
+ "recent_emails"
4877
+ ]
4878
+ },
4879
+ "InboxStatusNextAction": {
4880
+ "type": "object",
4881
+ "additionalProperties": false,
4882
+ "properties": {
4883
+ "kind": {
4884
+ "type": "string",
4885
+ "enum": [
4886
+ "add_domain",
4887
+ "verify_domain",
4888
+ "configure_processing",
4889
+ "send_test_email",
4890
+ "fix_failed_functions"
4891
+ ]
4892
+ },
4893
+ "message": {
4894
+ "type": "string",
4895
+ "description": "Human-readable next step."
4896
+ },
4897
+ "command": {
4898
+ "type": "string",
4899
+ "description": "Suggested Primitive CLI command when there is an obvious next step."
4900
+ }
4901
+ },
4902
+ "required": ["kind", "message"]
4903
+ },
4904
+ "InboxStatusDomain": {
4905
+ "type": "object",
4906
+ "additionalProperties": false,
4907
+ "properties": {
4908
+ "id": { "type": "string" },
4909
+ "domain": { "type": "string" },
4910
+ "verified": { "type": "boolean" },
4911
+ "active": { "type": "boolean" },
4912
+ "managed": { "type": "boolean" },
4913
+ "receiving_ready": { "type": "boolean" },
4914
+ "processing_ready": { "type": "boolean" },
4915
+ "processing_route_count": { "type": "integer" },
4916
+ "endpoint_count": { "type": "integer" },
4917
+ "enabled_endpoint_count": { "type": "integer" },
4918
+ "function_endpoint_count": { "type": "integer" },
4919
+ "email_count": {
4920
+ "type": "integer",
4921
+ "description": "Number of inbound emails received for this domain in the last 30 days."
4922
+ },
4923
+ "latest_email_received_at": {
4924
+ "type": ["string", "null"],
4925
+ "format": "date-time",
4926
+ "description": "Most recent inbound email received for this domain in the last 30 days."
4927
+ },
4928
+ "status": {
4929
+ "type": "string",
4930
+ "enum": [
4931
+ "ready",
4932
+ "stored_only",
4933
+ "pending_dns",
4934
+ "inactive"
4935
+ ]
4936
+ }
4937
+ },
4938
+ "required": [
4939
+ "id",
4940
+ "domain",
4941
+ "verified",
4942
+ "active",
4943
+ "managed",
4944
+ "receiving_ready",
4945
+ "processing_ready",
4946
+ "processing_route_count",
4947
+ "endpoint_count",
4948
+ "enabled_endpoint_count",
4949
+ "function_endpoint_count",
4950
+ "email_count",
4951
+ "latest_email_received_at",
4952
+ "status"
4953
+ ]
4954
+ },
4955
+ "InboxStatusEndpointSummary": {
4956
+ "type": "object",
4957
+ "additionalProperties": false,
4958
+ "properties": {
4959
+ "total": { "type": "integer" },
4960
+ "enabled": { "type": "integer" },
4961
+ "disabled": { "type": "integer" },
4962
+ "fallback_enabled": { "type": "integer" },
4963
+ "domain_scoped_enabled": { "type": "integer" },
4964
+ "http_enabled": { "type": "integer" },
4965
+ "function_enabled": { "type": "integer" }
4966
+ },
4967
+ "required": [
4968
+ "total",
4969
+ "enabled",
4970
+ "disabled",
4971
+ "fallback_enabled",
4972
+ "domain_scoped_enabled",
4973
+ "http_enabled",
4974
+ "function_enabled"
4975
+ ]
4976
+ },
4977
+ "InboxStatusFunctionSummary": {
4978
+ "type": "object",
4979
+ "additionalProperties": false,
4980
+ "properties": {
4981
+ "total": { "type": "integer" },
4982
+ "deployed": { "type": "integer" },
4983
+ "pending": { "type": "integer" },
4984
+ "failed": { "type": "integer" }
4985
+ },
4986
+ "required": [
4987
+ "total",
4988
+ "deployed",
4989
+ "pending",
4990
+ "failed"
4991
+ ]
4992
+ },
4993
+ "InboxStatusRecentEmailSummary": {
4994
+ "type": "object",
4995
+ "description": "Inbound email activity from the last 30 days.",
4996
+ "additionalProperties": false,
4997
+ "properties": {
4998
+ "total": {
4999
+ "type": "integer",
5000
+ "description": "Number of inbound emails received in the last 30 days."
5001
+ },
5002
+ "latest_received_at": {
5003
+ "type": ["string", "null"],
5004
+ "format": "date-time",
5005
+ "description": "Most recent inbound email received in the last 30 days."
5006
+ }
5007
+ },
5008
+ "required": ["total", "latest_received_at"]
5009
+ },
4733
5010
  "Domain": {
4734
- "description": "A domain can be either verified or unverified. Verified domains have\n`is_active` and `spam_threshold` fields. Unverified domains have a\n`verification_token` for DNS verification.\n",
5011
+ "description": "A domain can be either verified or unverified. Verified domains have\n`is_active` and `spam_threshold` fields. Unverified domains have a\n`verification_token` and `dns_records` for DNS setup.\n",
4735
5012
  "oneOf": [{ "$ref": "#/components/schemas/VerifiedDomain" }, { "$ref": "#/components/schemas/UnverifiedDomain" }]
4736
5013
  },
4737
5014
  "VerifiedDomain": {
@@ -4771,6 +5048,74 @@ const openapiDocument = {
4771
5048
  "created_at"
4772
5049
  ]
4773
5050
  },
5051
+ "DomainDnsRecord": {
5052
+ "type": "object",
5053
+ "additionalProperties": false,
5054
+ "properties": {
5055
+ "type": {
5056
+ "type": "string",
5057
+ "enum": ["MX", "TXT"],
5058
+ "description": "DNS record type."
5059
+ },
5060
+ "name": {
5061
+ "type": "string",
5062
+ "description": "DNS-provider host/name value relative to the managed root zone."
5063
+ },
5064
+ "fqdn": {
5065
+ "type": "string",
5066
+ "description": "Fully-qualified DNS record name."
5067
+ },
5068
+ "value": {
5069
+ "type": "string",
5070
+ "description": "Exact value to publish."
5071
+ },
5072
+ "priority": {
5073
+ "type": "integer",
5074
+ "description": "MX priority. Present only for MX records."
5075
+ },
5076
+ "ttl": {
5077
+ "type": "integer",
5078
+ "description": "Suggested TTL in seconds when the API can provide one."
5079
+ },
5080
+ "required": {
5081
+ "type": "boolean",
5082
+ "const": true
5083
+ },
5084
+ "purpose": {
5085
+ "type": "string",
5086
+ "enum": [
5087
+ "inbound_mx",
5088
+ "ownership_verification",
5089
+ "spf",
5090
+ "dkim",
5091
+ "dmarc",
5092
+ "tls_reporting"
5093
+ ]
5094
+ },
5095
+ "status": {
5096
+ "type": "string",
5097
+ "enum": [
5098
+ "pending",
5099
+ "found",
5100
+ "missing",
5101
+ "incorrect"
5102
+ ]
5103
+ },
5104
+ "message": {
5105
+ "type": "string",
5106
+ "description": "Short explanation of why this record is needed."
5107
+ }
5108
+ },
5109
+ "required": [
5110
+ "type",
5111
+ "name",
5112
+ "fqdn",
5113
+ "value",
5114
+ "required",
5115
+ "purpose",
5116
+ "status"
5117
+ ]
5118
+ },
4774
5119
  "UnverifiedDomain": {
4775
5120
  "type": "object",
4776
5121
  "properties": {
@@ -4791,6 +5136,11 @@ const openapiDocument = {
4791
5136
  "type": "string",
4792
5137
  "description": "Add this value as a TXT record to verify ownership"
4793
5138
  },
5139
+ "dns_records": {
5140
+ "type": "array",
5141
+ "description": "Exact DNS records to publish for this pending domain claim.",
5142
+ "items": { "$ref": "#/components/schemas/DomainDnsRecord" }
5143
+ },
4794
5144
  "created_at": {
4795
5145
  "type": "string",
4796
5146
  "format": "date-time"
@@ -4808,12 +5158,23 @@ const openapiDocument = {
4808
5158
  "AddDomainInput": {
4809
5159
  "type": "object",
4810
5160
  "additionalProperties": false,
4811
- "properties": { "domain": {
4812
- "type": "string",
4813
- "minLength": 1,
4814
- "maxLength": 253,
4815
- "description": "The domain name to claim (e.g. \"example.com\")"
4816
- } },
5161
+ "properties": {
5162
+ "domain": {
5163
+ "type": "string",
5164
+ "minLength": 1,
5165
+ "maxLength": 253,
5166
+ "description": "The domain name to claim (e.g. \"example.com\")"
5167
+ },
5168
+ "confirmed": {
5169
+ "type": "boolean",
5170
+ "description": "Set to true to confirm replacing an existing mailbox provider after an mx_conflict response."
5171
+ },
5172
+ "outbound": {
5173
+ "type": "boolean",
5174
+ "deprecated": true,
5175
+ "description": "Deprecated and ignored. Outbound DNS is provisioned for every new domain claim."
5176
+ }
5177
+ },
4817
5178
  "required": ["domain"]
4818
5179
  },
4819
5180
  "UpdateDomainInput": {
@@ -4835,10 +5196,17 @@ const openapiDocument = {
4835
5196
  },
4836
5197
  "DomainVerifyResult": { "oneOf": [{
4837
5198
  "type": "object",
4838
- "properties": { "verified": {
4839
- "type": "boolean",
4840
- "const": true
4841
- } },
5199
+ "properties": {
5200
+ "verified": {
5201
+ "type": "boolean",
5202
+ "const": true
5203
+ },
5204
+ "dns_records": {
5205
+ "type": "array",
5206
+ "description": "Exact DNS records checked for this verification attempt.",
5207
+ "items": { "$ref": "#/components/schemas/DomainDnsRecord" }
5208
+ }
5209
+ },
4842
5210
  "required": ["verified"]
4843
5211
  }, {
4844
5212
  "type": "object",
@@ -4855,6 +5223,27 @@ const openapiDocument = {
4855
5223
  "type": "boolean",
4856
5224
  "description": "Whether the TXT verification record was found"
4857
5225
  },
5226
+ "spfFound": {
5227
+ "type": "boolean",
5228
+ "description": "Whether the SPF record includes Primitive."
5229
+ },
5230
+ "dkimFound": {
5231
+ "type": "boolean",
5232
+ "description": "Whether the DKIM public key record was found."
5233
+ },
5234
+ "dmarcFound": {
5235
+ "type": "boolean",
5236
+ "description": "Whether the DMARC record was found."
5237
+ },
5238
+ "tlsRptFound": {
5239
+ "type": "boolean",
5240
+ "description": "Whether the TLS-RPT record was found."
5241
+ },
5242
+ "dns_records": {
5243
+ "type": "array",
5244
+ "description": "Exact DNS records checked for this verification attempt.",
5245
+ "items": { "$ref": "#/components/schemas/DomainDnsRecord" }
5246
+ },
4858
5247
  "error": {
4859
5248
  "type": "string",
4860
5249
  "description": "Human-readable verification failure reason"
@@ -5167,6 +5556,31 @@ const openapiDocument = {
5167
5556
  "created_at"
5168
5557
  ]
5169
5558
  },
5559
+ "SendMailAttachment": {
5560
+ "type": "object",
5561
+ "additionalProperties": false,
5562
+ "properties": {
5563
+ "filename": {
5564
+ "type": "string",
5565
+ "minLength": 1,
5566
+ "maxLength": 255,
5567
+ "description": "Attachment filename. Control characters are rejected."
5568
+ },
5569
+ "content_type": {
5570
+ "type": "string",
5571
+ "minLength": 1,
5572
+ "maxLength": 255,
5573
+ "description": "Optional MIME content type. Control characters are rejected."
5574
+ },
5575
+ "content_base64": {
5576
+ "type": "string",
5577
+ "minLength": 1,
5578
+ "maxLength": 44040192,
5579
+ "description": "Base64-encoded attachment bytes."
5580
+ }
5581
+ },
5582
+ "required": ["filename", "content_base64"]
5583
+ },
5170
5584
  "SendMailInput": {
5171
5585
  "type": "object",
5172
5586
  "additionalProperties": false,
@@ -5215,6 +5629,12 @@ const openapiDocument = {
5215
5629
  "pattern": "^[^\\x00-\\x1F\\x7F]+$"
5216
5630
  }
5217
5631
  },
5632
+ "attachments": {
5633
+ "type": "array",
5634
+ "maxItems": 100,
5635
+ "description": "Inline attachments. Send requests with attachments to https://api.primitive.dev/v1/send-mail. Combined raw decoded attachment bytes must be at most 31457280.",
5636
+ "items": { "$ref": "#/components/schemas/SendMailAttachment" }
5637
+ },
5218
5638
  "wait": {
5219
5639
  "type": "boolean",
5220
5640
  "description": "When true, wait for the first downstream SMTP delivery outcome before returning."
@@ -7502,7 +7922,7 @@ const operationManifest = [
7502
7922
  "binaryResponse": false,
7503
7923
  "bodyRequired": true,
7504
7924
  "command": "add-domain",
7505
- "description": "Creates an unverified domain claim. You will receive a\n`verification_token` to add as a DNS TXT record before\ncalling the verify endpoint.\n",
7925
+ "description": "Creates an unverified domain claim and returns the exact\nDNS records to publish in `dns_records`. Publish those\nrecords before calling the verify endpoint. To give users\nan importable DNS file, call `downloadDomainZoneFile` or run\n`primitive domains zone-file --id <domain-id>`.\n",
7506
7926
  "hasJsonBody": true,
7507
7927
  "method": "POST",
7508
7928
  "operationId": "addDomain",
@@ -7512,12 +7932,23 @@ const operationManifest = [
7512
7932
  "requestSchema": {
7513
7933
  "type": "object",
7514
7934
  "additionalProperties": false,
7515
- "properties": { "domain": {
7516
- "type": "string",
7517
- "minLength": 1,
7518
- "maxLength": 253,
7519
- "description": "The domain name to claim (e.g. \"example.com\")"
7520
- } },
7935
+ "properties": {
7936
+ "domain": {
7937
+ "type": "string",
7938
+ "minLength": 1,
7939
+ "maxLength": 253,
7940
+ "description": "The domain name to claim (e.g. \"example.com\")"
7941
+ },
7942
+ "confirmed": {
7943
+ "type": "boolean",
7944
+ "description": "Set to true to confirm replacing an existing mailbox provider after an mx_conflict response."
7945
+ },
7946
+ "outbound": {
7947
+ "type": "boolean",
7948
+ "deprecated": true,
7949
+ "description": "Deprecated and ignored. Outbound DNS is provisioned for every new domain claim."
7950
+ }
7951
+ },
7521
7952
  "required": ["domain"]
7522
7953
  },
7523
7954
  "responseSchema": {
@@ -7540,14 +7971,86 @@ const operationManifest = [
7540
7971
  "type": "string",
7541
7972
  "description": "Add this value as a TXT record to verify ownership"
7542
7973
  },
7543
- "created_at": {
7544
- "type": "string",
7545
- "format": "date-time"
7546
- }
7547
- },
7548
- "required": [
7549
- "id",
7550
- "org_id",
7974
+ "dns_records": {
7975
+ "type": "array",
7976
+ "description": "Exact DNS records to publish for this pending domain claim.",
7977
+ "items": {
7978
+ "type": "object",
7979
+ "additionalProperties": false,
7980
+ "properties": {
7981
+ "type": {
7982
+ "type": "string",
7983
+ "enum": ["MX", "TXT"],
7984
+ "description": "DNS record type."
7985
+ },
7986
+ "name": {
7987
+ "type": "string",
7988
+ "description": "DNS-provider host/name value relative to the managed root zone."
7989
+ },
7990
+ "fqdn": {
7991
+ "type": "string",
7992
+ "description": "Fully-qualified DNS record name."
7993
+ },
7994
+ "value": {
7995
+ "type": "string",
7996
+ "description": "Exact value to publish."
7997
+ },
7998
+ "priority": {
7999
+ "type": "integer",
8000
+ "description": "MX priority. Present only for MX records."
8001
+ },
8002
+ "ttl": {
8003
+ "type": "integer",
8004
+ "description": "Suggested TTL in seconds when the API can provide one."
8005
+ },
8006
+ "required": {
8007
+ "type": "boolean",
8008
+ "const": true
8009
+ },
8010
+ "purpose": {
8011
+ "type": "string",
8012
+ "enum": [
8013
+ "inbound_mx",
8014
+ "ownership_verification",
8015
+ "spf",
8016
+ "dkim",
8017
+ "dmarc",
8018
+ "tls_reporting"
8019
+ ]
8020
+ },
8021
+ "status": {
8022
+ "type": "string",
8023
+ "enum": [
8024
+ "pending",
8025
+ "found",
8026
+ "missing",
8027
+ "incorrect"
8028
+ ]
8029
+ },
8030
+ "message": {
8031
+ "type": "string",
8032
+ "description": "Short explanation of why this record is needed."
8033
+ }
8034
+ },
8035
+ "required": [
8036
+ "type",
8037
+ "name",
8038
+ "fqdn",
8039
+ "value",
8040
+ "required",
8041
+ "purpose",
8042
+ "status"
8043
+ ]
8044
+ }
8045
+ },
8046
+ "created_at": {
8047
+ "type": "string",
8048
+ "format": "date-time"
8049
+ }
8050
+ },
8051
+ "required": [
8052
+ "id",
8053
+ "org_id",
7551
8054
  "domain",
7552
8055
  "verified",
7553
8056
  "verification_token",
@@ -7583,6 +8086,36 @@ const operationManifest = [
7583
8086
  "tag": "Domains",
7584
8087
  "tagCommand": "domains"
7585
8088
  },
8089
+ {
8090
+ "binaryResponse": true,
8091
+ "bodyRequired": false,
8092
+ "command": "download-domain-zone-file",
8093
+ "description": "Downloads a BIND-format DNS zone file containing the DNS records\nrequired for a domain claim. Agents should offer this after\n`addDomain` when users want to import DNS records instead of\ncopying each record manually.\n",
8094
+ "hasJsonBody": false,
8095
+ "method": "GET",
8096
+ "operationId": "downloadDomainZoneFile",
8097
+ "path": "/domains/{id}/zone-file",
8098
+ "pathParams": [{
8099
+ "description": "Resource UUID",
8100
+ "enum": null,
8101
+ "name": "id",
8102
+ "required": true,
8103
+ "type": "string"
8104
+ }],
8105
+ "queryParams": [{
8106
+ "description": "When true, include only outbound DNS records. Verified domains\ndefault to outbound-only; pending claims default to all required\nrecords.\n",
8107
+ "enum": null,
8108
+ "name": "outbound_only",
8109
+ "required": false,
8110
+ "type": "boolean"
8111
+ }],
8112
+ "requestSchema": null,
8113
+ "responseSchema": null,
8114
+ "sdkName": "downloadDomainZoneFile",
8115
+ "summary": "Download domain DNS zone file",
8116
+ "tag": "Domains",
8117
+ "tagCommand": "domains"
8118
+ },
7586
8119
  {
7587
8120
  "binaryResponse": false,
7588
8121
  "bodyRequired": false,
@@ -7598,7 +8131,7 @@ const operationManifest = [
7598
8131
  "responseSchema": {
7599
8132
  "type": "array",
7600
8133
  "items": {
7601
- "description": "A domain can be either verified or unverified. Verified domains have\n`is_active` and `spam_threshold` fields. Unverified domains have a\n`verification_token` for DNS verification.\n",
8134
+ "description": "A domain can be either verified or unverified. Verified domains have\n`is_active` and `spam_threshold` fields. Unverified domains have a\n`verification_token` and `dns_records` for DNS setup.\n",
7602
8135
  "oneOf": [{
7603
8136
  "type": "object",
7604
8137
  "properties": {
@@ -7655,6 +8188,78 @@ const operationManifest = [
7655
8188
  "type": "string",
7656
8189
  "description": "Add this value as a TXT record to verify ownership"
7657
8190
  },
8191
+ "dns_records": {
8192
+ "type": "array",
8193
+ "description": "Exact DNS records to publish for this pending domain claim.",
8194
+ "items": {
8195
+ "type": "object",
8196
+ "additionalProperties": false,
8197
+ "properties": {
8198
+ "type": {
8199
+ "type": "string",
8200
+ "enum": ["MX", "TXT"],
8201
+ "description": "DNS record type."
8202
+ },
8203
+ "name": {
8204
+ "type": "string",
8205
+ "description": "DNS-provider host/name value relative to the managed root zone."
8206
+ },
8207
+ "fqdn": {
8208
+ "type": "string",
8209
+ "description": "Fully-qualified DNS record name."
8210
+ },
8211
+ "value": {
8212
+ "type": "string",
8213
+ "description": "Exact value to publish."
8214
+ },
8215
+ "priority": {
8216
+ "type": "integer",
8217
+ "description": "MX priority. Present only for MX records."
8218
+ },
8219
+ "ttl": {
8220
+ "type": "integer",
8221
+ "description": "Suggested TTL in seconds when the API can provide one."
8222
+ },
8223
+ "required": {
8224
+ "type": "boolean",
8225
+ "const": true
8226
+ },
8227
+ "purpose": {
8228
+ "type": "string",
8229
+ "enum": [
8230
+ "inbound_mx",
8231
+ "ownership_verification",
8232
+ "spf",
8233
+ "dkim",
8234
+ "dmarc",
8235
+ "tls_reporting"
8236
+ ]
8237
+ },
8238
+ "status": {
8239
+ "type": "string",
8240
+ "enum": [
8241
+ "pending",
8242
+ "found",
8243
+ "missing",
8244
+ "incorrect"
8245
+ ]
8246
+ },
8247
+ "message": {
8248
+ "type": "string",
8249
+ "description": "Short explanation of why this record is needed."
8250
+ }
8251
+ },
8252
+ "required": [
8253
+ "type",
8254
+ "name",
8255
+ "fqdn",
8256
+ "value",
8257
+ "required",
8258
+ "purpose",
8259
+ "status"
8260
+ ]
8261
+ }
8262
+ },
7658
8263
  "created_at": {
7659
8264
  "type": "string",
7660
8265
  "format": "date-time"
@@ -7756,7 +8361,7 @@ const operationManifest = [
7756
8361
  "binaryResponse": false,
7757
8362
  "bodyRequired": false,
7758
8363
  "command": "verify-domain",
7759
- "description": "Checks DNS records (MX and TXT) to verify domain ownership.\nOn success, the domain is promoted from unverified to verified.\nOn failure, returns which checks passed and which failed.\n",
8364
+ "description": "Checks DNS records required for inbound routing, ownership,\nand outbound authentication: MX, ownership TXT, SPF, DKIM,\nDMARC, and TLS-RPT.\nOn success, the domain is promoted from unverified to verified.\nOn failure, returns which checks passed and which failed,\nplus the exact DNS records still expected. To give users\nan importable DNS file for missing records, call\n`downloadDomainZoneFile` or run\n`primitive domains zone-file --id <domain-id>`.\n",
7760
8365
  "hasJsonBody": false,
7761
8366
  "method": "POST",
7762
8367
  "operationId": "verifyDomain",
@@ -7772,10 +8377,84 @@ const operationManifest = [
7772
8377
  "requestSchema": null,
7773
8378
  "responseSchema": { "oneOf": [{
7774
8379
  "type": "object",
7775
- "properties": { "verified": {
7776
- "type": "boolean",
7777
- "const": true
7778
- } },
8380
+ "properties": {
8381
+ "verified": {
8382
+ "type": "boolean",
8383
+ "const": true
8384
+ },
8385
+ "dns_records": {
8386
+ "type": "array",
8387
+ "description": "Exact DNS records checked for this verification attempt.",
8388
+ "items": {
8389
+ "type": "object",
8390
+ "additionalProperties": false,
8391
+ "properties": {
8392
+ "type": {
8393
+ "type": "string",
8394
+ "enum": ["MX", "TXT"],
8395
+ "description": "DNS record type."
8396
+ },
8397
+ "name": {
8398
+ "type": "string",
8399
+ "description": "DNS-provider host/name value relative to the managed root zone."
8400
+ },
8401
+ "fqdn": {
8402
+ "type": "string",
8403
+ "description": "Fully-qualified DNS record name."
8404
+ },
8405
+ "value": {
8406
+ "type": "string",
8407
+ "description": "Exact value to publish."
8408
+ },
8409
+ "priority": {
8410
+ "type": "integer",
8411
+ "description": "MX priority. Present only for MX records."
8412
+ },
8413
+ "ttl": {
8414
+ "type": "integer",
8415
+ "description": "Suggested TTL in seconds when the API can provide one."
8416
+ },
8417
+ "required": {
8418
+ "type": "boolean",
8419
+ "const": true
8420
+ },
8421
+ "purpose": {
8422
+ "type": "string",
8423
+ "enum": [
8424
+ "inbound_mx",
8425
+ "ownership_verification",
8426
+ "spf",
8427
+ "dkim",
8428
+ "dmarc",
8429
+ "tls_reporting"
8430
+ ]
8431
+ },
8432
+ "status": {
8433
+ "type": "string",
8434
+ "enum": [
8435
+ "pending",
8436
+ "found",
8437
+ "missing",
8438
+ "incorrect"
8439
+ ]
8440
+ },
8441
+ "message": {
8442
+ "type": "string",
8443
+ "description": "Short explanation of why this record is needed."
8444
+ }
8445
+ },
8446
+ "required": [
8447
+ "type",
8448
+ "name",
8449
+ "fqdn",
8450
+ "value",
8451
+ "required",
8452
+ "purpose",
8453
+ "status"
8454
+ ]
8455
+ }
8456
+ }
8457
+ },
7779
8458
  "required": ["verified"]
7780
8459
  }, {
7781
8460
  "type": "object",
@@ -7792,6 +8471,94 @@ const operationManifest = [
7792
8471
  "type": "boolean",
7793
8472
  "description": "Whether the TXT verification record was found"
7794
8473
  },
8474
+ "spfFound": {
8475
+ "type": "boolean",
8476
+ "description": "Whether the SPF record includes Primitive."
8477
+ },
8478
+ "dkimFound": {
8479
+ "type": "boolean",
8480
+ "description": "Whether the DKIM public key record was found."
8481
+ },
8482
+ "dmarcFound": {
8483
+ "type": "boolean",
8484
+ "description": "Whether the DMARC record was found."
8485
+ },
8486
+ "tlsRptFound": {
8487
+ "type": "boolean",
8488
+ "description": "Whether the TLS-RPT record was found."
8489
+ },
8490
+ "dns_records": {
8491
+ "type": "array",
8492
+ "description": "Exact DNS records checked for this verification attempt.",
8493
+ "items": {
8494
+ "type": "object",
8495
+ "additionalProperties": false,
8496
+ "properties": {
8497
+ "type": {
8498
+ "type": "string",
8499
+ "enum": ["MX", "TXT"],
8500
+ "description": "DNS record type."
8501
+ },
8502
+ "name": {
8503
+ "type": "string",
8504
+ "description": "DNS-provider host/name value relative to the managed root zone."
8505
+ },
8506
+ "fqdn": {
8507
+ "type": "string",
8508
+ "description": "Fully-qualified DNS record name."
8509
+ },
8510
+ "value": {
8511
+ "type": "string",
8512
+ "description": "Exact value to publish."
8513
+ },
8514
+ "priority": {
8515
+ "type": "integer",
8516
+ "description": "MX priority. Present only for MX records."
8517
+ },
8518
+ "ttl": {
8519
+ "type": "integer",
8520
+ "description": "Suggested TTL in seconds when the API can provide one."
8521
+ },
8522
+ "required": {
8523
+ "type": "boolean",
8524
+ "const": true
8525
+ },
8526
+ "purpose": {
8527
+ "type": "string",
8528
+ "enum": [
8529
+ "inbound_mx",
8530
+ "ownership_verification",
8531
+ "spf",
8532
+ "dkim",
8533
+ "dmarc",
8534
+ "tls_reporting"
8535
+ ]
8536
+ },
8537
+ "status": {
8538
+ "type": "string",
8539
+ "enum": [
8540
+ "pending",
8541
+ "found",
8542
+ "missing",
8543
+ "incorrect"
8544
+ ]
8545
+ },
8546
+ "message": {
8547
+ "type": "string",
8548
+ "description": "Short explanation of why this record is needed."
8549
+ }
8550
+ },
8551
+ "required": [
8552
+ "type",
8553
+ "name",
8554
+ "fqdn",
8555
+ "value",
8556
+ "required",
8557
+ "purpose",
8558
+ "status"
8559
+ ]
8560
+ }
8561
+ },
7795
8562
  "error": {
7796
8563
  "type": "string",
7797
8564
  "description": "Human-readable verification failure reason"
@@ -10299,39 +11066,226 @@ const operationManifest = [
10299
11066
  "pending",
10300
11067
  "deployed",
10301
11068
  "failed"
10302
- ],
10303
- "description": "Lifecycle state of the latest deploy attempt:\n * `pending` — deploy in flight; the runtime has not yet\n confirmed the new bundle is live.\n * `deployed` — the running edge handler is the latest code.\n * `failed` — the most recent deploy attempt failed; the\n previously-live code (if any) is still running. The\n `deploy_error` field carries the error message.\n"
10304
- },
10305
- "deploy_error": {
10306
- "type": ["string", "null"],
10307
- "description": "Error message from the most recent failed deploy, or null\nafter a successful deploy. Surface this to users to explain\na `failed` status without polling.\n"
10308
- },
10309
- "deployed_at": {
10310
- "type": ["string", "null"],
10311
- "format": "date-time"
10312
- },
10313
- "created_at": {
10314
- "type": "string",
10315
- "format": "date-time"
11069
+ ],
11070
+ "description": "Lifecycle state of the latest deploy attempt:\n * `pending` — deploy in flight; the runtime has not yet\n confirmed the new bundle is live.\n * `deployed` — the running edge handler is the latest code.\n * `failed` — the most recent deploy attempt failed; the\n previously-live code (if any) is still running. The\n `deploy_error` field carries the error message.\n"
11071
+ },
11072
+ "deploy_error": {
11073
+ "type": ["string", "null"],
11074
+ "description": "Error message from the most recent failed deploy, or null\nafter a successful deploy. Surface this to users to explain\na `failed` status without polling.\n"
11075
+ },
11076
+ "deployed_at": {
11077
+ "type": ["string", "null"],
11078
+ "format": "date-time"
11079
+ },
11080
+ "created_at": {
11081
+ "type": "string",
11082
+ "format": "date-time"
11083
+ },
11084
+ "updated_at": {
11085
+ "type": "string",
11086
+ "format": "date-time"
11087
+ }
11088
+ },
11089
+ "required": [
11090
+ "id",
11091
+ "name",
11092
+ "code",
11093
+ "deploy_status",
11094
+ "created_at",
11095
+ "updated_at"
11096
+ ]
11097
+ },
11098
+ "sdkName": "updateFunction",
11099
+ "summary": "Update and redeploy a function",
11100
+ "tag": "Functions",
11101
+ "tagCommand": "functions"
11102
+ },
11103
+ {
11104
+ "binaryResponse": false,
11105
+ "bodyRequired": false,
11106
+ "command": "get-inbox-status",
11107
+ "description": "Returns one consolidated view of inbound domain readiness,\nwebhook/function processing routes, deployed Functions, and\nrecent inbound email activity.\n\nAgents should call this before guiding a user through inbound\nsetup. It answers the practical questions \"can I receive mail\",\n\"will anything process that mail\", and \"what should I do next\"\nwithout forcing clients to stitch together domains, endpoints,\nfunctions, and emails manually.\n",
11108
+ "hasJsonBody": false,
11109
+ "method": "GET",
11110
+ "operationId": "getInboxStatus",
11111
+ "path": "/inbox/status",
11112
+ "pathParams": [],
11113
+ "queryParams": [],
11114
+ "requestSchema": null,
11115
+ "responseSchema": {
11116
+ "type": "object",
11117
+ "additionalProperties": false,
11118
+ "properties": {
11119
+ "ready": {
11120
+ "type": "boolean",
11121
+ "description": "True when at least one active inbound domain has an enabled processing route."
11122
+ },
11123
+ "receiving_ready": {
11124
+ "type": "boolean",
11125
+ "description": "True when at least one active verified or managed domain can receive mail."
11126
+ },
11127
+ "processing_ready": {
11128
+ "type": "boolean",
11129
+ "description": "True when at least one receiving-ready domain has an enabled webhook or function route."
11130
+ },
11131
+ "summary": {
11132
+ "type": "string",
11133
+ "description": "Short human-readable status summary."
11134
+ },
11135
+ "next_actions": {
11136
+ "type": "array",
11137
+ "items": {
11138
+ "type": "object",
11139
+ "additionalProperties": false,
11140
+ "properties": {
11141
+ "kind": {
11142
+ "type": "string",
11143
+ "enum": [
11144
+ "add_domain",
11145
+ "verify_domain",
11146
+ "configure_processing",
11147
+ "send_test_email",
11148
+ "fix_failed_functions"
11149
+ ]
11150
+ },
11151
+ "message": {
11152
+ "type": "string",
11153
+ "description": "Human-readable next step."
11154
+ },
11155
+ "command": {
11156
+ "type": "string",
11157
+ "description": "Suggested Primitive CLI command when there is an obvious next step."
11158
+ }
11159
+ },
11160
+ "required": ["kind", "message"]
11161
+ }
11162
+ },
11163
+ "domains": {
11164
+ "type": "array",
11165
+ "items": {
11166
+ "type": "object",
11167
+ "additionalProperties": false,
11168
+ "properties": {
11169
+ "id": { "type": "string" },
11170
+ "domain": { "type": "string" },
11171
+ "verified": { "type": "boolean" },
11172
+ "active": { "type": "boolean" },
11173
+ "managed": { "type": "boolean" },
11174
+ "receiving_ready": { "type": "boolean" },
11175
+ "processing_ready": { "type": "boolean" },
11176
+ "processing_route_count": { "type": "integer" },
11177
+ "endpoint_count": { "type": "integer" },
11178
+ "enabled_endpoint_count": { "type": "integer" },
11179
+ "function_endpoint_count": { "type": "integer" },
11180
+ "email_count": {
11181
+ "type": "integer",
11182
+ "description": "Number of inbound emails received for this domain in the last 30 days."
11183
+ },
11184
+ "latest_email_received_at": {
11185
+ "type": ["string", "null"],
11186
+ "format": "date-time",
11187
+ "description": "Most recent inbound email received for this domain in the last 30 days."
11188
+ },
11189
+ "status": {
11190
+ "type": "string",
11191
+ "enum": [
11192
+ "ready",
11193
+ "stored_only",
11194
+ "pending_dns",
11195
+ "inactive"
11196
+ ]
11197
+ }
11198
+ },
11199
+ "required": [
11200
+ "id",
11201
+ "domain",
11202
+ "verified",
11203
+ "active",
11204
+ "managed",
11205
+ "receiving_ready",
11206
+ "processing_ready",
11207
+ "processing_route_count",
11208
+ "endpoint_count",
11209
+ "enabled_endpoint_count",
11210
+ "function_endpoint_count",
11211
+ "email_count",
11212
+ "latest_email_received_at",
11213
+ "status"
11214
+ ]
11215
+ }
11216
+ },
11217
+ "endpoints": {
11218
+ "type": "object",
11219
+ "additionalProperties": false,
11220
+ "properties": {
11221
+ "total": { "type": "integer" },
11222
+ "enabled": { "type": "integer" },
11223
+ "disabled": { "type": "integer" },
11224
+ "fallback_enabled": { "type": "integer" },
11225
+ "domain_scoped_enabled": { "type": "integer" },
11226
+ "http_enabled": { "type": "integer" },
11227
+ "function_enabled": { "type": "integer" }
11228
+ },
11229
+ "required": [
11230
+ "total",
11231
+ "enabled",
11232
+ "disabled",
11233
+ "fallback_enabled",
11234
+ "domain_scoped_enabled",
11235
+ "http_enabled",
11236
+ "function_enabled"
11237
+ ]
11238
+ },
11239
+ "functions": {
11240
+ "type": "object",
11241
+ "additionalProperties": false,
11242
+ "properties": {
11243
+ "total": { "type": "integer" },
11244
+ "deployed": { "type": "integer" },
11245
+ "pending": { "type": "integer" },
11246
+ "failed": { "type": "integer" }
11247
+ },
11248
+ "required": [
11249
+ "total",
11250
+ "deployed",
11251
+ "pending",
11252
+ "failed"
11253
+ ]
10316
11254
  },
10317
- "updated_at": {
10318
- "type": "string",
10319
- "format": "date-time"
11255
+ "recent_emails": {
11256
+ "type": "object",
11257
+ "description": "Inbound email activity from the last 30 days.",
11258
+ "additionalProperties": false,
11259
+ "properties": {
11260
+ "total": {
11261
+ "type": "integer",
11262
+ "description": "Number of inbound emails received in the last 30 days."
11263
+ },
11264
+ "latest_received_at": {
11265
+ "type": ["string", "null"],
11266
+ "format": "date-time",
11267
+ "description": "Most recent inbound email received in the last 30 days."
11268
+ }
11269
+ },
11270
+ "required": ["total", "latest_received_at"]
10320
11271
  }
10321
11272
  },
10322
11273
  "required": [
10323
- "id",
10324
- "name",
10325
- "code",
10326
- "deploy_status",
10327
- "created_at",
10328
- "updated_at"
11274
+ "ready",
11275
+ "receiving_ready",
11276
+ "processing_ready",
11277
+ "summary",
11278
+ "next_actions",
11279
+ "domains",
11280
+ "endpoints",
11281
+ "functions",
11282
+ "recent_emails"
10329
11283
  ]
10330
11284
  },
10331
- "sdkName": "updateFunction",
10332
- "summary": "Update and redeploy a function",
10333
- "tag": "Functions",
10334
- "tagCommand": "functions"
11285
+ "sdkName": "getInboxStatus",
11286
+ "summary": "Get inbound inbox readiness",
11287
+ "tag": "Inbox",
11288
+ "tagCommand": "inbox"
10335
11289
  },
10336
11290
  {
10337
11291
  "binaryResponse": false,
@@ -11165,6 +12119,36 @@ const operationManifest = [
11165
12119
  "pattern": "^[^\\x00-\\x1F\\x7F]+$"
11166
12120
  }
11167
12121
  },
12122
+ "attachments": {
12123
+ "type": "array",
12124
+ "maxItems": 100,
12125
+ "description": "Inline attachments. Send requests with attachments to https://api.primitive.dev/v1/send-mail. Combined raw decoded attachment bytes must be at most 31457280.",
12126
+ "items": {
12127
+ "type": "object",
12128
+ "additionalProperties": false,
12129
+ "properties": {
12130
+ "filename": {
12131
+ "type": "string",
12132
+ "minLength": 1,
12133
+ "maxLength": 255,
12134
+ "description": "Attachment filename. Control characters are rejected."
12135
+ },
12136
+ "content_type": {
12137
+ "type": "string",
12138
+ "minLength": 1,
12139
+ "maxLength": 255,
12140
+ "description": "Optional MIME content type. Control characters are rejected."
12141
+ },
12142
+ "content_base64": {
12143
+ "type": "string",
12144
+ "minLength": 1,
12145
+ "maxLength": 44040192,
12146
+ "description": "Base64-encoded attachment bytes."
12147
+ }
12148
+ },
12149
+ "required": ["filename", "content_base64"]
12150
+ }
12151
+ },
11168
12152
  "wait": {
11169
12153
  "type": "boolean",
11170
12154
  "description": "When true, wait for the first downstream SMTP delivery outcome before returning."
@@ -12501,6 +13485,11 @@ function operationOutputPayload(envelope, includeEnvelope) {
12501
13485
  return includeEnvelope ? envelope ?? null : envelope?.data ?? null;
12502
13486
  }
12503
13487
  const OPERATION_HINTS = {
13488
+ addDomain: "Tip: after this returns a domain id, run `primitive domains zone-file --id <domain-id> --output <domain>.zone` when the user wants an importable DNS zone file.",
13489
+ verifyDomain: "Tip: if DNS is still missing, run `primitive domains zone-file --id <domain-id> --output <domain>.zone` to give the user an importable DNS zone file.",
13490
+ downloadDomainZoneFile: "Tip: prefer `primitive domains zone-file --id <domain-id> --output <domain>.zone` for CLI-friendly file output.",
13491
+ getInboxStatus: "Tip: prefer `primitive inbox status` for a compact readiness summary and next-step commands.",
13492
+ sendEmail: "Tip: prefer `primitive send --to <address> --body <text> --attachment <file>` for file attachments. This raw command exists for callers passing JSON.",
12504
13493
  createFunction: "Tip: prefer `primitive functions deploy --name <name> --file <bundle>` for file-input ergonomics. This raw command exists for callers passing JSON.",
12505
13494
  updateFunction: "Tip: prefer `primitive functions redeploy --id <id> --file <bundle>` for file-input ergonomics. This raw command exists for callers passing JSON.",
12506
13495
  createFunctionSecret: "Tip: prefer `primitive functions set-secret --id <id> --key <KEY> --value <value> [--redeploy]` for secret writes that also push the binding live. This raw command exists for callers passing JSON.",
@@ -12612,7 +13601,8 @@ const EMPTY_RESULT_HINTS = {
12612
13601
  listEndpoints: "(no results) No webhook endpoints configured. Add one with `primitive endpoints create --url <your-url>`.",
12613
13602
  listEmails: "(no results) No inbound emails received yet on this account. Send one to a verified domain to populate this list. For a compact view, prefer `primitive emails latest`.",
12614
13603
  listDomains: "(no results) No domains on this account. Add one with `primitive domains add --domain <yourdomain.example>`.",
12615
- listFilters: "(no results) No filter rules configured."
13604
+ listFilters: "(no results) No filter rules configured.",
13605
+ listFunctions: "(no results) No Functions configured yet. Start with `primitive functions templates`, then `primitive functions init --template <template>` and `primitive functions deploy --name <name> --file <bundle>`."
12616
13606
  };
12617
13607
  function canonicalizeCliReferences(description) {
12618
13608
  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'`");
@@ -13379,6 +14369,144 @@ var DoctorCommand = class DoctorCommand extends Command {
13379
14369
  }
13380
14370
  };
13381
14371
  //#endregion
14372
+ //#region src/oclif/commands/domains-zone-file.ts
14373
+ function zoneFileUrl(baseUrl, domainId, outboundOnly) {
14374
+ const url = new URL(`${baseUrl.replace(/\/$/, "")}/domains/${encodeURIComponent(domainId)}/zone-file`);
14375
+ if (outboundOnly) url.searchParams.set("outbound_only", "true");
14376
+ return url.toString();
14377
+ }
14378
+ function contentDispositionFilename(value) {
14379
+ if (!value) return null;
14380
+ const match = /filename\*=UTF-8''([^;]+)|filename="?([^";]+)"?/i.exec(value);
14381
+ const raw = match?.[1] ?? match?.[2];
14382
+ if (!raw) return null;
14383
+ try {
14384
+ return decodeURIComponent(raw);
14385
+ } catch {
14386
+ return raw;
14387
+ }
14388
+ }
14389
+ function findDomain(domains, domain) {
14390
+ const match = domains.find((entry) => entry.domain === domain);
14391
+ if (!match) throw new Errors.CLIError(`Domain ${domain} was not found.`, { exit: 1 });
14392
+ return match;
14393
+ }
14394
+ async function responseErrorPayload(response) {
14395
+ if ((response.headers.get("content-type")?.toLowerCase() ?? "").includes("application/json")) return response.json().catch(() => ({
14396
+ code: "http_error",
14397
+ message: `HTTP ${response.status} ${response.statusText}`.trim()
14398
+ }));
14399
+ return {
14400
+ code: "http_error",
14401
+ message: (await response.text().catch(() => "")).trim() || `HTTP ${response.status} ${response.statusText}`.trim()
14402
+ };
14403
+ }
14404
+ var DomainsZoneFileCommand = class DomainsZoneFileCommand extends Command {
14405
+ static description = `Download a BIND-format DNS zone file for a domain claim.
14406
+
14407
+ Use this when a DNS provider supports zone-file import and you want to publish all required records at once instead of copying each record manually. The file is generated by the Primitive API, matching the dashboard download flow.
14408
+
14409
+ Agents: after claiming a domain, tell users they can run \`primitive domains zone-file --id <domain-id> --output <domain>.zone\` to get an importable DNS zone file.`;
14410
+ static summary = "Download a DNS zone file for a domain";
14411
+ static examples = [
14412
+ "<%= config.bin %> domains zone-file --id <domain-id>",
14413
+ "<%= config.bin %> domains zone-file --id <domain-id> --output example.com.zone",
14414
+ "<%= config.bin %> domains zone-file --domain example.com --output example.com.zone",
14415
+ "<%= config.bin %> domains zone-file --id <domain-id> --outbound-only"
14416
+ ];
14417
+ static flags = {
14418
+ "api-key": Flags.string({
14419
+ description: "Primitive API key override (defaults to PRIMITIVE_API_KEY or saved OAuth login credentials)",
14420
+ env: "PRIMITIVE_API_KEY"
14421
+ }),
14422
+ "api-base-url-1": Flags.string({
14423
+ description: "Override the primary API base URL. Internal testing only; not documented to customers.",
14424
+ env: "PRIMITIVE_API_BASE_URL_1",
14425
+ hidden: true
14426
+ }),
14427
+ "api-base-url-2": Flags.string({
14428
+ description: "Override the attachments-supporting send host base URL. Internal testing only; not documented to customers.",
14429
+ env: "PRIMITIVE_API_BASE_URL_2",
14430
+ hidden: true
14431
+ }),
14432
+ domain: Flags.string({ description: "Domain name to look up before downloading its zone file. Prefer --id when you have the domain id from `primitive domains add`." }),
14433
+ id: Flags.string({ description: "Domain id returned by `primitive domains add` or `primitive domains list`." }),
14434
+ output: Flags.string({
14435
+ char: "o",
14436
+ description: "Write the zone file to this path instead of stdout. Defaults to stdout."
14437
+ }),
14438
+ "outbound-only": Flags.boolean({ description: "Include only outbound DNS records. Verified domains default to outbound-only at the API; this flag is explicit for agents and scripts." }),
14439
+ time: Flags.boolean({ description: TIME_FLAG_DESCRIPTION })
14440
+ };
14441
+ async run() {
14442
+ const { flags } = await this.parse(DomainsZoneFileCommand);
14443
+ if (flags.id && flags.domain) throw new Errors.CLIError("Use only one of --id or --domain.", { exit: 1 });
14444
+ if (!flags.id && !flags.domain) throw new Errors.CLIError("Pass --id <domain-id> or --domain <domain>.", { exit: 1 });
14445
+ await runWithTiming(flags.time, async () => {
14446
+ const { apiClient, auth, baseUrlOverridden, requestConfig } = await createAuthenticatedCliApiClient({
14447
+ apiKey: flags["api-key"],
14448
+ apiBaseUrl1: flags["api-base-url-1"],
14449
+ apiBaseUrl2: flags["api-base-url-2"],
14450
+ configDir: this.config.configDir
14451
+ });
14452
+ let domainId = flags.id;
14453
+ if (!domainId && flags.domain) {
14454
+ const result = await listDomains({
14455
+ client: apiClient.client,
14456
+ responseStyle: "fields"
14457
+ });
14458
+ if (result.error) {
14459
+ const errorPayload = extractErrorPayload(result.error);
14460
+ writeErrorWithHints(errorPayload);
14461
+ surfaceUnauthorizedHint({
14462
+ auth,
14463
+ baseUrlOverridden,
14464
+ configDir: this.config.configDir,
14465
+ payload: errorPayload
14466
+ });
14467
+ process.exitCode = 1;
14468
+ return;
14469
+ }
14470
+ const envelope = result.data;
14471
+ domainId = findDomain(envelope?.data ?? [], flags.domain).id;
14472
+ }
14473
+ if (!domainId) throw new Errors.CLIError("Could not resolve a domain id.", { exit: 1 });
14474
+ let response;
14475
+ try {
14476
+ response = await fetch(zoneFileUrl(requestConfig.resolvedApiBaseUrl1, domainId, flags["outbound-only"]), { headers: {
14477
+ ...requestConfig.headers ?? {},
14478
+ ...auth.apiKey ? { authorization: `Bearer ${auth.apiKey}` } : {}
14479
+ } });
14480
+ } catch (error) {
14481
+ writeErrorWithHints(error);
14482
+ process.exitCode = 1;
14483
+ return;
14484
+ }
14485
+ if (!response.ok) {
14486
+ const errorPayload = extractErrorPayload(await responseErrorPayload(response));
14487
+ writeErrorWithHints(errorPayload);
14488
+ surfaceUnauthorizedHint({
14489
+ auth,
14490
+ baseUrlOverridden,
14491
+ configDir: this.config.configDir,
14492
+ payload: errorPayload
14493
+ });
14494
+ process.exitCode = 1;
14495
+ return;
14496
+ }
14497
+ const zoneFile = await response.text();
14498
+ const output = flags.output;
14499
+ if (output) {
14500
+ writeFileSync(output, zoneFile, "utf8");
14501
+ const filename = contentDispositionFilename(response.headers.get("content-disposition"));
14502
+ process.stderr.write(`Wrote ${filename ?? "zone file"} to ${output}\n`);
14503
+ return;
14504
+ }
14505
+ process.stdout.write(zoneFile);
14506
+ });
14507
+ }
14508
+ };
14509
+ //#endregion
13382
14510
  //#region src/oclif/commands/emails-latest.ts
13383
14511
  const DEFAULT_LIMIT = 10;
13384
14512
  const MAX_LIMIT = 100;
@@ -13387,7 +14515,7 @@ const ADDRESS_DISPLAY_WIDTH = 32;
13387
14515
  const ID_DISPLAY_WIDTH_SHORT = 8;
13388
14516
  const ID_DISPLAY_WIDTH_FULL = 36;
13389
14517
  const RECEIVED_DISPLAY_WIDTH = 19;
13390
- function truncate(value, width) {
14518
+ function truncate$1(value, width) {
13391
14519
  if (value.length <= width) return value.padEnd(width);
13392
14520
  return `${value.slice(0, width - 3)}...`;
13393
14521
  }
@@ -13402,7 +14530,7 @@ function pickIdWidth(isTty) {
13402
14530
  return isTty ? ID_DISPLAY_WIDTH_SHORT : ID_DISPLAY_WIDTH_FULL;
13403
14531
  }
13404
14532
  function formatRow(email, idWidth) {
13405
- return `${truncate(email.id.slice(0, idWidth), idWidth)} ${formatReceivedAt(email.received_at)} ${truncate(email.sender ?? "", ADDRESS_DISPLAY_WIDTH)} ${truncate(email.recipient ?? "", ADDRESS_DISPLAY_WIDTH)} ${truncate((email.subject ?? "").replace(/\s+/g, " "), SUBJECT_DISPLAY_WIDTH)}`;
14533
+ return `${truncate$1(email.id.slice(0, idWidth), idWidth)} ${formatReceivedAt(email.received_at)} ${truncate$1(email.sender ?? "", ADDRESS_DISPLAY_WIDTH)} ${truncate$1(email.recipient ?? "", ADDRESS_DISPLAY_WIDTH)} ${truncate$1((email.subject ?? "").replace(/\s+/g, " "), SUBJECT_DISPLAY_WIDTH)}`;
13406
14534
  }
13407
14535
  function formatHeader(idWidth) {
13408
14536
  return `${"ID".padEnd(idWidth)} ${"RECEIVED (UTC)".padEnd(RECEIVED_DISPLAY_WIDTH)} ${"FROM".padEnd(ADDRESS_DISPLAY_WIDTH)} ${"TO".padEnd(ADDRESS_DISPLAY_WIDTH)} SUBJECT`;
@@ -15788,6 +16916,174 @@ var FunctionsTestFunctionCommand = class FunctionsTestFunctionCommand extends Co
15788
16916
  }
15789
16917
  };
15790
16918
  //#endregion
16919
+ //#region src/oclif/commands/inbox-status.ts
16920
+ const DOMAIN_DISPLAY_WIDTH = 34;
16921
+ const STATUS_DISPLAY_WIDTH = 12;
16922
+ const BOOL_DISPLAY_WIDTH = 7;
16923
+ const NUM_DISPLAY_WIDTH = 6;
16924
+ function plural(count, singular, pluralValue = `${singular}s`) {
16925
+ return `${count} ${count === 1 ? singular : pluralValue}`;
16926
+ }
16927
+ function statusText(status) {
16928
+ switch (status) {
16929
+ case "ready": return "ready";
16930
+ case "stored_only": return "stored-only";
16931
+ case "pending_dns": return "pending-dns";
16932
+ case "inactive": return "inactive";
16933
+ default: return String(status);
16934
+ }
16935
+ }
16936
+ function yesNo(value) {
16937
+ return value ? "yes" : "no";
16938
+ }
16939
+ function formatInboxDate(value) {
16940
+ if (!value) return "never";
16941
+ const d = new Date(value);
16942
+ if (Number.isNaN(d.getTime())) return value;
16943
+ const pad = (n) => String(n).padStart(2, "0");
16944
+ return `${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())} ${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}:${pad(d.getUTCSeconds())} UTC`;
16945
+ }
16946
+ function truncate(value, width) {
16947
+ if (value.length <= width) return value.padEnd(width);
16948
+ return `${value.slice(0, width - 3)}...`;
16949
+ }
16950
+ function domainSummary(domain) {
16951
+ switch (domain.status) {
16952
+ case "ready": return `${domain.domain} can receive mail and has ${plural(domain.processing_route_count, "processing route")}.`;
16953
+ case "stored_only": return `${domain.domain} can receive and store mail, but has no enabled processing route.`;
16954
+ case "pending_dns": return `${domain.domain} is waiting on DNS verification before it can receive mail.`;
16955
+ case "inactive": return `${domain.domain} is verified but inactive.`;
16956
+ default: return `${domain.domain} has status ${String(domain.status)}.`;
16957
+ }
16958
+ }
16959
+ function focusInboxStatus(status, domainName) {
16960
+ const normalized = domainName.toLowerCase();
16961
+ const domain = status.domains.find((entry) => entry.domain.toLowerCase() === normalized);
16962
+ if (!domain) throw new Errors.CLIError(`Domain ${domainName} was not found.`, { exit: 1 });
16963
+ return {
16964
+ ...status,
16965
+ domains: [domain],
16966
+ ready: domain.receiving_ready && domain.processing_ready,
16967
+ receiving_ready: domain.receiving_ready,
16968
+ processing_ready: domain.processing_ready,
16969
+ summary: domainSummary(domain),
16970
+ recent_emails: {
16971
+ total: domain.email_count,
16972
+ latest_received_at: domain.latest_email_received_at
16973
+ }
16974
+ };
16975
+ }
16976
+ function formatDomainHeader() {
16977
+ return [
16978
+ "DOMAIN".padEnd(DOMAIN_DISPLAY_WIDTH),
16979
+ "STATUS".padEnd(STATUS_DISPLAY_WIDTH),
16980
+ "RECEIVE".padEnd(BOOL_DISPLAY_WIDTH),
16981
+ "PROCESS".padEnd(BOOL_DISPLAY_WIDTH),
16982
+ "EMAILS".padStart(NUM_DISPLAY_WIDTH),
16983
+ "ROUTES".padStart(NUM_DISPLAY_WIDTH)
16984
+ ].join(" ");
16985
+ }
16986
+ function formatDomainRow(domain) {
16987
+ return [
16988
+ truncate(domain.domain, DOMAIN_DISPLAY_WIDTH),
16989
+ statusText(domain.status).padEnd(STATUS_DISPLAY_WIDTH),
16990
+ yesNo(domain.receiving_ready).padEnd(BOOL_DISPLAY_WIDTH),
16991
+ yesNo(domain.processing_ready).padEnd(BOOL_DISPLAY_WIDTH),
16992
+ String(domain.email_count).padStart(NUM_DISPLAY_WIDTH),
16993
+ String(domain.processing_route_count).padStart(NUM_DISPLAY_WIDTH)
16994
+ ].join(" ");
16995
+ }
16996
+ function formatNextAction(action) {
16997
+ return action.command ? `- ${action.message}\n ${action.command}` : `- ${action.message}`;
16998
+ }
16999
+ function formatInboxStatus(status) {
17000
+ const lines = [
17001
+ status.summary,
17002
+ "",
17003
+ "Domains"
17004
+ ];
17005
+ if (status.domains.length === 0) lines.push("No domains configured.");
17006
+ else {
17007
+ lines.push(formatDomainHeader());
17008
+ for (const domain of status.domains) lines.push(formatDomainRow(domain));
17009
+ }
17010
+ lines.push("", `Endpoints: ${status.endpoints.enabled}/${status.endpoints.total} enabled (${status.endpoints.fallback_enabled} fallback, ${status.endpoints.domain_scoped_enabled} domain-scoped, ${status.endpoints.function_enabled} function)`, `Functions: ${status.functions.deployed}/${status.functions.total} deployed (${status.functions.pending} pending, ${status.functions.failed} failed)`, `Recent inbound: ${plural(status.recent_emails.total, "email")} latest ${formatInboxDate(status.recent_emails.latest_received_at)}`);
17011
+ if (status.next_actions.length > 0) {
17012
+ lines.push("", "Next actions");
17013
+ for (const action of status.next_actions) lines.push(formatNextAction(action));
17014
+ }
17015
+ return lines.join("\n");
17016
+ }
17017
+ var InboxStatusCommand = class InboxStatusCommand extends Command {
17018
+ static description = `Show consolidated inbound email readiness.
17019
+
17020
+ This checks the server-owned inbox status API instead of reconstructing readiness locally from separate domain, endpoint, function, and email lists. Use it before testing inbound email setup: it tells you whether mail can be received, whether anything will process it, and which next command is most useful.`;
17021
+ static summary = "Show inbound email readiness";
17022
+ static examples = [
17023
+ "<%= config.bin %> inbox status",
17024
+ "<%= config.bin %> inbox status --domain example.com",
17025
+ "<%= config.bin %> inbox status --json | jq '.data.next_actions'"
17026
+ ];
17027
+ static flags = {
17028
+ "api-key": Flags.string({
17029
+ description: "Primitive API key override (defaults to PRIMITIVE_API_KEY or saved OAuth login credentials)",
17030
+ env: "PRIMITIVE_API_KEY"
17031
+ }),
17032
+ "api-base-url-1": Flags.string({
17033
+ description: API_BASE_URL_1_FLAG_DESCRIPTION,
17034
+ env: "PRIMITIVE_API_BASE_URL_1",
17035
+ hidden: true
17036
+ }),
17037
+ "api-base-url-2": Flags.string({
17038
+ description: API_BASE_URL_2_FLAG_DESCRIPTION,
17039
+ env: "PRIMITIVE_API_BASE_URL_2",
17040
+ hidden: true
17041
+ }),
17042
+ domain: Flags.string({ description: "Focus domain readiness and recent email fields on one domain returned by the inbox status API." }),
17043
+ json: Flags.boolean({ description: "Print the raw response envelope as JSON. With --domain, domain readiness and recent email fields are focused while endpoint, function, and next-action summaries remain account-level." }),
17044
+ time: Flags.boolean({ description: TIME_FLAG_DESCRIPTION })
17045
+ };
17046
+ async run() {
17047
+ const { flags } = await this.parse(InboxStatusCommand);
17048
+ await runWithTiming(flags.time, async () => {
17049
+ const { apiClient, auth, baseUrlOverridden } = await createAuthenticatedCliApiClient({
17050
+ apiKey: flags["api-key"],
17051
+ apiBaseUrl1: flags["api-base-url-1"],
17052
+ apiBaseUrl2: flags["api-base-url-2"],
17053
+ configDir: this.config.configDir
17054
+ });
17055
+ const result = await getInboxStatus({
17056
+ client: apiClient.client,
17057
+ responseStyle: "fields"
17058
+ });
17059
+ if (result.error) {
17060
+ const errorPayload = extractErrorPayload(result.error);
17061
+ writeErrorWithHints(errorPayload);
17062
+ surfaceUnauthorizedHint({
17063
+ auth,
17064
+ baseUrlOverridden,
17065
+ configDir: this.config.configDir,
17066
+ payload: errorPayload
17067
+ });
17068
+ process.exitCode = 1;
17069
+ return;
17070
+ }
17071
+ const envelope = result.data ?? {};
17072
+ const status = envelope.data;
17073
+ if (!status) throw new Errors.CLIError("Primitive API returned no inbox status.", { exit: 1 });
17074
+ const outputStatus = flags.domain ? focusInboxStatus(status, flags.domain) : status;
17075
+ if (flags.json) {
17076
+ this.log(JSON.stringify({
17077
+ ...envelope,
17078
+ data: outputStatus
17079
+ }, null, 2));
17080
+ return;
17081
+ }
17082
+ this.log(formatInboxStatus(outputStatus));
17083
+ });
17084
+ }
17085
+ };
17086
+ //#endregion
15791
17087
  //#region src/oclif/commands/login.ts
15792
17088
  const MAX_CLI_LOGIN_POLL_INTERVAL_SECONDS = 60;
15793
17089
  function cliError$3(message) {
@@ -16291,12 +17587,46 @@ var ReplyCommand = class ReplyCommand extends Command {
16291
17587
  }
16292
17588
  };
16293
17589
  //#endregion
17590
+ //#region src/oclif/attachments.ts
17591
+ function readAttachmentBytes(path, readFile) {
17592
+ try {
17593
+ return Buffer.from(readFile(path));
17594
+ } catch (error) {
17595
+ const detail = error instanceof Error ? error.message : String(error);
17596
+ throw new Errors.CLIError(`Could not read --attachment ${path}: ${detail}`, { exit: 1 });
17597
+ }
17598
+ }
17599
+ function hasControlCharacter(value) {
17600
+ return Array.from(value).some((character) => {
17601
+ const code = character.charCodeAt(0);
17602
+ return code <= 31 || code >= 127 && code <= 159;
17603
+ });
17604
+ }
17605
+ function validateAttachmentFilename(path, filename) {
17606
+ if (!filename) throw new Errors.CLIError(`Could not derive an attachment filename from ${path}. Pass a file path.`, { exit: 1 });
17607
+ if (hasControlCharacter(filename)) throw new Errors.CLIError(`Attachment filename ${filename} contains control characters.`, { exit: 1 });
17608
+ }
17609
+ function readAttachmentFiles(paths, readFile = readFileSync) {
17610
+ if (!paths || paths.length === 0) return void 0;
17611
+ return paths.map((path) => {
17612
+ const filename = basename(path);
17613
+ validateAttachmentFilename(path, filename);
17614
+ const bytes = readAttachmentBytes(path, readFile);
17615
+ if (bytes.length === 0) throw new Errors.CLIError(`Attachment file ${path} is empty. Attachments must contain at least one byte.`, { exit: 1 });
17616
+ return {
17617
+ content_base64: bytes.toString("base64"),
17618
+ filename
17619
+ };
17620
+ });
17621
+ }
17622
+ //#endregion
16294
17623
  //#region src/oclif/commands/send.ts
16295
17624
  var SendCommand = class SendCommand extends Command {
16296
17625
  static description = `Send an outbound email. Agent-grade shortcut for \`sending send\` with sensible defaults.
16297
17626
 
16298
17627
  --from defaults to agent@<your-first-verified-outbound-domain> when omitted.
16299
17628
  --subject defaults to the first line of the body when omitted.
17629
+ --attachment attaches a file; repeat it to attach multiple files.
16300
17630
 
16301
17631
  For the full flag set (custom message-id threading on the wire,
16302
17632
  references arrays, etc.), use \`primitive sending send\`.`;
@@ -16304,6 +17634,7 @@ var SendCommand = class SendCommand extends Command {
16304
17634
  static examples = [
16305
17635
  "<%= config.bin %> send --to alice@example.com --body 'Hi Alice!'",
16306
17636
  "<%= config.bin %> send --to alice@example.com --body-file ./message.txt",
17637
+ "<%= config.bin %> send --to alice@example.com --body 'See attached.' --attachment ./report.pdf",
16307
17638
  "<%= config.bin %> send --to alice@example.com --from support@yourcompany.com --subject 'Quick question' --body 'Are you free Thursday?'",
16308
17639
  "<%= config.bin %> send --to alice@example.com --html '<p>Hello!</p>'",
16309
17640
  "<%= config.bin %> send --to alice@example.com --body 'Confirmed' --wait",
@@ -16331,11 +17662,15 @@ var SendCommand = class SendCommand extends Command {
16331
17662
  from: Flags.string({ description: "Sender address. Defaults to agent@<your-first-verified-outbound-domain>." }),
16332
17663
  subject: Flags.string({ description: "Subject line. Defaults to the first line of --body / --html when omitted." }),
16333
17664
  body: Flags.string({ description: "Plain-text message body. Either --body or --html (or both) is required." }),
16334
- "body-file": Flags.string({ description: "Read the plain-text message body from a UTF-8 file. Mutually exclusive with --body and --body-stdin." }),
17665
+ "body-file": Flags.string({ description: "Read the plain-text message body from a UTF-8 file. This does not attach the file; use --attachment for file attachments. Mutually exclusive with --body and --body-stdin." }),
16335
17666
  "body-stdin": Flags.boolean({ description: "Read the plain-text message body from stdin. Mutually exclusive with --body and --body-file. Stdin can only be consumed once." }),
16336
17667
  html: Flags.string({ description: "HTML message body. Either --body or --html (or both) is required." }),
16337
17668
  "html-file": Flags.string({ description: "Read the HTML message body from a UTF-8 file. Mutually exclusive with --html and --html-stdin." }),
16338
17669
  "html-stdin": Flags.boolean({ description: "Read the HTML message body from stdin. Mutually exclusive with --html and --html-file. Stdin can only be consumed once." }),
17670
+ attachment: Flags.string({
17671
+ description: "Attach a file to the email. Repeatable. Sends file bytes as a MIME attachment; use --body-file only for message body text.",
17672
+ multiple: true
17673
+ }),
16339
17674
  "in-reply-to": Flags.string({ description: "Message-Id of the parent email when threading a reply on the wire. For replying to an inbound message you received, prefer `primitive reply --id <inbound-id>`." }),
16340
17675
  wait: Flags.boolean({ description: "Block until the receiving MTA returns an outcome. Without --wait, the call returns once Primitive has accepted the message for delivery." }),
16341
17676
  "wait-timeout-ms": Flags.integer({ description: "Maximum time to wait when --wait is set. Defaults to 30000ms." }),
@@ -16352,6 +17687,7 @@ var SendCommand = class SendCommand extends Command {
16352
17687
  htmlStdin: flags["html-stdin"]
16353
17688
  });
16354
17689
  if (bodies.kind === "error") throw new Errors.CLIError(bodies.message);
17690
+ const attachments = readAttachmentFiles(flags.attachment);
16355
17691
  await runWithTiming(flags.time, async () => {
16356
17692
  const { apiClient, auth, baseUrlOverridden } = await createAuthenticatedCliApiClient({
16357
17693
  apiKey: flags["api-key"],
@@ -16373,6 +17709,7 @@ var SendCommand = class SendCommand extends Command {
16373
17709
  subject,
16374
17710
  ...bodies.body !== void 0 ? { body_text: bodies.body } : {},
16375
17711
  ...bodies.html !== void 0 ? { body_html: bodies.html } : {},
17712
+ ...attachments !== void 0 ? { attachments } : {},
16376
17713
  ...flags["in-reply-to"] !== void 0 ? { in_reply_to: flags["in-reply-to"] } : {},
16377
17714
  ...flags.wait !== void 0 ? { wait: flags.wait } : {},
16378
17715
  ...flags["wait-timeout-ms"] !== void 0 ? { wait_timeout_ms: flags["wait-timeout-ms"] } : {}
@@ -17153,25 +18490,39 @@ var SigninOtpResendCommand = class SigninOtpResendCommand extends Command {
17153
18490
  };
17154
18491
  //#endregion
17155
18492
  //#region src/oclif/commands/whoami.ts
18493
+ function formatWhoamiSummary(account) {
18494
+ return [
18495
+ `Authenticated as ${account.email}`,
18496
+ `Account id: ${account.id}`,
18497
+ `Plan: ${account.plan}`
18498
+ ].join("\n");
18499
+ }
17156
18500
  var WhoamiCommand = class WhoamiCommand extends Command {
17157
- static description = `Print the account currently authenticated by saved OAuth credentials or an explicit API key. Useful as a credentials smoke test: confirms auth is live and shows which account it belongs to.`;
18501
+ static description = `Print the account currently authenticated by saved OAuth credentials or an explicit API key. Useful as a credentials smoke test: confirms auth is live and shows which account it belongs to.
18502
+
18503
+ The default output is a concise human summary. Pass --json only when a script intentionally needs the full /account response.`;
17158
18504
  static summary = "Print the authenticated account (credentials smoke test)";
17159
- static examples = ["<%= config.bin %> whoami", "<%= config.bin %> whoami --api-key prim_..."];
18505
+ static examples = [
18506
+ "<%= config.bin %> whoami",
18507
+ "<%= config.bin %> whoami --api-key prim_...",
18508
+ "<%= config.bin %> whoami --json | jq .id"
18509
+ ];
17160
18510
  static flags = {
17161
18511
  "api-key": Flags.string({
17162
18512
  description: "Primitive API key override (defaults to PRIMITIVE_API_KEY or saved OAuth login credentials)",
17163
18513
  env: "PRIMITIVE_API_KEY"
17164
18514
  }),
17165
18515
  "api-base-url-1": Flags.string({
17166
- description: "Override the primary API base URL. Internal testing only; not documented to customers.",
18516
+ description: API_BASE_URL_1_FLAG_DESCRIPTION,
17167
18517
  env: "PRIMITIVE_API_BASE_URL_1",
17168
18518
  hidden: true
17169
18519
  }),
17170
18520
  "api-base-url-2": Flags.string({
17171
- description: "Override the attachments-supporting send host base URL. Internal testing only; not documented to customers.",
18521
+ description: API_BASE_URL_2_FLAG_DESCRIPTION,
17172
18522
  env: "PRIMITIVE_API_BASE_URL_2",
17173
18523
  hidden: true
17174
18524
  }),
18525
+ json: Flags.boolean({ description: "Print the full account JSON response. Default output hides setup and billing internals." }),
17175
18526
  time: Flags.boolean({ description: TIME_FLAG_DESCRIPTION })
17176
18527
  };
17177
18528
  async run() {
@@ -17204,12 +18555,11 @@ var WhoamiCommand = class WhoamiCommand extends Command {
17204
18555
  process.stderr.write("Server returned an empty account body; this should not happen for a valid key.\n");
17205
18556
  throw new Errors.CLIError("unexpected empty response");
17206
18557
  }
17207
- const onboarding = account.onboarding_completed === true ? "complete" : account.onboarding_step ? `in progress (step: ${account.onboarding_step})` : "incomplete";
17208
- process.stderr.write(`Authenticated as ${account.email}\n`);
17209
- process.stderr.write(` Account id: ${account.id}\n`);
17210
- process.stderr.write(` Plan: ${account.plan}\n`);
17211
- process.stderr.write(` Onboarding: ${onboarding}\n`);
17212
- this.log(JSON.stringify(account, null, 2));
18558
+ if (flags.json) {
18559
+ this.log(JSON.stringify(account, null, 2));
18560
+ return;
18561
+ }
18562
+ this.log(formatWhoamiSummary(account));
17213
18563
  });
17214
18564
  }
17215
18565
  };
@@ -17283,36 +18633,88 @@ function renderFishCompletion(binName) {
17283
18633
  //#endregion
17284
18634
  //#region src/oclif/index.ts
17285
18635
  var ListOperationsCommand = class extends Command {
17286
- static description = "List all generated API operations as JSON. Useful for piping to `jq` to discover available commands, their request/response schemas, and per-field descriptions. For inspecting a single operation in detail, prefer `primitive describe <command>`.";
18636
+ static description = "List all generated API operations as JSON. Useful for piping to `jq` to discover available commands, their request/response schemas, and per-field descriptions. For inspecting a single operation in detail, prefer `primitive describe <command-or-operation-name>`.";
17287
18637
  static summary = "List all generated API operations (JSON)";
17288
18638
  async run() {
17289
18639
  this.log(JSON.stringify(operationManifest, null, 2));
17290
18640
  }
17291
18641
  };
18642
+ function operationId(operation) {
18643
+ return `${operation.tagCommand}:${operation.command}`;
18644
+ }
18645
+ function normalizeLookupToken(value) {
18646
+ return value.toLowerCase().replace(/[^a-z0-9]/g, "");
18647
+ }
18648
+ function unique(values) {
18649
+ return [...new Set(values)];
18650
+ }
18651
+ function operationLookupTokens(operation) {
18652
+ return unique([
18653
+ operationId(operation),
18654
+ operation.command,
18655
+ operation.operationId,
18656
+ operation.sdkName,
18657
+ `${operation.tagCommand}:${operation.operationId}`,
18658
+ `${operation.tagCommand}:${operation.sdkName}`
18659
+ ]);
18660
+ }
18661
+ function levenshteinDistance(left, right) {
18662
+ if (left === right) return 0;
18663
+ if (left.length === 0) return right.length;
18664
+ if (right.length === 0) return left.length;
18665
+ let previous = Array.from({ length: right.length + 1 }, (_, index) => index);
18666
+ for (let leftIndex = 0; leftIndex < left.length; leftIndex += 1) {
18667
+ const current = [leftIndex + 1];
18668
+ for (let rightIndex = 0; rightIndex < right.length; rightIndex += 1) {
18669
+ const substitutionCost = left[leftIndex] === right[rightIndex] ? 0 : 1;
18670
+ current[rightIndex + 1] = Math.min(current[rightIndex] + 1, previous[rightIndex + 1] + 1, previous[rightIndex] + substitutionCost);
18671
+ }
18672
+ previous = current;
18673
+ }
18674
+ return previous[right.length] ?? Number.POSITIVE_INFINITY;
18675
+ }
18676
+ function scoreLookupToken(query, token) {
18677
+ const normalizedQuery = normalizeLookupToken(query);
18678
+ const normalizedToken = normalizeLookupToken(token);
18679
+ if (!normalizedQuery || !normalizedToken) return 0;
18680
+ if (normalizedQuery === normalizedToken) return 100;
18681
+ if (normalizedToken.includes(normalizedQuery)) return Math.max(50, 90 - (normalizedToken.length - normalizedQuery.length));
18682
+ if (normalizedQuery.includes(normalizedToken)) return Math.max(45, 80 - (normalizedQuery.length - normalizedToken.length));
18683
+ const distance = levenshteinDistance(normalizedQuery, normalizedToken);
18684
+ const maxLength = Math.max(normalizedQuery.length, normalizedToken.length);
18685
+ return Math.round((1 - distance / maxLength) * 75);
18686
+ }
18687
+ function scoreOperation(query, operation) {
18688
+ return Math.max(...operationLookupTokens(operation).map((token) => scoreLookupToken(query, token)));
18689
+ }
17292
18690
  function lookupOperation(id) {
17293
18691
  const trimmed = resolveOperationAlias(id.trim());
17294
- const sep = trimmed.indexOf(":");
17295
- const tag = sep === -1 ? "" : trimmed.slice(0, sep);
17296
- const cmd = sep === -1 ? trimmed : trimmed.slice(sep + 1);
17297
- const match = operationManifest.find((op) => op.command === cmd && op.tagCommand === tag) ?? null;
18692
+ const match = operationManifest.find((op) => operationLookupTokens(op).some((token) => token === trimmed || normalizeLookupToken(token) === normalizeLookupToken(trimmed))) ?? null;
17298
18693
  if (match) return {
17299
18694
  match,
17300
18695
  candidates: []
17301
18696
  };
17302
18697
  return {
17303
18698
  match: null,
17304
- candidates: operationManifest.filter((op) => op.command.includes(cmd) || op.tagCommand.includes(tag)).slice(0, 5).map((op) => op.tagCommand ? `${op.tagCommand}:${op.command}` : op.command)
18699
+ candidates: operationManifest.map((op) => ({
18700
+ id: operationId(op),
18701
+ score: scoreOperation(trimmed, op)
18702
+ })).filter(({ score }) => score >= 45).sort((left, right) => right.score - left.score || left.id.localeCompare(right.id)).slice(0, 5).map(({ id }) => id)
17305
18703
  };
17306
18704
  }
17307
18705
  var DescribeCommand = class DescribeCommand extends Command {
17308
18706
  static args = { command: Args.string({
17309
- description: "Command id to describe, e.g. `emails:list` or `emails:get-email`. Run `primitive list-operations | jq -r '.[] | \"\\(.tagCommand):\\(.command)\"'` to enumerate generated operation ids.",
18707
+ description: "Command id, alias, or SDK operation name to describe, e.g. `emails:list`, `emails:get-email`, or `getEmail`. Run `primitive list-operations | jq -r '.[] | \"\\(.tagCommand):\\(.command) \\(.operationId)\"'` to enumerate generated operation ids.",
17310
18708
  required: true
17311
18709
  }) };
17312
18710
  static description = `Print the full operation manifest entry for a single API command, including the path, request schema, response schema, and per-field descriptions sourced from the OpenAPI spec.
17313
18711
 
17314
18712
  The manifest entry's \`responseSchema\` carries the inlined JSON Schema for the operation's 200/201 \`data\` envelope contents (\`$ref\`s resolved). Use it to look up what specific response fields mean. Examples:
17315
18713
 
18714
+ # Domain setup records returned by add/verify
18715
+ primitive describe domains:add
18716
+ primitive describe addDomain
18717
+
17316
18718
  # Which of EmailDetail's sender-shaped fields is canonical?
17317
18719
  primitive describe emails:get | jq '.responseSchema.properties | keys'
17318
18720
  primitive describe emails:get | jq -r '.responseSchema.properties.from_email.description'
@@ -17322,7 +18724,12 @@ var DescribeCommand = class DescribeCommand extends Command {
17322
18724
 
17323
18725
  \`requestSchema\` is the same shape for the request body when one exists. For a single field across many operations at once, use \`primitive list-operations | jq\` instead.`;
17324
18726
  static summary = "Describe a single API operation in detail";
17325
- static examples = ["<%= config.bin %> describe emails:get", "<%= config.bin %> describe sent:get"];
18727
+ static examples = [
18728
+ "<%= config.bin %> describe addDomain",
18729
+ "<%= config.bin %> describe domains:add",
18730
+ "<%= config.bin %> describe emails:get",
18731
+ "<%= config.bin %> describe sent:get"
18732
+ ];
17326
18733
  async run() {
17327
18734
  const { args } = await this.parse(DescribeCommand);
17328
18735
  const { match, candidates } = lookupOperation(args.command);
@@ -17355,9 +18762,6 @@ var CompletionCommand = class CompletionCommand extends Command {
17355
18762
  await this.config.runCommand("autocomplete", [args.shell]);
17356
18763
  }
17357
18764
  };
17358
- function commandId(operation) {
17359
- return `${operation.tagCommand}:${operation.command}`;
17360
- }
17361
18765
  const CANONICAL_OPERATION_ALIASES = {
17362
18766
  "account:show": "account:get-account",
17363
18767
  "account:storage": "account:get-storage-stats",
@@ -17402,14 +18806,19 @@ const CANONICAL_OPERATION_ALIASES = {
17402
18806
  };
17403
18807
  const DESCRIBE_OPERATION_ALIASES = {
17404
18808
  ...CANONICAL_OPERATION_ALIASES,
18809
+ "domains:zone-file": "domains:download-domain-zone-file",
17405
18810
  "functions:logs": "functions:list-function-logs",
17406
18811
  reply: "sending:reply-to-email"
17407
18812
  };
17408
18813
  function resolveOperationAlias(id) {
17409
18814
  return DESCRIBE_OPERATION_ALIASES[id] ?? id;
17410
18815
  }
17411
- const OVERRIDDEN_OPERATION_IDS = new Set(["functions:test-function"]);
17412
- const generatedCommands = Object.fromEntries(operationManifest.filter((operation) => !OVERRIDDEN_OPERATION_IDS.has(commandId(operation))).map((operation) => [commandId(operation), createOperationCommand(operation)]));
18816
+ const OVERRIDDEN_OPERATION_IDS = new Set([
18817
+ "domains:download-domain-zone-file",
18818
+ "functions:test-function",
18819
+ "inbox:get-inbox-status"
18820
+ ]);
18821
+ const generatedCommands = Object.fromEntries(operationManifest.filter((operation) => !OVERRIDDEN_OPERATION_IDS.has(operationId(operation))).map((operation) => [operationId(operation), createOperationCommand(operation)]));
17413
18822
  const COMMANDS = {
17414
18823
  completion: CompletionCommand,
17415
18824
  "list-operations": ListOperationsCommand,
@@ -17438,6 +18847,10 @@ const COMMANDS = {
17438
18847
  "emails:latest": EmailsLatestCommand,
17439
18848
  "emails:watch": EmailsWatchCommand,
17440
18849
  "emails:wait": EmailsWaitCommand,
18850
+ "domains:zone-file": DomainsZoneFileCommand,
18851
+ "domains:download-domain-zone-file": DomainsZoneFileCommand,
18852
+ "inbox:status": InboxStatusCommand,
18853
+ "inbox:get-inbox-status": InboxStatusCommand,
17441
18854
  "functions:init": FunctionsInitCommand,
17442
18855
  "functions:templates": FunctionsTemplatesCommand,
17443
18856
  "functions:deploy": FunctionsDeployCommand,