@primitivedotdev/cli 0.30.3 → 0.31.1

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 +526 -252
  2. package/package.json +1 -1
@@ -1478,9 +1478,8 @@ const getSentEmail = (options) => (options.client ?? client).get({
1478
1478
  * List functions
1479
1479
  *
1480
1480
  * Returns every active (non-deleted) function in the org, newest
1481
- * first. Each entry carries the deploy status and the gateway URL
1482
- * that the platform's webhook delivery loop posts to. To inspect
1483
- * the source code or deploy errors, use `GET /functions/{id}`.
1481
+ * first. Each entry carries deploy status and timestamps. To
1482
+ * inspect the source code or deploy errors, use `GET /functions/{id}`.
1484
1483
  *
1485
1484
  */
1486
1485
  const listFunctions = (options) => (options?.client ?? client).get({
@@ -1496,13 +1495,14 @@ const listFunctions = (options) => (options?.client ?? client).get({
1496
1495
  *
1497
1496
  * Creates and deploys a new function. The handler must be a single
1498
1497
  * ESM module whose default export is an object with an async
1499
- * `fetch(request, env)` method (Workers-style). The gateway
1500
- * HMAC-verifies the POST against the org's webhook secret before
1501
- * invoking the handler; the request body parses to an
1502
- * `email.received` event (see `EmailReceivedEvent` and the
1503
- * Webhook payload section for the full schema). Code is bundled
1504
- * before being uploaded; ship a single self-contained file rather
1505
- * than relying on external imports.
1498
+ * `fetch(request, env)` method (Workers-style). Primitive signs
1499
+ * each delivery and forwards the `Primitive-Signature` header to
1500
+ * the handler. Verify the raw request body with
1501
+ * `PRIMITIVE_WEBHOOK_SECRET` before parsing JSON; after verification
1502
+ * the request body parses to an `email.received` event (see
1503
+ * `EmailReceivedEvent` and the Webhook payload section for the full
1504
+ * schema). Code is bundled before being uploaded; ship a single
1505
+ * self-contained file rather than relying on external imports.
1506
1506
  *
1507
1507
  * **Code limits.** `code` is capped at 1 MiB UTF-8. `sourceMap`
1508
1508
  * (optional) is capped at 5 MiB UTF-8, stored with each deployment
@@ -1512,12 +1512,12 @@ const listFunctions = (options) => (options?.client ?? client).get({
1512
1512
  * **Auto-wiring.** On successful deploy, Primitive automatically
1513
1513
  * creates a webhook endpoint that delivers inbound mail to the
1514
1514
  * function. There is nothing to configure on the Endpoints API
1515
- * for this to work; the gateway URL returned here is for
1516
- * reference only and is not directly callable from outside.
1515
+ * for this to work; the internal runtime URL is not returned by
1516
+ * the API and is not a customer-facing integration surface.
1517
1517
  *
1518
1518
  * **Secrets.** New functions ship with the managed secrets
1519
- * (`PRIMITIVE_WEBHOOK_SECRET`, `PRIMITIVE_API_KEY`) already
1520
- * bound. Add user-set secrets via
1519
+ * (`PRIMITIVE_WEBHOOK_SECRET`, `PRIMITIVE_API_KEY`,
1520
+ * `PRIMITIVE_API_BASE_URL`) already bound. Add user-set secrets via
1521
1521
  * `POST /functions/{id}/secrets`; secret writes only land in the
1522
1522
  * running handler on the next redeploy.
1523
1523
  *
@@ -1663,9 +1663,9 @@ const getFunctionTestRunTrace = (options) => (options.client ?? client).get({
1663
1663
  * never returned.** Secret writes are write-only.
1664
1664
  *
1665
1665
  * Managed entries (e.g. `PRIMITIVE_WEBHOOK_SECRET`,
1666
- * `PRIMITIVE_API_KEY`) carry a `description` instead of
1667
- * `created_at` / `updated_at`. They cannot be created, updated,
1668
- * or deleted via this API.
1666
+ * `PRIMITIVE_API_KEY`, `PRIMITIVE_API_BASE_URL`) carry a
1667
+ * `description` instead of `created_at` / `updated_at`. They
1668
+ * cannot be created, updated, or deleted via this API.
1669
1669
  *
1670
1670
  */
1671
1671
  const listFunctionSecrets = (options) => (options.client ?? client).get({
@@ -1831,7 +1831,7 @@ const openapiDocument = {
1831
1831
  },
1832
1832
  {
1833
1833
  "name": "Functions",
1834
- "description": "Deploy JavaScript handlers that run on inbound mail. Each function\nis a single ESM module whose default export is an object with an\nasync `fetch(request, env)` method, in the shape of a Workers-style\nhandler. The gateway HMAC-verifies the inbound POST against the\norg's webhook secret before invoking `fetch`; the request body\nparses to an `email.received` event (see `EmailReceivedEvent` and\nthe Webhook payload section for the full schema). Code runs on\nPrimitive's edge runtime; there is no infrastructure to manage.\nSecrets land in `env` as encrypted bindings and are refreshed on\nevery redeploy.\n"
1834
+ "description": "Deploy JavaScript handlers that run on inbound mail. Each function\nis a single ESM module whose default export is an object with an\nasync `fetch(request, env)` method, in the shape of a Workers-style\nhandler. Primitive signs each delivery and forwards the\n`Primitive-Signature` header to the handler; verify the raw request\nbody with `PRIMITIVE_WEBHOOK_SECRET` before trusting the parsed\n`email.received` event (see `EmailReceivedEvent` and the Webhook\npayload section for the full schema). Code runs on\nPrimitive's edge runtime; there is no infrastructure to manage.\nSecrets land in `env` as encrypted bindings and are refreshed on\nevery redeploy.\n"
1835
1835
  }
1836
1836
  ],
1837
1837
  "paths": {
@@ -2413,6 +2413,15 @@ const openapiDocument = {
2413
2413
  },
2414
2414
  "description": "Filter by domain ID."
2415
2415
  },
2416
+ {
2417
+ "name": "reply_to_sent_email_id",
2418
+ "in": "query",
2419
+ "schema": {
2420
+ "type": "string",
2421
+ "format": "uuid"
2422
+ },
2423
+ "description": "Filter to inbound emails that are replies to a specific\noutbound send. The value is a `sent_emails.id` (UUID). At\ninbound ingest, Primitive matches the parsed In-Reply-To\nheader (or References as a fallback) against\n`sent_emails.message_id` in the same org and records the\nresolved id on `emails.reply_to_sent_email_id`. This filter\nis the strict-threading lookup behind `primitive chat` and\nany UI that wants to show the inbound reply to a given\nsend. NULL on inbound that isn't a threaded reply to one\nof your sends, so existing emails received before this\ningestion landed will not match.\n"
2424
+ },
2416
2425
  {
2417
2426
  "name": "status",
2418
2427
  "in": "query",
@@ -3162,7 +3171,7 @@ const openapiDocument = {
3162
3171
  "get": {
3163
3172
  "operationId": "listFunctions",
3164
3173
  "summary": "List functions",
3165
- "description": "Returns every active (non-deleted) function in the org, newest\nfirst. Each entry carries the deploy status and the gateway URL\nthat the platform's webhook delivery loop posts to. To inspect\nthe source code or deploy errors, use `GET /functions/{id}`.\n",
3174
+ "description": "Returns every active (non-deleted) function in the org, newest\nfirst. Each entry carries deploy status and timestamps. To\ninspect the source code or deploy errors, use `GET /functions/{id}`.\n",
3166
3175
  "tags": ["Functions"],
3167
3176
  "responses": {
3168
3177
  "200": {
@@ -3181,7 +3190,7 @@ const openapiDocument = {
3181
3190
  "post": {
3182
3191
  "operationId": "createFunction",
3183
3192
  "summary": "Deploy a function",
3184
- "description": "Creates and deploys a new function. The handler must be a single\nESM module whose default export is an object with an async\n`fetch(request, env)` method (Workers-style). The gateway\nHMAC-verifies the POST against the org's webhook secret before\ninvoking the handler; the request body parses to an\n`email.received` event (see `EmailReceivedEvent` and the\nWebhook payload section for the full schema). Code is bundled\nbefore being uploaded; ship a single self-contained file rather\nthan relying on external imports.\n\n**Code limits.** `code` is capped at 1 MiB UTF-8. `sourceMap`\n(optional) is capped at 5 MiB UTF-8, stored with each deployment\nattempt, and sent to the runtime so stack traces can resolve to\noriginal source files.\n\n**Auto-wiring.** On successful deploy, Primitive automatically\ncreates a webhook endpoint that delivers inbound mail to the\nfunction. There is nothing to configure on the Endpoints API\nfor this to work; the gateway URL returned here is for\nreference only and is not directly callable from outside.\n\n**Secrets.** New functions ship with the managed secrets\n(`PRIMITIVE_WEBHOOK_SECRET`, `PRIMITIVE_API_KEY`) already\nbound. Add user-set secrets via\n`POST /functions/{id}/secrets`; secret writes only land in the\nrunning handler on the next redeploy.\n",
3193
+ "description": "Creates and deploys a new function. The handler must be a single\nESM module whose default export is an object with an async\n`fetch(request, env)` method (Workers-style). Primitive signs\neach delivery and forwards the `Primitive-Signature` header to\nthe handler. Verify the raw request body with\n`PRIMITIVE_WEBHOOK_SECRET` before parsing JSON; after verification\nthe request body parses to an `email.received` event (see\n`EmailReceivedEvent` and the Webhook payload section for the full\nschema). Code is bundled before being uploaded; ship a single\nself-contained file rather than relying on external imports.\n\n**Code limits.** `code` is capped at 1 MiB UTF-8. `sourceMap`\n(optional) is capped at 5 MiB UTF-8, stored with each deployment\nattempt, and sent to the runtime so stack traces can resolve to\noriginal source files.\n\n**Auto-wiring.** On successful deploy, Primitive automatically\ncreates a webhook endpoint that delivers inbound mail to the\nfunction. There is nothing to configure on the Endpoints API\nfor this to work; the internal runtime URL is not returned by\nthe API and is not a customer-facing integration surface.\n\n**Secrets.** New functions ship with the managed secrets\n(`PRIMITIVE_WEBHOOK_SECRET`, `PRIMITIVE_API_KEY`,\n`PRIMITIVE_API_BASE_URL`) already bound. Add user-set secrets via\n`POST /functions/{id}/secrets`; secret writes only land in the\nrunning handler on the next redeploy.\n",
3185
3194
  "tags": ["Functions"],
3186
3195
  "requestBody": {
3187
3196
  "required": true,
@@ -3350,7 +3359,7 @@ const openapiDocument = {
3350
3359
  "get": {
3351
3360
  "operationId": "listFunctionSecrets",
3352
3361
  "summary": "List a function's secrets",
3353
- "description": "Returns metadata for every secret bound to the function, with\nmanaged entries (provisioned by Primitive) listed first and\nuser-set entries listed alphabetically after. **Values are\nnever returned.** Secret writes are write-only.\n\nManaged entries (e.g. `PRIMITIVE_WEBHOOK_SECRET`,\n`PRIMITIVE_API_KEY`) carry a `description` instead of\n`created_at` / `updated_at`. They cannot be created, updated,\nor deleted via this API.\n",
3362
+ "description": "Returns metadata for every secret bound to the function, with\nmanaged entries (provisioned by Primitive) listed first and\nuser-set entries listed alphabetically after. **Values are\nnever returned.** Secret writes are write-only.\n\nManaged entries (e.g. `PRIMITIVE_WEBHOOK_SECRET`,\n`PRIMITIVE_API_KEY`, `PRIMITIVE_API_BASE_URL`) carry a\n`description` instead of `created_at` / `updated_at`. They\ncannot be created, updated, or deleted via this API.\n",
3354
3363
  "tags": ["Functions"],
3355
3364
  "responses": {
3356
3365
  "200": {
@@ -4666,6 +4675,11 @@ const openapiDocument = {
4666
4675
  "type": "array",
4667
4676
  "description": "Sent emails recorded as replies to this inbound, in send\norder (ascending). Populated when a customer's send-mail\nrequest carries an `in_reply_to` Message-ID that matches\nthis inbound's `message_id` in the same org. Includes\nattempts that were gate-denied, so the array reflects every\nrecorded reply attempt regardless of outcome.\n",
4668
4677
  "items": { "$ref": "#/components/schemas/EmailDetailReply" }
4678
+ },
4679
+ "reply_to_sent_email_id": {
4680
+ "type": ["string", "null"],
4681
+ "format": "uuid",
4682
+ "description": "The `sent_emails.id` of the outbound this inbound was a\nreply to, when resolvable. Set at inbound ingest by\nmatching the parsed In-Reply-To (or References, as a\nfallback) against `sent_emails.message_id` in the same\norg. The mirror of `sent_emails.in_reply_to_email_id` for\nthe inbound side of a thread. NULL when the inbound is\nnot a threaded reply to one of your sends, when neither\nheader survived the path through intermediate MTAs, or on\ninbound received before this auto-link landed.\n"
4669
4683
  }
4670
4684
  },
4671
4685
  "required": [
@@ -5511,11 +5525,6 @@ const openapiDocument = {
5511
5525
  "format": "date-time",
5512
5526
  "description": "Timestamp of the most recent successful deploy. Null until the first deploy succeeds."
5513
5527
  },
5514
- "gateway_url": {
5515
- "type": "string",
5516
- "format": "uri",
5517
- "description": "URL the platform's webhook delivery loop posts to in order\nto invoke the function. Reference only; not directly\ncallable from outside.\n"
5518
- },
5519
5528
  "created_at": {
5520
5529
  "type": "string",
5521
5530
  "format": "date-time"
@@ -5529,7 +5538,6 @@ const openapiDocument = {
5529
5538
  "id",
5530
5539
  "name",
5531
5540
  "deploy_status",
5532
- "gateway_url",
5533
5541
  "created_at",
5534
5542
  "updated_at"
5535
5543
  ]
@@ -5556,10 +5564,6 @@ const openapiDocument = {
5556
5564
  "type": ["string", "null"],
5557
5565
  "format": "date-time"
5558
5566
  },
5559
- "gateway_url": {
5560
- "type": "string",
5561
- "format": "uri"
5562
- },
5563
5567
  "created_at": {
5564
5568
  "type": "string",
5565
5569
  "format": "date-time"
@@ -5574,7 +5578,6 @@ const openapiDocument = {
5574
5578
  "name",
5575
5579
  "code",
5576
5580
  "deploy_status",
5577
- "gateway_url",
5578
5581
  "created_at",
5579
5582
  "updated_at"
5580
5583
  ]
@@ -5612,17 +5615,12 @@ const openapiDocument = {
5612
5615
  "format": "uuid"
5613
5616
  },
5614
5617
  "name": { "type": "string" },
5615
- "deploy_status": { "$ref": "#/components/schemas/FunctionDeployStatus" },
5616
- "gateway_url": {
5617
- "type": "string",
5618
- "format": "uri"
5619
- }
5618
+ "deploy_status": { "$ref": "#/components/schemas/FunctionDeployStatus" }
5620
5619
  },
5621
5620
  "required": [
5622
5621
  "id",
5623
5622
  "name",
5624
- "deploy_status",
5625
- "gateway_url"
5623
+ "deploy_status"
5626
5624
  ]
5627
5625
  },
5628
5626
  "UpdateFunctionInput": {
@@ -6098,7 +6096,7 @@ const openapiDocument = {
6098
6096
  "key": {
6099
6097
  "type": "string",
6100
6098
  "pattern": "^[A-Z_][A-Z0-9_]*$",
6101
- "description": "Uppercase letters, digits, and underscores. Must start with\na letter or underscore. System-managed keys (e.g.\nPRIMITIVE_WEBHOOK_SECRET) are reserved.\n"
6099
+ "description": "Uppercase letters, digits, and underscores. Must start with\na letter or underscore. System-managed keys (e.g.\nPRIMITIVE_WEBHOOK_SECRET, PRIMITIVE_API_KEY, and\nPRIMITIVE_API_BASE_URL) are reserved.\n"
6102
6100
  },
6103
6101
  "value": {
6104
6102
  "type": "string",
@@ -7336,6 +7334,11 @@ const operationManifest = [
7336
7334
  "created_at"
7337
7335
  ]
7338
7336
  }
7337
+ },
7338
+ "reply_to_sent_email_id": {
7339
+ "type": ["string", "null"],
7340
+ "format": "uuid",
7341
+ "description": "The `sent_emails.id` of the outbound this inbound was a\nreply to, when resolvable. Set at inbound ingest by\nmatching the parsed In-Reply-To (or References, as a\nfallback) against `sent_emails.message_id` in the same\norg. The mirror of `sent_emails.in_reply_to_email_id` for\nthe inbound side of a thread. NULL when the inbound is\nnot a threaded reply to one of your sends, when neither\nheader survived the path through intermediate MTAs, or on\ninbound received before this auto-link landed.\n"
7339
7342
  }
7340
7343
  },
7341
7344
  "required": [
@@ -7588,6 +7591,13 @@ const operationManifest = [
7588
7591
  "required": false,
7589
7592
  "type": "string"
7590
7593
  },
7594
+ {
7595
+ "description": "Filter to inbound emails that are replies to a specific\noutbound send. The value is a `sent_emails.id` (UUID). At\ninbound ingest, Primitive matches the parsed In-Reply-To\nheader (or References as a fallback) against\n`sent_emails.message_id` in the same org and records the\nresolved id on `emails.reply_to_sent_email_id`. This filter\nis the strict-threading lookup behind `primitive chat` and\nany UI that wants to show the inbound reply to a given\nsend. NULL on inbound that isn't a threaded reply to one\nof your sends, so existing emails received before this\ningestion landed will not match.\n",
7596
+ "enum": null,
7597
+ "name": "reply_to_sent_email_id",
7598
+ "required": false,
7599
+ "type": "string"
7600
+ },
7591
7601
  {
7592
7602
  "description": "Filter by inbound email lifecycle status.",
7593
7603
  "enum": null,
@@ -8411,7 +8421,7 @@ const operationManifest = [
8411
8421
  "binaryResponse": false,
8412
8422
  "bodyRequired": true,
8413
8423
  "command": "create-function",
8414
- "description": "Creates and deploys a new function. The handler must be a single\nESM module whose default export is an object with an async\n`fetch(request, env)` method (Workers-style). The gateway\nHMAC-verifies the POST against the org's webhook secret before\ninvoking the handler; the request body parses to an\n`email.received` event (see `EmailReceivedEvent` and the\nWebhook payload section for the full schema). Code is bundled\nbefore being uploaded; ship a single self-contained file rather\nthan relying on external imports.\n\n**Code limits.** `code` is capped at 1 MiB UTF-8. `sourceMap`\n(optional) is capped at 5 MiB UTF-8, stored with each deployment\nattempt, and sent to the runtime so stack traces can resolve to\noriginal source files.\n\n**Auto-wiring.** On successful deploy, Primitive automatically\ncreates a webhook endpoint that delivers inbound mail to the\nfunction. There is nothing to configure on the Endpoints API\nfor this to work; the gateway URL returned here is for\nreference only and is not directly callable from outside.\n\n**Secrets.** New functions ship with the managed secrets\n(`PRIMITIVE_WEBHOOK_SECRET`, `PRIMITIVE_API_KEY`) already\nbound. Add user-set secrets via\n`POST /functions/{id}/secrets`; secret writes only land in the\nrunning handler on the next redeploy.\n",
8424
+ "description": "Creates and deploys a new function. The handler must be a single\nESM module whose default export is an object with an async\n`fetch(request, env)` method (Workers-style). Primitive signs\neach delivery and forwards the `Primitive-Signature` header to\nthe handler. Verify the raw request body with\n`PRIMITIVE_WEBHOOK_SECRET` before parsing JSON; after verification\nthe request body parses to an `email.received` event (see\n`EmailReceivedEvent` and the Webhook payload section for the full\nschema). Code is bundled before being uploaded; ship a single\nself-contained file rather than relying on external imports.\n\n**Code limits.** `code` is capped at 1 MiB UTF-8. `sourceMap`\n(optional) is capped at 5 MiB UTF-8, stored with each deployment\nattempt, and sent to the runtime so stack traces can resolve to\noriginal source files.\n\n**Auto-wiring.** On successful deploy, Primitive automatically\ncreates a webhook endpoint that delivers inbound mail to the\nfunction. There is nothing to configure on the Endpoints API\nfor this to work; the internal runtime URL is not returned by\nthe API and is not a customer-facing integration surface.\n\n**Secrets.** New functions ship with the managed secrets\n(`PRIMITIVE_WEBHOOK_SECRET`, `PRIMITIVE_API_KEY`,\n`PRIMITIVE_API_BASE_URL`) already bound. Add user-set secrets via\n`POST /functions/{id}/secrets`; secret writes only land in the\nrunning handler on the next redeploy.\n",
8415
8425
  "hasJsonBody": true,
8416
8426
  "method": "POST",
8417
8427
  "operationId": "createFunction",
@@ -8459,17 +8469,12 @@ const operationManifest = [
8459
8469
  "failed"
8460
8470
  ],
8461
8471
  "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"
8462
- },
8463
- "gateway_url": {
8464
- "type": "string",
8465
- "format": "uri"
8466
8472
  }
8467
8473
  },
8468
8474
  "required": [
8469
8475
  "id",
8470
8476
  "name",
8471
- "deploy_status",
8472
- "gateway_url"
8477
+ "deploy_status"
8473
8478
  ]
8474
8479
  },
8475
8480
  "sdkName": "createFunction",
@@ -8502,7 +8507,7 @@ const operationManifest = [
8502
8507
  "key": {
8503
8508
  "type": "string",
8504
8509
  "pattern": "^[A-Z_][A-Z0-9_]*$",
8505
- "description": "Uppercase letters, digits, and underscores. Must start with\na letter or underscore. System-managed keys (e.g.\nPRIMITIVE_WEBHOOK_SECRET) are reserved.\n"
8510
+ "description": "Uppercase letters, digits, and underscores. Must start with\na letter or underscore. System-managed keys (e.g.\nPRIMITIVE_WEBHOOK_SECRET, PRIMITIVE_API_KEY, and\nPRIMITIVE_API_BASE_URL) are reserved.\n"
8506
8511
  },
8507
8512
  "value": {
8508
8513
  "type": "string",
@@ -8645,10 +8650,6 @@ const operationManifest = [
8645
8650
  "type": ["string", "null"],
8646
8651
  "format": "date-time"
8647
8652
  },
8648
- "gateway_url": {
8649
- "type": "string",
8650
- "format": "uri"
8651
- },
8652
8653
  "created_at": {
8653
8654
  "type": "string",
8654
8655
  "format": "date-time"
@@ -8663,7 +8664,6 @@ const operationManifest = [
8663
8664
  "name",
8664
8665
  "code",
8665
8666
  "deploy_status",
8666
- "gateway_url",
8667
8667
  "created_at",
8668
8668
  "updated_at"
8669
8669
  ]
@@ -9209,7 +9209,7 @@ const operationManifest = [
9209
9209
  "binaryResponse": false,
9210
9210
  "bodyRequired": false,
9211
9211
  "command": "list-function-secrets",
9212
- "description": "Returns metadata for every secret bound to the function, with\nmanaged entries (provisioned by Primitive) listed first and\nuser-set entries listed alphabetically after. **Values are\nnever returned.** Secret writes are write-only.\n\nManaged entries (e.g. `PRIMITIVE_WEBHOOK_SECRET`,\n`PRIMITIVE_API_KEY`) carry a `description` instead of\n`created_at` / `updated_at`. They cannot be created, updated,\nor deleted via this API.\n",
9212
+ "description": "Returns metadata for every secret bound to the function, with\nmanaged entries (provisioned by Primitive) listed first and\nuser-set entries listed alphabetically after. **Values are\nnever returned.** Secret writes are write-only.\n\nManaged entries (e.g. `PRIMITIVE_WEBHOOK_SECRET`,\n`PRIMITIVE_API_KEY`, `PRIMITIVE_API_BASE_URL`) carry a\n`description` instead of `created_at` / `updated_at`. They\ncannot be created, updated, or deleted via this API.\n",
9213
9213
  "hasJsonBody": false,
9214
9214
  "method": "GET",
9215
9215
  "operationId": "listFunctionSecrets",
@@ -9265,7 +9265,7 @@ const operationManifest = [
9265
9265
  "binaryResponse": false,
9266
9266
  "bodyRequired": false,
9267
9267
  "command": "list-functions",
9268
- "description": "Returns every active (non-deleted) function in the org, newest\nfirst. Each entry carries the deploy status and the gateway URL\nthat the platform's webhook delivery loop posts to. To inspect\nthe source code or deploy errors, use `GET /functions/{id}`.\n",
9268
+ "description": "Returns every active (non-deleted) function in the org, newest\nfirst. Each entry carries deploy status and timestamps. To\ninspect the source code or deploy errors, use `GET /functions/{id}`.\n",
9269
9269
  "hasJsonBody": false,
9270
9270
  "method": "GET",
9271
9271
  "operationId": "listFunctions",
@@ -9302,11 +9302,6 @@ const operationManifest = [
9302
9302
  "format": "date-time",
9303
9303
  "description": "Timestamp of the most recent successful deploy. Null until the first deploy succeeds."
9304
9304
  },
9305
- "gateway_url": {
9306
- "type": "string",
9307
- "format": "uri",
9308
- "description": "URL the platform's webhook delivery loop posts to in order\nto invoke the function. Reference only; not directly\ncallable from outside.\n"
9309
- },
9310
9305
  "created_at": {
9311
9306
  "type": "string",
9312
9307
  "format": "date-time"
@@ -9320,7 +9315,6 @@ const operationManifest = [
9320
9315
  "id",
9321
9316
  "name",
9322
9317
  "deploy_status",
9323
- "gateway_url",
9324
9318
  "created_at",
9325
9319
  "updated_at"
9326
9320
  ]
@@ -9549,10 +9543,6 @@ const operationManifest = [
9549
9543
  "type": ["string", "null"],
9550
9544
  "format": "date-time"
9551
9545
  },
9552
- "gateway_url": {
9553
- "type": "string",
9554
- "format": "uri"
9555
- },
9556
9546
  "created_at": {
9557
9547
  "type": "string",
9558
9548
  "format": "date-time"
@@ -9567,7 +9557,6 @@ const operationManifest = [
9567
9557
  "name",
9568
9558
  "code",
9569
9559
  "deploy_status",
9570
- "gateway_url",
9571
9560
  "created_at",
9572
9561
  "updated_at"
9573
9562
  ]
@@ -11062,7 +11051,7 @@ function resolveConfigEnvironment(config) {
11062
11051
  } : null;
11063
11052
  }
11064
11053
  function upsertCliEnvironment(params) {
11065
- const name = normalizeCliEnvironmentName(params.environmentName ?? DEFAULT_ENVIRONMENT);
11054
+ const name = normalizeCliEnvironmentName(params.environmentName ?? "default");
11066
11055
  const existing = params.config.environments[name] ?? {};
11067
11056
  const nextHeaders = { ...existing.headers ?? {} };
11068
11057
  for (const assignment of params.headers ?? []) {
@@ -11139,6 +11128,7 @@ function resolveCliApiRequestConfig(params) {
11139
11128
  const currentEnvironment = resolveConfigEnvironment(loadCliConfig(params.configDir));
11140
11129
  const configuredApiBaseUrl1 = currentEnvironment?.config.api_base_url_1;
11141
11130
  const configuredApiBaseUrl2 = currentEnvironment?.config.api_base_url_2;
11131
+ if (currentEnvironment !== null && currentEnvironment.name !== "default" && params.apiBaseUrl1 === void 0 && configuredApiBaseUrl1 === void 0) throw new Errors.CLIError(`The active Primitive CLI environment \`${currentEnvironment.name}\` does not specify an api_base_url_1. Set one with \`primitive config set --environment ${currentEnvironment.name} --api-base-url-1 https://...\`, or switch to a different environment with \`primitive config use <name>\`. Refusing to fall back to the production default for a non-default environment.`, { exit: 1 });
11142
11132
  const apiBaseUrl1 = params.apiBaseUrl1 !== void 0 ? normalizeApiBaseUrl1(params.apiBaseUrl1) : configuredApiBaseUrl1;
11143
11133
  const apiBaseUrl2 = params.apiBaseUrl2 !== void 0 ? normalizeApiBaseUrl2(params.apiBaseUrl2) : configuredApiBaseUrl2;
11144
11134
  return {
@@ -11410,26 +11400,26 @@ function coerceParameterValue(parameter, value) {
11410
11400
  if (typeof value === "boolean" || typeof value === "number" || typeof value === "string") return value;
11411
11401
  throw new Errors.CLIError(`Unsupported flag value for --${parameter.name}`);
11412
11402
  }
11413
- function cliError$5(message) {
11403
+ function cliError$6(message) {
11414
11404
  return new Errors.CLIError(message, { exit: 1 });
11415
11405
  }
11416
11406
  function parseJson(source, flagLabel) {
11417
11407
  try {
11418
11408
  return JSON.parse(source);
11419
11409
  } catch (error) {
11420
- throw cliError$5(`${flagLabel} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
11410
+ throw cliError$6(`${flagLabel} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
11421
11411
  }
11422
11412
  }
11423
11413
  function readJsonBody(flags) {
11424
11414
  const bodyFile = flags["body-file"];
11425
11415
  const rawBody = flags["raw-body"];
11426
- if (bodyFile && rawBody) throw cliError$5("Use either --raw-body or --body-file, not both");
11416
+ if (bodyFile && rawBody) throw cliError$6("Use either --raw-body or --body-file, not both");
11427
11417
  if (typeof bodyFile === "string") {
11428
11418
  let contents;
11429
11419
  try {
11430
11420
  contents = readFileSync(bodyFile, "utf8");
11431
11421
  } catch (error) {
11432
- throw cliError$5(`Could not read --body-file ${bodyFile}: ${error instanceof Error ? error.message : String(error)}`);
11422
+ throw cliError$6(`Could not read --body-file ${bodyFile}: ${error instanceof Error ? error.message : String(error)}`);
11433
11423
  }
11434
11424
  return parseJson(contents, `--body-file ${bodyFile}`);
11435
11425
  }
@@ -11439,7 +11429,7 @@ function readTextFileFlag(path, flagLabel) {
11439
11429
  try {
11440
11430
  return readFileSync(path, "utf8");
11441
11431
  } catch (error) {
11442
- throw cliError$5(`Could not read ${flagLabel} ${path}: ${error instanceof Error ? error.message : String(error)}`);
11432
+ throw cliError$6(`Could not read ${flagLabel} ${path}: ${error instanceof Error ? error.message : String(error)}`);
11443
11433
  }
11444
11434
  }
11445
11435
  function extractErrorPayload(raw) {
@@ -11504,15 +11494,27 @@ function writeErrorWithHints(payload) {
11504
11494
  }
11505
11495
  if (code in NETWORK_ERROR_HINTS) process.stderr.write(`${NETWORK_ERROR_HINTS[code]}\n`);
11506
11496
  }
11507
- function removeStaleSavedCredentialOnUnauthorized(params) {
11508
- if (extractErrorCode(params.payload) !== API_ERROR_CODES.unauthorized || params.auth.source !== "stored") return false;
11497
+ /**
11498
+ * Surface a user-facing hint when a request comes back unauthorized.
11499
+ *
11500
+ * Deliberately does NOT mutate the saved credentials.json. Auto-
11501
+ * deleting on any 401 made transient rejections look like permanent
11502
+ * credential failures and forced unnecessary re-login cycles; we
11503
+ * surface a hint and let the user decide whether to re-authenticate.
11504
+ *
11505
+ * The one legitimate auto-delete case lives in `primitive login`'s
11506
+ * `checkExistingLogin`: the user has explicitly asked to log in,
11507
+ * existing credentials are probed, and if they fail we clean up
11508
+ * before minting a new key. That path calls `deleteCliCredentials`
11509
+ * directly rather than going through this function.
11510
+ */
11511
+ function surfaceUnauthorizedHint(params) {
11512
+ if (extractErrorCode(params.payload) !== API_ERROR_CODES.unauthorized || params.auth.source !== "stored") return;
11509
11513
  if (params.baseUrlOverridden && params.auth.credentials !== null && params.auth.apiBaseUrl1 !== params.auth.credentials.api_base_url_1) {
11510
- process.stderr.write("Saved Primitive CLI credentials were rejected by the overridden API base URL. The local credential was not removed; unset PRIMITIVE_API_BASE_URL_1, run `primitive config reset` to clear configured URL overrides, or run `primitive logout` to remove the stored credential.\n");
11511
- return false;
11514
+ process.stderr.write("Saved Primitive CLI credentials were rejected by the overridden API base URL. The saved credential is preserved; unset PRIMITIVE_API_BASE_URL_1, run `primitive config reset` to clear configured URL overrides, or run `primitive logout` to remove the stored credential.\n");
11515
+ return;
11512
11516
  }
11513
- deleteCliCredentials(params.configDir);
11514
- process.stderr.write("Removed saved Primitive CLI credentials because the backing API key is no longer valid. Run `primitive login` to create a new one.\n");
11515
- return true;
11517
+ process.stderr.write("Your saved Primitive CLI credential was rejected. If the command was working a moment ago, please retry; brief retries often clear transient rejections. If it keeps failing, run `primitive logout && primitive login` to mint a fresh credential.\n");
11516
11518
  }
11517
11519
  function formatElapsed(ms) {
11518
11520
  const seconds = ms / 1e3;
@@ -11676,7 +11678,7 @@ function createOperationCommand(operation) {
11676
11678
  if (result.error) {
11677
11679
  const errorPayload = extractErrorPayload(result.error);
11678
11680
  writeErrorWithHints(errorPayload);
11679
- removeStaleSavedCredentialOnUnauthorized({
11681
+ surfaceUnauthorizedHint({
11680
11682
  auth,
11681
11683
  baseUrlOverridden,
11682
11684
  configDir: this.config.configDir,
@@ -11737,6 +11739,384 @@ function canonicalizeCliReferences(description) {
11737
11739
  return description.replaceAll("`primitive emails:latest`", "`primitive emails latest`").replaceAll("`primitive describe emails:get-email | jq '.responseSchema.properties'`", "`primitive describe emails:get | jq '.responseSchema.properties'`");
11738
11740
  }
11739
11741
  //#endregion
11742
+ //#region src/oclif/outbound-defaults.ts
11743
+ const SUBJECT_MAX_LENGTH = 200;
11744
+ function deriveSubject(body) {
11745
+ for (const line of body.split("\n")) {
11746
+ const trimmed = line.trim();
11747
+ if (!trimmed) continue;
11748
+ return trimmed.length > SUBJECT_MAX_LENGTH ? `${trimmed.slice(0, SUBJECT_MAX_LENGTH - 3)}...` : trimmed;
11749
+ }
11750
+ return "Message";
11751
+ }
11752
+ function isVerifiedDomain(domain) {
11753
+ return domain.is_active === true;
11754
+ }
11755
+ async function pickDefaultFromAddress(apiClient, authFailureContext) {
11756
+ const result = await listDomains({
11757
+ client: apiClient.client,
11758
+ responseStyle: "fields"
11759
+ });
11760
+ if (result.error) {
11761
+ const errorPayload = extractErrorPayload(result.error);
11762
+ if (extractErrorCode(errorPayload) === API_ERROR_CODES.unauthorized) {
11763
+ writeErrorWithHints(errorPayload);
11764
+ surfaceUnauthorizedHint({
11765
+ ...authFailureContext,
11766
+ payload: errorPayload
11767
+ });
11768
+ throw new Errors.CLIError("Cannot send: API key is missing or invalid (see hint above).", { exit: 1 });
11769
+ }
11770
+ throw new Errors.CLIError(`Could not look up your verified domains to default --from. Pass --from explicitly. Underlying error: ${formatErrorPayload(errorPayload)}`);
11771
+ }
11772
+ const first = result.data?.data?.find(isVerifiedDomain);
11773
+ if (!first) throw new Errors.CLIError("No active verified outbound domain found on this account; pass --from explicitly. To set up outbound, claim a domain via `primitive domains add` and verify it.");
11774
+ return `agent@${first.domain}`;
11775
+ }
11776
+ //#endregion
11777
+ //#region src/oclif/commands/emails-poll.ts
11778
+ function quoteDslValue(value) {
11779
+ if (/^[^\s"]+$/.test(value)) return value;
11780
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
11781
+ }
11782
+ function combineQ(q, domain) {
11783
+ const parts = [q?.trim(), domain ? `domain:${quoteDslValue(domain.trim())}` : void 0].filter((part) => Boolean(part));
11784
+ return parts.length > 0 ? parts.join(" ") : void 0;
11785
+ }
11786
+ function normalizeIsoDate(value, label) {
11787
+ const parsed = new Date(value);
11788
+ if (Number.isNaN(parsed.getTime())) throw new Error(`${label} must be a valid date or ISO-8601 timestamp.`);
11789
+ return parsed.toISOString();
11790
+ }
11791
+ function filtersFromFlags(flags) {
11792
+ return {
11793
+ body: flags.body,
11794
+ domain: flags.domain,
11795
+ domainId: flags["domain-id"],
11796
+ from: flags.from,
11797
+ hasAttachment: flags["has-attachment"],
11798
+ q: flags.q,
11799
+ replyToSentEmailId: flags["reply-to-sent-email-id"],
11800
+ spamScoreGte: flags["spam-score-gte"],
11801
+ spamScoreLt: flags["spam-score-lt"],
11802
+ subject: flags.subject,
11803
+ to: flags.to
11804
+ };
11805
+ }
11806
+ function sinceFromFlags(flags) {
11807
+ if (flags.since) return normalizeIsoDate(flags.since, "--since");
11808
+ return flags["include-existing"] ? void 0 : (/* @__PURE__ */ new Date()).toISOString();
11809
+ }
11810
+ function buildEmailSearchQuery(params) {
11811
+ const query = {
11812
+ include_facets: "false",
11813
+ limit: params.pageSize,
11814
+ snippet: "false",
11815
+ sort: "received_at_asc"
11816
+ };
11817
+ const q = combineQ(params.filters.q, params.filters.domain);
11818
+ if (q) query.q = q;
11819
+ if (params.filters.body) query.body = params.filters.body;
11820
+ if (params.filters.domainId) query.domain_id = params.filters.domainId;
11821
+ if (params.filters.from) query.from = params.filters.from;
11822
+ if (params.filters.hasAttachment !== void 0) query.has_attachment = params.filters.hasAttachment ? "true" : "false";
11823
+ if (params.filters.spamScoreGte !== void 0) query.spam_score_gte = params.filters.spamScoreGte;
11824
+ if (params.filters.spamScoreLt !== void 0) query.spam_score_lt = params.filters.spamScoreLt;
11825
+ if (params.filters.replyToSentEmailId) query.reply_to_sent_email_id = params.filters.replyToSentEmailId;
11826
+ if (params.filters.subject) query.subject = params.filters.subject;
11827
+ if (params.filters.to) query.to = params.filters.to;
11828
+ if (params.since) query.date_from = params.since;
11829
+ if (params.cursor) query.cursor = params.cursor;
11830
+ return query;
11831
+ }
11832
+ function encodeReceivedAtSearchCursor(email) {
11833
+ const raw = `r|${new Date(email.received_at).toISOString()}|${email.id}`;
11834
+ return Buffer.from(raw, "utf8").toString("base64url");
11835
+ }
11836
+ function cursorFromRows(rows) {
11837
+ const last = rows.at(-1);
11838
+ return last ? encodeReceivedAtSearchCursor(last) : null;
11839
+ }
11840
+ function collectNewAcceptedEmails(rows, seenIds) {
11841
+ const fresh = [];
11842
+ for (const row of rows) {
11843
+ if (row.status !== "accepted" && row.status !== "completed") continue;
11844
+ if (seenIds.has(row.id)) continue;
11845
+ seenIds.add(row.id);
11846
+ fresh.push(row);
11847
+ }
11848
+ return fresh;
11849
+ }
11850
+ async function fetchEmailSearchPage(params) {
11851
+ const result = await searchEmails({
11852
+ client: params.apiClient.client,
11853
+ query: buildEmailSearchQuery({
11854
+ cursor: params.cursor,
11855
+ filters: params.filters,
11856
+ pageSize: params.pageSize,
11857
+ since: params.since
11858
+ }),
11859
+ responseStyle: "fields"
11860
+ });
11861
+ if (result.error) return {
11862
+ ok: false,
11863
+ error: result.error
11864
+ };
11865
+ const envelope = result.data;
11866
+ const rows = envelope?.data ?? [];
11867
+ return {
11868
+ ok: true,
11869
+ cursor: envelope?.meta.cursor ?? cursorFromRows(rows),
11870
+ rows
11871
+ };
11872
+ }
11873
+ function sleep$1(ms) {
11874
+ return new Promise((resolve) => setTimeout(resolve, ms));
11875
+ }
11876
+ //#endregion
11877
+ //#region src/oclif/commands/chat.ts
11878
+ const DEFAULT_CHAT_TIMEOUT_SECONDS = 120;
11879
+ const DEFAULT_STRICT_PHASE_SECONDS = 60;
11880
+ function cliError$5(message) {
11881
+ return new Errors.CLIError(message, { exit: 1 });
11882
+ }
11883
+ async function readStdinToString() {
11884
+ if (process.stdin.isTTY) throw cliError$5("No message provided. Pass the message as the second positional argument or pipe it via stdin.");
11885
+ const chunks = [];
11886
+ for await (const chunk of process.stdin) chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
11887
+ return Buffer.concat(chunks).toString("utf8");
11888
+ }
11889
+ var ChatCommand = class ChatCommand extends Command {
11890
+ static description = `Send a message to an address and wait for the reply.
11891
+
11892
+ This is the first-party verb for talking to agents that live behind
11893
+ email addresses. \`primitive send\` is transport (fire-and-forget);
11894
+ \`primitive chat\` is semantic (send + wait for the threaded reply).
11895
+
11896
+ The message body can be given as the second positional argument or
11897
+ piped via stdin. The reply body is written to stdout; --json emits a
11898
+ structured envelope with both sides of the exchange.
11899
+
11900
+ Matching the reply: the wait phase polls inbound mail filtered by
11901
+ the recipient as sender and the send time as a lower bound. The
11902
+ first match is taken; the full inbound row is then fetched for the
11903
+ body. Exits non-zero on timeout.`;
11904
+ static summary = "Chat with an agent over email (send and wait for the reply)";
11905
+ static examples = [
11906
+ "<%= config.bin %> chat help@agent.acme.dev 'how do I rotate my API key?'",
11907
+ "cat error.log | <%= config.bin %> chat help@agent.acme.dev --subject 'webhook 401s'",
11908
+ "<%= config.bin %> chat help@agent.acme.dev 'follow up question' --json",
11909
+ "<%= config.bin %> chat help@agent.acme.dev 'one more thing' --timeout 300"
11910
+ ];
11911
+ static args = {
11912
+ recipient: Args.string({
11913
+ description: "Address to chat with (e.g. help@agent.acme.dev).",
11914
+ required: true
11915
+ }),
11916
+ message: Args.string({ description: "Message body. If omitted, read from stdin." })
11917
+ };
11918
+ static flags = {
11919
+ "api-key": Flags.string({
11920
+ description: "Primitive API key (defaults to PRIMITIVE_API_KEY or saved `primitive login` credentials)",
11921
+ env: "PRIMITIVE_API_KEY"
11922
+ }),
11923
+ "api-base-url-1": Flags.string({
11924
+ description: "Override the primary API base URL. Internal testing only; not documented to customers.",
11925
+ env: "PRIMITIVE_API_BASE_URL_1",
11926
+ hidden: true
11927
+ }),
11928
+ "api-base-url-2": Flags.string({
11929
+ description: "Override the attachments-supporting send host base URL. Internal testing only; not documented to customers.",
11930
+ env: "PRIMITIVE_API_BASE_URL_2",
11931
+ hidden: true
11932
+ }),
11933
+ from: Flags.string({ description: "Sender address. Defaults to agent@<your-first-verified-outbound-domain>." }),
11934
+ subject: Flags.string({ description: "Subject line. Defaults to the first line of the message when omitted." }),
11935
+ "in-reply-to": Flags.string({ description: "Message-Id of the parent email to thread this against. Use when continuing a prior conversation from outside the CLI; for an inbound you received via Primitive, prefer `primitive reply --id <inbound-id>`." }),
11936
+ json: Flags.boolean({ description: "Emit a structured JSON envelope { sent, reply } on stdout instead of just the reply body." }),
11937
+ timeout: Flags.integer({
11938
+ default: DEFAULT_CHAT_TIMEOUT_SECONDS,
11939
+ description: "Seconds to wait for a reply before exiting non-zero; 0 waits forever.",
11940
+ min: 0
11941
+ }),
11942
+ "strict-phase-seconds": Flags.integer({
11943
+ default: DEFAULT_STRICT_PHASE_SECONDS,
11944
+ description: "Seconds to wait in strict-threading mode (filter by reply_to_sent_email_id) before falling back to time-window matching. Set to the full --timeout to disable the fallback; --strict-only is the explicit way to do that.",
11945
+ min: 1
11946
+ }),
11947
+ "strict-only": Flags.boolean({ description: "Disable the time-window fallback. Only accept inbounds whose threading headers (In-Reply-To / References) resolve to this send. Recommended when correctness matters more than success rate (e.g. agents talking to agents)." }),
11948
+ interval: Flags.integer({
11949
+ default: 2,
11950
+ description: "Seconds between polls while waiting for the reply.",
11951
+ min: 1
11952
+ }),
11953
+ "page-size": Flags.integer({
11954
+ default: 50,
11955
+ description: "Inbound emails to fetch per poll while waiting (1-100). Internal tuning knob.",
11956
+ max: 100,
11957
+ min: 1,
11958
+ hidden: true
11959
+ }),
11960
+ time: Flags.boolean({ description: TIME_FLAG_DESCRIPTION })
11961
+ };
11962
+ async run() {
11963
+ const { args, flags } = await this.parse(ChatCommand);
11964
+ const message = args.message !== void 0 && args.message !== "" ? args.message : await readStdinToString();
11965
+ if (!message.trim()) throw cliError$5("Message body is empty.");
11966
+ await runWithTiming(flags.time, async () => {
11967
+ const { apiClient, auth, baseUrlOverridden } = createAuthenticatedCliApiClient({
11968
+ apiKey: flags["api-key"],
11969
+ apiBaseUrl1: flags["api-base-url-1"],
11970
+ apiBaseUrl2: flags["api-base-url-2"],
11971
+ configDir: this.config.configDir
11972
+ });
11973
+ const authFailureContext = {
11974
+ auth,
11975
+ baseUrlOverridden,
11976
+ configDir: this.config.configDir
11977
+ };
11978
+ const from = flags.from ?? await pickDefaultFromAddress(apiClient, authFailureContext);
11979
+ const subject = flags.subject ?? deriveSubject(message);
11980
+ const sentAtIso = (/* @__PURE__ */ new Date()).toISOString();
11981
+ const sendResult = await sendEmail({
11982
+ body: {
11983
+ from,
11984
+ to: args.recipient,
11985
+ subject,
11986
+ body_text: message,
11987
+ ...flags["in-reply-to"] !== void 0 ? { in_reply_to: flags["in-reply-to"] } : {}
11988
+ },
11989
+ client: apiClient._sendClient,
11990
+ responseStyle: "fields"
11991
+ });
11992
+ if (sendResult.error) {
11993
+ const errorPayload = extractErrorPayload(sendResult.error);
11994
+ writeErrorWithHints(errorPayload);
11995
+ surfaceUnauthorizedHint({
11996
+ ...authFailureContext,
11997
+ payload: errorPayload
11998
+ });
11999
+ process.exitCode = 1;
12000
+ return;
12001
+ }
12002
+ const sent = sendResult.data?.data;
12003
+ if (!sent) throw cliError$5("Send succeeded but the API returned no data.");
12004
+ const reply = await waitForReply({
12005
+ apiClient,
12006
+ authFailureContext,
12007
+ from,
12008
+ interval: flags.interval,
12009
+ pageSize: flags["page-size"],
12010
+ recipient: args.recipient,
12011
+ sentAtIso,
12012
+ sentId: sent.id,
12013
+ strictOnly: flags["strict-only"],
12014
+ strictPhaseSeconds: flags["strict-phase-seconds"],
12015
+ timeoutSeconds: flags.timeout
12016
+ });
12017
+ if (reply === null) {
12018
+ process.stderr.write(`Timed out after ${flags.timeout}s waiting for a reply from ${args.recipient}.\n`);
12019
+ process.exitCode = 1;
12020
+ return;
12021
+ }
12022
+ if (flags.json) {
12023
+ const envelope = {
12024
+ sent,
12025
+ reply
12026
+ };
12027
+ this.log(JSON.stringify(envelope, null, 2));
12028
+ } else {
12029
+ const body = reply.body_text ?? reply.body_html ?? "";
12030
+ this.log(body);
12031
+ }
12032
+ });
12033
+ }
12034
+ };
12035
+ async function waitForReply(params) {
12036
+ const totalDeadline = params.timeoutSeconds === 0 ? null : Date.now() + params.timeoutSeconds * 1e3;
12037
+ const strictDeadlineFromBudget = Date.now() + params.strictPhaseSeconds * 1e3;
12038
+ const strictDeadline = params.strictOnly ? totalDeadline : totalDeadline === null ? strictDeadlineFromBudget : Math.min(strictDeadlineFromBudget, totalDeadline);
12039
+ const phases = [{
12040
+ label: "strict",
12041
+ filters: { replyToSentEmailId: params.sentId },
12042
+ deadline: strictDeadline
12043
+ }];
12044
+ if (!params.strictOnly) phases.push({
12045
+ label: "fallback",
12046
+ filters: {
12047
+ from: params.recipient,
12048
+ to: params.from
12049
+ },
12050
+ deadline: totalDeadline
12051
+ });
12052
+ let strictFilterUnsupported = false;
12053
+ for (const phase of phases) {
12054
+ if (phase.label === "strict" && strictFilterUnsupported) continue;
12055
+ const seenIds = /* @__PURE__ */ new Set();
12056
+ let cursor = null;
12057
+ while (true) {
12058
+ if (phase.deadline !== null && Date.now() >= phase.deadline) break;
12059
+ const page = await fetchEmailSearchPage({
12060
+ apiClient: params.apiClient,
12061
+ cursor,
12062
+ filters: phase.filters,
12063
+ pageSize: params.pageSize,
12064
+ since: params.sentAtIso
12065
+ });
12066
+ if (!page.ok) {
12067
+ const payload = extractErrorPayload(page.error);
12068
+ writeErrorWithHints(payload);
12069
+ surfaceUnauthorizedHint({
12070
+ ...params.authFailureContext,
12071
+ payload
12072
+ });
12073
+ throw new Errors.CLIError("Failed to poll for reply.", { exit: 1 });
12074
+ }
12075
+ let lastAccepted;
12076
+ for (let i = page.rows.length - 1; i >= 0; i--) {
12077
+ const row = page.rows[i];
12078
+ if (row.status === "accepted" || row.status === "completed") {
12079
+ lastAccepted = row;
12080
+ break;
12081
+ }
12082
+ }
12083
+ if (lastAccepted) cursor = encodeReceivedAtSearchCursor(lastAccepted);
12084
+ const matches = collectNewAcceptedEmails(page.rows, seenIds);
12085
+ for (const match of matches) {
12086
+ const full = await getEmail({
12087
+ client: params.apiClient.client,
12088
+ path: { id: match.id },
12089
+ responseStyle: "fields"
12090
+ });
12091
+ if (full.error) {
12092
+ const payload = extractErrorPayload(full.error);
12093
+ writeErrorWithHints(payload);
12094
+ surfaceUnauthorizedHint({
12095
+ ...params.authFailureContext,
12096
+ payload
12097
+ });
12098
+ throw new Errors.CLIError(`Reply landed but fetching the full body failed (id=${match.id}).`, { exit: 1 });
12099
+ }
12100
+ const envelope = full.data;
12101
+ const detail = envelope?.data ?? envelope ?? null;
12102
+ if (!detail) throw new Errors.CLIError(`Reply landed but the email body could not be loaded (id=${match.id}).`, { exit: 1 });
12103
+ if (phase.label === "strict" && detail.reply_to_sent_email_id !== params.sentId) {
12104
+ if (!strictFilterUnsupported) process.stderr.write(params.strictOnly ? "Strict-phase reply matching is not supported by this Primitive API host; --strict-only requires server support so the command will exit without a match.\n" : "Strict-phase reply matching is not supported by this Primitive API host; falling back to time-window matching.\n");
12105
+ strictFilterUnsupported = true;
12106
+ continue;
12107
+ }
12108
+ return detail;
12109
+ }
12110
+ if (strictFilterUnsupported && phase.label === "strict") break;
12111
+ if (lastAccepted !== void 0) continue;
12112
+ if (phase.deadline !== null && Date.now() >= phase.deadline) break;
12113
+ if (totalDeadline !== null && Date.now() >= totalDeadline) return null;
12114
+ await sleep$1(params.interval * 1e3);
12115
+ }
12116
+ }
12117
+ return null;
12118
+ }
12119
+ //#endregion
11740
12120
  //#region src/oclif/commands/config.ts
11741
12121
  function loadOrCreateConfig(configDir) {
11742
12122
  return loadCliConfig(configDir) ?? emptyCliConfig();
@@ -12197,7 +12577,7 @@ var EmailsLatestCommand = class EmailsLatestCommand extends Command {
12197
12577
  if (result.error) {
12198
12578
  const errorPayload = extractErrorPayload(result.error);
12199
12579
  writeErrorWithHints(errorPayload);
12200
- removeStaleSavedCredentialOnUnauthorized({
12580
+ surfaceUnauthorizedHint({
12201
12581
  auth,
12202
12582
  baseUrlOverridden,
12203
12583
  configDir: this.config.configDir,
@@ -12223,104 +12603,6 @@ var EmailsLatestCommand = class EmailsLatestCommand extends Command {
12223
12603
  }
12224
12604
  };
12225
12605
  //#endregion
12226
- //#region src/oclif/commands/emails-poll.ts
12227
- function quoteDslValue(value) {
12228
- if (/^[^\s"]+$/.test(value)) return value;
12229
- return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
12230
- }
12231
- function combineQ(q, domain) {
12232
- const parts = [q?.trim(), domain ? `domain:${quoteDslValue(domain.trim())}` : void 0].filter((part) => Boolean(part));
12233
- return parts.length > 0 ? parts.join(" ") : void 0;
12234
- }
12235
- function normalizeIsoDate(value, label) {
12236
- const parsed = new Date(value);
12237
- if (Number.isNaN(parsed.getTime())) throw new Error(`${label} must be a valid date or ISO-8601 timestamp.`);
12238
- return parsed.toISOString();
12239
- }
12240
- function filtersFromFlags(flags) {
12241
- return {
12242
- body: flags.body,
12243
- domain: flags.domain,
12244
- domainId: flags["domain-id"],
12245
- from: flags.from,
12246
- hasAttachment: flags["has-attachment"],
12247
- q: flags.q,
12248
- spamScoreGte: flags["spam-score-gte"],
12249
- spamScoreLt: flags["spam-score-lt"],
12250
- subject: flags.subject,
12251
- to: flags.to
12252
- };
12253
- }
12254
- function sinceFromFlags(flags) {
12255
- if (flags.since) return normalizeIsoDate(flags.since, "--since");
12256
- return flags["include-existing"] ? void 0 : (/* @__PURE__ */ new Date()).toISOString();
12257
- }
12258
- function buildEmailSearchQuery(params) {
12259
- const query = {
12260
- include_facets: "false",
12261
- limit: params.pageSize,
12262
- snippet: "false",
12263
- sort: "received_at_asc"
12264
- };
12265
- const q = combineQ(params.filters.q, params.filters.domain);
12266
- if (q) query.q = q;
12267
- if (params.filters.body) query.body = params.filters.body;
12268
- if (params.filters.domainId) query.domain_id = params.filters.domainId;
12269
- if (params.filters.from) query.from = params.filters.from;
12270
- if (params.filters.hasAttachment !== void 0) query.has_attachment = params.filters.hasAttachment ? "true" : "false";
12271
- if (params.filters.spamScoreGte !== void 0) query.spam_score_gte = params.filters.spamScoreGte;
12272
- if (params.filters.spamScoreLt !== void 0) query.spam_score_lt = params.filters.spamScoreLt;
12273
- if (params.filters.subject) query.subject = params.filters.subject;
12274
- if (params.filters.to) query.to = params.filters.to;
12275
- if (params.since) query.date_from = params.since;
12276
- if (params.cursor) query.cursor = params.cursor;
12277
- return query;
12278
- }
12279
- function encodeReceivedAtSearchCursor(email) {
12280
- const raw = `r|${new Date(email.received_at).toISOString()}|${email.id}`;
12281
- return Buffer.from(raw, "utf8").toString("base64url");
12282
- }
12283
- function cursorFromRows(rows) {
12284
- const last = rows.at(-1);
12285
- return last ? encodeReceivedAtSearchCursor(last) : null;
12286
- }
12287
- function collectNewAcceptedEmails(rows, seenIds) {
12288
- const fresh = [];
12289
- for (const row of rows) {
12290
- if (row.status !== "accepted" && row.status !== "completed") continue;
12291
- if (seenIds.has(row.id)) continue;
12292
- seenIds.add(row.id);
12293
- fresh.push(row);
12294
- }
12295
- return fresh;
12296
- }
12297
- async function fetchEmailSearchPage(params) {
12298
- const result = await searchEmails({
12299
- client: params.apiClient.client,
12300
- query: buildEmailSearchQuery({
12301
- cursor: params.cursor,
12302
- filters: params.filters,
12303
- pageSize: params.pageSize,
12304
- since: params.since
12305
- }),
12306
- responseStyle: "fields"
12307
- });
12308
- if (result.error) return {
12309
- ok: false,
12310
- error: result.error
12311
- };
12312
- const envelope = result.data;
12313
- const rows = envelope?.data ?? [];
12314
- return {
12315
- ok: true,
12316
- cursor: envelope?.meta.cursor ?? cursorFromRows(rows),
12317
- rows
12318
- };
12319
- }
12320
- function sleep$1(ms) {
12321
- return new Promise((resolve) => setTimeout(resolve, ms));
12322
- }
12323
- //#endregion
12324
12606
  //#region src/oclif/commands/emails-wait.ts
12325
12607
  const DEFAULT_WAIT_TIMEOUT_SECONDS$1 = 300;
12326
12608
  function cliError$4(message) {
@@ -12373,6 +12655,7 @@ var EmailsWaitCommand = class EmailsWaitCommand extends Command {
12373
12655
  min: 1
12374
12656
  }),
12375
12657
  q: Flags.string({ description: "Full-text search DSL query" }),
12658
+ "reply-to-sent-email-id": Flags.string({ description: "Filter to inbound emails that are threaded replies to a specific outbound send (UUID from a /v1/send-mail response). Combine with --to and --since for the strictest version of the wait-for-reply pattern." }),
12376
12659
  since: Flags.string({ description: "Only match emails received on or after this date/time" }),
12377
12660
  "spam-score-gte": Flags.integer({ description: "Only match emails with spam score greater than or equal to this value" }),
12378
12661
  "spam-score-lt": Flags.integer({ description: "Only match emails with spam score below this value" }),
@@ -12417,7 +12700,7 @@ var EmailsWaitCommand = class EmailsWaitCommand extends Command {
12417
12700
  if (!page.ok) {
12418
12701
  const payload = extractErrorPayload(page.error);
12419
12702
  writeErrorWithHints(payload);
12420
- removeStaleSavedCredentialOnUnauthorized({
12703
+ surfaceUnauthorizedHint({
12421
12704
  auth,
12422
12705
  baseUrlOverridden,
12423
12706
  configDir: this.config.configDir,
@@ -12539,7 +12822,7 @@ var EmailsWatchCommand = class EmailsWatchCommand extends Command {
12539
12822
  if (!page.ok) {
12540
12823
  const payload = extractErrorPayload(page.error);
12541
12824
  writeErrorWithHints(payload);
12542
- removeStaleSavedCredentialOnUnauthorized({
12825
+ surfaceUnauthorizedHint({
12543
12826
  auth,
12544
12827
  baseUrlOverridden,
12545
12828
  configDir: this.config.configDir,
@@ -12593,7 +12876,6 @@ function toDeployWaitSnapshot(value) {
12593
12876
  ...value.deploy_error !== void 0 ? { deploy_error: value.deploy_error } : {},
12594
12877
  deploy_status: value.deploy_status,
12595
12878
  ...value.deployed_at !== void 0 ? { deployed_at: value.deployed_at } : {},
12596
- gateway_url: value.gateway_url,
12597
12879
  id: value.id,
12598
12880
  name: value.name,
12599
12881
  ...value.updated_at !== void 0 ? { updated_at: value.updated_at } : {}
@@ -13261,7 +13543,7 @@ var FunctionsDeployCommand = class FunctionsDeployCommand extends Command {
13261
13543
  process.stderr.write(`Function ${outcome.created.name} (${outcome.created.id}) was created and secrets [${succeeded}] were written, but the final redeploy failed; the new bindings are NOT yet live. Re-run \`primitive functions redeploy --id ${outcome.created.id} --file <bundle>\` once the cause is fixed.\n`);
13262
13544
  }
13263
13545
  writeErrorWithHints(outcome.payload);
13264
- removeStaleSavedCredentialOnUnauthorized({
13546
+ surfaceUnauthorizedHint({
13265
13547
  ...authFailureContext,
13266
13548
  payload: outcome.payload
13267
13549
  });
@@ -13284,7 +13566,7 @@ var FunctionsDeployCommand = class FunctionsDeployCommand extends Command {
13284
13566
  });
13285
13567
  if (waitResult.kind === "error") {
13286
13568
  writeErrorWithHints(waitResult.payload);
13287
- removeStaleSavedCredentialOnUnauthorized({
13569
+ surfaceUnauthorizedHint({
13288
13570
  ...authFailureContext,
13289
13571
  payload: waitResult.payload
13290
13572
  });
@@ -13317,22 +13599,33 @@ const PRIMITIVE_TEAM_AUTHOR = {
13317
13599
  name: "Primitive Team",
13318
13600
  url: "https://primitive.dev"
13319
13601
  };
13320
- const SDK_VERSION_RANGE = "^0.30.3";
13321
- const CLI_VERSION_RANGE = "^0.30.3";
13602
+ const SDK_VERSION_RANGE = "^0.31.1";
13603
+ const CLI_VERSION_RANGE = "^0.31.1";
13322
13604
  const ESBUILD_VERSION_RANGE = "^0.27.0";
13323
13605
  function renderHandler() {
13324
- return `// env.PRIMITIVE_API_KEY is auto-injected by the Primitive Functions runtime.
13606
+ return `// env.PRIMITIVE_API_KEY, env.PRIMITIVE_WEBHOOK_SECRET, and
13607
+ // env.PRIMITIVE_API_BASE_URL are auto-injected by the Primitive Functions runtime.
13325
13608
  //
13326
13609
  // Imports are taken from the \`/api\` subpath, NOT the package root.
13327
13610
  // The root export pulls in webhook signing helpers that depend on
13328
13611
  // \`node:crypto\`, which breaks Workers-style bundles. The \`/api\`
13329
- // subpath is Workers-safe and exposes everything a handler needs.
13612
+ // subpath is Workers-safe and exposes everything a handler needs,
13613
+ // including Web Crypto signature verification.
13330
13614
  import {
13331
13615
  createPrimitiveClient,
13332
13616
  normalizeReceivedEmail,
13617
+ PRIMITIVE_SIGNATURE_HEADER,
13333
13618
  type EmailReceivedEvent,
13619
+ verifyWebhookSignature,
13620
+ WebhookVerificationError,
13334
13621
  } from "@primitivedotdev/sdk/api";
13335
13622
 
13623
+ interface Env {
13624
+ PRIMITIVE_API_KEY: string;
13625
+ PRIMITIVE_API_BASE_URL: string;
13626
+ PRIMITIVE_WEBHOOK_SECRET: string;
13627
+ }
13628
+
13336
13629
  // Loop-protection knob. Only used by the isLoop helper below; the
13337
13630
  // handler's outbound reply address is server-defaulted (no
13338
13631
  // from-address parameter is passed to client.reply). Update this if
@@ -13382,10 +13675,26 @@ export function isLoop(event: EmailReceivedEvent): boolean {
13382
13675
  export default {
13383
13676
  async fetch(
13384
13677
  req: Request,
13385
- env: { PRIMITIVE_API_KEY: string },
13678
+ env: Env,
13386
13679
  ): Promise<Response> {
13387
13680
  try {
13388
- const event = (await req.json()) as EmailReceivedEvent;
13681
+ const rawBody = await req.text();
13682
+ const signatureHeader = req.headers.get(PRIMITIVE_SIGNATURE_HEADER) ?? "";
13683
+
13684
+ try {
13685
+ await verifyWebhookSignature({
13686
+ rawBody,
13687
+ signatureHeader,
13688
+ secret: env.PRIMITIVE_WEBHOOK_SECRET,
13689
+ });
13690
+ } catch (signatureError) {
13691
+ if (signatureError instanceof WebhookVerificationError) {
13692
+ return new Response("invalid signature", { status: 401 });
13693
+ }
13694
+ throw signatureError;
13695
+ }
13696
+
13697
+ const event = JSON.parse(rawBody) as EmailReceivedEvent;
13389
13698
 
13390
13699
  // Only "email.received" exists today. Future event types will
13391
13700
  // arrive with a different discriminator; return 2xx so the
@@ -13395,15 +13704,17 @@ export default {
13395
13704
  return Response.json({ ok: true, skipped: event.event });
13396
13705
  }
13397
13706
 
13398
- // Loop protection runs immediately after the event-type check
13399
- // (the gateway has already HMAC-verified the request before it
13400
- // reaches this handler). See isLoop above for what's covered and
13401
- // how to extend it.
13707
+ // Loop protection runs immediately after signature verification
13708
+ // and the event-type check. See isLoop above for what's covered
13709
+ // and how to extend it.
13402
13710
  if (isLoop(event)) {
13403
13711
  return Response.json({ ok: true, skipped: "loop" });
13404
13712
  }
13405
13713
 
13406
- const client = createPrimitiveClient({ apiKey: env.PRIMITIVE_API_KEY });
13714
+ const client = createPrimitiveClient({
13715
+ apiKey: env.PRIMITIVE_API_KEY,
13716
+ apiBaseUrl1: env.PRIMITIVE_API_BASE_URL,
13717
+ });
13407
13718
 
13408
13719
  // Recipient gate
13409
13720
  // https://www.primitive.dev/docs/sending#who-you-can-send-to
@@ -13571,7 +13882,7 @@ function renderEmailReplyTemplateFiles(name) {
13571
13882
  const FUNCTION_TEMPLATES = [{
13572
13883
  author: PRIMITIVE_TEAM_AUTHOR,
13573
13884
  dependencies: ["@primitivedotdev/sdk"],
13574
- description: "A deployable TypeScript email handler that validates email.received events, skips likely loops, and replies with the Primitive SDK.",
13885
+ description: "A deployable TypeScript email handler that verifies signed email.received events, skips likely loops, and replies with the Primitive SDK.",
13575
13886
  devDependencies: [
13576
13887
  "@primitivedotdev/cli",
13577
13888
  "esbuild",
@@ -13828,7 +14139,7 @@ var FunctionsLogsCommand = class FunctionsLogsCommand extends Command {
13828
14139
  if (result.error) {
13829
14140
  const errorPayload = extractErrorPayload(result.error);
13830
14141
  writeErrorWithHints(errorPayload);
13831
- removeStaleSavedCredentialOnUnauthorized({
14142
+ surfaceUnauthorizedHint({
13832
14143
  auth,
13833
14144
  baseUrlOverridden,
13834
14145
  configDir: this.config.configDir,
@@ -14084,7 +14395,7 @@ var FunctionsRedeployCommand = class FunctionsRedeployCommand extends Command {
14084
14395
  process.stderr.write(`Secrets [${succeeded}] were written, but the redeploy step failed; the new bindings are NOT yet live. Re-run \`primitive functions redeploy --id ${flags.id} --file <bundle>\` once the cause is fixed.\n`);
14085
14396
  }
14086
14397
  writeErrorWithHints(outcome.payload);
14087
- removeStaleSavedCredentialOnUnauthorized({
14398
+ surfaceUnauthorizedHint({
14088
14399
  ...authFailureContext,
14089
14400
  payload: outcome.payload
14090
14401
  });
@@ -14106,7 +14417,7 @@ var FunctionsRedeployCommand = class FunctionsRedeployCommand extends Command {
14106
14417
  });
14107
14418
  if (waitResult.kind === "error") {
14108
14419
  writeErrorWithHints(waitResult.payload);
14109
- removeStaleSavedCredentialOnUnauthorized({
14420
+ surfaceUnauthorizedHint({
14110
14421
  ...authFailureContext,
14111
14422
  payload: waitResult.payload
14112
14423
  });
@@ -14307,7 +14618,7 @@ var FunctionsSetSecretCommand = class FunctionsSetSecretCommand extends Command
14307
14618
  if (outcome.stage === "get-function") process.stderr.write("Secret was written, but reading current function code for redeploy failed; the secret is NOT yet live. Re-run with --redeploy, or call `primitive functions redeploy --id <id> --file <bundle>` once you have the bundle.\n");
14308
14619
  else if (outcome.stage === "redeploy") process.stderr.write("Secret was written, but the redeploy step failed; the secret is NOT yet live. Inspect the function's deploy_error and re-run `primitive functions redeploy --id <id> --file <bundle>` once the cause is fixed.\n");
14309
14620
  writeErrorWithHints(outcome.payload);
14310
- removeStaleSavedCredentialOnUnauthorized({
14621
+ surfaceUnauthorizedHint({
14311
14622
  ...authFailureContext,
14312
14623
  payload: outcome.payload
14313
14624
  });
@@ -14495,7 +14806,7 @@ var FunctionsTestFunctionCommand = class FunctionsTestFunctionCommand extends Co
14495
14806
  if (triggerResult.error) {
14496
14807
  const payload = extractErrorPayload(triggerResult.error);
14497
14808
  writeErrorWithHints(payload);
14498
- removeStaleSavedCredentialOnUnauthorized({
14809
+ surfaceUnauthorizedHint({
14499
14810
  auth,
14500
14811
  baseUrlOverridden,
14501
14812
  configDir: this.config.configDir,
@@ -14533,7 +14844,7 @@ var FunctionsTestFunctionCommand = class FunctionsTestFunctionCommand extends Co
14533
14844
  if (!page.ok) {
14534
14845
  const payload = extractErrorPayload(page.error);
14535
14846
  writeErrorWithHints(payload);
14536
- removeStaleSavedCredentialOnUnauthorized({
14847
+ surfaceUnauthorizedHint({
14537
14848
  auth,
14538
14849
  baseUrlOverridden,
14539
14850
  configDir: this.config.configDir,
@@ -14561,7 +14872,7 @@ var FunctionsTestFunctionCommand = class FunctionsTestFunctionCommand extends Co
14561
14872
  if (result.error) {
14562
14873
  const payload = extractErrorPayload(result.error);
14563
14874
  writeErrorWithHints(payload);
14564
- removeStaleSavedCredentialOnUnauthorized({
14875
+ surfaceUnauthorizedHint({
14565
14876
  auth,
14566
14877
  baseUrlOverridden,
14567
14878
  configDir: this.config.configDir,
@@ -14641,22 +14952,17 @@ async function checkExistingLogin(params) {
14641
14952
  })))(apiClient);
14642
14953
  if (!result.error) return { status: "valid" };
14643
14954
  const payload = extractErrorPayload(result.error);
14644
- if (removeStaleSavedCredentialOnUnauthorized({
14645
- auth: {
14646
- apiKey: params.credentials.api_key,
14647
- apiBaseUrl1: probeApiBaseUrl1,
14648
- apiBaseUrl2: normalizeApiBaseUrl2(void 0),
14649
- credentials: params.credentials,
14650
- source: "stored"
14651
- },
14652
- baseUrlOverridden: requestConfig.baseUrlOverridden,
14653
- configDir: params.configDir,
14654
- payload
14655
- })) return { status: "removed_stale" };
14955
+ const code = extractErrorCode(payload);
14956
+ const baseUrlDiffersFromSaved = requestConfig.baseUrlOverridden && requestConfig.apiBaseUrl1 !== params.credentials.api_base_url_1;
14957
+ if (code === API_ERROR_CODES.unauthorized && !baseUrlDiffersFromSaved) {
14958
+ deleteCliCredentials(params.configDir);
14959
+ process.stderr.write("Removed saved Primitive CLI credentials because the existing key was rejected during login. Continuing with a fresh login.\n");
14960
+ return { status: "removed_stale" };
14961
+ }
14656
14962
  return {
14657
14963
  status: "blocked",
14658
14964
  payload,
14659
- message: extractErrorCode(payload) === API_ERROR_CODES.unauthorized ? "Saved Primitive CLI credentials were rejected. Run `primitive logout` to remove them before logging in again." : "A saved Primitive CLI login exists, but the CLI could not verify whether it is still valid. Run `primitive logout` before logging in again."
14965
+ message: code === API_ERROR_CODES.unauthorized ? "Saved Primitive CLI credentials were rejected by an API URL different from the one they were saved with. Run `primitive logout` to remove them, or switch back to the original environment before logging in again." : "A saved Primitive CLI login exists, but the CLI could not verify whether it is still valid. Run `primitive logout` before logging in again."
14660
14966
  };
14661
14967
  }
14662
14968
  var LoginCommand = class LoginCommand extends Command {
@@ -15041,7 +15347,7 @@ var ReplyCommand = class ReplyCommand extends Command {
15041
15347
  if (result.error) {
15042
15348
  const errorPayload = extractErrorPayload(result.error);
15043
15349
  writeErrorWithHints(errorPayload);
15044
- removeStaleSavedCredentialOnUnauthorized({
15350
+ surfaceUnauthorizedHint({
15045
15351
  auth,
15046
15352
  baseUrlOverridden,
15047
15353
  configDir: this.config.configDir,
@@ -15060,39 +15366,6 @@ var ReplyCommand = class ReplyCommand extends Command {
15060
15366
  };
15061
15367
  //#endregion
15062
15368
  //#region src/oclif/commands/send.ts
15063
- const SUBJECT_MAX_LENGTH = 200;
15064
- function deriveSubject(body) {
15065
- for (const line of body.split("\n")) {
15066
- const trimmed = line.trim();
15067
- if (!trimmed) continue;
15068
- return trimmed.length > SUBJECT_MAX_LENGTH ? `${trimmed.slice(0, SUBJECT_MAX_LENGTH - 3)}...` : trimmed;
15069
- }
15070
- return "Message";
15071
- }
15072
- function isVerifiedDomain(domain) {
15073
- return domain.is_active === true;
15074
- }
15075
- async function pickDefaultFromAddress(apiClient, authFailureContext) {
15076
- const result = await listDomains({
15077
- client: apiClient.client,
15078
- responseStyle: "fields"
15079
- });
15080
- if (result.error) {
15081
- const errorPayload = extractErrorPayload(result.error);
15082
- if (extractErrorCode(errorPayload) === API_ERROR_CODES.unauthorized) {
15083
- writeErrorWithHints(errorPayload);
15084
- removeStaleSavedCredentialOnUnauthorized({
15085
- ...authFailureContext,
15086
- payload: errorPayload
15087
- });
15088
- throw new Errors.CLIError("Cannot send: API key is missing or invalid (see hint above).", { exit: 1 });
15089
- }
15090
- throw new Errors.CLIError(`Could not look up your verified domains to default --from. Pass --from explicitly. Underlying error: ${formatErrorPayload(errorPayload)}`);
15091
- }
15092
- const first = result.data?.data?.find(isVerifiedDomain);
15093
- if (!first) throw new Errors.CLIError("No active verified outbound domain found on this account; pass --from explicitly. To set up outbound, claim a domain via `primitive domains add` and verify it.");
15094
- return `agent@${first.domain}`;
15095
- }
15096
15369
  var SendCommand = class SendCommand extends Command {
15097
15370
  static description = `Send an outbound email. Agent-grade shortcut for \`sending send\` with sensible defaults.
15098
15371
 
@@ -15184,7 +15457,7 @@ var SendCommand = class SendCommand extends Command {
15184
15457
  if (result.error) {
15185
15458
  const errorPayload = extractErrorPayload(result.error);
15186
15459
  writeErrorWithHints(errorPayload);
15187
- removeStaleSavedCredentialOnUnauthorized({
15460
+ surfaceUnauthorizedHint({
15188
15461
  ...authFailureContext,
15189
15462
  payload: errorPayload
15190
15463
  });
@@ -15629,7 +15902,7 @@ var WhoamiCommand = class WhoamiCommand extends Command {
15629
15902
  if (result.error) {
15630
15903
  const errorPayload = extractErrorPayload(result.error);
15631
15904
  writeErrorWithHints(errorPayload);
15632
- removeStaleSavedCredentialOnUnauthorized({
15905
+ surfaceUnauthorizedHint({
15633
15906
  auth,
15634
15907
  baseUrlOverridden,
15635
15908
  configDir: this.config.configDir,
@@ -15860,6 +16133,7 @@ const COMMANDS = {
15860
16133
  describe: DescribeCommand,
15861
16134
  send: SendCommand,
15862
16135
  reply: ReplyCommand,
16136
+ chat: ChatCommand,
15863
16137
  login: LoginCommand,
15864
16138
  signup: SignupCommand,
15865
16139
  logout: LogoutCommand,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@primitivedotdev/cli",
3
- "version": "0.30.3",
3
+ "version": "0.31.1",
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,