primitive 1.23.0 → 1.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/oclif/index.js +765 -152
  2. package/package.json +3 -3
@@ -1,14 +1,15 @@
1
1
  import { A as saveActiveChatState, C as resolveCliAuth, D as deleteChatState, E as chatStatePath, M as createClient, N as createConfig, O as loadActiveChatState, S as normalizeApiBaseUrl, T as saveSignupCredentials, _ as deleteCliCredentials, a as normalizeCliEnvironmentName, b as hasStoredCliLogin, c as resolveConfigEnvironment, d as validateCliHeaderName, f as validateCliHeaderValue, g as credentialsPath, h as credentialsLockPath, i as loadCliConfig, j as PrimitiveApiClient, k as loadChatConversationByLocalId, l as saveCliConfig, m as cliAccessTokenExpiresAt, n as deleteCliConfig, o as redactCliEnvironment, p as acquireCliCredentialsLock, r as emptyCliConfig, s as removeCliEnvironment, u as upsertCliEnvironment, v as deleteCliCredentialsLock, w as saveCliCredentials, x as loadCliCredentials, y as detectPrimitiveKeyEnvMisname } from "../cli-config-BKso8oJu.js";
2
2
  import { Args, Command, Errors, Flags, ux } from "@oclif/core";
3
- import { chmodSync, closeSync, existsSync, fsyncSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readdirSync, renameSync, rmSync, rmdirSync, statSync, unlinkSync, writeFileSync, writeSync } from "node:fs";
4
- import { createHash, randomUUID } from "node:crypto";
3
+ import { chmodSync, closeSync, constants, existsSync, fstatSync, fsyncSync, linkSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readdirSync, renameSync, rmSync, rmdirSync, statSync, unlinkSync, writeFileSync } from "node:fs";
4
+ import { createHash, randomUUID, scryptSync } from "node:crypto";
5
5
  import path, { basename, dirname, join, relative, resolve, sep, win32 } from "node:path";
6
6
  import process$1 from "node:process";
7
7
  import { createInterface } from "node:readline/promises";
8
+ import { setTimeout as setTimeout$1 } from "node:timers/promises";
8
9
  import { execFileSync, spawn } from "node:child_process";
9
10
  import { request } from "node:http";
10
11
  import { request as request$1 } from "node:https";
11
- import { performance } from "node:perf_hooks";
12
+ import { performance as performance$1 } from "node:perf_hooks";
12
13
  import { hostname } from "node:os";
13
14
  import { gunzipSync } from "node:zlib";
14
15
  import { concat, getAddress, hexToBytes, keccak256, stringToBytes } from "viem";
@@ -66,7 +67,9 @@ var sdk_gen_exports = /* @__PURE__ */ __exportAll({
66
67
  discardEmailContent: () => discardEmailContent,
67
68
  downloadAttachments: () => downloadAttachments,
68
69
  downloadDomainZoneFile: () => downloadDomainZoneFile,
70
+ downloadEmailAttachmentPart: () => downloadEmailAttachmentPart,
69
71
  downloadRawEmail: () => downloadRawEmail,
72
+ downloadSentAttachmentPart: () => downloadSentAttachmentPart,
70
73
  getAccount: () => getAccount,
71
74
  getAgent: () => getAgent,
72
75
  getChallenge: () => getChallenge,
@@ -784,6 +787,49 @@ const downloadAttachments = (options) => (options.client ?? client).get({
784
787
  ...options
785
788
  });
786
789
  /**
790
+ * Download one inbound email attachment
791
+ *
792
+ * Pending service release. This contract does not establish live availability.
793
+ * Downloads the original bytes of one ordinary email attachment, selected by
794
+ * its metadata `part_index`, not its position in an attachments array. Refresh
795
+ * the email detail after `attachment_changed` before choosing an index again.
796
+ * Uses bearer authentication only; signed raw-email download tokens are not accepted.
797
+ * A paired-agent credential may read only a message addressed to its claimed address.
798
+ * Function credentials are denied. Other bearer credentials retain their
799
+ * existing email access boundaries. This endpoint serves email bytes only.
800
+ *
801
+ */
802
+ const downloadEmailAttachmentPart = (options) => (options.client ?? client).get({
803
+ security: [{
804
+ scheme: "bearer",
805
+ type: "http"
806
+ }],
807
+ url: "/emails/{id}/attachments/{part_index}",
808
+ ...options
809
+ });
810
+ /**
811
+ * Download one sent email attachment
812
+ *
813
+ * Pending service release. This contract does not establish live availability.
814
+ * Downloads the original bytes of one ordinary email attachment, selected by
815
+ * its metadata `part_index`, not its position in an attachments array. Refresh
816
+ * the email detail after `attachment_changed` before choosing an index again.
817
+ * Uses bearer authentication only; signed raw-email download tokens are not accepted.
818
+ * A paired-agent credential may read only mail sent from its claimed address.
819
+ * Receiving a copy does not authorize downloading another sender's sent content.
820
+ * Function credentials are denied. Other bearer credentials retain their
821
+ * existing email access boundaries. This endpoint serves email bytes only.
822
+ *
823
+ */
824
+ const downloadSentAttachmentPart = (options) => (options.client ?? client).get({
825
+ security: [{
826
+ scheme: "bearer",
827
+ type: "http"
828
+ }],
829
+ url: "/sent-emails/{id}/attachments/{part_index}",
830
+ ...options
831
+ });
832
+ /**
787
833
  * Reply to an inbound email
788
834
  *
789
835
  * Sends an outbound reply to the inbound email identified by `id`.
@@ -3887,6 +3933,100 @@ const openapiDocument = {
3887
3933
  }
3888
3934
  }
3889
3935
  },
3936
+ "/emails/{id}/attachments/{part_index}": {
3937
+ "parameters": [{ "$ref": "#/components/parameters/ResourceId" }, { "$ref": "#/components/parameters/AttachmentPartIndex" }],
3938
+ "get": {
3939
+ "operationId": "downloadEmailAttachmentPart",
3940
+ "summary": "Download one inbound email attachment",
3941
+ "description": "Pending service release. This contract does not establish live availability.\nDownloads the original bytes of one ordinary email attachment, selected by\nits metadata `part_index`, not its position in an attachments array. Refresh\nthe email detail after `attachment_changed` before choosing an index again.\nUses bearer authentication only; signed raw-email download tokens are not accepted.\nA paired-agent credential may read only a message addressed to its claimed address.\nFunction credentials are denied. Other bearer credentials retain their\nexisting email access boundaries. This endpoint serves email bytes only.\n",
3942
+ "tags": ["Emails"],
3943
+ "security": [{ "BearerAuth": [] }],
3944
+ "responses": {
3945
+ "200": { "$ref": "#/components/responses/AttachmentPart" },
3946
+ "400": { "$ref": "#/components/responses/ValidationError" },
3947
+ "401": { "$ref": "#/components/responses/Unauthorized" },
3948
+ "403": { "$ref": "#/components/responses/Forbidden" },
3949
+ "404": {
3950
+ "description": "Missing or inaccessible message, or missing attachment part",
3951
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }
3952
+ },
3953
+ "409": {
3954
+ "description": "attachment_changed; refresh email detail before selecting a part again",
3955
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }
3956
+ },
3957
+ "410": {
3958
+ "description": "content_discarded; the original content is no longer available",
3959
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }
3960
+ },
3961
+ "413": {
3962
+ "description": "attachment_limit_exceeded; the attachment exceeds download limits",
3963
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }
3964
+ },
3965
+ "502": {
3966
+ "description": "attachment_integrity_failed; stored content failed integrity validation",
3967
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }
3968
+ },
3969
+ "503": {
3970
+ "description": "attachment_not_ready or attachment_storage_unavailable; retry after the indicated delay",
3971
+ "headers": { "Retry-After": {
3972
+ "description": "Delay in seconds before retrying this read",
3973
+ "schema": {
3974
+ "type": "integer",
3975
+ "minimum": 0
3976
+ }
3977
+ } },
3978
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }
3979
+ }
3980
+ }
3981
+ }
3982
+ },
3983
+ "/sent-emails/{id}/attachments/{part_index}": {
3984
+ "parameters": [{ "$ref": "#/components/parameters/ResourceId" }, { "$ref": "#/components/parameters/AttachmentPartIndex" }],
3985
+ "get": {
3986
+ "operationId": "downloadSentAttachmentPart",
3987
+ "summary": "Download one sent email attachment",
3988
+ "description": "Pending service release. This contract does not establish live availability.\nDownloads the original bytes of one ordinary email attachment, selected by\nits metadata `part_index`, not its position in an attachments array. Refresh\nthe email detail after `attachment_changed` before choosing an index again.\nUses bearer authentication only; signed raw-email download tokens are not accepted.\nA paired-agent credential may read only mail sent from its claimed address.\nReceiving a copy does not authorize downloading another sender's sent content.\nFunction credentials are denied. Other bearer credentials retain their\nexisting email access boundaries. This endpoint serves email bytes only.\n",
3989
+ "tags": ["Sending"],
3990
+ "security": [{ "BearerAuth": [] }],
3991
+ "responses": {
3992
+ "200": { "$ref": "#/components/responses/AttachmentPart" },
3993
+ "400": { "$ref": "#/components/responses/ValidationError" },
3994
+ "401": { "$ref": "#/components/responses/Unauthorized" },
3995
+ "403": { "$ref": "#/components/responses/Forbidden" },
3996
+ "404": {
3997
+ "description": "Missing or inaccessible message, or missing attachment part",
3998
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }
3999
+ },
4000
+ "409": {
4001
+ "description": "attachment_changed; refresh email detail before selecting a part again",
4002
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }
4003
+ },
4004
+ "410": {
4005
+ "description": "content_discarded; the original content is no longer available",
4006
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }
4007
+ },
4008
+ "413": {
4009
+ "description": "attachment_limit_exceeded; the attachment exceeds download limits",
4010
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }
4011
+ },
4012
+ "502": {
4013
+ "description": "attachment_integrity_failed; stored content failed integrity validation",
4014
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }
4015
+ },
4016
+ "503": {
4017
+ "description": "attachment_not_ready or attachment_storage_unavailable; retry after the indicated delay",
4018
+ "headers": { "Retry-After": {
4019
+ "description": "Delay in seconds before retrying this read",
4020
+ "schema": {
4021
+ "type": "integer",
4022
+ "minimum": 0
4023
+ }
4024
+ } },
4025
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }
4026
+ }
4027
+ }
4028
+ }
4029
+ },
3890
4030
  "/emails/{id}/reply": {
3891
4031
  "parameters": [{ "$ref": "#/components/parameters/ResourceId" }],
3892
4032
  "post": {
@@ -6570,6 +6710,18 @@ const openapiDocument = {
6570
6710
  }
6571
6711
  },
6572
6712
  "parameters": {
6713
+ "AttachmentPartIndex": {
6714
+ "name": "part_index",
6715
+ "in": "path",
6716
+ "required": true,
6717
+ "description": "The attachment metadata `part_index`, not its offset in the attachments array",
6718
+ "schema": {
6719
+ "type": "integer",
6720
+ "format": "int32",
6721
+ "minimum": 0,
6722
+ "maximum": 2147483647
6723
+ }
6724
+ },
6573
6725
  "ResourceId": {
6574
6726
  "name": "id",
6575
6727
  "in": "path",
@@ -6640,6 +6792,33 @@ const openapiDocument = {
6640
6792
  }
6641
6793
  },
6642
6794
  "responses": {
6795
+ "AttachmentPart": {
6796
+ "description": "Original attachment bytes; never a JSON envelope or a base64 string",
6797
+ "content": { "application/octet-stream": { "schema": {
6798
+ "type": "string",
6799
+ "format": "binary"
6800
+ } } },
6801
+ "headers": {
6802
+ "X-Content-SHA256": {
6803
+ "description": "SHA-256 hex digest of the original bytes",
6804
+ "schema": {
6805
+ "type": "string",
6806
+ "pattern": "^[a-fA-F0-9]{64}$"
6807
+ }
6808
+ },
6809
+ "Content-Disposition": {
6810
+ "description": "Safe attachment disposition with a sanitized filename",
6811
+ "schema": { "type": "string" }
6812
+ },
6813
+ "Cache-Control": {
6814
+ "description": "Attachment responses are private and must not be stored by caches",
6815
+ "schema": {
6816
+ "type": "string",
6817
+ "example": "private, no-store"
6818
+ }
6819
+ }
6820
+ }
6821
+ },
6643
6822
  "PullContentGone": {
6644
6823
  "description": "Queued content is no longer available",
6645
6824
  "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }
@@ -7658,6 +7837,12 @@ const openapiDocument = {
7658
7837
  "conflict",
7659
7838
  "mx_conflict",
7660
7839
  "not_scheduled",
7840
+ "attachment_changed",
7841
+ "content_discarded",
7842
+ "attachment_limit_exceeded",
7843
+ "attachment_integrity_failed",
7844
+ "attachment_not_ready",
7845
+ "attachment_storage_unavailable",
7661
7846
  "outbound_disabled",
7662
7847
  "cannot_send_from_domain",
7663
7848
  "recipient_not_allowed",
@@ -10467,6 +10652,53 @@ const openapiDocument = {
10467
10652
  "body_html": {
10468
10653
  "type": ["string", "null"],
10469
10654
  "description": "HTML body sent on the wire. Null when the send\ncarried only a plain-text body, or when bodies\nhave been discarded post-send.\n"
10655
+ },
10656
+ "attachments": {
10657
+ "type": "array",
10658
+ "description": "Metadata for submitted inline attachments; download availability is reported separately. Metadata may exist when the archive is unavailable, and legacy sends may have no metadata. Offloaded payload files are not included in the inline archive.",
10659
+ "items": {
10660
+ "type": "object",
10661
+ "description": "Metadata for one submitted inline attachment.",
10662
+ "additionalProperties": true,
10663
+ "properties": {
10664
+ "filename": { "type": ["string", "null"] },
10665
+ "content_type": { "type": "string" },
10666
+ "size_bytes": {
10667
+ "type": "integer",
10668
+ "minimum": 0,
10669
+ "maximum": 9007199254740991
10670
+ },
10671
+ "sha256": { "type": "string" },
10672
+ "part_index": {
10673
+ "type": "integer",
10674
+ "minimum": 0,
10675
+ "maximum": 9007199254740991
10676
+ },
10677
+ "tar_path": { "type": "string" }
10678
+ },
10679
+ "required": [
10680
+ "filename",
10681
+ "content_type",
10682
+ "size_bytes",
10683
+ "sha256",
10684
+ "part_index",
10685
+ "tar_path"
10686
+ ]
10687
+ }
10688
+ },
10689
+ "attachments_size_bytes": {
10690
+ "type": "integer",
10691
+ "minimum": 0,
10692
+ "maximum": 9007199254740991,
10693
+ "description": "Original decoded attachment byte total accepted for this send, not the compressed archive size. It may exceed the retained inline total after offloading."
10694
+ },
10695
+ "attachments_complete": {
10696
+ "type": "boolean",
10697
+ "description": "Whether attachments enumerates the complete original submitted attachment set, including zero-byte and inline CID parts. True requires no submitted payload-reference, offloaded, or visibility-filtered attachment to be omitted, retained content, and a published archive when the attachment inventory is nonempty. False or absent provides no completeness proof, including for legacy or discarded content. This field grants no download access and has no interaction semantics."
10698
+ },
10699
+ "attachments_download_available": {
10700
+ "type": "boolean",
10701
+ "description": "Whether an inline attachment archive has been successfully retained and is available for download. Download authorization is checked separately; address-bound agent connection keys cannot download archives."
10470
10702
  }
10471
10703
  }
10472
10704
  }]
@@ -16256,6 +16488,38 @@ const operationManifest = [
16256
16488
  "tag": "Emails",
16257
16489
  "tagCommand": "emails"
16258
16490
  },
16491
+ {
16492
+ "binaryResponse": true,
16493
+ "bodyRequired": false,
16494
+ "command": "download-email-attachment-part",
16495
+ "description": "Pending service release. This contract does not establish live availability.\nDownloads the original bytes of one ordinary email attachment, selected by\nits metadata `part_index`, not its position in an attachments array. Refresh\nthe email detail after `attachment_changed` before choosing an index again.\nUses bearer authentication only; signed raw-email download tokens are not accepted.\nA paired-agent credential may read only a message addressed to its claimed address.\nFunction credentials are denied. Other bearer credentials retain their\nexisting email access boundaries. This endpoint serves email bytes only.\n",
16496
+ "hasJsonBody": false,
16497
+ "method": "GET",
16498
+ "operationId": "downloadEmailAttachmentPart",
16499
+ "path": "/emails/{id}/attachments/{part_index}",
16500
+ "pathParams": [{
16501
+ "description": "Resource UUID",
16502
+ "enum": null,
16503
+ "name": "id",
16504
+ "required": true,
16505
+ "type": "string"
16506
+ }, {
16507
+ "description": "The attachment metadata `part_index`, not its offset in the attachments array",
16508
+ "enum": null,
16509
+ "maximum": 2147483647,
16510
+ "minimum": 0,
16511
+ "name": "part_index",
16512
+ "required": true,
16513
+ "type": "integer"
16514
+ }],
16515
+ "queryParams": [],
16516
+ "requestSchema": null,
16517
+ "responseSchema": null,
16518
+ "sdkName": "downloadEmailAttachmentPart",
16519
+ "summary": "Download one inbound email attachment",
16520
+ "tag": "Emails",
16521
+ "tagCommand": "emails"
16522
+ },
16259
16523
  {
16260
16524
  "binaryResponse": true,
16261
16525
  "bodyRequired": false,
@@ -23742,6 +24006,53 @@ const operationManifest = [
23742
24006
  "body_html": {
23743
24007
  "type": ["string", "null"],
23744
24008
  "description": "HTML body sent on the wire. Null when the send\ncarried only a plain-text body, or when bodies\nhave been discarded post-send.\n"
24009
+ },
24010
+ "attachments": {
24011
+ "type": "array",
24012
+ "description": "Metadata for submitted inline attachments; download availability is reported separately. Metadata may exist when the archive is unavailable, and legacy sends may have no metadata. Offloaded payload files are not included in the inline archive.",
24013
+ "items": {
24014
+ "type": "object",
24015
+ "description": "Metadata for one submitted inline attachment.",
24016
+ "additionalProperties": true,
24017
+ "properties": {
24018
+ "filename": { "type": ["string", "null"] },
24019
+ "content_type": { "type": "string" },
24020
+ "size_bytes": {
24021
+ "type": "integer",
24022
+ "minimum": 0,
24023
+ "maximum": 9007199254740991
24024
+ },
24025
+ "sha256": { "type": "string" },
24026
+ "part_index": {
24027
+ "type": "integer",
24028
+ "minimum": 0,
24029
+ "maximum": 9007199254740991
24030
+ },
24031
+ "tar_path": { "type": "string" }
24032
+ },
24033
+ "required": [
24034
+ "filename",
24035
+ "content_type",
24036
+ "size_bytes",
24037
+ "sha256",
24038
+ "part_index",
24039
+ "tar_path"
24040
+ ]
24041
+ }
24042
+ },
24043
+ "attachments_size_bytes": {
24044
+ "type": "integer",
24045
+ "minimum": 0,
24046
+ "maximum": 9007199254740991,
24047
+ "description": "Original decoded attachment byte total accepted for this send, not the compressed archive size. It may exceed the retained inline total after offloading."
24048
+ },
24049
+ "attachments_complete": {
24050
+ "type": "boolean",
24051
+ "description": "Whether attachments enumerates the complete original submitted attachment set, including zero-byte and inline CID parts. True requires no submitted payload-reference, offloaded, or visibility-filtered attachment to be omitted, retained content, and a published archive when the attachment inventory is nonempty. False or absent provides no completeness proof, including for legacy or discarded content. This field grants no download access and has no interaction semantics."
24052
+ },
24053
+ "attachments_download_available": {
24054
+ "type": "boolean",
24055
+ "description": "Whether an inline attachment archive has been successfully retained and is available for download. Download authorization is checked separately; address-bound agent connection keys cannot download archives."
23745
24056
  }
23746
24057
  }
23747
24058
  }]
@@ -23751,6 +24062,38 @@ const operationManifest = [
23751
24062
  "tag": "Sending",
23752
24063
  "tagCommand": "sending"
23753
24064
  },
24065
+ {
24066
+ "binaryResponse": true,
24067
+ "bodyRequired": false,
24068
+ "command": "download-sent-attachment-part",
24069
+ "description": "Pending service release. This contract does not establish live availability.\nDownloads the original bytes of one ordinary email attachment, selected by\nits metadata `part_index`, not its position in an attachments array. Refresh\nthe email detail after `attachment_changed` before choosing an index again.\nUses bearer authentication only; signed raw-email download tokens are not accepted.\nA paired-agent credential may read only mail sent from its claimed address.\nReceiving a copy does not authorize downloading another sender's sent content.\nFunction credentials are denied. Other bearer credentials retain their\nexisting email access boundaries. This endpoint serves email bytes only.\n",
24070
+ "hasJsonBody": false,
24071
+ "method": "GET",
24072
+ "operationId": "downloadSentAttachmentPart",
24073
+ "path": "/sent-emails/{id}/attachments/{part_index}",
24074
+ "pathParams": [{
24075
+ "description": "Resource UUID",
24076
+ "enum": null,
24077
+ "name": "id",
24078
+ "required": true,
24079
+ "type": "string"
24080
+ }, {
24081
+ "description": "The attachment metadata `part_index`, not its offset in the attachments array",
24082
+ "enum": null,
24083
+ "maximum": 2147483647,
24084
+ "minimum": 0,
24085
+ "name": "part_index",
24086
+ "required": true,
24087
+ "type": "integer"
24088
+ }],
24089
+ "queryParams": [],
24090
+ "requestSchema": null,
24091
+ "responseSchema": null,
24092
+ "sdkName": "downloadSentAttachmentPart",
24093
+ "summary": "Download one sent email attachment",
24094
+ "tag": "Sending",
24095
+ "tagCommand": "sending"
24096
+ },
23754
24097
  {
23755
24098
  "binaryResponse": false,
23756
24099
  "bodyRequired": false,
@@ -24219,6 +24562,53 @@ const operationManifest = [
24219
24562
  "body_html": {
24220
24563
  "type": ["string", "null"],
24221
24564
  "description": "HTML body sent on the wire. Null when the send\ncarried only a plain-text body, or when bodies\nhave been discarded post-send.\n"
24565
+ },
24566
+ "attachments": {
24567
+ "type": "array",
24568
+ "description": "Metadata for submitted inline attachments; download availability is reported separately. Metadata may exist when the archive is unavailable, and legacy sends may have no metadata. Offloaded payload files are not included in the inline archive.",
24569
+ "items": {
24570
+ "type": "object",
24571
+ "description": "Metadata for one submitted inline attachment.",
24572
+ "additionalProperties": true,
24573
+ "properties": {
24574
+ "filename": { "type": ["string", "null"] },
24575
+ "content_type": { "type": "string" },
24576
+ "size_bytes": {
24577
+ "type": "integer",
24578
+ "minimum": 0,
24579
+ "maximum": 9007199254740991
24580
+ },
24581
+ "sha256": { "type": "string" },
24582
+ "part_index": {
24583
+ "type": "integer",
24584
+ "minimum": 0,
24585
+ "maximum": 9007199254740991
24586
+ },
24587
+ "tar_path": { "type": "string" }
24588
+ },
24589
+ "required": [
24590
+ "filename",
24591
+ "content_type",
24592
+ "size_bytes",
24593
+ "sha256",
24594
+ "part_index",
24595
+ "tar_path"
24596
+ ]
24597
+ }
24598
+ },
24599
+ "attachments_size_bytes": {
24600
+ "type": "integer",
24601
+ "minimum": 0,
24602
+ "maximum": 9007199254740991,
24603
+ "description": "Original decoded attachment byte total accepted for this send, not the compressed archive size. It may exceed the retained inline total after offloading."
24604
+ },
24605
+ "attachments_complete": {
24606
+ "type": "boolean",
24607
+ "description": "Whether attachments enumerates the complete original submitted attachment set, including zero-byte and inline CID parts. True requires no submitted payload-reference, offloaded, or visibility-filtered attachment to be omitted, retained content, and a published archive when the attachment inventory is nonempty. False or absent provides no completeness proof, including for legacy or discarded content. This field grants no download access and has no interaction semantics."
24608
+ },
24609
+ "attachments_download_available": {
24610
+ "type": "boolean",
24611
+ "description": "Whether an inline attachment archive has been successfully retained and is available for download. Download authorization is checked separately; address-bound agent connection keys cannot download archives."
24222
24612
  }
24223
24613
  }
24224
24614
  }]
@@ -24943,6 +25333,53 @@ const operationManifest = [
24943
25333
  "body_html": {
24944
25334
  "type": ["string", "null"],
24945
25335
  "description": "HTML body sent on the wire. Null when the send\ncarried only a plain-text body, or when bodies\nhave been discarded post-send.\n"
25336
+ },
25337
+ "attachments": {
25338
+ "type": "array",
25339
+ "description": "Metadata for submitted inline attachments; download availability is reported separately. Metadata may exist when the archive is unavailable, and legacy sends may have no metadata. Offloaded payload files are not included in the inline archive.",
25340
+ "items": {
25341
+ "type": "object",
25342
+ "description": "Metadata for one submitted inline attachment.",
25343
+ "additionalProperties": true,
25344
+ "properties": {
25345
+ "filename": { "type": ["string", "null"] },
25346
+ "content_type": { "type": "string" },
25347
+ "size_bytes": {
25348
+ "type": "integer",
25349
+ "minimum": 0,
25350
+ "maximum": 9007199254740991
25351
+ },
25352
+ "sha256": { "type": "string" },
25353
+ "part_index": {
25354
+ "type": "integer",
25355
+ "minimum": 0,
25356
+ "maximum": 9007199254740991
25357
+ },
25358
+ "tar_path": { "type": "string" }
25359
+ },
25360
+ "required": [
25361
+ "filename",
25362
+ "content_type",
25363
+ "size_bytes",
25364
+ "sha256",
25365
+ "part_index",
25366
+ "tar_path"
25367
+ ]
25368
+ }
25369
+ },
25370
+ "attachments_size_bytes": {
25371
+ "type": "integer",
25372
+ "minimum": 0,
25373
+ "maximum": 9007199254740991,
25374
+ "description": "Original decoded attachment byte total accepted for this send, not the compressed archive size. It may exceed the retained inline total after offloading."
25375
+ },
25376
+ "attachments_complete": {
25377
+ "type": "boolean",
25378
+ "description": "Whether attachments enumerates the complete original submitted attachment set, including zero-byte and inline CID parts. True requires no submitted payload-reference, offloaded, or visibility-filtered attachment to be omitted, retained content, and a published archive when the attachment inventory is nonempty. False or absent provides no completeness proof, including for legacy or discarded content. This field grants no download access and has no interaction semantics."
25379
+ },
25380
+ "attachments_download_available": {
25381
+ "type": "boolean",
25382
+ "description": "Whether an inline attachment archive has been successfully retained and is available for download. Download authorization is checked separately; address-bound agent connection keys cannot download archives."
24946
25383
  }
24947
25384
  }
24948
25385
  }]
@@ -27828,60 +28265,54 @@ function readAttachmentFiles(paths, readFile = readFileSync) {
27828
28265
  //#endregion
27829
28266
  //#region src/oclif/chat-lock.ts
27830
28267
  /**
27831
- * Filesystem mutex around the chat-state read → POST → save sequence.
27832
- *
27833
- * Why this exists. The chat reply flow is read-modify-write across an
27834
- * RTT to the API: `loadActiveChatState` → `replyToEmail` → poll → save.
27835
- * Two concurrent invocations (e.g. the user re-runs `primitive chat
27836
- * reply` before the first cycle finishes its 5–12 s poll) read the
27837
- * same stale `last_reply_email_id` and POST to it, producing a
27838
- * duplicate /v1/emails/{id}/reply that the server deduplicates by
27839
- * content_hash. The second invocation then polls forever for a reply
27840
- * that arrived in response to the *first* send and has already been
27841
- * surfaced by the *first* invocation.
27842
- *
27843
- * The lock is per-process-config-dir, not per-conversation: holding it
27844
- * for a few seconds while one chat reply completes is a reasonable UX
27845
- * constraint and clearly explained on contention. The lock is
27846
- * re-entrant within a single Node process (ChatReplyCommand wraps
27847
- * ChatCommand internally), but rejects cross-process contention.
27848
- *
27849
- * Liveness. The lock file stores the holder's PID. On EEXIST we probe
27850
- * the holder with `process.kill(pid, 0)`; if the holder is gone (e.g.
27851
- * a previous chat invocation crashed without releasing), we steal the
27852
- * lock. This avoids needing a heartbeat or mtime-based stale check,
27853
- * either of which has its own race surface.
27854
- *
27855
- * Releases. The returned function is idempotent. Callers must call it
27856
- * in a finally block. We also register process-exit / signal handlers
27857
- * so a Ctrl-C during the poll loop still cleans up.
28268
+ * Conversation locks cover one request/reply cycle. The default lock retains
28269
+ * its historical filename and protects short shared chat-state transactions.
28270
+ * Scope names are hashed so addresses and other identifiers stay out of paths.
27858
28271
  */
27859
28272
  const LOCK_FILENAME = "chat-state.lock";
27860
- let processHolder = null;
27861
- /**
27862
- * Whether we've installed our exit / signal listeners on the
27863
- * `process` object. Done lazily on first acquire and never undone:
27864
- * adding handlers per-acquire would let them accumulate (each
27865
- * acquire registers four listeners, and `process.once` can't be
27866
- * un-once'd), which is harmless in production but produces
27867
- * `MaxListenersExceededWarning` + cascading no-op fires in tests
27868
- * that acquire/release across many `it()` blocks. Greptile P2.
27869
- *
27870
- * The handlers consult the current `processHolder` and act only if
27871
- * a lock is actually held, so leaving them installed across
27872
- * release boundaries is safe: a released or never-acquired lock
27873
- * makes them no-ops.
27874
- */
28273
+ const STATE_LOCK_TIMEOUT_MS = 5e3;
28274
+ const STATE_LOCK_RETRY_MS = 25;
28275
+ const processHolders = /* @__PURE__ */ new Map();
27875
28276
  let exitListenersInstalled = false;
28277
+ function lockPath(configDir, scope) {
28278
+ const name = scope === void 0 ? LOCK_FILENAME : `chat-${createHash("sha256").update(scope).digest("hex")}.lock`;
28279
+ return join(resolve(configDir), name);
28280
+ }
28281
+ function readSnapshot(path) {
28282
+ let fd;
28283
+ try {
28284
+ if (!lstatSync(path).isFile()) return null;
28285
+ fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW);
28286
+ } catch (error) {
28287
+ if (error.code === "ENOENT") return null;
28288
+ throw error;
28289
+ }
28290
+ try {
28291
+ const stat = fstatSync(fd);
28292
+ return {
28293
+ device: stat.dev,
28294
+ inode: stat.ino,
28295
+ contents: stat.size <= 128 ? readFileSync(fd, "utf8") : ""
28296
+ };
28297
+ } finally {
28298
+ closeSync(fd);
28299
+ }
28300
+ }
28301
+ function matchesSnapshot(path, snapshot) {
28302
+ const current = readSnapshot(path);
28303
+ return current !== null && current.device === snapshot.device && current.inode === snapshot.inode && current.contents === snapshot.contents;
28304
+ }
28305
+ function removeOwnedLock(holder) {
28306
+ try {
28307
+ if (matchesSnapshot(holder.path, holder)) unlinkSync(holder.path);
28308
+ } catch {}
28309
+ }
27876
28310
  function installExitListenersOnce() {
27877
28311
  if (exitListenersInstalled) return;
27878
28312
  exitListenersInstalled = true;
27879
28313
  const cleanup = () => {
27880
- if (processHolder === null) return;
27881
- try {
27882
- unlinkSync(lockPath(processHolder.configDir));
27883
- } catch {}
27884
- processHolder = null;
28314
+ for (const holder of processHolders.values()) removeOwnedLock(holder);
28315
+ processHolders.clear();
27885
28316
  };
27886
28317
  process.on("exit", cleanup);
27887
28318
  for (const signal of [
@@ -27897,100 +28328,218 @@ function installExitListenersOnce() {
27897
28328
  process.on(signal, handler);
27898
28329
  }
27899
28330
  }
27900
- function lockPath(configDir) {
27901
- return join(configDir, LOCK_FILENAME);
27902
- }
27903
- /**
27904
- * `process.kill(pid, 0)` returns true if the process exists and we
27905
- * have permission to signal it. Throws ESRCH when the pid is gone,
27906
- * EPERM if it exists but isn't ours. Both "exists" cases mean we
27907
- * should NOT steal the lock; only ESRCH proves the holder is dead.
27908
- */
27909
28331
  function pidIsAlive(pid) {
27910
28332
  try {
27911
28333
  process.kill(pid, 0);
27912
28334
  return true;
27913
- } catch (err) {
27914
- if (err.code === "ESRCH") return false;
27915
- return true;
28335
+ } catch (error) {
28336
+ return error.code !== "ESRCH";
27916
28337
  }
27917
28338
  }
27918
- function readHolderPid(configDir) {
27919
- try {
27920
- const raw = readFileSync(lockPath(configDir), "utf8").trim();
27921
- const pid = Number.parseInt(raw, 10);
27922
- return Number.isFinite(pid) && pid > 0 ? pid : null;
27923
- } catch {
27924
- return null;
28339
+ function holderPid(snapshot) {
28340
+ const lines = snapshot?.contents.trim().split("\n");
28341
+ if (!lines || lines.length > 2 || !/^[1-9]\d*$/.test(lines[0] ?? "")) return null;
28342
+ if (lines.length === 2 && !/^[0-9a-f-]{36}$/.test(lines[1] ?? "")) return null;
28343
+ const pid = Number(lines[0]);
28344
+ return Number.isSafeInteger(pid) && pid > 0 ? pid : null;
28345
+ }
28346
+ function reclaimStaleLock(path, previous, temporary, contender) {
28347
+ const guards = [];
28348
+ const visited = /* @__PURE__ */ new Set();
28349
+ let target = previous;
28350
+ for (;;) {
28351
+ const guardPath = `${path}.reclaim-${createHash("sha256").update(`${target.device}:${target.inode}:${target.contents}`).digest("hex")}`;
28352
+ if (visited.has(guardPath)) throw new ChatLockContentionError(0);
28353
+ visited.add(guardPath);
28354
+ try {
28355
+ linkSync(temporary, guardPath);
28356
+ } catch (error) {
28357
+ if (error.code !== "EEXIST") throw error;
28358
+ const guard = readSnapshot(guardPath);
28359
+ const pid = holderPid(guard);
28360
+ if (!guard || pid === null || pidIsAlive(pid)) throw new ChatLockContentionError(pid ?? 0);
28361
+ guards.push({
28362
+ path: guardPath,
28363
+ snapshot: guard
28364
+ });
28365
+ target = guard;
28366
+ continue;
28367
+ }
28368
+ guards.push({
28369
+ path: guardPath,
28370
+ snapshot: contender
28371
+ });
28372
+ let removed = false;
28373
+ try {
28374
+ const pid = holderPid(previous);
28375
+ if (matchesSnapshot(path, previous) && pid !== null && !pidIsAlive(pid)) unlinkSync(path);
28376
+ removed = !matchesSnapshot(path, previous);
28377
+ } finally {
28378
+ if (removed) for (const guard of guards) try {
28379
+ if (matchesSnapshot(guard.path, guard.snapshot)) unlinkSync(guard.path);
28380
+ } catch {}
28381
+ }
28382
+ return;
27925
28383
  }
27926
28384
  }
27927
28385
  var ChatLockContentionError = class extends Error {
27928
28386
  constructor(holderPid) {
27929
- super(`Another \`primitive chat\` invocation (pid ${holderPid}) is in progress. Wait for it to finish, or kill it before retrying.`);
28387
+ super(holderPid > 0 ? `Another \`primitive chat\` invocation (pid ${holderPid}) is in progress. Wait for it to finish, or kill it before retrying.` : "The chat lock holder could not be verified. Retry after the current invocation finishes.");
27930
28388
  this.holderPid = holderPid;
27931
28389
  this.name = "ChatLockContentionError";
27932
28390
  }
27933
28391
  };
27934
- /**
27935
- * Acquire the chat-state mutex for this configDir. Returns a release
27936
- * function that is safe to call any number of times. Throws
27937
- * `ChatLockContentionError` if another live process holds the lock.
27938
- */
27939
- function acquireChatLock(configDir) {
27940
- if (processHolder?.configDir === configDir) {
27941
- processHolder.depth += 1;
27942
- let released = false;
27943
- return () => {
27944
- if (released) return;
27945
- released = true;
27946
- if (processHolder !== null) processHolder.depth -= 1;
27947
- };
28392
+ function releaseHolder(holder) {
28393
+ let released = false;
28394
+ return () => {
28395
+ if (released) return;
28396
+ released = true;
28397
+ if (processHolders.get(holder.path) !== holder) return;
28398
+ holder.depth -= 1;
28399
+ if (holder.depth === 0) {
28400
+ removeOwnedLock(holder);
28401
+ processHolders.delete(holder.path);
28402
+ }
28403
+ };
28404
+ }
28405
+ /** Acquire synchronously; the same scope is reentrant within this process. */
28406
+ function acquireChatLock(configDir, scope) {
28407
+ const path = lockPath(configDir, scope);
28408
+ const held = processHolders.get(path);
28409
+ if (held) {
28410
+ if (!matchesSnapshot(path, held)) throw new ChatLockContentionError(0);
28411
+ held.depth += 1;
28412
+ return releaseHolder(held);
27948
28413
  }
27949
- mkdirSync(configDir, {
28414
+ mkdirSync(resolve(configDir), {
27950
28415
  mode: 448,
27951
28416
  recursive: true
27952
28417
  });
27953
- for (let attempt = 0; attempt < 2; attempt++) {
27954
- let fd;
28418
+ const contents = `${process.pid}\n${randomUUID()}\n`;
28419
+ const temporary = `${path}.${randomUUID()}.tmp`;
28420
+ const fd = openSync(temporary, "wx", 384);
28421
+ let snapshot;
28422
+ try {
28423
+ writeFileSync(fd, contents);
28424
+ const stat = fstatSync(fd);
28425
+ snapshot = {
28426
+ device: stat.dev,
28427
+ inode: stat.ino,
28428
+ contents
28429
+ };
28430
+ } catch (error) {
28431
+ closeSync(fd);
28432
+ unlinkSync(temporary);
28433
+ throw error;
28434
+ }
28435
+ closeSync(fd);
28436
+ const publish = () => {
27955
28437
  try {
27956
- fd = openSync(lockPath(configDir), "wx", 384);
27957
- } catch (err) {
27958
- if (err.code !== "EEXIST") throw err;
27959
- const holder = readHolderPid(configDir);
27960
- if (holder === null || !pidIsAlive(holder)) {
27961
- try {
27962
- unlinkSync(lockPath(configDir));
27963
- } catch (unlinkErr) {
27964
- if (unlinkErr.code !== "ENOENT") throw unlinkErr;
27965
- }
27966
- continue;
27967
- }
27968
- throw new ChatLockContentionError(holder);
28438
+ linkSync(temporary, path);
28439
+ return true;
28440
+ } catch (error) {
28441
+ if (error.code === "EEXIST") return false;
28442
+ throw error;
27969
28443
  }
27970
- writeSync(fd, `${process.pid}\n`);
27971
- closeSync(fd);
27972
- processHolder = {
27973
- configDir,
28444
+ };
28445
+ try {
28446
+ let acquired = publish();
28447
+ if (!acquired) {
28448
+ const previous = readSnapshot(path);
28449
+ const pid = holderPid(previous);
28450
+ if (!previous || pid === null || pidIsAlive(pid)) throw new ChatLockContentionError(pid ?? 0);
28451
+ reclaimStaleLock(path, previous, temporary, snapshot);
28452
+ acquired = publish();
28453
+ if (!acquired) throw new ChatLockContentionError(holderPid(readSnapshot(path)) ?? 0);
28454
+ }
28455
+ const holder = {
28456
+ ...snapshot,
28457
+ path,
27974
28458
  depth: 1
27975
28459
  };
28460
+ processHolders.set(path, holder);
27976
28461
  installExitListenersOnce();
27977
- let released = false;
27978
- return () => {
27979
- if (released) return;
27980
- released = true;
27981
- if (processHolder?.configDir === configDir) {
27982
- processHolder.depth -= 1;
27983
- if (processHolder.depth <= 0) {
27984
- try {
27985
- unlinkSync(lockPath(configDir));
27986
- } catch {}
27987
- processHolder = null;
27988
- }
27989
- }
28462
+ return releaseHolder(holder);
28463
+ } finally {
28464
+ try {
28465
+ unlinkSync(temporary);
28466
+ } catch {}
28467
+ }
28468
+ }
28469
+ /**
28470
+ * Serialize short state transactions, including independent callers in this
28471
+ * process. Do not nest this API or hold its release function across network IO.
28472
+ */
28473
+ async function acquireChatStateLock(configDir) {
28474
+ const deadline = performance.now() + STATE_LOCK_TIMEOUT_MS;
28475
+ const path = lockPath(configDir);
28476
+ for (;;) try {
28477
+ if (processHolders.has(path)) throw new ChatLockContentionError(process.pid);
28478
+ return acquireChatLock(configDir);
28479
+ } catch (error) {
28480
+ if (!(error instanceof ChatLockContentionError)) throw error;
28481
+ const remaining = deadline - performance.now();
28482
+ if (remaining <= 0) throw error;
28483
+ await setTimeout$1(Math.min(STATE_LOCK_RETRY_MS, remaining));
28484
+ }
28485
+ }
28486
+ //#endregion
28487
+ //#region src/oclif/chat-receipt.ts
28488
+ /** Keep API credentials out of request fingerprints, including low-entropy local keys. */
28489
+ function chatCredentialIdentity(key, apiOrigin) {
28490
+ return key === void 0 ? void 0 : scryptSync(key, apiOrigin, 32).toString("hex");
28491
+ }
28492
+ function chatRequestHash(value) {
28493
+ return createHash("sha256").update(JSON.stringify(value)).digest("hex");
28494
+ }
28495
+ function saveChatReceipt(receipt) {
28496
+ const temporary = `${receipt.path}.${process.pid}.${randomUUID()}.tmp`;
28497
+ try {
28498
+ writeFileSync(temporary, `${JSON.stringify(receipt.data)}\n`, { mode: 384 });
28499
+ renameSync(temporary, receipt.path);
28500
+ } finally {
28501
+ rmSync(temporary, { force: true });
28502
+ }
28503
+ }
28504
+ function parseReceipt(raw) {
28505
+ const value = JSON.parse(raw);
28506
+ if (!value || value.version !== 1 || typeof value.request_hash !== "string" || typeof value.sent_at !== "string" || !Number.isFinite(Date.parse(value.sent_at)) || typeof value.completed !== "boolean" || value.sent !== null && (!value.sent || typeof value.sent.id !== "string" || !value.sent.id || typeof value.sent.status !== "string")) throw new Error("Invalid chat receipt");
28507
+ return value;
28508
+ }
28509
+ /** Caller holds the request/parent lock until completion. Unknown POSTs never retry. */
28510
+ function beginChatReceipt(configDir, scope, requestHash) {
28511
+ const directory = join(configDir, "chat-receipts");
28512
+ mkdirSync(directory, {
28513
+ recursive: true,
28514
+ mode: 448
28515
+ });
28516
+ const path = join(directory, `${chatRequestHash(scope)}.json`);
28517
+ let previous;
28518
+ try {
28519
+ previous = parseReceipt(readFileSync(path, "utf8"));
28520
+ } catch (error) {
28521
+ if (error.code !== "ENOENT") throw new Error(`Cannot safely read chat receipt ${path}. Inspect it and sent history before retrying.`, { cause: error });
28522
+ }
28523
+ if (previous && !previous.completed) {
28524
+ if (previous.sent === null) throw new Error(`A previous send has an uncertain outcome. Inspect sent history before sending again. Receipt: ${path}`);
28525
+ if (previous.request_hash !== requestHash) throw new Error(`This reply still awaits a response to sent email ${previous.sent.id}. Retry the same message to resume, or inspect that send. Receipt: ${path}`);
28526
+ return {
28527
+ path,
28528
+ data: previous
27990
28529
  };
27991
28530
  }
27992
- /* v8 ignore next 4 -- the for-loop returns or throws on every iteration; the unreachable trailer keeps TS happy. */
27993
- throw new Error("acquireChatLock: exhausted retries (this is a bug — should not be reachable)");
28531
+ const receipt = {
28532
+ path,
28533
+ data: {
28534
+ version: 1,
28535
+ request_hash: requestHash,
28536
+ sent_at: (/* @__PURE__ */ new Date()).toISOString(),
28537
+ sent: null,
28538
+ completed: false
28539
+ }
28540
+ };
28541
+ saveChatReceipt(receipt);
28542
+ return receipt;
27994
28543
  }
27995
28544
  //#endregion
27996
28545
  //#region src/oclif/outbound-defaults.ts
@@ -28465,7 +29014,8 @@ function buildChatJsonEnvelope(context) {
28465
29014
  follow_up_commands: buildChatFollowUpCommands(context)
28466
29015
  };
28467
29016
  }
28468
- function persistActiveChat(params) {
29017
+ async function persistActiveChat(params) {
29018
+ const release = await acquireChatStateLock(params.configDir);
28469
29019
  try {
28470
29020
  return saveActiveChatState(params.configDir, {
28471
29021
  from: params.context.from,
@@ -28482,6 +29032,8 @@ function persistActiveChat(params) {
28482
29032
  const detail = error instanceof Error ? error.message : String(error);
28483
29033
  params.writeWarning?.(`Warning: could not save local chat state: ${detail}\n`);
28484
29034
  return null;
29035
+ } finally {
29036
+ release();
28485
29037
  }
28486
29038
  }
28487
29039
  /**
@@ -28636,7 +29188,12 @@ var ChatCommand = class ChatCommand extends Command {
28636
29188
  is less exact than strict matching. Use --strict-only when matching
28637
29189
  the wrong reply is worse than timing out. Progress is written to
28638
29190
  stderr while the CLI waits. Exits non-zero on timeout and prints
28639
- recovery commands when the send succeeded but no reply was returned.`;
29191
+ recovery commands when the send succeeded but no reply was returned.
29192
+
29193
+ Independent addressed chats can run concurrently. Replies reserve their
29194
+ selected local chat. The last successfully saved reply becomes active.
29195
+ Retrying an interrupted or timed-out request resumes its recorded send;
29196
+ an uncertain send outcome requires inspection instead of a blind resend.`;
28640
29197
  static summary = "Chat with an agent over email (send and wait for the reply)";
28641
29198
  static examples = [
28642
29199
  "<%= config.bin %> chat help@agent.acme.dev 'how do I rotate my API key?'",
@@ -28721,12 +29278,6 @@ var ChatCommand = class ChatCommand extends Command {
28721
29278
  if (!message.trim()) throw cliError$6(replyMode ? "Reply body is empty." : "Message body is empty.");
28722
29279
  await runWithTiming(flags.time, async () => {
28723
29280
  let releaseLock;
28724
- try {
28725
- releaseLock = acquireChatLock(this.config.configDir);
28726
- } catch (err) {
28727
- if (err instanceof ChatLockContentionError) throw cliError$6(err.message);
28728
- throw err;
28729
- }
28730
29281
  try {
28731
29282
  const { apiClient, auth, baseUrlOverridden } = await createAuthenticatedCliApiClient({
28732
29283
  apiKey: flags["api-key"],
@@ -28792,10 +29343,46 @@ var ChatCommand = class ChatCommand extends Command {
28792
29343
  from = flags.from ?? await pickDefaultFromAddress(apiClient, authFailureContext);
28793
29344
  subject = flags.subject ?? deriveSubject(message);
28794
29345
  }
28795
- const sentAtIso = (/* @__PURE__ */ new Date()).toISOString();
28796
- if (replyMode) progress?.update(`Sending reply to ${args.recipient}`);
29346
+ const requestHash = chatRequestHash({
29347
+ api: auth.apiBaseUrl,
29348
+ account: auth.credentials?.org_id ?? chatCredentialIdentity(auth.apiKey, auth.apiBaseUrl),
29349
+ from,
29350
+ recipient: args.recipient,
29351
+ subject,
29352
+ message,
29353
+ parent: parentReply?.id ?? flags["in-reply-to"],
29354
+ attachments
29355
+ });
29356
+ const scope = parentReply ? chatRequestHash({
29357
+ api: auth.apiBaseUrl,
29358
+ from,
29359
+ parent: parentReply.id
29360
+ }) : requestHash;
29361
+ try {
29362
+ releaseLock = acquireChatLock(this.config.configDir, scope);
29363
+ } catch (error) {
29364
+ if (error instanceof ChatLockContentionError) throw cliError$6(error.message);
29365
+ throw error;
29366
+ }
29367
+ if (flags["chat-local-id"] !== void 0) {
29368
+ const releaseState = await acquireChatStateLock(this.config.configDir);
29369
+ try {
29370
+ const current = loadChatConversationByLocalId(this.config.configDir, flags["chat-local-id"]);
29371
+ if (!current || current.last_reply_email_id !== parentReply?.id) throw cliError$6("This local chat advanced while preparing the reply. Retry to use its latest reply.");
29372
+ } finally {
29373
+ releaseState();
29374
+ }
29375
+ }
29376
+ const receipt = beginChatReceipt(this.config.configDir, scope, requestHash);
29377
+ const sentAtIso = receipt.data.sent_at;
29378
+ process.stderr.write(`Chat receipt: ${receipt.path}\n`);
29379
+ if (receipt.data.sent !== null) progress?.start("Resuming the recorded send without sending again");
29380
+ else if (replyMode) progress?.update(`Sending reply to ${args.recipient}`);
28797
29381
  else progress?.start(`Sending message to ${args.recipient}`);
28798
- const sendResult = parentReply !== void 0 ? await replyToEmail({
29382
+ const sendResult = receipt.data.sent !== null ? {
29383
+ data: { data: receipt.data.sent },
29384
+ error: void 0
29385
+ } : parentReply !== void 0 ? await replyToEmail({
28799
29386
  body: {
28800
29387
  body_text: message,
28801
29388
  from,
@@ -28817,6 +29404,20 @@ var ChatCommand = class ChatCommand extends Command {
28817
29404
  responseStyle: "fields"
28818
29405
  });
28819
29406
  if (sendResult.error) {
29407
+ const status = "response" in sendResult ? sendResult.response?.status : void 0;
29408
+ if (status !== void 0 && [
29409
+ 400,
29410
+ 401,
29411
+ 402,
29412
+ 403,
29413
+ 404,
29414
+ 413,
29415
+ 422,
29416
+ 429
29417
+ ].includes(status)) {
29418
+ receipt.data.completed = true;
29419
+ saveChatReceipt(receipt);
29420
+ }
28820
29421
  progress?.fail(replyMode ? "Reply send failed." : "Message send failed.");
28821
29422
  const errorPayload = extractErrorPayload(sendResult.error);
28822
29423
  writeErrorWithHints(errorPayload);
@@ -28832,6 +29433,8 @@ var ChatCommand = class ChatCommand extends Command {
28832
29433
  progress?.fail("Send succeeded but the API returned no data.");
28833
29434
  throw cliError$6("Send succeeded but the API returned no data.");
28834
29435
  }
29436
+ receipt.data.sent = sent;
29437
+ saveChatReceipt(receipt);
28835
29438
  const replyAddress = sent.from || from;
28836
29439
  const baseContext = {
28837
29440
  from: replyAddress,
@@ -28878,7 +29481,7 @@ var ChatCommand = class ChatCommand extends Command {
28878
29481
  matchStrategy: "strict",
28879
29482
  reply: detail
28880
29483
  };
28881
- const localChatId = persistActiveChat({
29484
+ const localChatId = await persistActiveChat({
28882
29485
  configDir: this.config.configDir,
28883
29486
  context: outputContext,
28884
29487
  preferredLocalId: flags["chat-local-id"],
@@ -28888,6 +29491,8 @@ var ChatCommand = class ChatCommand extends Command {
28888
29491
  ...outputContext,
28889
29492
  localChatId
28890
29493
  };
29494
+ receipt.data.completed = localChatId !== null;
29495
+ saveChatReceipt(receipt);
28891
29496
  if (flags.json) this.log(JSON.stringify(buildChatJsonEnvelope(outputContext), null, 2));
28892
29497
  else this.log(formatChatResponse(outputContext));
28893
29498
  return;
@@ -28942,7 +29547,7 @@ var ChatCommand = class ChatCommand extends Command {
28942
29547
  matchStrategy: replyResult.matchStrategy,
28943
29548
  reply: replyResult.reply
28944
29549
  };
28945
- const localChatId = persistActiveChat({
29550
+ const localChatId = await persistActiveChat({
28946
29551
  configDir: this.config.configDir,
28947
29552
  context: outputContext,
28948
29553
  preferredLocalId: flags["chat-local-id"],
@@ -28952,10 +29557,12 @@ var ChatCommand = class ChatCommand extends Command {
28952
29557
  ...outputContext,
28953
29558
  localChatId
28954
29559
  };
29560
+ receipt.data.completed = localChatId !== null;
29561
+ saveChatReceipt(receipt);
28955
29562
  if (flags.json) this.log(JSON.stringify(buildChatJsonEnvelope(outputContext), null, 2));
28956
29563
  else this.log(formatChatResponse(outputContext));
28957
29564
  } finally {
28958
- releaseLock();
29565
+ releaseLock?.();
28959
29566
  }
28960
29567
  });
28961
29568
  }
@@ -29035,15 +29642,19 @@ var ChatReplyCommand = class ChatReplyCommand extends Command {
29035
29642
  if (flags.id === void 0 && args.message !== void 0 && positionalLocalId === null) throw cliError$6("When passing two positional arguments to `primitive chat reply`, the first must be a local chat id. Use `primitive chat reply '<message>'` for the active chat or `primitive chat reply --id <id> '<message>'` for a specific chat.");
29036
29643
  if (flags.id !== void 0 && args.message !== void 0) throw cliError$6("With --id, pass the reply body as a single positional argument or pipe it via stdin.");
29037
29644
  let release;
29038
- try {
29039
- release = acquireChatLock(this.config.configDir);
29040
- } catch (err) {
29041
- if (err instanceof ChatLockContentionError) throw cliError$6(err.message);
29042
- throw err;
29043
- }
29044
29645
  try {
29045
29646
  const localId = flags.id ?? (typeof positionalLocalId === "number" ? positionalLocalId : void 0);
29046
- const state = localId === void 0 ? loadActiveChatState(this.config.configDir) : loadChatConversationByLocalId(this.config.configDir, localId);
29647
+ const releaseState = await acquireChatStateLock(this.config.configDir);
29648
+ let state;
29649
+ try {
29650
+ state = localId === void 0 ? loadActiveChatState(this.config.configDir) : loadChatConversationByLocalId(this.config.configDir, localId);
29651
+ if (state) release = acquireChatLock(this.config.configDir, `local:${state.local_id}`);
29652
+ } catch (error) {
29653
+ if (error instanceof ChatLockContentionError) throw cliError$6(error.message);
29654
+ throw error;
29655
+ } finally {
29656
+ releaseState();
29657
+ }
29047
29658
  if (!state) throw cliError$6(localId === void 0 ? "No open chat. Start one with `primitive chat <email> '<message>'`." : `No local chat ${localId}. Start one with \`primitive chat <email> '<message>'\` or omit --id to use the active chat.`);
29048
29659
  const message = args.message !== void 0 ? args.message : args.idOrMessage !== void 0 && args.idOrMessage !== "" ? args.idOrMessage : await readStdinToString("No reply body provided. Pass the reply body as a positional argument or pipe it via stdin.");
29049
29660
  if (!message.trim()) throw cliError$6("Reply body is empty.");
@@ -29075,7 +29686,7 @@ var ChatReplyCommand = class ChatReplyCommand extends Command {
29075
29686
  if (flags.time) argv.push("--time");
29076
29687
  await ChatCommand.run(argv, { root: this.config.root });
29077
29688
  } finally {
29078
- release();
29689
+ release?.();
29079
29690
  }
29080
29691
  }
29081
29692
  };
@@ -31226,8 +31837,8 @@ const PRIMITIVE_TEAM_AUTHOR = {
31226
31837
  name: "Primitive Team",
31227
31838
  url: "https://primitive.dev"
31228
31839
  };
31229
- const SDK_VERSION_RANGE = "^1.23.0";
31230
- const CLI_VERSION_RANGE = "^1.23.0";
31840
+ const SDK_VERSION_RANGE = "^1.24.0";
31841
+ const CLI_VERSION_RANGE = "^1.24.0";
31231
31842
  const ESBUILD_VERSION_RANGE = "^0.27.0";
31232
31843
  function renderHandler() {
31233
31844
  return `// env.PRIMITIVE_API_KEY, env.PRIMITIVE_WEBHOOK_SECRET, and
@@ -33475,7 +34086,7 @@ function createListenHandler(options = {}) {
33475
34086
  validateListenHandlerOptions(options);
33476
34087
  return async (delivery, signal) => {
33477
34088
  signal.throwIfAborted();
33478
- const start = performance.now();
34089
+ const start = performance$1.now();
33479
34090
  const work = new AbortController();
33480
34091
  let timedOut = false;
33481
34092
  const cancel = () => work.abort(signal.reason);
@@ -33484,7 +34095,7 @@ function createListenHandler(options = {}) {
33484
34095
  timedOut = true;
33485
34096
  work.abort(/* @__PURE__ */ new Error("Event handler timed out."));
33486
34097
  }, HANDLER_TIMEOUT_MS);
33487
- const duration = () => Math.min(HANDLER_TIMEOUT_MS, Math.max(0, Math.round(performance.now() - start)));
34098
+ const duration = () => Math.min(HANDLER_TIMEOUT_MS, Math.max(0, Math.round(performance$1.now() - start)));
33488
34099
  try {
33489
34100
  if (options.exec !== void 0) {
33490
34101
  const code = await execute(options.exec, delivery, options.stderr ?? process.stderr, work.signal);
@@ -36122,7 +36733,7 @@ var OrgSecretsSetCommand = class OrgSecretsSetCommand extends Command {
36122
36733
  }
36123
36734
  };
36124
36735
  //#endregion
36125
- //#region ../sdk-node/dist/payloads-CQtSsUT6.js
36736
+ //#region ../sdk-node/dist/payloads-BfJAtqW9.js
36126
36737
  /**
36127
36738
  * Load a Node builtin at call time. Exported for direct testing of the
36128
36739
  * unavailable-module error path; production code goes through the typed
@@ -36155,7 +36766,9 @@ const MANIFEST_VERSION = 1;
36155
36766
  const CHUNK_KDF_INFO = "payloads-chunk";
36156
36767
  const OBJECT_ID_BYTES = 16;
36157
36768
  const CEK_BYTES = 32;
36158
- const subtle = globalThis.crypto.subtle;
36769
+ function subtle() {
36770
+ return globalThis.crypto.subtle;
36771
+ }
36159
36772
  const textEncoder = new TextEncoder();
36160
36773
  function randomBytes(length) {
36161
36774
  const out = new Uint8Array(length);
@@ -36175,7 +36788,7 @@ function fromHex(hex) {
36175
36788
  return out;
36176
36789
  }
36177
36790
  async function contentHash(bytes) {
36178
- return new Uint8Array(await subtle.digest("SHA-256", bytes));
36791
+ return new Uint8Array(await subtle().digest("SHA-256", bytes));
36179
36792
  }
36180
36793
  async function contentHashHex(bytes) {
36181
36794
  return toHex(await contentHash(bytes));
@@ -36199,9 +36812,9 @@ async function merkleRoot(leafHashesHex) {
36199
36812
  return toHex(level[0]);
36200
36813
  }
36201
36814
  async function deriveChunkKey(cek, index) {
36202
- const baseKey = await subtle.importKey("raw", cek, "HKDF", false, ["deriveKey"]);
36815
+ const baseKey = await subtle().importKey("raw", cek, "HKDF", false, ["deriveKey"]);
36203
36816
  const info = textEncoder.encode(`${CHUNK_KDF_INFO}:${index}`);
36204
- return subtle.deriveKey({
36817
+ return subtle().deriveKey({
36205
36818
  name: "HKDF",
36206
36819
  hash: "SHA-256",
36207
36820
  salt: new Uint8Array(32),
@@ -36224,7 +36837,7 @@ function chunkAad(objectId, index) {
36224
36837
  }
36225
36838
  async function encryptChunk(cek, objectId, index, plaintext) {
36226
36839
  const key = await deriveChunkKey(cek, index);
36227
- const ct = await subtle.encrypt({
36840
+ const ct = await subtle().encrypt({
36228
36841
  name: "AES-GCM",
36229
36842
  iv: chunkNonce(index),
36230
36843
  additionalData: chunkAad(objectId, index)
@@ -36233,7 +36846,7 @@ async function encryptChunk(cek, objectId, index, plaintext) {
36233
36846
  }
36234
36847
  async function decryptChunk(cek, objectId, index, ciphertext) {
36235
36848
  const key = await deriveChunkKey(cek, index);
36236
- const pt = await subtle.decrypt({
36849
+ const pt = await subtle().decrypt({
36237
36850
  name: "AES-GCM",
36238
36851
  iv: chunkNonce(index),
36239
36852
  additionalData: chunkAad(objectId, index)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "primitive",
3
- "version": "1.23.0",
3
+ "version": "1.24.0",
4
4
  "description": "Official Primitive CLI: deploy Primitive Functions, send and inspect mail, manage endpoints, all from the terminal. Wraps the @primitivedotdev/sdk runtime client with one-shot commands.",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -172,12 +172,12 @@
172
172
  "@primitivedotdev/api-core": "workspace:*",
173
173
  "@primitivedotdev/sdk": "workspace:*",
174
174
  "@types/node": "^22.10.2",
175
- "@vitest/coverage-v8": "^4.1.4",
175
+ "@vitest/coverage-v8": "^4.1.11",
176
176
  "oclif": "^4.23.0",
177
177
  "tsdown": "^0.21.10",
178
178
  "tsx": "^4.21.0",
179
179
  "typescript": "^5.7.2",
180
180
  "vite": "^8.0.8",
181
- "vitest": "^4.1.4"
181
+ "vitest": "^4.1.11"
182
182
  }
183
183
  }