@primitivedotdev/cli 0.31.3 → 0.31.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/oclif/index.js +866 -47
  2. package/package.json +2 -2
@@ -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,6 +645,7 @@ 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,
@@ -941,9 +942,11 @@ const listDomains = (options) => (options?.client ?? client).get({
941
942
  /**
942
943
  * Claim a new domain
943
944
  *
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.
945
+ * Creates an unverified domain claim and returns the exact
946
+ * DNS records to publish in `dns_records`. Publish those
947
+ * records before calling the verify endpoint. To give users
948
+ * an importable DNS file, call `downloadDomainZoneFile` or run
949
+ * `primitive domains zone-file --id <domain-id>`.
947
950
  *
948
951
  */
949
952
  const addDomain = (options) => (options.client ?? client).post({
@@ -993,9 +996,15 @@ const updateDomain = (options) => (options.client ?? client).patch({
993
996
  /**
994
997
  * Verify domain ownership
995
998
  *
996
- * Checks DNS records (MX and TXT) to verify domain ownership.
999
+ * Checks DNS records required for inbound routing, ownership,
1000
+ * and outbound authentication: MX, ownership TXT, SPF, DKIM,
1001
+ * DMARC, and TLS-RPT.
997
1002
  * On success, the domain is promoted from unverified to verified.
998
- * On failure, returns which checks passed and which failed.
1003
+ * On failure, returns which checks passed and which failed,
1004
+ * plus the exact DNS records still expected. To give users
1005
+ * an importable DNS file for missing records, call
1006
+ * `downloadDomainZoneFile` or run
1007
+ * `primitive domains zone-file --id <domain-id>`.
999
1008
  *
1000
1009
  */
1001
1010
  const verifyDomain = (options) => (options.client ?? client).post({
@@ -1007,6 +1016,23 @@ const verifyDomain = (options) => (options.client ?? client).post({
1007
1016
  ...options
1008
1017
  });
1009
1018
  /**
1019
+ * Download domain DNS zone file
1020
+ *
1021
+ * Downloads a BIND-format DNS zone file containing the DNS records
1022
+ * required for a domain claim. Agents should offer this after
1023
+ * `addDomain` when users want to import DNS records instead of
1024
+ * copying each record manually.
1025
+ *
1026
+ */
1027
+ const downloadDomainZoneFile = (options) => (options.client ?? client).get({
1028
+ security: [{
1029
+ scheme: "bearer",
1030
+ type: "http"
1031
+ }],
1032
+ url: "/domains/{id}/zone-file",
1033
+ ...options
1034
+ });
1035
+ /**
1010
1036
  * List inbound emails
1011
1037
  *
1012
1038
  * Returns a paginated list of INBOUND emails received at your
@@ -2333,7 +2359,7 @@ const openapiDocument = {
2333
2359
  "post": {
2334
2360
  "operationId": "addDomain",
2335
2361
  "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",
2362
+ "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
2363
  "tags": ["Domains"],
2338
2364
  "requestBody": {
2339
2365
  "required": true,
@@ -2426,7 +2452,7 @@ const openapiDocument = {
2426
2452
  "post": {
2427
2453
  "operationId": "verifyDomain",
2428
2454
  "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",
2455
+ "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
2456
  "tags": ["Domains"],
2431
2457
  "responses": {
2432
2458
  "200": {
@@ -2442,6 +2468,38 @@ const openapiDocument = {
2442
2468
  }
2443
2469
  }
2444
2470
  },
2471
+ "/domains/{id}/zone-file": {
2472
+ "parameters": [{ "$ref": "#/components/parameters/ResourceId" }],
2473
+ "get": {
2474
+ "operationId": "downloadDomainZoneFile",
2475
+ "summary": "Download domain DNS zone file",
2476
+ "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",
2477
+ "tags": ["Domains"],
2478
+ "parameters": [{
2479
+ "name": "outbound_only",
2480
+ "in": "query",
2481
+ "schema": { "type": "boolean" },
2482
+ "description": "When true, include only outbound DNS records. Verified domains\ndefault to outbound-only; pending claims default to all required\nrecords.\n"
2483
+ }],
2484
+ "responses": {
2485
+ "200": {
2486
+ "description": "BIND-format zone file",
2487
+ "content": { "text/plain": { "schema": {
2488
+ "type": "string",
2489
+ "format": "binary"
2490
+ } } },
2491
+ "headers": { "Content-Disposition": { "schema": {
2492
+ "type": "string",
2493
+ "example": "attachment; filename=\"example.com.zone\""
2494
+ } } }
2495
+ },
2496
+ "400": { "$ref": "#/components/responses/ValidationError" },
2497
+ "401": { "$ref": "#/components/responses/Unauthorized" },
2498
+ "404": { "$ref": "#/components/responses/NotFound" },
2499
+ "429": { "$ref": "#/components/responses/RateLimited" }
2500
+ }
2501
+ }
2502
+ },
2445
2503
  "/emails": { "get": {
2446
2504
  "operationId": "listEmails",
2447
2505
  "summary": "List inbound emails",
@@ -4731,7 +4789,7 @@ const openapiDocument = {
4731
4789
  "required": ["secret"]
4732
4790
  },
4733
4791
  "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",
4792
+ "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
4793
  "oneOf": [{ "$ref": "#/components/schemas/VerifiedDomain" }, { "$ref": "#/components/schemas/UnverifiedDomain" }]
4736
4794
  },
4737
4795
  "VerifiedDomain": {
@@ -4771,6 +4829,74 @@ const openapiDocument = {
4771
4829
  "created_at"
4772
4830
  ]
4773
4831
  },
4832
+ "DomainDnsRecord": {
4833
+ "type": "object",
4834
+ "additionalProperties": false,
4835
+ "properties": {
4836
+ "type": {
4837
+ "type": "string",
4838
+ "enum": ["MX", "TXT"],
4839
+ "description": "DNS record type."
4840
+ },
4841
+ "name": {
4842
+ "type": "string",
4843
+ "description": "DNS-provider host/name value relative to the managed root zone."
4844
+ },
4845
+ "fqdn": {
4846
+ "type": "string",
4847
+ "description": "Fully-qualified DNS record name."
4848
+ },
4849
+ "value": {
4850
+ "type": "string",
4851
+ "description": "Exact value to publish."
4852
+ },
4853
+ "priority": {
4854
+ "type": "integer",
4855
+ "description": "MX priority. Present only for MX records."
4856
+ },
4857
+ "ttl": {
4858
+ "type": "integer",
4859
+ "description": "Suggested TTL in seconds when the API can provide one."
4860
+ },
4861
+ "required": {
4862
+ "type": "boolean",
4863
+ "const": true
4864
+ },
4865
+ "purpose": {
4866
+ "type": "string",
4867
+ "enum": [
4868
+ "inbound_mx",
4869
+ "ownership_verification",
4870
+ "spf",
4871
+ "dkim",
4872
+ "dmarc",
4873
+ "tls_reporting"
4874
+ ]
4875
+ },
4876
+ "status": {
4877
+ "type": "string",
4878
+ "enum": [
4879
+ "pending",
4880
+ "found",
4881
+ "missing",
4882
+ "incorrect"
4883
+ ]
4884
+ },
4885
+ "message": {
4886
+ "type": "string",
4887
+ "description": "Short explanation of why this record is needed."
4888
+ }
4889
+ },
4890
+ "required": [
4891
+ "type",
4892
+ "name",
4893
+ "fqdn",
4894
+ "value",
4895
+ "required",
4896
+ "purpose",
4897
+ "status"
4898
+ ]
4899
+ },
4774
4900
  "UnverifiedDomain": {
4775
4901
  "type": "object",
4776
4902
  "properties": {
@@ -4791,6 +4917,11 @@ const openapiDocument = {
4791
4917
  "type": "string",
4792
4918
  "description": "Add this value as a TXT record to verify ownership"
4793
4919
  },
4920
+ "dns_records": {
4921
+ "type": "array",
4922
+ "description": "Exact DNS records to publish for this pending domain claim.",
4923
+ "items": { "$ref": "#/components/schemas/DomainDnsRecord" }
4924
+ },
4794
4925
  "created_at": {
4795
4926
  "type": "string",
4796
4927
  "format": "date-time"
@@ -4808,12 +4939,23 @@ const openapiDocument = {
4808
4939
  "AddDomainInput": {
4809
4940
  "type": "object",
4810
4941
  "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
- } },
4942
+ "properties": {
4943
+ "domain": {
4944
+ "type": "string",
4945
+ "minLength": 1,
4946
+ "maxLength": 253,
4947
+ "description": "The domain name to claim (e.g. \"example.com\")"
4948
+ },
4949
+ "confirmed": {
4950
+ "type": "boolean",
4951
+ "description": "Set to true to confirm replacing an existing mailbox provider after an mx_conflict response."
4952
+ },
4953
+ "outbound": {
4954
+ "type": "boolean",
4955
+ "deprecated": true,
4956
+ "description": "Deprecated and ignored. Outbound DNS is provisioned for every new domain claim."
4957
+ }
4958
+ },
4817
4959
  "required": ["domain"]
4818
4960
  },
4819
4961
  "UpdateDomainInput": {
@@ -4835,10 +4977,17 @@ const openapiDocument = {
4835
4977
  },
4836
4978
  "DomainVerifyResult": { "oneOf": [{
4837
4979
  "type": "object",
4838
- "properties": { "verified": {
4839
- "type": "boolean",
4840
- "const": true
4841
- } },
4980
+ "properties": {
4981
+ "verified": {
4982
+ "type": "boolean",
4983
+ "const": true
4984
+ },
4985
+ "dns_records": {
4986
+ "type": "array",
4987
+ "description": "Exact DNS records checked for this verification attempt.",
4988
+ "items": { "$ref": "#/components/schemas/DomainDnsRecord" }
4989
+ }
4990
+ },
4842
4991
  "required": ["verified"]
4843
4992
  }, {
4844
4993
  "type": "object",
@@ -4855,6 +5004,27 @@ const openapiDocument = {
4855
5004
  "type": "boolean",
4856
5005
  "description": "Whether the TXT verification record was found"
4857
5006
  },
5007
+ "spfFound": {
5008
+ "type": "boolean",
5009
+ "description": "Whether the SPF record includes Primitive."
5010
+ },
5011
+ "dkimFound": {
5012
+ "type": "boolean",
5013
+ "description": "Whether the DKIM public key record was found."
5014
+ },
5015
+ "dmarcFound": {
5016
+ "type": "boolean",
5017
+ "description": "Whether the DMARC record was found."
5018
+ },
5019
+ "tlsRptFound": {
5020
+ "type": "boolean",
5021
+ "description": "Whether the TLS-RPT record was found."
5022
+ },
5023
+ "dns_records": {
5024
+ "type": "array",
5025
+ "description": "Exact DNS records checked for this verification attempt.",
5026
+ "items": { "$ref": "#/components/schemas/DomainDnsRecord" }
5027
+ },
4858
5028
  "error": {
4859
5029
  "type": "string",
4860
5030
  "description": "Human-readable verification failure reason"
@@ -5167,6 +5337,31 @@ const openapiDocument = {
5167
5337
  "created_at"
5168
5338
  ]
5169
5339
  },
5340
+ "SendMailAttachment": {
5341
+ "type": "object",
5342
+ "additionalProperties": false,
5343
+ "properties": {
5344
+ "filename": {
5345
+ "type": "string",
5346
+ "minLength": 1,
5347
+ "maxLength": 255,
5348
+ "description": "Attachment filename. Control characters are rejected."
5349
+ },
5350
+ "content_type": {
5351
+ "type": "string",
5352
+ "minLength": 1,
5353
+ "maxLength": 255,
5354
+ "description": "Optional MIME content type. Control characters are rejected."
5355
+ },
5356
+ "content_base64": {
5357
+ "type": "string",
5358
+ "minLength": 1,
5359
+ "maxLength": 44040192,
5360
+ "description": "Base64-encoded attachment bytes."
5361
+ }
5362
+ },
5363
+ "required": ["filename", "content_base64"]
5364
+ },
5170
5365
  "SendMailInput": {
5171
5366
  "type": "object",
5172
5367
  "additionalProperties": false,
@@ -5215,6 +5410,12 @@ const openapiDocument = {
5215
5410
  "pattern": "^[^\\x00-\\x1F\\x7F]+$"
5216
5411
  }
5217
5412
  },
5413
+ "attachments": {
5414
+ "type": "array",
5415
+ "maxItems": 100,
5416
+ "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.",
5417
+ "items": { "$ref": "#/components/schemas/SendMailAttachment" }
5418
+ },
5218
5419
  "wait": {
5219
5420
  "type": "boolean",
5220
5421
  "description": "When true, wait for the first downstream SMTP delivery outcome before returning."
@@ -7502,7 +7703,7 @@ const operationManifest = [
7502
7703
  "binaryResponse": false,
7503
7704
  "bodyRequired": true,
7504
7705
  "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",
7706
+ "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
7707
  "hasJsonBody": true,
7507
7708
  "method": "POST",
7508
7709
  "operationId": "addDomain",
@@ -7512,12 +7713,23 @@ const operationManifest = [
7512
7713
  "requestSchema": {
7513
7714
  "type": "object",
7514
7715
  "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
- } },
7716
+ "properties": {
7717
+ "domain": {
7718
+ "type": "string",
7719
+ "minLength": 1,
7720
+ "maxLength": 253,
7721
+ "description": "The domain name to claim (e.g. \"example.com\")"
7722
+ },
7723
+ "confirmed": {
7724
+ "type": "boolean",
7725
+ "description": "Set to true to confirm replacing an existing mailbox provider after an mx_conflict response."
7726
+ },
7727
+ "outbound": {
7728
+ "type": "boolean",
7729
+ "deprecated": true,
7730
+ "description": "Deprecated and ignored. Outbound DNS is provisioned for every new domain claim."
7731
+ }
7732
+ },
7521
7733
  "required": ["domain"]
7522
7734
  },
7523
7735
  "responseSchema": {
@@ -7540,6 +7752,78 @@ const operationManifest = [
7540
7752
  "type": "string",
7541
7753
  "description": "Add this value as a TXT record to verify ownership"
7542
7754
  },
7755
+ "dns_records": {
7756
+ "type": "array",
7757
+ "description": "Exact DNS records to publish for this pending domain claim.",
7758
+ "items": {
7759
+ "type": "object",
7760
+ "additionalProperties": false,
7761
+ "properties": {
7762
+ "type": {
7763
+ "type": "string",
7764
+ "enum": ["MX", "TXT"],
7765
+ "description": "DNS record type."
7766
+ },
7767
+ "name": {
7768
+ "type": "string",
7769
+ "description": "DNS-provider host/name value relative to the managed root zone."
7770
+ },
7771
+ "fqdn": {
7772
+ "type": "string",
7773
+ "description": "Fully-qualified DNS record name."
7774
+ },
7775
+ "value": {
7776
+ "type": "string",
7777
+ "description": "Exact value to publish."
7778
+ },
7779
+ "priority": {
7780
+ "type": "integer",
7781
+ "description": "MX priority. Present only for MX records."
7782
+ },
7783
+ "ttl": {
7784
+ "type": "integer",
7785
+ "description": "Suggested TTL in seconds when the API can provide one."
7786
+ },
7787
+ "required": {
7788
+ "type": "boolean",
7789
+ "const": true
7790
+ },
7791
+ "purpose": {
7792
+ "type": "string",
7793
+ "enum": [
7794
+ "inbound_mx",
7795
+ "ownership_verification",
7796
+ "spf",
7797
+ "dkim",
7798
+ "dmarc",
7799
+ "tls_reporting"
7800
+ ]
7801
+ },
7802
+ "status": {
7803
+ "type": "string",
7804
+ "enum": [
7805
+ "pending",
7806
+ "found",
7807
+ "missing",
7808
+ "incorrect"
7809
+ ]
7810
+ },
7811
+ "message": {
7812
+ "type": "string",
7813
+ "description": "Short explanation of why this record is needed."
7814
+ }
7815
+ },
7816
+ "required": [
7817
+ "type",
7818
+ "name",
7819
+ "fqdn",
7820
+ "value",
7821
+ "required",
7822
+ "purpose",
7823
+ "status"
7824
+ ]
7825
+ }
7826
+ },
7543
7827
  "created_at": {
7544
7828
  "type": "string",
7545
7829
  "format": "date-time"
@@ -7583,6 +7867,36 @@ const operationManifest = [
7583
7867
  "tag": "Domains",
7584
7868
  "tagCommand": "domains"
7585
7869
  },
7870
+ {
7871
+ "binaryResponse": true,
7872
+ "bodyRequired": false,
7873
+ "command": "download-domain-zone-file",
7874
+ "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",
7875
+ "hasJsonBody": false,
7876
+ "method": "GET",
7877
+ "operationId": "downloadDomainZoneFile",
7878
+ "path": "/domains/{id}/zone-file",
7879
+ "pathParams": [{
7880
+ "description": "Resource UUID",
7881
+ "enum": null,
7882
+ "name": "id",
7883
+ "required": true,
7884
+ "type": "string"
7885
+ }],
7886
+ "queryParams": [{
7887
+ "description": "When true, include only outbound DNS records. Verified domains\ndefault to outbound-only; pending claims default to all required\nrecords.\n",
7888
+ "enum": null,
7889
+ "name": "outbound_only",
7890
+ "required": false,
7891
+ "type": "boolean"
7892
+ }],
7893
+ "requestSchema": null,
7894
+ "responseSchema": null,
7895
+ "sdkName": "downloadDomainZoneFile",
7896
+ "summary": "Download domain DNS zone file",
7897
+ "tag": "Domains",
7898
+ "tagCommand": "domains"
7899
+ },
7586
7900
  {
7587
7901
  "binaryResponse": false,
7588
7902
  "bodyRequired": false,
@@ -7598,7 +7912,7 @@ const operationManifest = [
7598
7912
  "responseSchema": {
7599
7913
  "type": "array",
7600
7914
  "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",
7915
+ "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
7916
  "oneOf": [{
7603
7917
  "type": "object",
7604
7918
  "properties": {
@@ -7655,6 +7969,78 @@ const operationManifest = [
7655
7969
  "type": "string",
7656
7970
  "description": "Add this value as a TXT record to verify ownership"
7657
7971
  },
7972
+ "dns_records": {
7973
+ "type": "array",
7974
+ "description": "Exact DNS records to publish for this pending domain claim.",
7975
+ "items": {
7976
+ "type": "object",
7977
+ "additionalProperties": false,
7978
+ "properties": {
7979
+ "type": {
7980
+ "type": "string",
7981
+ "enum": ["MX", "TXT"],
7982
+ "description": "DNS record type."
7983
+ },
7984
+ "name": {
7985
+ "type": "string",
7986
+ "description": "DNS-provider host/name value relative to the managed root zone."
7987
+ },
7988
+ "fqdn": {
7989
+ "type": "string",
7990
+ "description": "Fully-qualified DNS record name."
7991
+ },
7992
+ "value": {
7993
+ "type": "string",
7994
+ "description": "Exact value to publish."
7995
+ },
7996
+ "priority": {
7997
+ "type": "integer",
7998
+ "description": "MX priority. Present only for MX records."
7999
+ },
8000
+ "ttl": {
8001
+ "type": "integer",
8002
+ "description": "Suggested TTL in seconds when the API can provide one."
8003
+ },
8004
+ "required": {
8005
+ "type": "boolean",
8006
+ "const": true
8007
+ },
8008
+ "purpose": {
8009
+ "type": "string",
8010
+ "enum": [
8011
+ "inbound_mx",
8012
+ "ownership_verification",
8013
+ "spf",
8014
+ "dkim",
8015
+ "dmarc",
8016
+ "tls_reporting"
8017
+ ]
8018
+ },
8019
+ "status": {
8020
+ "type": "string",
8021
+ "enum": [
8022
+ "pending",
8023
+ "found",
8024
+ "missing",
8025
+ "incorrect"
8026
+ ]
8027
+ },
8028
+ "message": {
8029
+ "type": "string",
8030
+ "description": "Short explanation of why this record is needed."
8031
+ }
8032
+ },
8033
+ "required": [
8034
+ "type",
8035
+ "name",
8036
+ "fqdn",
8037
+ "value",
8038
+ "required",
8039
+ "purpose",
8040
+ "status"
8041
+ ]
8042
+ }
8043
+ },
7658
8044
  "created_at": {
7659
8045
  "type": "string",
7660
8046
  "format": "date-time"
@@ -7756,7 +8142,7 @@ const operationManifest = [
7756
8142
  "binaryResponse": false,
7757
8143
  "bodyRequired": false,
7758
8144
  "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",
8145
+ "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
8146
  "hasJsonBody": false,
7761
8147
  "method": "POST",
7762
8148
  "operationId": "verifyDomain",
@@ -7772,10 +8158,84 @@ const operationManifest = [
7772
8158
  "requestSchema": null,
7773
8159
  "responseSchema": { "oneOf": [{
7774
8160
  "type": "object",
7775
- "properties": { "verified": {
7776
- "type": "boolean",
7777
- "const": true
7778
- } },
8161
+ "properties": {
8162
+ "verified": {
8163
+ "type": "boolean",
8164
+ "const": true
8165
+ },
8166
+ "dns_records": {
8167
+ "type": "array",
8168
+ "description": "Exact DNS records checked for this verification attempt.",
8169
+ "items": {
8170
+ "type": "object",
8171
+ "additionalProperties": false,
8172
+ "properties": {
8173
+ "type": {
8174
+ "type": "string",
8175
+ "enum": ["MX", "TXT"],
8176
+ "description": "DNS record type."
8177
+ },
8178
+ "name": {
8179
+ "type": "string",
8180
+ "description": "DNS-provider host/name value relative to the managed root zone."
8181
+ },
8182
+ "fqdn": {
8183
+ "type": "string",
8184
+ "description": "Fully-qualified DNS record name."
8185
+ },
8186
+ "value": {
8187
+ "type": "string",
8188
+ "description": "Exact value to publish."
8189
+ },
8190
+ "priority": {
8191
+ "type": "integer",
8192
+ "description": "MX priority. Present only for MX records."
8193
+ },
8194
+ "ttl": {
8195
+ "type": "integer",
8196
+ "description": "Suggested TTL in seconds when the API can provide one."
8197
+ },
8198
+ "required": {
8199
+ "type": "boolean",
8200
+ "const": true
8201
+ },
8202
+ "purpose": {
8203
+ "type": "string",
8204
+ "enum": [
8205
+ "inbound_mx",
8206
+ "ownership_verification",
8207
+ "spf",
8208
+ "dkim",
8209
+ "dmarc",
8210
+ "tls_reporting"
8211
+ ]
8212
+ },
8213
+ "status": {
8214
+ "type": "string",
8215
+ "enum": [
8216
+ "pending",
8217
+ "found",
8218
+ "missing",
8219
+ "incorrect"
8220
+ ]
8221
+ },
8222
+ "message": {
8223
+ "type": "string",
8224
+ "description": "Short explanation of why this record is needed."
8225
+ }
8226
+ },
8227
+ "required": [
8228
+ "type",
8229
+ "name",
8230
+ "fqdn",
8231
+ "value",
8232
+ "required",
8233
+ "purpose",
8234
+ "status"
8235
+ ]
8236
+ }
8237
+ }
8238
+ },
7779
8239
  "required": ["verified"]
7780
8240
  }, {
7781
8241
  "type": "object",
@@ -7792,6 +8252,94 @@ const operationManifest = [
7792
8252
  "type": "boolean",
7793
8253
  "description": "Whether the TXT verification record was found"
7794
8254
  },
8255
+ "spfFound": {
8256
+ "type": "boolean",
8257
+ "description": "Whether the SPF record includes Primitive."
8258
+ },
8259
+ "dkimFound": {
8260
+ "type": "boolean",
8261
+ "description": "Whether the DKIM public key record was found."
8262
+ },
8263
+ "dmarcFound": {
8264
+ "type": "boolean",
8265
+ "description": "Whether the DMARC record was found."
8266
+ },
8267
+ "tlsRptFound": {
8268
+ "type": "boolean",
8269
+ "description": "Whether the TLS-RPT record was found."
8270
+ },
8271
+ "dns_records": {
8272
+ "type": "array",
8273
+ "description": "Exact DNS records checked for this verification attempt.",
8274
+ "items": {
8275
+ "type": "object",
8276
+ "additionalProperties": false,
8277
+ "properties": {
8278
+ "type": {
8279
+ "type": "string",
8280
+ "enum": ["MX", "TXT"],
8281
+ "description": "DNS record type."
8282
+ },
8283
+ "name": {
8284
+ "type": "string",
8285
+ "description": "DNS-provider host/name value relative to the managed root zone."
8286
+ },
8287
+ "fqdn": {
8288
+ "type": "string",
8289
+ "description": "Fully-qualified DNS record name."
8290
+ },
8291
+ "value": {
8292
+ "type": "string",
8293
+ "description": "Exact value to publish."
8294
+ },
8295
+ "priority": {
8296
+ "type": "integer",
8297
+ "description": "MX priority. Present only for MX records."
8298
+ },
8299
+ "ttl": {
8300
+ "type": "integer",
8301
+ "description": "Suggested TTL in seconds when the API can provide one."
8302
+ },
8303
+ "required": {
8304
+ "type": "boolean",
8305
+ "const": true
8306
+ },
8307
+ "purpose": {
8308
+ "type": "string",
8309
+ "enum": [
8310
+ "inbound_mx",
8311
+ "ownership_verification",
8312
+ "spf",
8313
+ "dkim",
8314
+ "dmarc",
8315
+ "tls_reporting"
8316
+ ]
8317
+ },
8318
+ "status": {
8319
+ "type": "string",
8320
+ "enum": [
8321
+ "pending",
8322
+ "found",
8323
+ "missing",
8324
+ "incorrect"
8325
+ ]
8326
+ },
8327
+ "message": {
8328
+ "type": "string",
8329
+ "description": "Short explanation of why this record is needed."
8330
+ }
8331
+ },
8332
+ "required": [
8333
+ "type",
8334
+ "name",
8335
+ "fqdn",
8336
+ "value",
8337
+ "required",
8338
+ "purpose",
8339
+ "status"
8340
+ ]
8341
+ }
8342
+ },
7795
8343
  "error": {
7796
8344
  "type": "string",
7797
8345
  "description": "Human-readable verification failure reason"
@@ -11165,6 +11713,36 @@ const operationManifest = [
11165
11713
  "pattern": "^[^\\x00-\\x1F\\x7F]+$"
11166
11714
  }
11167
11715
  },
11716
+ "attachments": {
11717
+ "type": "array",
11718
+ "maxItems": 100,
11719
+ "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.",
11720
+ "items": {
11721
+ "type": "object",
11722
+ "additionalProperties": false,
11723
+ "properties": {
11724
+ "filename": {
11725
+ "type": "string",
11726
+ "minLength": 1,
11727
+ "maxLength": 255,
11728
+ "description": "Attachment filename. Control characters are rejected."
11729
+ },
11730
+ "content_type": {
11731
+ "type": "string",
11732
+ "minLength": 1,
11733
+ "maxLength": 255,
11734
+ "description": "Optional MIME content type. Control characters are rejected."
11735
+ },
11736
+ "content_base64": {
11737
+ "type": "string",
11738
+ "minLength": 1,
11739
+ "maxLength": 44040192,
11740
+ "description": "Base64-encoded attachment bytes."
11741
+ }
11742
+ },
11743
+ "required": ["filename", "content_base64"]
11744
+ }
11745
+ },
11168
11746
  "wait": {
11169
11747
  "type": "boolean",
11170
11748
  "description": "When true, wait for the first downstream SMTP delivery outcome before returning."
@@ -12501,6 +13079,10 @@ function operationOutputPayload(envelope, includeEnvelope) {
12501
13079
  return includeEnvelope ? envelope ?? null : envelope?.data ?? null;
12502
13080
  }
12503
13081
  const OPERATION_HINTS = {
13082
+ 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.",
13083
+ 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.",
13084
+ downloadDomainZoneFile: "Tip: prefer `primitive domains zone-file --id <domain-id> --output <domain>.zone` for CLI-friendly file output.",
13085
+ sendEmail: "Tip: prefer `primitive send --to <address> --body <text> --attachment <file>` for file attachments. This raw command exists for callers passing JSON.",
12504
13086
  createFunction: "Tip: prefer `primitive functions deploy --name <name> --file <bundle>` for file-input ergonomics. This raw command exists for callers passing JSON.",
12505
13087
  updateFunction: "Tip: prefer `primitive functions redeploy --id <id> --file <bundle>` for file-input ergonomics. This raw command exists for callers passing JSON.",
12506
13088
  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 +13194,8 @@ const EMPTY_RESULT_HINTS = {
12612
13194
  listEndpoints: "(no results) No webhook endpoints configured. Add one with `primitive endpoints create --url <your-url>`.",
12613
13195
  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
13196
  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."
13197
+ listFilters: "(no results) No filter rules configured.",
13198
+ 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
13199
  };
12617
13200
  function canonicalizeCliReferences(description) {
12618
13201
  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 +13962,144 @@ var DoctorCommand = class DoctorCommand extends Command {
13379
13962
  }
13380
13963
  };
13381
13964
  //#endregion
13965
+ //#region src/oclif/commands/domains-zone-file.ts
13966
+ function zoneFileUrl(baseUrl, domainId, outboundOnly) {
13967
+ const url = new URL(`${baseUrl.replace(/\/$/, "")}/domains/${encodeURIComponent(domainId)}/zone-file`);
13968
+ if (outboundOnly) url.searchParams.set("outbound_only", "true");
13969
+ return url.toString();
13970
+ }
13971
+ function contentDispositionFilename(value) {
13972
+ if (!value) return null;
13973
+ const match = /filename\*=UTF-8''([^;]+)|filename="?([^";]+)"?/i.exec(value);
13974
+ const raw = match?.[1] ?? match?.[2];
13975
+ if (!raw) return null;
13976
+ try {
13977
+ return decodeURIComponent(raw);
13978
+ } catch {
13979
+ return raw;
13980
+ }
13981
+ }
13982
+ function findDomain(domains, domain) {
13983
+ const match = domains.find((entry) => entry.domain === domain);
13984
+ if (!match) throw new Errors.CLIError(`Domain ${domain} was not found.`, { exit: 1 });
13985
+ return match;
13986
+ }
13987
+ async function responseErrorPayload(response) {
13988
+ if ((response.headers.get("content-type")?.toLowerCase() ?? "").includes("application/json")) return response.json().catch(() => ({
13989
+ code: "http_error",
13990
+ message: `HTTP ${response.status} ${response.statusText}`.trim()
13991
+ }));
13992
+ return {
13993
+ code: "http_error",
13994
+ message: (await response.text().catch(() => "")).trim() || `HTTP ${response.status} ${response.statusText}`.trim()
13995
+ };
13996
+ }
13997
+ var DomainsZoneFileCommand = class DomainsZoneFileCommand extends Command {
13998
+ static description = `Download a BIND-format DNS zone file for a domain claim.
13999
+
14000
+ 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.
14001
+
14002
+ 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.`;
14003
+ static summary = "Download a DNS zone file for a domain";
14004
+ static examples = [
14005
+ "<%= config.bin %> domains zone-file --id <domain-id>",
14006
+ "<%= config.bin %> domains zone-file --id <domain-id> --output example.com.zone",
14007
+ "<%= config.bin %> domains zone-file --domain example.com --output example.com.zone",
14008
+ "<%= config.bin %> domains zone-file --id <domain-id> --outbound-only"
14009
+ ];
14010
+ static flags = {
14011
+ "api-key": Flags.string({
14012
+ description: "Primitive API key override (defaults to PRIMITIVE_API_KEY or saved OAuth login credentials)",
14013
+ env: "PRIMITIVE_API_KEY"
14014
+ }),
14015
+ "api-base-url-1": Flags.string({
14016
+ description: "Override the primary API base URL. Internal testing only; not documented to customers.",
14017
+ env: "PRIMITIVE_API_BASE_URL_1",
14018
+ hidden: true
14019
+ }),
14020
+ "api-base-url-2": Flags.string({
14021
+ description: "Override the attachments-supporting send host base URL. Internal testing only; not documented to customers.",
14022
+ env: "PRIMITIVE_API_BASE_URL_2",
14023
+ hidden: true
14024
+ }),
14025
+ 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`." }),
14026
+ id: Flags.string({ description: "Domain id returned by `primitive domains add` or `primitive domains list`." }),
14027
+ output: Flags.string({
14028
+ char: "o",
14029
+ description: "Write the zone file to this path instead of stdout. Defaults to stdout."
14030
+ }),
14031
+ "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." }),
14032
+ time: Flags.boolean({ description: TIME_FLAG_DESCRIPTION })
14033
+ };
14034
+ async run() {
14035
+ const { flags } = await this.parse(DomainsZoneFileCommand);
14036
+ if (flags.id && flags.domain) throw new Errors.CLIError("Use only one of --id or --domain.", { exit: 1 });
14037
+ if (!flags.id && !flags.domain) throw new Errors.CLIError("Pass --id <domain-id> or --domain <domain>.", { exit: 1 });
14038
+ await runWithTiming(flags.time, async () => {
14039
+ const { apiClient, auth, baseUrlOverridden, requestConfig } = await createAuthenticatedCliApiClient({
14040
+ apiKey: flags["api-key"],
14041
+ apiBaseUrl1: flags["api-base-url-1"],
14042
+ apiBaseUrl2: flags["api-base-url-2"],
14043
+ configDir: this.config.configDir
14044
+ });
14045
+ let domainId = flags.id;
14046
+ if (!domainId && flags.domain) {
14047
+ const result = await listDomains({
14048
+ client: apiClient.client,
14049
+ responseStyle: "fields"
14050
+ });
14051
+ if (result.error) {
14052
+ const errorPayload = extractErrorPayload(result.error);
14053
+ writeErrorWithHints(errorPayload);
14054
+ surfaceUnauthorizedHint({
14055
+ auth,
14056
+ baseUrlOverridden,
14057
+ configDir: this.config.configDir,
14058
+ payload: errorPayload
14059
+ });
14060
+ process.exitCode = 1;
14061
+ return;
14062
+ }
14063
+ const envelope = result.data;
14064
+ domainId = findDomain(envelope?.data ?? [], flags.domain).id;
14065
+ }
14066
+ if (!domainId) throw new Errors.CLIError("Could not resolve a domain id.", { exit: 1 });
14067
+ let response;
14068
+ try {
14069
+ response = await fetch(zoneFileUrl(requestConfig.resolvedApiBaseUrl1, domainId, flags["outbound-only"]), { headers: {
14070
+ ...requestConfig.headers ?? {},
14071
+ ...auth.apiKey ? { authorization: `Bearer ${auth.apiKey}` } : {}
14072
+ } });
14073
+ } catch (error) {
14074
+ writeErrorWithHints(error);
14075
+ process.exitCode = 1;
14076
+ return;
14077
+ }
14078
+ if (!response.ok) {
14079
+ const errorPayload = extractErrorPayload(await responseErrorPayload(response));
14080
+ writeErrorWithHints(errorPayload);
14081
+ surfaceUnauthorizedHint({
14082
+ auth,
14083
+ baseUrlOverridden,
14084
+ configDir: this.config.configDir,
14085
+ payload: errorPayload
14086
+ });
14087
+ process.exitCode = 1;
14088
+ return;
14089
+ }
14090
+ const zoneFile = await response.text();
14091
+ const output = flags.output;
14092
+ if (output) {
14093
+ writeFileSync(output, zoneFile, "utf8");
14094
+ const filename = contentDispositionFilename(response.headers.get("content-disposition"));
14095
+ process.stderr.write(`Wrote ${filename ?? "zone file"} to ${output}\n`);
14096
+ return;
14097
+ }
14098
+ process.stdout.write(zoneFile);
14099
+ });
14100
+ }
14101
+ };
14102
+ //#endregion
13382
14103
  //#region src/oclif/commands/emails-latest.ts
13383
14104
  const DEFAULT_LIMIT = 10;
13384
14105
  const MAX_LIMIT = 100;
@@ -16291,12 +17012,46 @@ var ReplyCommand = class ReplyCommand extends Command {
16291
17012
  }
16292
17013
  };
16293
17014
  //#endregion
17015
+ //#region src/oclif/attachments.ts
17016
+ function readAttachmentBytes(path, readFile) {
17017
+ try {
17018
+ return Buffer.from(readFile(path));
17019
+ } catch (error) {
17020
+ const detail = error instanceof Error ? error.message : String(error);
17021
+ throw new Errors.CLIError(`Could not read --attachment ${path}: ${detail}`, { exit: 1 });
17022
+ }
17023
+ }
17024
+ function hasControlCharacter(value) {
17025
+ return Array.from(value).some((character) => {
17026
+ const code = character.charCodeAt(0);
17027
+ return code <= 31 || code >= 127 && code <= 159;
17028
+ });
17029
+ }
17030
+ function validateAttachmentFilename(path, filename) {
17031
+ if (!filename) throw new Errors.CLIError(`Could not derive an attachment filename from ${path}. Pass a file path.`, { exit: 1 });
17032
+ if (hasControlCharacter(filename)) throw new Errors.CLIError(`Attachment filename ${filename} contains control characters.`, { exit: 1 });
17033
+ }
17034
+ function readAttachmentFiles(paths, readFile = readFileSync) {
17035
+ if (!paths || paths.length === 0) return void 0;
17036
+ return paths.map((path) => {
17037
+ const filename = basename(path);
17038
+ validateAttachmentFilename(path, filename);
17039
+ const bytes = readAttachmentBytes(path, readFile);
17040
+ if (bytes.length === 0) throw new Errors.CLIError(`Attachment file ${path} is empty. Attachments must contain at least one byte.`, { exit: 1 });
17041
+ return {
17042
+ content_base64: bytes.toString("base64"),
17043
+ filename
17044
+ };
17045
+ });
17046
+ }
17047
+ //#endregion
16294
17048
  //#region src/oclif/commands/send.ts
16295
17049
  var SendCommand = class SendCommand extends Command {
16296
17050
  static description = `Send an outbound email. Agent-grade shortcut for \`sending send\` with sensible defaults.
16297
17051
 
16298
17052
  --from defaults to agent@<your-first-verified-outbound-domain> when omitted.
16299
17053
  --subject defaults to the first line of the body when omitted.
17054
+ --attachment attaches a file; repeat it to attach multiple files.
16300
17055
 
16301
17056
  For the full flag set (custom message-id threading on the wire,
16302
17057
  references arrays, etc.), use \`primitive sending send\`.`;
@@ -16304,6 +17059,7 @@ var SendCommand = class SendCommand extends Command {
16304
17059
  static examples = [
16305
17060
  "<%= config.bin %> send --to alice@example.com --body 'Hi Alice!'",
16306
17061
  "<%= config.bin %> send --to alice@example.com --body-file ./message.txt",
17062
+ "<%= config.bin %> send --to alice@example.com --body 'See attached.' --attachment ./report.pdf",
16307
17063
  "<%= config.bin %> send --to alice@example.com --from support@yourcompany.com --subject 'Quick question' --body 'Are you free Thursday?'",
16308
17064
  "<%= config.bin %> send --to alice@example.com --html '<p>Hello!</p>'",
16309
17065
  "<%= config.bin %> send --to alice@example.com --body 'Confirmed' --wait",
@@ -16331,11 +17087,15 @@ var SendCommand = class SendCommand extends Command {
16331
17087
  from: Flags.string({ description: "Sender address. Defaults to agent@<your-first-verified-outbound-domain>." }),
16332
17088
  subject: Flags.string({ description: "Subject line. Defaults to the first line of --body / --html when omitted." }),
16333
17089
  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." }),
17090
+ "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
17091
  "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
17092
  html: Flags.string({ description: "HTML message body. Either --body or --html (or both) is required." }),
16337
17093
  "html-file": Flags.string({ description: "Read the HTML message body from a UTF-8 file. Mutually exclusive with --html and --html-stdin." }),
16338
17094
  "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." }),
17095
+ attachment: Flags.string({
17096
+ description: "Attach a file to the email. Repeatable. Sends file bytes as a MIME attachment; use --body-file only for message body text.",
17097
+ multiple: true
17098
+ }),
16339
17099
  "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
17100
  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
17101
  "wait-timeout-ms": Flags.integer({ description: "Maximum time to wait when --wait is set. Defaults to 30000ms." }),
@@ -16352,6 +17112,7 @@ var SendCommand = class SendCommand extends Command {
16352
17112
  htmlStdin: flags["html-stdin"]
16353
17113
  });
16354
17114
  if (bodies.kind === "error") throw new Errors.CLIError(bodies.message);
17115
+ const attachments = readAttachmentFiles(flags.attachment);
16355
17116
  await runWithTiming(flags.time, async () => {
16356
17117
  const { apiClient, auth, baseUrlOverridden } = await createAuthenticatedCliApiClient({
16357
17118
  apiKey: flags["api-key"],
@@ -16373,6 +17134,7 @@ var SendCommand = class SendCommand extends Command {
16373
17134
  subject,
16374
17135
  ...bodies.body !== void 0 ? { body_text: bodies.body } : {},
16375
17136
  ...bodies.html !== void 0 ? { body_html: bodies.html } : {},
17137
+ ...attachments !== void 0 ? { attachments } : {},
16376
17138
  ...flags["in-reply-to"] !== void 0 ? { in_reply_to: flags["in-reply-to"] } : {},
16377
17139
  ...flags.wait !== void 0 ? { wait: flags.wait } : {},
16378
17140
  ...flags["wait-timeout-ms"] !== void 0 ? { wait_timeout_ms: flags["wait-timeout-ms"] } : {}
@@ -17283,36 +18045,88 @@ function renderFishCompletion(binName) {
17283
18045
  //#endregion
17284
18046
  //#region src/oclif/index.ts
17285
18047
  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>`.";
18048
+ 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
18049
  static summary = "List all generated API operations (JSON)";
17288
18050
  async run() {
17289
18051
  this.log(JSON.stringify(operationManifest, null, 2));
17290
18052
  }
17291
18053
  };
18054
+ function operationId(operation) {
18055
+ return `${operation.tagCommand}:${operation.command}`;
18056
+ }
18057
+ function normalizeLookupToken(value) {
18058
+ return value.toLowerCase().replace(/[^a-z0-9]/g, "");
18059
+ }
18060
+ function unique(values) {
18061
+ return [...new Set(values)];
18062
+ }
18063
+ function operationLookupTokens(operation) {
18064
+ return unique([
18065
+ operationId(operation),
18066
+ operation.command,
18067
+ operation.operationId,
18068
+ operation.sdkName,
18069
+ `${operation.tagCommand}:${operation.operationId}`,
18070
+ `${operation.tagCommand}:${operation.sdkName}`
18071
+ ]);
18072
+ }
18073
+ function levenshteinDistance(left, right) {
18074
+ if (left === right) return 0;
18075
+ if (left.length === 0) return right.length;
18076
+ if (right.length === 0) return left.length;
18077
+ let previous = Array.from({ length: right.length + 1 }, (_, index) => index);
18078
+ for (let leftIndex = 0; leftIndex < left.length; leftIndex += 1) {
18079
+ const current = [leftIndex + 1];
18080
+ for (let rightIndex = 0; rightIndex < right.length; rightIndex += 1) {
18081
+ const substitutionCost = left[leftIndex] === right[rightIndex] ? 0 : 1;
18082
+ current[rightIndex + 1] = Math.min(current[rightIndex] + 1, previous[rightIndex + 1] + 1, previous[rightIndex] + substitutionCost);
18083
+ }
18084
+ previous = current;
18085
+ }
18086
+ return previous[right.length] ?? Number.POSITIVE_INFINITY;
18087
+ }
18088
+ function scoreLookupToken(query, token) {
18089
+ const normalizedQuery = normalizeLookupToken(query);
18090
+ const normalizedToken = normalizeLookupToken(token);
18091
+ if (!normalizedQuery || !normalizedToken) return 0;
18092
+ if (normalizedQuery === normalizedToken) return 100;
18093
+ if (normalizedToken.includes(normalizedQuery)) return Math.max(50, 90 - (normalizedToken.length - normalizedQuery.length));
18094
+ if (normalizedQuery.includes(normalizedToken)) return Math.max(45, 80 - (normalizedQuery.length - normalizedToken.length));
18095
+ const distance = levenshteinDistance(normalizedQuery, normalizedToken);
18096
+ const maxLength = Math.max(normalizedQuery.length, normalizedToken.length);
18097
+ return Math.round((1 - distance / maxLength) * 75);
18098
+ }
18099
+ function scoreOperation(query, operation) {
18100
+ return Math.max(...operationLookupTokens(operation).map((token) => scoreLookupToken(query, token)));
18101
+ }
17292
18102
  function lookupOperation(id) {
17293
18103
  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;
18104
+ const match = operationManifest.find((op) => operationLookupTokens(op).some((token) => token === trimmed || normalizeLookupToken(token) === normalizeLookupToken(trimmed))) ?? null;
17298
18105
  if (match) return {
17299
18106
  match,
17300
18107
  candidates: []
17301
18108
  };
17302
18109
  return {
17303
18110
  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)
18111
+ candidates: operationManifest.map((op) => ({
18112
+ id: operationId(op),
18113
+ score: scoreOperation(trimmed, op)
18114
+ })).filter(({ score }) => score >= 45).sort((left, right) => right.score - left.score || left.id.localeCompare(right.id)).slice(0, 5).map(({ id }) => id)
17305
18115
  };
17306
18116
  }
17307
18117
  var DescribeCommand = class DescribeCommand extends Command {
17308
18118
  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.",
18119
+ 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
18120
  required: true
17311
18121
  }) };
17312
18122
  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
18123
 
17314
18124
  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
18125
 
18126
+ # Domain setup records returned by add/verify
18127
+ primitive describe domains:add
18128
+ primitive describe addDomain
18129
+
17316
18130
  # Which of EmailDetail's sender-shaped fields is canonical?
17317
18131
  primitive describe emails:get | jq '.responseSchema.properties | keys'
17318
18132
  primitive describe emails:get | jq -r '.responseSchema.properties.from_email.description'
@@ -17322,7 +18136,12 @@ var DescribeCommand = class DescribeCommand extends Command {
17322
18136
 
17323
18137
  \`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
18138
  static summary = "Describe a single API operation in detail";
17325
- static examples = ["<%= config.bin %> describe emails:get", "<%= config.bin %> describe sent:get"];
18139
+ static examples = [
18140
+ "<%= config.bin %> describe addDomain",
18141
+ "<%= config.bin %> describe domains:add",
18142
+ "<%= config.bin %> describe emails:get",
18143
+ "<%= config.bin %> describe sent:get"
18144
+ ];
17326
18145
  async run() {
17327
18146
  const { args } = await this.parse(DescribeCommand);
17328
18147
  const { match, candidates } = lookupOperation(args.command);
@@ -17355,9 +18174,6 @@ var CompletionCommand = class CompletionCommand extends Command {
17355
18174
  await this.config.runCommand("autocomplete", [args.shell]);
17356
18175
  }
17357
18176
  };
17358
- function commandId(operation) {
17359
- return `${operation.tagCommand}:${operation.command}`;
17360
- }
17361
18177
  const CANONICAL_OPERATION_ALIASES = {
17362
18178
  "account:show": "account:get-account",
17363
18179
  "account:storage": "account:get-storage-stats",
@@ -17402,14 +18218,15 @@ const CANONICAL_OPERATION_ALIASES = {
17402
18218
  };
17403
18219
  const DESCRIBE_OPERATION_ALIASES = {
17404
18220
  ...CANONICAL_OPERATION_ALIASES,
18221
+ "domains:zone-file": "domains:download-domain-zone-file",
17405
18222
  "functions:logs": "functions:list-function-logs",
17406
18223
  reply: "sending:reply-to-email"
17407
18224
  };
17408
18225
  function resolveOperationAlias(id) {
17409
18226
  return DESCRIBE_OPERATION_ALIASES[id] ?? id;
17410
18227
  }
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)]));
18228
+ const OVERRIDDEN_OPERATION_IDS = new Set(["domains:download-domain-zone-file", "functions:test-function"]);
18229
+ const generatedCommands = Object.fromEntries(operationManifest.filter((operation) => !OVERRIDDEN_OPERATION_IDS.has(operationId(operation))).map((operation) => [operationId(operation), createOperationCommand(operation)]));
17413
18230
  const COMMANDS = {
17414
18231
  completion: CompletionCommand,
17415
18232
  "list-operations": ListOperationsCommand,
@@ -17438,6 +18255,8 @@ const COMMANDS = {
17438
18255
  "emails:latest": EmailsLatestCommand,
17439
18256
  "emails:watch": EmailsWatchCommand,
17440
18257
  "emails:wait": EmailsWaitCommand,
18258
+ "domains:zone-file": DomainsZoneFileCommand,
18259
+ "domains:download-domain-zone-file": DomainsZoneFileCommand,
17441
18260
  "functions:init": FunctionsInitCommand,
17442
18261
  "functions:templates": FunctionsTemplatesCommand,
17443
18262
  "functions:deploy": FunctionsDeployCommand,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@primitivedotdev/cli",
3
- "version": "0.31.3",
3
+ "version": "0.31.4",
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,
@@ -38,7 +38,7 @@
38
38
  "description": "Agent signup and authentication API operations"
39
39
  },
40
40
  "domains": {
41
- "description": "Claim, verify, and manage email domains"
41
+ "description": "Claim, verify, manage email domains, and download DNS zone files"
42
42
  },
43
43
  "emails": {
44
44
  "description": "List, inspect, and wait for received emails. Prefer task aliases like `primitive emails list`, `primitive emails get`, `primitive emails latest`, `primitive emails wait`, and `primitive emails watch`; generated API names remain available for compatibility."