@primitivedotdev/cli 0.34.0 → 0.35.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.
@@ -1,5 +1,5 @@
1
- import { A as createClient, C as saveCliCredentials, D as loadChatConversationByLocalId, E as loadActiveChatState, O as saveActiveChatState, S as resolveCliAuth, T as deleteChatState, _ as deleteCliCredentials, a as normalizeCliEnvironmentName, b as normalizeApiBaseUrl1, c as resolveConfigEnvironment, d as validateCliHeaderName, f as validateCliHeaderValue, g as credentialsPath, h as credentialsLockPath, i as loadCliConfig, j as createConfig, k as PrimitiveApiClient, 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 chatStatePath, x as normalizeApiBaseUrl2, y as loadCliCredentials } from "../cli-config-D9nB6fOW.js";
2
- import { Args, Command, Errors, Flags } from "@oclif/core";
1
+ import { A as createClient, C as saveCliCredentials, D as loadChatConversationByLocalId, E as loadActiveChatState, O as saveActiveChatState, S as resolveCliAuth, T as deleteChatState, _ as deleteCliCredentials, a as normalizeCliEnvironmentName, b as normalizeApiBaseUrl1, c as resolveConfigEnvironment, d as validateCliHeaderName, f as validateCliHeaderValue, g as credentialsPath, h as credentialsLockPath, i as loadCliConfig, j as createConfig, k as PrimitiveApiClient, 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 chatStatePath, x as normalizeApiBaseUrl2, y as loadCliCredentials } from "../cli-config-SktG2dzR.js";
2
+ import { Args, Command, Errors, Flags, ux } from "@oclif/core";
3
3
  import { chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
4
4
  import { randomUUID } from "node:crypto";
5
5
  import { basename, dirname, join, relative, resolve, sep } from "node:path";
@@ -68,6 +68,7 @@ var sdk_gen_exports = /* @__PURE__ */ __exportAll({
68
68
  resendCliSignupVerification: () => resendCliSignupVerification,
69
69
  rotateWebhookSecret: () => rotateWebhookSecret,
70
70
  searchEmails: () => searchEmails,
71
+ semanticSearch: () => semanticSearch,
71
72
  sendEmail: () => sendEmail,
72
73
  setFunctionSecret: () => setFunctionSecret,
73
74
  startAgentSignup: () => startAgentSignup,
@@ -595,9 +596,9 @@ const downloadAttachments = (options) => (options.client ?? client).get({
595
596
  * derivation (Reply-To, then From, then bare sender), and the
596
597
  * `Re:` subject prefix are all derived server-side from the
597
598
  * stored inbound row. The request body carries only the message
598
- * body and optional `wait` flag; passing any header or recipient
599
- * override is rejected by the schema (`additionalProperties:
600
- * false`).
599
+ * body, optional From override, optional attachments, and optional
600
+ * `wait` flag; passing any header or recipient override is
601
+ * rejected by the schema (`additionalProperties: false`).
601
602
  *
602
603
  * Forwards through the same gates as `/send-mail`: the response
603
604
  * status, error envelope, and `idempotent_replay` flag mirror
@@ -956,6 +957,42 @@ const sendEmail = (options) => (options.client ?? client).post({
956
957
  }
957
958
  });
958
959
  /**
960
+ * Semantic search across received and sent mail
961
+ *
962
+ * Ranked search across both received and sent mail. The `mode`
963
+ * field selects the ranking strategy:
964
+ *
965
+ * - `keyword`: lexical full-text matching only (no embeddings).
966
+ * - `semantic`: meaning-based matching using vector embeddings.
967
+ * - `hybrid` (default): blends the semantic and keyword signals.
968
+ *
969
+ * Results are ordered by a relevance `score`. Every row reports the
970
+ * fields it matched (`matched_fields`), a match-centered excerpt per
971
+ * field (`snippets`), and a `score_breakdown` whose components account
972
+ * for the `score`. Page through results by passing the prior
973
+ * response's `meta.cursor` back as `cursor`.
974
+ *
975
+ * Requires the Pro plan and the `semantic_search_enabled`
976
+ * entitlement; callers without them receive `403`.
977
+ *
978
+ * Host routing: this operation is served only by the search host
979
+ * (`https://api.primitive.dev/v1`). The typed SDKs route it there
980
+ * automatically.
981
+ *
982
+ */
983
+ const semanticSearch = (options) => (options.client ?? client).post({
984
+ security: [{
985
+ scheme: "bearer",
986
+ type: "http"
987
+ }],
988
+ url: "/semantic-search",
989
+ ...options,
990
+ headers: {
991
+ ...options.body !== void 0 && { "Content-Type": "application/json" },
992
+ ...options.headers
993
+ }
994
+ });
995
+ /**
959
996
  * List outbound sent emails
960
997
  *
961
998
  * Returns a paginated list of OUTBOUND emails the caller's
@@ -1378,6 +1415,10 @@ const openapiDocument = {
1378
1415
  "name": "Emails",
1379
1416
  "description": "List, inspect, and manage received emails"
1380
1417
  },
1418
+ {
1419
+ "name": "Search",
1420
+ "description": "Semantic and hybrid search across received and sent mail"
1421
+ },
1381
1422
  {
1382
1423
  "name": "Sending",
1383
1424
  "description": "Send outbound emails through the Primitive API"
@@ -2366,7 +2407,14 @@ const openapiDocument = {
2366
2407
  "post": {
2367
2408
  "operationId": "replyToEmail",
2368
2409
  "summary": "Reply to an inbound email",
2369
- "description": "Sends an outbound reply to the inbound email identified by `id`.\nThreading headers (`In-Reply-To`, `References`), recipient\nderivation (Reply-To, then From, then bare sender), and the\n`Re:` subject prefix are all derived server-side from the\nstored inbound row. The request body carries only the message\nbody and optional `wait` flag; passing any header or recipient\noverride is rejected by the schema (`additionalProperties:\nfalse`).\n\nForwards through the same gates as `/send-mail`: the response\nstatus, error envelope, and `idempotent_replay` flag mirror\nthe send-mail contract verbatim.\n",
2410
+ "description": "Sends an outbound reply to the inbound email identified by `id`.\nThreading headers (`In-Reply-To`, `References`), recipient\nderivation (Reply-To, then From, then bare sender), and the\n`Re:` subject prefix are all derived server-side from the\nstored inbound row. The request body carries only the message\nbody, optional From override, optional attachments, and optional\n`wait` flag; passing any header or recipient override is\nrejected by the schema (`additionalProperties: false`).\n\nForwards through the same gates as `/send-mail`: the response\nstatus, error envelope, and `idempotent_replay` flag mirror\nthe send-mail contract verbatim.\n",
2411
+ "servers": [{
2412
+ "url": "https://api.primitive.dev/v1",
2413
+ "description": "Attachments-supporting send host (recommended)"
2414
+ }, {
2415
+ "url": "https://www.primitive.dev/api/v1",
2416
+ "description": "Primary host (attachment-free replies only)"
2417
+ }],
2370
2418
  "tags": ["Sending"],
2371
2419
  "requestBody": {
2372
2420
  "required": true,
@@ -2816,6 +2864,42 @@ const openapiDocument = {
2816
2864
  "503": { "$ref": "#/components/responses/ServiceUnavailable" }
2817
2865
  }
2818
2866
  } },
2867
+ "/semantic-search": { "post": {
2868
+ "operationId": "semanticSearch",
2869
+ "summary": "Semantic search across received and sent mail",
2870
+ "description": "Ranked search across both received and sent mail. The `mode`\nfield selects the ranking strategy:\n\n- `keyword`: lexical full-text matching only (no embeddings).\n- `semantic`: meaning-based matching using vector embeddings.\n- `hybrid` (default): blends the semantic and keyword signals.\n\nResults are ordered by a relevance `score`. Every row reports the\nfields it matched (`matched_fields`), a match-centered excerpt per\nfield (`snippets`), and a `score_breakdown` whose components account\nfor the `score`. Page through results by passing the prior\nresponse's `meta.cursor` back as `cursor`.\n\nRequires the Pro plan and the `semantic_search_enabled`\nentitlement; callers without them receive `403`.\n\nHost routing: this operation is served only by the search host\n(`https://api.primitive.dev/v1`). The typed SDKs route it there\nautomatically.\n",
2871
+ "servers": [{
2872
+ "url": "https://api.primitive.dev/v1",
2873
+ "description": "Search host"
2874
+ }],
2875
+ "tags": ["Search"],
2876
+ "requestBody": {
2877
+ "required": true,
2878
+ "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SemanticSearchInput" } } }
2879
+ },
2880
+ "responses": {
2881
+ "200": {
2882
+ "description": "Ranked search results",
2883
+ "content": { "application/json": { "schema": { "allOf": [{ "$ref": "#/components/schemas/SuccessEnvelope" }, {
2884
+ "type": "object",
2885
+ "properties": {
2886
+ "data": {
2887
+ "type": "array",
2888
+ "items": { "$ref": "#/components/schemas/SemanticSearchResult" }
2889
+ },
2890
+ "meta": { "$ref": "#/components/schemas/SemanticSearchMeta" }
2891
+ },
2892
+ "required": ["data", "meta"]
2893
+ }] } } }
2894
+ },
2895
+ "400": { "$ref": "#/components/responses/ValidationError" },
2896
+ "401": { "$ref": "#/components/responses/Unauthorized" },
2897
+ "403": { "$ref": "#/components/responses/Forbidden" },
2898
+ "429": { "$ref": "#/components/responses/RateLimited" },
2899
+ "500": { "$ref": "#/components/responses/InternalError" },
2900
+ "503": { "$ref": "#/components/responses/ServiceUnavailable" }
2901
+ }
2902
+ } },
2819
2903
  "/sent-emails": { "get": {
2820
2904
  "operationId": "listSentEmails",
2821
2905
  "summary": "List outbound sent emails",
@@ -5660,6 +5744,236 @@ const openapiDocument = {
5660
5744
  "body_size_bytes"
5661
5745
  ]
5662
5746
  },
5747
+ "SemanticSearchField": {
5748
+ "type": "string",
5749
+ "enum": [
5750
+ "subject",
5751
+ "headers",
5752
+ "addresses",
5753
+ "body"
5754
+ ],
5755
+ "description": "A searchable email field."
5756
+ },
5757
+ "SemanticSearchInput": {
5758
+ "type": "object",
5759
+ "properties": {
5760
+ "query": {
5761
+ "type": "string",
5762
+ "minLength": 1,
5763
+ "maxLength": 2048,
5764
+ "description": "Free-text query. Required for `semantic` and `hybrid` modes;\noptional for `keyword` mode.\n"
5765
+ },
5766
+ "mode": {
5767
+ "type": "string",
5768
+ "enum": [
5769
+ "hybrid",
5770
+ "semantic",
5771
+ "keyword"
5772
+ ],
5773
+ "default": "hybrid",
5774
+ "description": "Ranking strategy. `keyword` is lexical only, `semantic` is\nembedding-based, `hybrid` blends both.\n"
5775
+ },
5776
+ "corpus": {
5777
+ "type": "array",
5778
+ "items": {
5779
+ "type": "string",
5780
+ "enum": ["inbound", "outbound"]
5781
+ },
5782
+ "minItems": 1,
5783
+ "maxItems": 2,
5784
+ "description": "Which mail to search. Defaults to both received (`inbound`)\nand sent (`outbound`).\n"
5785
+ },
5786
+ "search_in": {
5787
+ "type": "array",
5788
+ "items": { "$ref": "#/components/schemas/SemanticSearchField" },
5789
+ "description": "Restrict matching to these fields. Defaults to all."
5790
+ },
5791
+ "exclude": {
5792
+ "type": "array",
5793
+ "items": { "$ref": "#/components/schemas/SemanticSearchField" },
5794
+ "description": "Exclude these fields from matching."
5795
+ },
5796
+ "date_from": {
5797
+ "type": "string",
5798
+ "format": "date-time",
5799
+ "description": "Only include mail at or after this timestamp."
5800
+ },
5801
+ "date_to": {
5802
+ "type": "string",
5803
+ "format": "date-time",
5804
+ "description": "Only include mail at or before this timestamp."
5805
+ },
5806
+ "include": {
5807
+ "type": "array",
5808
+ "items": {
5809
+ "type": "string",
5810
+ "enum": ["coverage"]
5811
+ },
5812
+ "description": "Opt-in extras. `coverage` adds an index-coverage snapshot to\n`meta`. Matched fields, snippets, and the score breakdown are\nalways returned regardless of this field.\n"
5813
+ },
5814
+ "limit": {
5815
+ "type": "integer",
5816
+ "minimum": 1,
5817
+ "maximum": 100,
5818
+ "default": 10,
5819
+ "description": "Maximum number of results to return."
5820
+ },
5821
+ "cursor": {
5822
+ "type": "string",
5823
+ "description": "Opaque pagination cursor from a prior response's `meta.cursor`."
5824
+ }
5825
+ }
5826
+ },
5827
+ "SemanticSearchSnippet": {
5828
+ "type": "object",
5829
+ "properties": {
5830
+ "field": {
5831
+ "type": "string",
5832
+ "description": "The field this excerpt came from."
5833
+ },
5834
+ "text": {
5835
+ "type": "string",
5836
+ "description": "Plain-text excerpt centered on the match (no markup)."
5837
+ }
5838
+ },
5839
+ "required": ["field", "text"]
5840
+ },
5841
+ "SemanticSearchScoreBreakdown": {
5842
+ "type": "object",
5843
+ "description": "Additive contributions to `score`. `semantic` and `keyword` are the\nraw signals times the mode's weight (null when not applicable);\nthese plus `field_boost` and `recency` sum to `score` before each\nvalue is independently rounded to 5 decimal places.\n",
5844
+ "properties": {
5845
+ "semantic": { "type": ["number", "null"] },
5846
+ "keyword": { "type": ["number", "null"] },
5847
+ "field_boost": { "type": "number" },
5848
+ "recency": { "type": "number" }
5849
+ },
5850
+ "required": [
5851
+ "semantic",
5852
+ "keyword",
5853
+ "field_boost",
5854
+ "recency"
5855
+ ]
5856
+ },
5857
+ "SemanticSearchResult": {
5858
+ "type": "object",
5859
+ "properties": {
5860
+ "source_type": {
5861
+ "type": "string",
5862
+ "enum": ["inbound_email", "sent_email"],
5863
+ "description": "Whether this row is a received or sent message."
5864
+ },
5865
+ "id": {
5866
+ "type": "string",
5867
+ "description": "Message id. Combine with `api_url` to fetch the full record."
5868
+ },
5869
+ "subject": { "type": ["string", "null"] },
5870
+ "from": { "type": ["string", "null"] },
5871
+ "to": { "type": ["string", "null"] },
5872
+ "timestamp": {
5873
+ "type": "string",
5874
+ "description": "Message timestamp (received_at for inbound, created_at for sent)."
5875
+ },
5876
+ "status": {
5877
+ "type": "string",
5878
+ "description": "Lifecycle status of the message."
5879
+ },
5880
+ "score": {
5881
+ "type": "number",
5882
+ "description": "Overall relevance score; the `score_breakdown` components account for it."
5883
+ },
5884
+ "semantic_score": {
5885
+ "type": ["number", "null"],
5886
+ "description": "Raw semantic similarity signal, or null when not applicable."
5887
+ },
5888
+ "keyword_score": {
5889
+ "type": ["number", "null"],
5890
+ "description": "Raw keyword (lexical) signal, or null when not applicable."
5891
+ },
5892
+ "matched_fields": {
5893
+ "type": "array",
5894
+ "items": { "$ref": "#/components/schemas/SemanticSearchField" },
5895
+ "description": "Fields where the query matched."
5896
+ },
5897
+ "snippets": {
5898
+ "type": "array",
5899
+ "items": { "$ref": "#/components/schemas/SemanticSearchSnippet" },
5900
+ "description": "Match-centered excerpts, one per matched field."
5901
+ },
5902
+ "score_breakdown": { "$ref": "#/components/schemas/SemanticSearchScoreBreakdown" },
5903
+ "api_url": {
5904
+ "type": ["string", "null"],
5905
+ "description": "Relative API path to fetch the full message."
5906
+ }
5907
+ },
5908
+ "required": [
5909
+ "source_type",
5910
+ "id",
5911
+ "subject",
5912
+ "from",
5913
+ "to",
5914
+ "timestamp",
5915
+ "status",
5916
+ "score",
5917
+ "semantic_score",
5918
+ "keyword_score",
5919
+ "matched_fields",
5920
+ "snippets",
5921
+ "score_breakdown",
5922
+ "api_url"
5923
+ ]
5924
+ },
5925
+ "SemanticSearchCoverage": {
5926
+ "type": "object",
5927
+ "description": "Index-coverage snapshot for the org, returned only when the `coverage` include option is requested.",
5928
+ "properties": {
5929
+ "embedded_chunks": { "type": "integer" },
5930
+ "pending_chunks": { "type": "integer" },
5931
+ "skipped_plan_chunks": { "type": "integer" },
5932
+ "skipped_quota_chunks": { "type": "integer" },
5933
+ "unsupported_attachment_chunks": { "type": "integer" },
5934
+ "failed_chunks": { "type": "integer" }
5935
+ },
5936
+ "required": [
5937
+ "embedded_chunks",
5938
+ "pending_chunks",
5939
+ "skipped_plan_chunks",
5940
+ "skipped_quota_chunks",
5941
+ "unsupported_attachment_chunks",
5942
+ "failed_chunks"
5943
+ ]
5944
+ },
5945
+ "SemanticSearchMeta": {
5946
+ "type": "object",
5947
+ "properties": {
5948
+ "limit": {
5949
+ "type": "integer",
5950
+ "description": "Page size used for this request."
5951
+ },
5952
+ "cursor": {
5953
+ "type": ["string", "null"],
5954
+ "description": "Cursor for the next page, or null if there are no more results."
5955
+ },
5956
+ "mode": {
5957
+ "type": "string",
5958
+ "enum": [
5959
+ "hybrid",
5960
+ "semantic",
5961
+ "keyword"
5962
+ ],
5963
+ "description": "Ranking mode used for this response."
5964
+ },
5965
+ "coverage": {
5966
+ "oneOf": [{ "$ref": "#/components/schemas/SemanticSearchCoverage" }, { "type": "null" }],
5967
+ "description": "Index-coverage snapshot, present only when requested via\n`include: [coverage]`; otherwise null.\n"
5968
+ }
5969
+ },
5970
+ "required": [
5971
+ "limit",
5972
+ "cursor",
5973
+ "mode",
5974
+ "coverage"
5975
+ ]
5976
+ },
5663
5977
  "SentEmailDetail": {
5664
5978
  "description": "Full sent-email record, including `body_text` and\n`body_html`. Returned by /sent-emails/{id}.\n",
5665
5979
  "allOf": [{ "$ref": "#/components/schemas/SentEmailSummary" }, {
@@ -5698,6 +6012,12 @@ const openapiDocument = {
5698
6012
  "wait": {
5699
6013
  "type": "boolean",
5700
6014
  "description": "When true, wait for the first downstream SMTP delivery outcome before returning, mirroring the send-mail `wait` semantics."
6015
+ },
6016
+ "attachments": {
6017
+ "type": "array",
6018
+ "maxItems": 100,
6019
+ "description": "Inline attachments for this reply. Use https://api.primitive.dev/v1 for replies with attachments. Combined raw decoded attachment bytes must be at most 31457280.",
6020
+ "items": { "$ref": "#/components/schemas/SendMailAttachment" }
5701
6021
  }
5702
6022
  }
5703
6023
  },
@@ -11458,70 +11778,282 @@ const operationManifest = [
11458
11778
  },
11459
11779
  {
11460
11780
  "binaryResponse": false,
11461
- "bodyRequired": false,
11462
- "command": "get-send-permissions",
11463
- "description": "Returns a flat list of rules describing every recipient the\ncaller may send to. Each rule has a `type`, a kind-specific\npayload, and a human-readable `description`. If any rule\nmatches the recipient, /send-mail will accept the send under\nthe recipient-scope check.\n\nThe endpoint is the answer to \"where can I send\" without\nexposing internal entitlement names. Agents that don't\nrecognize a `type` can still read the `description` prose\nand act on it.\n\nRule kinds, ordered broadest-first so an agent can stop\nscanning at the first match:\n\n 1. `any_recipient` (one entry, only when the org can send\n anywhere): every other rule below it is redundant.\n 2. `managed_zone` (always emitted, one per Primitive-managed\n zone): sends to any address at *.primitive.email or\n *.email.works always succeed; no entitlement required.\n 3. `your_domain` (one per active verified outbound domain\n owned by the org): sends to that domain are approved.\n 4. `address` (one per address that has authenticated\n inbound mail to the org, capped at `meta.address_cap`):\n sends to that exact address are approved.\n\nThe list is informational, not an authorization check.\n/send-mail remains the source of truth on whether an\nindividual send will succeed (it also enforces the\nfrom-address and the `send_mail` entitlement, which are\nnot recipient-scope concerns and are not represented here).\n",
11464
- "hasJsonBody": false,
11465
- "method": "GET",
11466
- "operationId": "getSendPermissions",
11467
- "path": "/send-permissions",
11781
+ "bodyRequired": true,
11782
+ "command": "semantic-search",
11783
+ "description": "Ranked search across both received and sent mail. The `mode`\nfield selects the ranking strategy:\n\n- `keyword`: lexical full-text matching only (no embeddings).\n- `semantic`: meaning-based matching using vector embeddings.\n- `hybrid` (default): blends the semantic and keyword signals.\n\nResults are ordered by a relevance `score`. Every row reports the\nfields it matched (`matched_fields`), a match-centered excerpt per\nfield (`snippets`), and a `score_breakdown` whose components account\nfor the `score`. Page through results by passing the prior\nresponse's `meta.cursor` back as `cursor`.\n\nRequires the Pro plan and the `semantic_search_enabled`\nentitlement; callers without them receive `403`.\n\nHost routing: this operation is served only by the search host\n(`https://api.primitive.dev/v1`). The typed SDKs route it there\nautomatically.\n",
11784
+ "hasJsonBody": true,
11785
+ "method": "POST",
11786
+ "operationId": "semanticSearch",
11787
+ "path": "/semantic-search",
11468
11788
  "pathParams": [],
11469
11789
  "queryParams": [],
11470
- "requestSchema": null,
11471
- "responseSchema": {
11472
- "type": "array",
11473
- "items": {
11474
- "description": "One recipient-scope rule describing a destination the caller\nmay send to. Discriminated on `type`. Each rule carries a\nhuman-prose `description` field intended for display.\n\nRule kinds are stable within an SDK release. A response\ncontaining a `type` value not enumerated in this schema\nmeans the server is running a newer version than the SDK;\nupgrade the SDK to the release that matches the server's\nschema. Strict-parsing SDKs (Go, Python) will raise a\ndecode error in that case rather than silently dropping\nthe unknown rule, since silent drops would let an outbound\nagent reason from an incomplete view of its own permissions.\n",
11475
- "discriminator": {
11476
- "propertyName": "type",
11477
- "mapping": {
11478
- "any_recipient": "#/components/schemas/SendPermissionAnyRecipient",
11479
- "managed_zone": "#/components/schemas/SendPermissionManagedZone",
11480
- "your_domain": "#/components/schemas/SendPermissionYourDomain",
11481
- "address": "#/components/schemas/SendPermissionAddress"
11482
- }
11790
+ "requestSchema": {
11791
+ "type": "object",
11792
+ "properties": {
11793
+ "query": {
11794
+ "type": "string",
11795
+ "minLength": 1,
11796
+ "maxLength": 2048,
11797
+ "description": "Free-text query. Required for `semantic` and `hybrid` modes;\noptional for `keyword` mode.\n"
11483
11798
  },
11484
- "oneOf": [
11485
- {
11486
- "type": "object",
11487
- "description": "The caller can send to any recipient. When this rule is\npresent, every other rule in the response is redundant.\n",
11488
- "properties": {
11489
- "type": {
11490
- "type": "string",
11491
- "enum": ["any_recipient"]
11492
- },
11493
- "description": {
11494
- "type": "string",
11495
- "description": "Human-prose summary of the rule."
11496
- }
11497
- },
11498
- "required": ["type", "description"]
11799
+ "mode": {
11800
+ "type": "string",
11801
+ "enum": [
11802
+ "hybrid",
11803
+ "semantic",
11804
+ "keyword"
11805
+ ],
11806
+ "default": "hybrid",
11807
+ "description": "Ranking strategy. `keyword` is lexical only, `semantic` is\nembedding-based, `hybrid` blends both.\n"
11808
+ },
11809
+ "corpus": {
11810
+ "type": "array",
11811
+ "items": {
11812
+ "type": "string",
11813
+ "enum": ["inbound", "outbound"]
11499
11814
  },
11500
- {
11501
- "type": "object",
11502
- "description": "The caller can send to any address at the named\nPrimitive-managed zone. Always emitted (no entitlement\nrequired) because Primitive owns the zone and every mailbox\nbelongs to a Primitive customer by construction.\n",
11503
- "properties": {
11504
- "type": {
11505
- "type": "string",
11506
- "enum": ["managed_zone"]
11507
- },
11508
- "zone": {
11509
- "type": "string",
11510
- "description": "The managed apex domain. Sends are accepted to any\naddress at the apex itself or any subdomain (e.g.\n`alice@primitive.email` and `alice@acme.primitive.email`\nboth match the `primitive.email` zone rule).\n"
11511
- },
11512
- "description": {
11513
- "type": "string",
11514
- "description": "Human-prose summary of the rule."
11515
- }
11516
- },
11517
- "required": [
11518
- "type",
11519
- "zone",
11520
- "description"
11521
- ]
11815
+ "minItems": 1,
11816
+ "maxItems": 2,
11817
+ "description": "Which mail to search. Defaults to both received (`inbound`)\nand sent (`outbound`).\n"
11818
+ },
11819
+ "search_in": {
11820
+ "type": "array",
11821
+ "items": {
11822
+ "type": "string",
11823
+ "enum": [
11824
+ "subject",
11825
+ "headers",
11826
+ "addresses",
11827
+ "body"
11828
+ ],
11829
+ "description": "A searchable email field."
11522
11830
  },
11523
- {
11524
- "type": "object",
11831
+ "description": "Restrict matching to these fields. Defaults to all."
11832
+ },
11833
+ "exclude": {
11834
+ "type": "array",
11835
+ "items": {
11836
+ "type": "string",
11837
+ "enum": [
11838
+ "subject",
11839
+ "headers",
11840
+ "addresses",
11841
+ "body"
11842
+ ],
11843
+ "description": "A searchable email field."
11844
+ },
11845
+ "description": "Exclude these fields from matching."
11846
+ },
11847
+ "date_from": {
11848
+ "type": "string",
11849
+ "format": "date-time",
11850
+ "description": "Only include mail at or after this timestamp."
11851
+ },
11852
+ "date_to": {
11853
+ "type": "string",
11854
+ "format": "date-time",
11855
+ "description": "Only include mail at or before this timestamp."
11856
+ },
11857
+ "include": {
11858
+ "type": "array",
11859
+ "items": {
11860
+ "type": "string",
11861
+ "enum": ["coverage"]
11862
+ },
11863
+ "description": "Opt-in extras. `coverage` adds an index-coverage snapshot to\n`meta`. Matched fields, snippets, and the score breakdown are\nalways returned regardless of this field.\n"
11864
+ },
11865
+ "limit": {
11866
+ "type": "integer",
11867
+ "minimum": 1,
11868
+ "maximum": 100,
11869
+ "default": 10,
11870
+ "description": "Maximum number of results to return."
11871
+ },
11872
+ "cursor": {
11873
+ "type": "string",
11874
+ "description": "Opaque pagination cursor from a prior response's `meta.cursor`."
11875
+ }
11876
+ }
11877
+ },
11878
+ "responseSchema": {
11879
+ "type": "array",
11880
+ "items": {
11881
+ "type": "object",
11882
+ "properties": {
11883
+ "source_type": {
11884
+ "type": "string",
11885
+ "enum": ["inbound_email", "sent_email"],
11886
+ "description": "Whether this row is a received or sent message."
11887
+ },
11888
+ "id": {
11889
+ "type": "string",
11890
+ "description": "Message id. Combine with `api_url` to fetch the full record."
11891
+ },
11892
+ "subject": { "type": ["string", "null"] },
11893
+ "from": { "type": ["string", "null"] },
11894
+ "to": { "type": ["string", "null"] },
11895
+ "timestamp": {
11896
+ "type": "string",
11897
+ "description": "Message timestamp (received_at for inbound, created_at for sent)."
11898
+ },
11899
+ "status": {
11900
+ "type": "string",
11901
+ "description": "Lifecycle status of the message."
11902
+ },
11903
+ "score": {
11904
+ "type": "number",
11905
+ "description": "Overall relevance score; the `score_breakdown` components account for it."
11906
+ },
11907
+ "semantic_score": {
11908
+ "type": ["number", "null"],
11909
+ "description": "Raw semantic similarity signal, or null when not applicable."
11910
+ },
11911
+ "keyword_score": {
11912
+ "type": ["number", "null"],
11913
+ "description": "Raw keyword (lexical) signal, or null when not applicable."
11914
+ },
11915
+ "matched_fields": {
11916
+ "type": "array",
11917
+ "items": {
11918
+ "type": "string",
11919
+ "enum": [
11920
+ "subject",
11921
+ "headers",
11922
+ "addresses",
11923
+ "body"
11924
+ ],
11925
+ "description": "A searchable email field."
11926
+ },
11927
+ "description": "Fields where the query matched."
11928
+ },
11929
+ "snippets": {
11930
+ "type": "array",
11931
+ "items": {
11932
+ "type": "object",
11933
+ "properties": {
11934
+ "field": {
11935
+ "type": "string",
11936
+ "description": "The field this excerpt came from."
11937
+ },
11938
+ "text": {
11939
+ "type": "string",
11940
+ "description": "Plain-text excerpt centered on the match (no markup)."
11941
+ }
11942
+ },
11943
+ "required": ["field", "text"]
11944
+ },
11945
+ "description": "Match-centered excerpts, one per matched field."
11946
+ },
11947
+ "score_breakdown": {
11948
+ "type": "object",
11949
+ "description": "Additive contributions to `score`. `semantic` and `keyword` are the\nraw signals times the mode's weight (null when not applicable);\nthese plus `field_boost` and `recency` sum to `score` before each\nvalue is independently rounded to 5 decimal places.\n",
11950
+ "properties": {
11951
+ "semantic": { "type": ["number", "null"] },
11952
+ "keyword": { "type": ["number", "null"] },
11953
+ "field_boost": { "type": "number" },
11954
+ "recency": { "type": "number" }
11955
+ },
11956
+ "required": [
11957
+ "semantic",
11958
+ "keyword",
11959
+ "field_boost",
11960
+ "recency"
11961
+ ]
11962
+ },
11963
+ "api_url": {
11964
+ "type": ["string", "null"],
11965
+ "description": "Relative API path to fetch the full message."
11966
+ }
11967
+ },
11968
+ "required": [
11969
+ "source_type",
11970
+ "id",
11971
+ "subject",
11972
+ "from",
11973
+ "to",
11974
+ "timestamp",
11975
+ "status",
11976
+ "score",
11977
+ "semantic_score",
11978
+ "keyword_score",
11979
+ "matched_fields",
11980
+ "snippets",
11981
+ "score_breakdown",
11982
+ "api_url"
11983
+ ]
11984
+ }
11985
+ },
11986
+ "sdkName": "semanticSearch",
11987
+ "summary": "Semantic search across received and sent mail",
11988
+ "tag": "Search",
11989
+ "tagCommand": "search"
11990
+ },
11991
+ {
11992
+ "binaryResponse": false,
11993
+ "bodyRequired": false,
11994
+ "command": "get-send-permissions",
11995
+ "description": "Returns a flat list of rules describing every recipient the\ncaller may send to. Each rule has a `type`, a kind-specific\npayload, and a human-readable `description`. If any rule\nmatches the recipient, /send-mail will accept the send under\nthe recipient-scope check.\n\nThe endpoint is the answer to \"where can I send\" without\nexposing internal entitlement names. Agents that don't\nrecognize a `type` can still read the `description` prose\nand act on it.\n\nRule kinds, ordered broadest-first so an agent can stop\nscanning at the first match:\n\n 1. `any_recipient` (one entry, only when the org can send\n anywhere): every other rule below it is redundant.\n 2. `managed_zone` (always emitted, one per Primitive-managed\n zone): sends to any address at *.primitive.email or\n *.email.works always succeed; no entitlement required.\n 3. `your_domain` (one per active verified outbound domain\n owned by the org): sends to that domain are approved.\n 4. `address` (one per address that has authenticated\n inbound mail to the org, capped at `meta.address_cap`):\n sends to that exact address are approved.\n\nThe list is informational, not an authorization check.\n/send-mail remains the source of truth on whether an\nindividual send will succeed (it also enforces the\nfrom-address and the `send_mail` entitlement, which are\nnot recipient-scope concerns and are not represented here).\n",
11996
+ "hasJsonBody": false,
11997
+ "method": "GET",
11998
+ "operationId": "getSendPermissions",
11999
+ "path": "/send-permissions",
12000
+ "pathParams": [],
12001
+ "queryParams": [],
12002
+ "requestSchema": null,
12003
+ "responseSchema": {
12004
+ "type": "array",
12005
+ "items": {
12006
+ "description": "One recipient-scope rule describing a destination the caller\nmay send to. Discriminated on `type`. Each rule carries a\nhuman-prose `description` field intended for display.\n\nRule kinds are stable within an SDK release. A response\ncontaining a `type` value not enumerated in this schema\nmeans the server is running a newer version than the SDK;\nupgrade the SDK to the release that matches the server's\nschema. Strict-parsing SDKs (Go, Python) will raise a\ndecode error in that case rather than silently dropping\nthe unknown rule, since silent drops would let an outbound\nagent reason from an incomplete view of its own permissions.\n",
12007
+ "discriminator": {
12008
+ "propertyName": "type",
12009
+ "mapping": {
12010
+ "any_recipient": "#/components/schemas/SendPermissionAnyRecipient",
12011
+ "managed_zone": "#/components/schemas/SendPermissionManagedZone",
12012
+ "your_domain": "#/components/schemas/SendPermissionYourDomain",
12013
+ "address": "#/components/schemas/SendPermissionAddress"
12014
+ }
12015
+ },
12016
+ "oneOf": [
12017
+ {
12018
+ "type": "object",
12019
+ "description": "The caller can send to any recipient. When this rule is\npresent, every other rule in the response is redundant.\n",
12020
+ "properties": {
12021
+ "type": {
12022
+ "type": "string",
12023
+ "enum": ["any_recipient"]
12024
+ },
12025
+ "description": {
12026
+ "type": "string",
12027
+ "description": "Human-prose summary of the rule."
12028
+ }
12029
+ },
12030
+ "required": ["type", "description"]
12031
+ },
12032
+ {
12033
+ "type": "object",
12034
+ "description": "The caller can send to any address at the named\nPrimitive-managed zone. Always emitted (no entitlement\nrequired) because Primitive owns the zone and every mailbox\nbelongs to a Primitive customer by construction.\n",
12035
+ "properties": {
12036
+ "type": {
12037
+ "type": "string",
12038
+ "enum": ["managed_zone"]
12039
+ },
12040
+ "zone": {
12041
+ "type": "string",
12042
+ "description": "The managed apex domain. Sends are accepted to any\naddress at the apex itself or any subdomain (e.g.\n`alice@primitive.email` and `alice@acme.primitive.email`\nboth match the `primitive.email` zone rule).\n"
12043
+ },
12044
+ "description": {
12045
+ "type": "string",
12046
+ "description": "Human-prose summary of the rule."
12047
+ }
12048
+ },
12049
+ "required": [
12050
+ "type",
12051
+ "zone",
12052
+ "description"
12053
+ ]
12054
+ },
12055
+ {
12056
+ "type": "object",
11525
12057
  "description": "The caller can send to any address at one of their own\nverified outbound domains. Emitted once per active row in\nthe org's `domains` table.\n",
11526
12058
  "properties": {
11527
12059
  "type": {
@@ -12108,7 +12640,7 @@ const operationManifest = [
12108
12640
  "binaryResponse": false,
12109
12641
  "bodyRequired": true,
12110
12642
  "command": "reply-to-email",
12111
- "description": "Sends an outbound reply to the inbound email identified by `id`.\nThreading headers (`In-Reply-To`, `References`), recipient\nderivation (Reply-To, then From, then bare sender), and the\n`Re:` subject prefix are all derived server-side from the\nstored inbound row. The request body carries only the message\nbody and optional `wait` flag; passing any header or recipient\noverride is rejected by the schema (`additionalProperties:\nfalse`).\n\nForwards through the same gates as `/send-mail`: the response\nstatus, error envelope, and `idempotent_replay` flag mirror\nthe send-mail contract verbatim.\n",
12643
+ "description": "Sends an outbound reply to the inbound email identified by `id`.\nThreading headers (`In-Reply-To`, `References`), recipient\nderivation (Reply-To, then From, then bare sender), and the\n`Re:` subject prefix are all derived server-side from the\nstored inbound row. The request body carries only the message\nbody, optional From override, optional attachments, and optional\n`wait` flag; passing any header or recipient override is\nrejected by the schema (`additionalProperties: false`).\n\nForwards through the same gates as `/send-mail`: the response\nstatus, error envelope, and `idempotent_replay` flag mirror\nthe send-mail contract verbatim.\n",
12112
12644
  "hasJsonBody": true,
12113
12645
  "method": "POST",
12114
12646
  "operationId": "replyToEmail",
@@ -12143,6 +12675,36 @@ const operationManifest = [
12143
12675
  "wait": {
12144
12676
  "type": "boolean",
12145
12677
  "description": "When true, wait for the first downstream SMTP delivery outcome before returning, mirroring the send-mail `wait` semantics."
12678
+ },
12679
+ "attachments": {
12680
+ "type": "array",
12681
+ "maxItems": 100,
12682
+ "description": "Inline attachments for this reply. Use https://api.primitive.dev/v1 for replies with attachments. Combined raw decoded attachment bytes must be at most 31457280.",
12683
+ "items": {
12684
+ "type": "object",
12685
+ "additionalProperties": false,
12686
+ "properties": {
12687
+ "filename": {
12688
+ "type": "string",
12689
+ "minLength": 1,
12690
+ "maxLength": 255,
12691
+ "description": "Attachment filename. Control characters are rejected."
12692
+ },
12693
+ "content_type": {
12694
+ "type": "string",
12695
+ "minLength": 1,
12696
+ "maxLength": 255,
12697
+ "description": "Optional MIME content type. Control characters are rejected."
12698
+ },
12699
+ "content_base64": {
12700
+ "type": "string",
12701
+ "minLength": 1,
12702
+ "maxLength": 44040192,
12703
+ "description": "Base64-encoded attachment bytes."
12704
+ }
12705
+ },
12706
+ "required": ["filename", "content_base64"]
12707
+ }
12146
12708
  }
12147
12709
  }
12148
12710
  },
@@ -13249,7 +13811,7 @@ async function runWithTiming(enabled, fn) {
13249
13811
  const TIME_FLAG_DESCRIPTION = "Print the wall-clock duration of this command to stderr after it completes (e.g. `[time: 1.34s]`). Useful for measuring `--wait` send latency, comparing CLI overhead, or capturing timing in scripts.";
13250
13812
  const API_BASE_URL_1_FLAG_DESCRIPTION = "Override the primary API base URL. Internal testing only; not documented to customers.";
13251
13813
  const API_BASE_URL_2_FLAG_DESCRIPTION = "Override the attachments-supporting send host base URL. Internal testing only; not documented to customers.";
13252
- const HOST_2_OPERATIONS = new Set(["sendEmail"]);
13814
+ const HOST_2_OPERATIONS = new Set(["sendEmail", "replyToEmail"]);
13253
13815
  const RESERVED_FLAG_NAMES = new Set([
13254
13816
  "api-key",
13255
13817
  "api-base-url-1",
@@ -13480,6 +14042,39 @@ function canonicalizeCliReferences(description) {
13480
14042
  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'`");
13481
14043
  }
13482
14044
  //#endregion
14045
+ //#region src/oclif/attachments.ts
14046
+ function readAttachmentBytes(path, readFile) {
14047
+ try {
14048
+ return Buffer.from(readFile(path));
14049
+ } catch (error) {
14050
+ const detail = error instanceof Error ? error.message : String(error);
14051
+ throw new Errors.CLIError(`Could not read --attachment ${path}: ${detail}`, { exit: 1 });
14052
+ }
14053
+ }
14054
+ function hasControlCharacter(value) {
14055
+ return Array.from(value).some((character) => {
14056
+ const code = character.charCodeAt(0);
14057
+ return code <= 31 || code >= 127 && code <= 159;
14058
+ });
14059
+ }
14060
+ function validateAttachmentFilename(path, filename) {
14061
+ if (!filename) throw new Errors.CLIError(`Could not derive an attachment filename from ${path}. Pass a file path.`, { exit: 1 });
14062
+ if (hasControlCharacter(filename)) throw new Errors.CLIError(`Attachment filename ${filename} contains control characters.`, { exit: 1 });
14063
+ }
14064
+ function readAttachmentFiles(paths, readFile = readFileSync) {
14065
+ if (!paths || paths.length === 0) return void 0;
14066
+ return paths.map((path) => {
14067
+ const filename = basename(path);
14068
+ validateAttachmentFilename(path, filename);
14069
+ const bytes = readAttachmentBytes(path, readFile);
14070
+ if (bytes.length === 0) throw new Errors.CLIError(`Attachment file ${path} is empty. Attachments must contain at least one byte.`, { exit: 1 });
14071
+ return {
14072
+ content_base64: bytes.toString("base64"),
14073
+ filename
14074
+ };
14075
+ });
14076
+ }
14077
+ //#endregion
13483
14078
  //#region src/oclif/outbound-defaults.ts
13484
14079
  const SUBJECT_MAX_LENGTH = 200;
13485
14080
  function deriveSubject(body) {
@@ -13630,6 +14225,30 @@ async function readStdinToString(missingMessage = "No message provided. Pass the
13630
14225
  for await (const chunk of process.stdin) chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
13631
14226
  return Buffer.concat(chunks).toString("utf8");
13632
14227
  }
14228
+ function chatColor(color, text) {
14229
+ return ux.colorize(color, text);
14230
+ }
14231
+ function chatCommandText(command) {
14232
+ return chatColor("cyan", command);
14233
+ }
14234
+ function chatDetailLine(line) {
14235
+ return chatColor("dim", line);
14236
+ }
14237
+ function chatFailureText(message) {
14238
+ return chatColor("red", message);
14239
+ }
14240
+ function chatHeading(text) {
14241
+ return chatColor("bold", text);
14242
+ }
14243
+ function chatNoticeText(message) {
14244
+ return chatColor("yellow", message);
14245
+ }
14246
+ function chatProgressText(message) {
14247
+ return chatColor("cyan", message);
14248
+ }
14249
+ function chatSuccessText(message) {
14250
+ return chatColor("greenBright", message);
14251
+ }
13633
14252
  var ChatProgressIndicator = class {
13634
14253
  currentMessage = null;
13635
14254
  frameIndex = 0;
@@ -13650,7 +14269,7 @@ var ChatProgressIndicator = class {
13650
14269
  this.timer.unref?.();
13651
14270
  return;
13652
14271
  }
13653
- this.stream.write(`${message}\n`);
14272
+ this.stream.write(`${chatProgressText(message)}\n`);
13654
14273
  }
13655
14274
  update(message, options = {}) {
13656
14275
  this.currentMessage = message;
@@ -13663,10 +14282,10 @@ var ChatProgressIndicator = class {
13663
14282
  return;
13664
14283
  }
13665
14284
  this.stopTimer();
13666
- this.stream.write(`${message}\n`);
14285
+ this.stream.write(`${chatProgressText(message)}\n`);
13667
14286
  if (options.heartbeatMs !== void 0) {
13668
14287
  this.timer = setInterval(() => {
13669
- this.stream.write(`${formatWaitingHeartbeat(message, this.now() - this.startedAt, options.timeoutSeconds)}\n`);
14288
+ this.stream.write(`${chatProgressText(formatWaitingHeartbeat(message, this.now() - this.startedAt, options.timeoutSeconds))}\n`);
13670
14289
  }, options.heartbeatMs);
13671
14290
  this.timer.unref?.();
13672
14291
  }
@@ -13675,17 +14294,17 @@ var ChatProgressIndicator = class {
13675
14294
  if (this.stream.isTTY) {
13676
14295
  const currentMessage = this.currentMessage;
13677
14296
  this.clearLine();
13678
- this.stream.write(`${message}\n`);
14297
+ this.stream.write(`${chatNoticeText(message)}\n`);
13679
14298
  if (currentMessage !== null && this.timer !== null) this.render(currentMessage);
13680
14299
  return;
13681
14300
  }
13682
- this.stream.write(`${message}\n`);
14301
+ this.stream.write(`${chatNoticeText(message)}\n`);
13683
14302
  }
13684
14303
  succeed(message) {
13685
- this.finish(`${message} after ${formatElapsed(this.now() - this.startedAt)}.`);
14304
+ this.finish(chatSuccessText(`${message} after ${formatElapsed(this.now() - this.startedAt)}.`));
13686
14305
  }
13687
14306
  fail(message) {
13688
- this.finish(message);
14307
+ this.finish(chatFailureText(message));
13689
14308
  }
13690
14309
  finish(message) {
13691
14310
  this.stopTimer();
@@ -13702,9 +14321,10 @@ var ChatProgressIndicator = class {
13702
14321
  ];
13703
14322
  const frame = frames[this.frameIndex % frames.length];
13704
14323
  this.frameIndex += 1;
13705
- const line = `${frame} ${message} (${formatElapsed(this.now() - this.startedAt)})`;
13706
- this.lastLineLength = Math.max(this.lastLineLength, line.length);
13707
- this.stream.write(`\r${line}`);
14324
+ const elapsed = `(${formatElapsed(this.now() - this.startedAt)})`;
14325
+ const plainLine = `${frame} ${message} ${elapsed}`;
14326
+ this.lastLineLength = Math.max(this.lastLineLength, plainLine.length);
14327
+ this.stream.write(`\r${chatProgressText(`${frame} ${message}`)} ${chatColor("dim", elapsed)}`);
13708
14328
  }
13709
14329
  clearLine() {
13710
14330
  if (this.lastLineLength > 0) {
@@ -13950,45 +14570,46 @@ function formatChatResponse(context) {
13950
14570
  const accepted = context.sent.accepted.join(", ") || context.recipient;
13951
14571
  const responseBody = resolveChatResponseBody(context.reply);
13952
14572
  const lines = [
13953
- "Reply received",
14573
+ chatSuccessText("Reply received"),
13954
14574
  "",
13955
- "Sent",
13956
- ` To: ${accepted}`,
13957
- ` From: ${context.sent.from || context.from}`,
13958
- ` Subject: ${context.subject}`,
13959
- ` Sent email id: ${context.sent.id}`,
13960
- ` Delivery status: ${context.sent.delivery_status ?? context.sent.status}`,
14575
+ chatHeading("Sent"),
14576
+ chatDetailLine(` To: ${accepted}`),
14577
+ chatDetailLine(` From: ${context.sent.from || context.from}`),
14578
+ chatDetailLine(` Subject: ${context.subject}`),
14579
+ chatDetailLine(` Sent email id: ${context.sent.id}`),
14580
+ chatColor("green", ` Delivery status: ${context.sent.delivery_status ?? context.sent.status}`),
13961
14581
  "",
13962
- "Reply",
13963
- ` Email id: ${context.reply.id}`,
13964
- ` From: ${context.reply.from_email}`,
13965
- ` To: ${context.reply.to_email}`,
13966
- ` Subject: ${context.reply.subject ?? "(no subject)"}`,
13967
- ` Received: ${context.reply.received_at}`,
13968
- ` Match: ${matchDescription(context.matchStrategy)}`
14582
+ chatHeading("Reply"),
14583
+ chatDetailLine(` Email id: ${context.reply.id}`),
14584
+ chatDetailLine(` From: ${context.reply.from_email}`),
14585
+ chatDetailLine(` To: ${context.reply.to_email}`),
14586
+ chatDetailLine(` Subject: ${context.reply.subject ?? "(no subject)"}`),
14587
+ chatDetailLine(` Received: ${context.reply.received_at}`),
14588
+ chatDetailLine(` Match: ${matchDescription(context.matchStrategy)}`)
13969
14589
  ];
13970
- if (context.reply.reply_to_sent_email_id) lines.push(` Reply to sent email id: ${context.reply.reply_to_sent_email_id}`);
13971
- if (context.reply.message_id) lines.push(` Message-Id: ${context.reply.message_id}`);
13972
- if (context.localChatId !== void 0) lines.push(` Local chat id: ${context.localChatId}`);
13973
- lines.push("", "Helpful follow-up commands", " Replace <message> before running commands that include it.", " Commands are templates; use --json for parse-safe output.", " When shown, --strict-only prefers timing out over matching the wrong reply.");
13974
- for (const { description, command } of buildChatFollowUpCommands(context)) lines.push(` ${description}:`, ` ${command}`);
13975
- lines.push("", `Response body (${responseBody.format}; use --json for parsing)`, "----- BEGIN RESPONSE -----", responseBody.body || "(empty response)", "----- END RESPONSE -----");
14590
+ if (context.reply.reply_to_sent_email_id) lines.push(chatDetailLine(` Reply to sent email id: ${context.reply.reply_to_sent_email_id}`));
14591
+ if (context.reply.message_id) lines.push(chatDetailLine(` Message-Id: ${context.reply.message_id}`));
14592
+ if (context.localChatId !== void 0) lines.push(chatDetailLine(` Local chat id: ${context.localChatId}`));
14593
+ lines.push("", chatHeading("Helpful follow-up commands"), chatDetailLine(" Replace <message> before running commands that include it."), chatDetailLine(" Commands are templates; use --json for parse-safe output."), chatDetailLine(" When shown, --strict-only prefers timing out over matching the wrong reply."));
14594
+ for (const { description, command } of buildChatFollowUpCommands(context)) lines.push(chatHeading(` ${description}:`), ` ${chatCommandText(command)}`);
14595
+ lines.push("", chatHeading(`Response body (${responseBody.format}; use --json for parsing)`), "----- BEGIN RESPONSE -----", responseBody.body || "(empty response)", "----- END RESPONSE -----");
13976
14596
  return lines.join("\n");
13977
14597
  }
13978
14598
  function formatChatRecoveryContext(context) {
14599
+ const accepted = context.sent.accepted.join(", ") || context.recipient;
13979
14600
  const lines = [
13980
14601
  "",
13981
- "Sent message context",
13982
- ` To: ${context.sent.accepted.join(", ") || context.recipient}`,
13983
- ` From: ${context.sent.from || context.from}`,
13984
- ` Subject: ${context.subject}`,
13985
- ` Sent email id: ${context.sent.id}`,
13986
- ` Delivery status: ${context.sent.delivery_status ?? context.sent.status}`,
13987
- ` Poll since: ${context.sentAtIso}`,
14602
+ chatHeading("Sent message context"),
14603
+ chatDetailLine(` To: ${accepted}`),
14604
+ chatDetailLine(` From: ${context.sent.from || context.from}`),
14605
+ chatDetailLine(` Subject: ${context.subject}`),
14606
+ chatDetailLine(` Sent email id: ${context.sent.id}`),
14607
+ chatColor("green", ` Delivery status: ${context.sent.delivery_status ?? context.sent.status}`),
14608
+ chatDetailLine(` Poll since: ${context.sentAtIso}`),
13988
14609
  "",
13989
- "Helpful recovery commands"
14610
+ chatHeading("Helpful recovery commands")
13990
14611
  ];
13991
- for (const { description, command } of buildChatRecoveryCommands(context)) lines.push(` ${description}:`, ` ${command}`);
14612
+ for (const { description, command } of buildChatRecoveryCommands(context)) lines.push(chatHeading(` ${description}:`), ` ${chatCommandText(command)}`);
13992
14613
  return lines.join("\n");
13993
14614
  }
13994
14615
  async function loadInboundEmailDetail(params) {
@@ -14081,8 +14702,10 @@ var ChatCommand = class ChatCommand extends Command {
14081
14702
  "<%= config.bin %> chat help@agent.acme.dev 'how do I rotate my API key?'",
14082
14703
  "cat error.log | <%= config.bin %> chat help@agent.acme.dev",
14083
14704
  "<%= config.bin %> chat reply 'one more thing'",
14705
+ "<%= config.bin %> chat reply 'see attached' --attachment ./report.pdf",
14084
14706
  "<%= config.bin %> chat help@agent.acme.dev --reply 'one more thing'",
14085
14707
  "<%= config.bin %> chat help@agent.acme.dev --reply 'one more thing' --reply-to-email-id <inbound-email-id>",
14708
+ "<%= config.bin %> chat help@agent.acme.dev 'can you review this?' --attachment ./report.pdf",
14086
14709
  "<%= config.bin %> chat help@agent.acme.dev 'follow up question' --json",
14087
14710
  "<%= config.bin %> chat help@agent.acme.dev 'one more thing' --timeout 300"
14088
14711
  ];
@@ -14116,6 +14739,11 @@ var ChatCommand = class ChatCommand extends Command {
14116
14739
  reply: Flags.string({ description: "Reply body. Continues the latest inbound email from the recipient to your sender address; pass --reply-to-email-id for an exact thread." }),
14117
14740
  "reply-to-email-id": Flags.string({ description: "Inbound email id to continue exactly. Uses Primitive's reply endpoint, so recipient, subject, and threading headers are derived from the inbound email." }),
14118
14741
  "in-reply-to": Flags.string({ description: "Raw Message-Id of the parent email to thread a new send against. Prefer --reply-to-email-id with --reply when continuing an inbound email stored by Primitive." }),
14742
+ attachment: Flags.string({
14743
+ char: "a",
14744
+ description: "Attach a file to this chat message. Repeat --attachment to attach multiple files.",
14745
+ multiple: true
14746
+ }),
14119
14747
  "chat-local-id": Flags.integer({
14120
14748
  description: "Local chat id to update after this command succeeds. Internal plumbing for `primitive chat reply`.",
14121
14749
  hidden: true,
@@ -14169,6 +14797,7 @@ var ChatCommand = class ChatCommand extends Command {
14169
14797
  configDir: this.config.configDir
14170
14798
  };
14171
14799
  const progress = flags.quiet ? null : new ChatProgressIndicator(process.stderr);
14800
+ const attachments = readAttachmentFiles(flags.attachment);
14172
14801
  let from;
14173
14802
  let parentReply;
14174
14803
  let subject;
@@ -14227,9 +14856,10 @@ var ChatCommand = class ChatCommand extends Command {
14227
14856
  const sendResult = parentReply !== void 0 ? await replyToEmail({
14228
14857
  body: {
14229
14858
  body_text: message,
14230
- from
14859
+ from,
14860
+ ...attachments !== void 0 ? { attachments } : {}
14231
14861
  },
14232
- client: apiClient.client,
14862
+ client: apiClient._sendClient,
14233
14863
  path: { id: parentReply.id },
14234
14864
  responseStyle: "fields"
14235
14865
  }) : await sendEmail({
@@ -14238,7 +14868,8 @@ var ChatCommand = class ChatCommand extends Command {
14238
14868
  to: args.recipient,
14239
14869
  subject,
14240
14870
  body_text: message,
14241
- ...flags["in-reply-to"] !== void 0 ? { in_reply_to: flags["in-reply-to"] } : {}
14871
+ ...flags["in-reply-to"] !== void 0 ? { in_reply_to: flags["in-reply-to"] } : {},
14872
+ ...attachments !== void 0 ? { attachments } : {}
14242
14873
  },
14243
14874
  client: apiClient._sendClient,
14244
14875
  responseStyle: "fields"
@@ -14353,6 +14984,7 @@ var ChatReplyCommand = class ChatReplyCommand extends Command {
14353
14984
  "<%= config.bin %> chat reply 'one more thing'",
14354
14985
  "<%= config.bin %> chat reply 0 'one more thing'",
14355
14986
  "<%= config.bin %> chat reply --id 0 'one more thing'",
14987
+ "<%= config.bin %> chat reply 'see attached' --attachment ./report.pdf",
14356
14988
  "cat follow-up.txt | <%= config.bin %> chat reply"
14357
14989
  ];
14358
14990
  static args = {
@@ -14389,6 +15021,11 @@ var ChatReplyCommand = class ChatReplyCommand extends Command {
14389
15021
  min: 1
14390
15022
  }),
14391
15023
  "strict-only": Flags.boolean({ description: "Disable the time-window fallback. If the active chat was saved from a strict match, this is already the default." }),
15024
+ attachment: Flags.string({
15025
+ char: "a",
15026
+ description: "Attach a file to the reply. Repeat --attachment to attach multiple files.",
15027
+ multiple: true
15028
+ }),
14392
15029
  interval: Flags.integer({
14393
15030
  description: "Seconds between polls while waiting for the reply.",
14394
15031
  min: 1
@@ -14435,6 +15072,7 @@ var ChatReplyCommand = class ChatReplyCommand extends Command {
14435
15072
  if (flags["api-base-url-2"] !== void 0) argv.push("--api-base-url-2", flags["api-base-url-2"]);
14436
15073
  if (flags.json) argv.push("--json");
14437
15074
  if (flags.quiet) argv.push("--quiet");
15075
+ for (const attachment of flags.attachment ?? []) argv.push("--attachment", attachment);
14438
15076
  if (state.strict_only || flags["strict-only"]) argv.push("--strict-only");
14439
15077
  if (flags.time) argv.push("--time");
14440
15078
  await ChatCommand.run(argv, { root: this.config.root });
@@ -16444,8 +17082,8 @@ const PRIMITIVE_TEAM_AUTHOR = {
16444
17082
  name: "Primitive Team",
16445
17083
  url: "https://primitive.dev"
16446
17084
  };
16447
- const SDK_VERSION_RANGE = "^0.34.0";
16448
- const CLI_VERSION_RANGE = "^0.34.0";
17085
+ const SDK_VERSION_RANGE = "^0.35.0";
17086
+ const CLI_VERSION_RANGE = "^0.35.0";
16449
17087
  const ESBUILD_VERSION_RANGE = "^0.27.0";
16450
17088
  function renderHandler() {
16451
17089
  return `// env.PRIMITIVE_API_KEY, env.PRIMITIVE_WEBHOOK_SECRET, and
@@ -17818,6 +18456,7 @@ const DOMAIN_DISPLAY_WIDTH = 34;
17818
18456
  const STATUS_DISPLAY_WIDTH = 12;
17819
18457
  const BOOL_DISPLAY_WIDTH = 7;
17820
18458
  const NUM_DISPLAY_WIDTH = 6;
18459
+ const DEFAULT_PRIMITIVE_LOCAL_PART = "agent";
17821
18460
  function plural(count, singular, pluralValue = `${singular}s`) {
17822
18461
  return `${count} ${count === 1 ? singular : pluralValue}`;
17823
18462
  }
@@ -17853,6 +18492,14 @@ function domainSummary(domain) {
17853
18492
  default: return `${domain.domain} has status ${String(domain.status)}.`;
17854
18493
  }
17855
18494
  }
18495
+ function findSuggestedPrimitiveAddress(domains) {
18496
+ const domain = domains.find((entry) => entry.managed && entry.active && entry.receiving_ready);
18497
+ if (!domain) return null;
18498
+ return {
18499
+ address: `${DEFAULT_PRIMITIVE_LOCAL_PART}@${domain.domain}`,
18500
+ domain: domain.domain
18501
+ };
18502
+ }
17856
18503
  function focusInboxStatus(status, domainName) {
17857
18504
  const normalized = domainName.toLowerCase();
17858
18505
  const domain = status.domains.find((entry) => entry.domain.toLowerCase() === normalized);
@@ -17899,12 +18546,14 @@ function formatInboxStatus(status) {
17899
18546
  "",
17900
18547
  "Domains"
17901
18548
  ];
18549
+ const suggestedAddress = findSuggestedPrimitiveAddress(status.domains);
17902
18550
  if (status.domains.length === 0) lines.push("No domains configured.");
17903
18551
  else {
17904
18552
  lines.push(formatDomainHeader());
17905
18553
  for (const domain of status.domains) lines.push(formatDomainRow(domain));
17906
18554
  }
17907
18555
  lines.push("", `Endpoints: ${status.endpoints.enabled}/${status.endpoints.total} enabled (${status.endpoints.fallback_enabled} fallback, ${status.endpoints.domain_scoped_enabled} domain-scoped, ${status.endpoints.function_enabled} function)`, `Functions: ${status.functions.deployed}/${status.functions.total} deployed (${status.functions.pending} pending, ${status.functions.failed} failed)`, `Recent inbound: ${plural(status.recent_emails.total, "email")} latest ${formatInboxDate(status.recent_emails.latest_received_at)}`);
18556
+ if (suggestedAddress) lines.push("", `Primitive address: ${suggestedAddress.address}`, ` Any local-part at ${suggestedAddress.domain} can receive mail.`, ` Try: primitive send --to ${suggestedAddress.address} --subject "hello" --body "test"`);
17908
18557
  if (status.next_actions.length > 0) {
17909
18558
  lines.push("", "Next actions");
17910
18559
  for (const action of status.next_actions) lines.push(formatNextAction(action));
@@ -17981,6 +18630,180 @@ var InboxStatusCommand = class InboxStatusCommand extends Command {
17981
18630
  }
17982
18631
  };
17983
18632
  //#endregion
18633
+ //#region src/oclif/commands/inbox-setup.ts
18634
+ const DEFAULT_FUNCTION_NAME = "inbound-reply";
18635
+ const DEFAULT_LOCAL_PART = "inbox";
18636
+ const FUNCTION_ID_PLACEHOLDER = "<function-id>";
18637
+ function firstUsableManagedDomain(status) {
18638
+ return status.domains.find((domain) => domain.managed && domain.receiving_ready && domain.active) ?? status.domains.find((domain) => domain.managed && domain.receiving_ready) ?? null;
18639
+ }
18640
+ function buildInboxSetupCommands(functionName = DEFAULT_FUNCTION_NAME) {
18641
+ return {
18642
+ scaffold: [
18643
+ `primitive functions init ${functionName}`,
18644
+ `cd ${functionName}`,
18645
+ "npm install",
18646
+ "npm run build",
18647
+ `primitive functions deploy --name ${functionName} --file ./dist/handler.js --wait`,
18648
+ `primitive functions test --id ${FUNCTION_ID_PLACEHOLDER} --wait --show-sends`
18649
+ ],
18650
+ logs: `primitive functions logs --id ${FUNCTION_ID_PLACEHOLDER}`,
18651
+ status: "primitive inbox status"
18652
+ };
18653
+ }
18654
+ function buildInboxSetupProof(commands) {
18655
+ return {
18656
+ after_test: [
18657
+ "inbound id for the generated test email",
18658
+ "function id matching the deployed Function",
18659
+ "invocation status completed, failed, or send_failed",
18660
+ "reply/send result emitted by the handler"
18661
+ ],
18662
+ logs_command: commands.logs
18663
+ };
18664
+ }
18665
+ function buildInboxSetupGuide(status) {
18666
+ const domain = firstUsableManagedDomain(status);
18667
+ const commands = buildInboxSetupCommands();
18668
+ const mode = !status.receiving_ready ? "not_receiving" : status.processing_ready ? "actively_processed" : "stored_only";
18669
+ return {
18670
+ readiness: {
18671
+ ready: status.ready,
18672
+ receiving_ready: status.receiving_ready,
18673
+ processing_ready: status.processing_ready,
18674
+ mode,
18675
+ summary: status.summary
18676
+ },
18677
+ receive: {
18678
+ address: domain ? `${DEFAULT_LOCAL_PART}@${domain.domain}` : null,
18679
+ domain: domain?.domain ?? null,
18680
+ managed: domain?.managed ?? false,
18681
+ placeholder_local_part: domain ? DEFAULT_LOCAL_PART : null
18682
+ },
18683
+ processing: {
18684
+ stored_only: status.receiving_ready && !status.processing_ready,
18685
+ active: status.processing_ready,
18686
+ enabled_endpoints: status.endpoints.enabled,
18687
+ deployed_functions: status.functions.deployed
18688
+ },
18689
+ commands,
18690
+ proof: buildInboxSetupProof(commands),
18691
+ status
18692
+ };
18693
+ }
18694
+ function formatReadiness(guide) {
18695
+ const readiness = guide.readiness.ready ? "ready" : "not ready";
18696
+ const receiving = guide.readiness.receiving_ready ? "yes" : "no";
18697
+ const processing = guide.readiness.processing_ready ? "yes" : "no";
18698
+ const mode = guide.readiness.mode === "actively_processed" ? "actively processed" : guide.readiness.mode === "stored_only" ? "stored-only" : "not receiving";
18699
+ return [
18700
+ `Readiness: ${readiness}`,
18701
+ `Receiving: ${receiving}`,
18702
+ `Processing: ${processing}`,
18703
+ `Mode: ${mode}`
18704
+ ].join("\n");
18705
+ }
18706
+ function formatReceiveAddress(guide) {
18707
+ if (!guide.receive.domain || !guide.receive.address) return "Receive address: none found on a receiving-ready Primitive-managed domain";
18708
+ return [`Receive address: ${guide.receive.address}`, `Receive domain: ${guide.receive.domain} (Primitive-managed)`].join("\n");
18709
+ }
18710
+ function formatDomainDetails(status) {
18711
+ if (status.domains.length === 0) return ["Domains: none configured"];
18712
+ return status.domains.map((domain) => `- ${domain.domain}: ${statusText(domain.status)}, receive ${domain.receiving_ready ? "yes" : "no"}, process ${domain.processing_ready ? "yes" : "no"}, routes ${domain.processing_route_count}`);
18713
+ }
18714
+ function formatScaffoldCommands(commands) {
18715
+ return commands.scaffold.map((command) => ` ${command}`);
18716
+ }
18717
+ function formatInboxSetupGuide(guide) {
18718
+ const lines = [
18719
+ "Inbound setup",
18720
+ "",
18721
+ guide.readiness.summary,
18722
+ "",
18723
+ formatReadiness(guide),
18724
+ "",
18725
+ formatReceiveAddress(guide),
18726
+ "",
18727
+ "Domains",
18728
+ ...formatDomainDetails(guide.status),
18729
+ "",
18730
+ `Processing routes: ${guide.processing.enabled_endpoints} enabled endpoint(s), ${guide.processing.deployed_functions} deployed Function(s)`
18731
+ ];
18732
+ if (guide.readiness.mode === "not_receiving") lines.push("", "Next actions", "Make a receiving-ready domain available, then re-run:", ` ${guide.commands.status}`);
18733
+ else if (!guide.processing.active) lines.push("", "Next actions", "No processing route is enabled. Scaffold, deploy, and test an email Function:", ...formatScaffoldCommands(guide.commands));
18734
+ else lines.push("", "Next actions", "Inbound mail has an active processing route. Run a Function test when you know the Function id:", ` primitive functions test --id ${FUNCTION_ID_PLACEHOLDER} --wait --show-sends`);
18735
+ if (guide.status.next_actions.length > 0) {
18736
+ lines.push("", "API suggested actions");
18737
+ for (const action of guide.status.next_actions) lines.push(action.command ? `- ${action.message}\n ${action.command}` : `- ${action.message}`);
18738
+ }
18739
+ lines.push("", "Proof after functions test", "- Inbound id: the generated test email should have an inbound id.", "- Function id: the run should point at the Function id you deployed.", "- Invocation status: expect completed; failed or send_failed identifies the failing stage.", "- Reply/send result: --show-sends should show the handler's outbound result when it replies or sends.", "- Logs:", ` ${guide.proof.logs_command}`);
18740
+ return lines.join("\n");
18741
+ }
18742
+ var InboxSetupCommand = class InboxSetupCommand extends Command {
18743
+ static description = `Guide inbound email setup from the server-owned inbox status API.
18744
+
18745
+ This command does not scaffold, deploy, or run tests. It verifies auth, fetches inbox readiness, shows the first usable Primitive-managed receive address/domain, explains whether inbound mail is stored-only or actively processed, and prints the exact commands to add a Function processing route when one is missing.`;
18746
+ static summary = "Guide inbound email setup";
18747
+ static examples = ["<%= config.bin %> inbox setup", "<%= config.bin %> inbox setup --json"];
18748
+ static flags = {
18749
+ "api-key": Flags.string({
18750
+ description: "Primitive API key override (defaults to PRIMITIVE_API_KEY or saved OAuth login credentials)",
18751
+ env: "PRIMITIVE_API_KEY"
18752
+ }),
18753
+ "api-base-url-1": Flags.string({
18754
+ description: API_BASE_URL_1_FLAG_DESCRIPTION,
18755
+ env: "PRIMITIVE_API_BASE_URL_1",
18756
+ hidden: true
18757
+ }),
18758
+ "api-base-url-2": Flags.string({
18759
+ description: API_BASE_URL_2_FLAG_DESCRIPTION,
18760
+ env: "PRIMITIVE_API_BASE_URL_2",
18761
+ hidden: true
18762
+ }),
18763
+ json: Flags.boolean({ description: "Print structured readiness, receive address, commands, proof metadata, and raw status as JSON." }),
18764
+ time: Flags.boolean({ description: TIME_FLAG_DESCRIPTION })
18765
+ };
18766
+ async run() {
18767
+ const { flags } = await this.parse(InboxSetupCommand);
18768
+ await runWithTiming(flags.time, async () => {
18769
+ const { apiClient, auth, baseUrlOverridden } = await createAuthenticatedCliApiClient({
18770
+ apiKey: flags["api-key"],
18771
+ apiBaseUrl1: flags["api-base-url-1"],
18772
+ apiBaseUrl2: flags["api-base-url-2"],
18773
+ configDir: this.config.configDir
18774
+ });
18775
+ const result = await getInboxStatus({
18776
+ client: apiClient.client,
18777
+ responseStyle: "fields"
18778
+ });
18779
+ if (result.error) {
18780
+ const errorPayload = extractErrorPayload(result.error);
18781
+ writeErrorWithHints(errorPayload);
18782
+ surfaceUnauthorizedHint({
18783
+ auth,
18784
+ baseUrlOverridden,
18785
+ configDir: this.config.configDir,
18786
+ payload: errorPayload
18787
+ });
18788
+ process.exitCode = 1;
18789
+ return;
18790
+ }
18791
+ const envelope = result.data ?? {};
18792
+ const status = envelope.data;
18793
+ if (!status) throw new Errors.CLIError("Primitive API returned no inbox status.", { exit: 1 });
18794
+ const guide = buildInboxSetupGuide(status);
18795
+ if (flags.json) {
18796
+ this.log(JSON.stringify({
18797
+ ...envelope,
18798
+ data: guide
18799
+ }, null, 2));
18800
+ return;
18801
+ }
18802
+ this.log(formatInboxSetupGuide(guide));
18803
+ });
18804
+ }
18805
+ };
18806
+ //#endregion
17984
18807
  //#region src/oclif/commands/login.ts
17985
18808
  const MAX_CLI_LOGIN_POLL_INTERVAL_SECONDS = 60;
17986
18809
  function cliError$3(message) {
@@ -18303,11 +19126,100 @@ function loadPendingAgentSignup(configDir, apiBaseUrl1) {
18303
19126
  expires_in: Math.max(0, Math.ceil((new Date(pending.expires_at).getTime() - Date.now()) / 1e3))
18304
19127
  };
18305
19128
  }
19129
+ function readPendingAgentSignupState(configDir, apiBaseUrl1) {
19130
+ const path = pendingSignupPath(configDir);
19131
+ let contents;
19132
+ try {
19133
+ contents = readFileSync(path, "utf8");
19134
+ } catch (error) {
19135
+ if (error && typeof error === "object" && error.code === "ENOENT") return null;
19136
+ throw error;
19137
+ }
19138
+ let pending;
19139
+ try {
19140
+ pending = pendingSignupFromJson(JSON.parse(contents));
19141
+ } catch {
19142
+ pending = null;
19143
+ }
19144
+ if (!pending) {
19145
+ deletePendingAgentSignup(configDir);
19146
+ return null;
19147
+ }
19148
+ if (pending.api_base_url_1 !== apiBaseUrl1) return null;
19149
+ return pending;
19150
+ }
19151
+ function pendingSignupStartCommand(email) {
19152
+ return `primitive signup ${email ?? "<email>"} --signup-code <invite-code> --accept-terms`;
19153
+ }
19154
+ function buildSignupStatus(params) {
19155
+ const copy = params.copy ?? DEFAULT_SIGNUP_COMMAND_COPY;
19156
+ const pending = readPendingAgentSignupState(params.configDir, params.apiBaseUrl1);
19157
+ if (!pending) return {
19158
+ code_length: null,
19159
+ confirm_command: null,
19160
+ email: null,
19161
+ expired: false,
19162
+ expires_at: null,
19163
+ expires_in: null,
19164
+ pending: false,
19165
+ resend_after: null,
19166
+ resend_command: null,
19167
+ signup_command: pendingSignupStartCommand(params.email)
19168
+ };
19169
+ if (params.email && normalizeEmail(pending.email) !== normalizeEmail(params.email)) throw cliError$2(`Pending ${copy.actionNoun} is for ${pending.email}, not ${params.email}. Run \`primitive signup status\` without an email argument to inspect it.`);
19170
+ const expiresAtMs = new Date(pending.expires_at).getTime();
19171
+ const expiresIn = Number.isFinite(expiresAtMs) ? Math.ceil((expiresAtMs - Date.now()) / 1e3) : null;
19172
+ return {
19173
+ code_length: pending.verification_code_length,
19174
+ confirm_command: `primitive ${copy.confirmCommand(pending.email)}`,
19175
+ email: pending.email,
19176
+ expired: expiresIn !== null && expiresIn <= 0,
19177
+ expires_at: pending.expires_at,
19178
+ expires_in: expiresIn === null ? null : Math.max(0, expiresIn),
19179
+ pending: true,
19180
+ resend_after: pending.resend_after,
19181
+ resend_command: `primitive ${copy.resendCommand(pending.email)}`
19182
+ };
19183
+ }
19184
+ function writeSignupStatus(status) {
19185
+ if (!status.pending) {
19186
+ process$1.stdout.write("No pending Primitive signup found.\n");
19187
+ process$1.stdout.write(`Start one with \`${status.signup_command ?? pendingSignupStartCommand()}\`.\n`);
19188
+ return;
19189
+ }
19190
+ process$1.stdout.write(`Pending Primitive signup for ${status.email}.\n`);
19191
+ if (status.code_length !== null) process$1.stdout.write(`Verification code length: ${status.code_length}\n`);
19192
+ if (status.expires_at) if (status.expired) process$1.stdout.write(`Expired at: ${status.expires_at}\n`);
19193
+ else {
19194
+ process$1.stdout.write(`Expires at: ${status.expires_at}\n`);
19195
+ process$1.stdout.write(`Expires in: ${formatSignupSeconds(status.expires_in)}\n`);
19196
+ }
19197
+ if (status.resend_after !== null) process$1.stdout.write(`Resend after: ${formatSignupSeconds(status.resend_after)}\n`);
19198
+ if (status.confirm_command) process$1.stdout.write(`Confirm: ${status.confirm_command}\n`);
19199
+ if (status.resend_command) process$1.stdout.write(`Resend: ${status.resend_command}\n`);
19200
+ }
19201
+ function runSignupStatus(params) {
19202
+ const { requestConfig } = createCliApiClient({
19203
+ apiBaseUrl1: params.flags["api-base-url-1"],
19204
+ configDir: params.configDir
19205
+ });
19206
+ const status = buildSignupStatus({
19207
+ apiBaseUrl1: requestConfig.resolvedApiBaseUrl1,
19208
+ configDir: params.configDir,
19209
+ copy: params.copy,
19210
+ email: params.email
19211
+ });
19212
+ if (params.flags.json) {
19213
+ process$1.stdout.write(`${JSON.stringify(status, null, 2)}\n`);
19214
+ return;
19215
+ }
19216
+ writeSignupStatus(status);
19217
+ }
18306
19218
  function requirePendingSignupForEmail(params) {
18307
19219
  const copy = params.copy ?? DEFAULT_SIGNUP_COMMAND_COPY;
18308
19220
  const pending = loadPendingAgentSignup(params.configDir, params.apiBaseUrl1);
18309
- if (!pending) throw cliError$2(`No pending ${copy.actionNoun} for ${params.email}. Run \`primitive ${copy.startCommand(params.email)}\` first.`);
18310
- if (normalizeEmail(pending.email) !== normalizeEmail(params.email)) throw cliError$2(`Pending ${copy.actionNoun} is for ${pending.email}, not ${params.email}. Run \`primitive ${copy.startCommand(params.email)} --force\` to replace it.`);
19221
+ if (!pending) throw cliError$2(`No pending ${copy.actionNoun} for ${params.email}. Run \`primitive signup status ${params.email}\` to inspect pending state, or \`primitive ${copy.startCommand(params.email)}\` first.`);
19222
+ if (normalizeEmail(pending.email) !== normalizeEmail(params.email)) throw cliError$2(`Pending ${copy.actionNoun} is for ${pending.email}, not ${params.email}. Run \`primitive signup status\` to inspect it, or \`primitive ${copy.startCommand(params.email)} --force\` to replace it.`);
18311
19223
  return pending;
18312
19224
  }
18313
19225
  function retryAfterSeconds(result) {
@@ -18410,13 +19322,13 @@ async function startSignup(params) {
18410
19322
  if (existingPending && !params.flags.force) {
18411
19323
  if (normalizeEmail(existingPending.email) === normalizeEmail(params.email)) {
18412
19324
  process$1.stderr.write(`Continuing pending Primitive ${copy.actionNoun} for ${existingPending.email}.\n`);
18413
- process$1.stderr.write(`Run \`primitive ${copy.confirmCommand(existingPending.email)}\` to finish, or \`primitive ${copy.resendCommand(existingPending.email)}\` to send a new code.\n`);
19325
+ process$1.stderr.write(`Run \`primitive ${copy.confirmCommand(existingPending.email)}\` to finish, \`primitive ${copy.resendCommand(existingPending.email)}\` to send a new code, or \`primitive signup status\` to inspect it.\n`);
18414
19326
  return {
18415
19327
  pending: existingPending,
18416
19328
  started: false
18417
19329
  };
18418
19330
  }
18419
- throw cliError$2(`Pending ${copy.actionNoun} is for ${existingPending.email}. Run \`primitive ${copy.startCommand(params.email)} --force\` to replace it.`);
19331
+ throw cliError$2(`Pending ${copy.actionNoun} is for ${existingPending.email}. Run \`primitive signup status\` to inspect it, or \`primitive ${copy.startCommand(params.email)} --force\` to replace it.`);
18420
19332
  }
18421
19333
  if (params.flags.force) deletePendingAgentSignup(params.configDir);
18422
19334
  const promptRequiredFn = params.deps.promptRequired ?? promptRequired;
@@ -18552,23 +19464,25 @@ async function runSignupConfirmWithCredentialLock(params) {
18552
19464
  }
18553
19465
  const payload = extractErrorPayload(verified.error);
18554
19466
  const code = extractErrorCode(payload);
18555
- if (code === INVALID_VERIFICATION_CODE) throw cliError$2(`Invalid verification code. Try again or run ${(params.copy ?? DEFAULT_SIGNUP_COMMAND_COPY).resendCommand(params.email)}.`);
19467
+ if (code === INVALID_VERIFICATION_CODE) throw cliError$2(`Invalid verification code. Try again, run ${(params.copy ?? DEFAULT_SIGNUP_COMMAND_COPY).resendCommand(params.email)}, or run primitive signup status.`);
18556
19468
  if (code === EXPIRED_TOKEN || code === INVALID_SIGNUP_TOKEN) deletePendingAgentSignup(configDir);
18557
19469
  writeErrorWithHints(payload);
18558
19470
  throw cliError$2("Primitive agent signup failed while verifying the account.");
18559
19471
  }
18560
19472
  async function runSignupResendWithCredentialLock(params) {
18561
19473
  const deps = params.deps ?? {};
19474
+ const copy = params.copy ?? DEFAULT_SIGNUP_COMMAND_COPY;
18562
19475
  const { apiClient, requestConfig } = createCliApiClient({
18563
19476
  apiBaseUrl1: params.flags["api-base-url-1"],
18564
19477
  configDir: params.configDir
18565
19478
  });
18566
- const pending = requirePendingSignupForEmail({
19479
+ const pending = params.email ? requirePendingSignupForEmail({
18567
19480
  apiBaseUrl1: requestConfig.resolvedApiBaseUrl1,
18568
- copy: params.copy,
19481
+ copy,
18569
19482
  configDir: params.configDir,
18570
19483
  email: params.email
18571
- });
19484
+ }) : loadPendingAgentSignup(params.configDir, requestConfig.resolvedApiBaseUrl1);
19485
+ if (!pending) throw cliError$2(`No pending ${copy.actionNoun} found. Run \`primitive signup status\` to inspect pending state, or start one with \`${pendingSignupStartCommand()}\`.`);
18572
19486
  const resend = await resendVerificationCode({
18573
19487
  apiBaseUrl1: requestConfig.resolvedApiBaseUrl1,
18574
19488
  apiClient,
@@ -18741,12 +19655,12 @@ var SignupConfirmCommand = class SignupConfirmCommand extends Command {
18741
19655
  };
18742
19656
  var SignupResendCommand = class SignupResendCommand extends Command {
18743
19657
  static args = { email: Args.string({
18744
- description: "Email address used to start signup",
18745
- required: true
19658
+ description: "Email address used to start signup. Defaults to the saved pending signup.",
19659
+ required: false
18746
19660
  }) };
18747
19661
  static description = "Resend the verification code for a pending signup.";
18748
19662
  static summary = "Resend signup verification code";
18749
- static examples = ["<%= config.bin %> signup resend user@example.com"];
19663
+ static examples = ["<%= config.bin %> signup resend", "<%= config.bin %> signup resend user@example.com"];
18750
19664
  static flags = { "api-base-url-1": Flags.string({
18751
19665
  description: "Override the primary API base URL. Internal testing only; not documented to customers.",
18752
19666
  env: "PRIMITIVE_API_BASE_URL_1",
@@ -18771,6 +19685,35 @@ var SignupResendCommand = class SignupResendCommand extends Command {
18771
19685
  }
18772
19686
  }
18773
19687
  };
19688
+ var SignupStatusCommand = class SignupStatusCommand extends Command {
19689
+ static args = { email: Args.string({
19690
+ description: "Email address expected in the pending signup",
19691
+ required: false
19692
+ }) };
19693
+ static description = "Inspect the locally saved pending Primitive signup state.";
19694
+ static summary = "Show pending signup status";
19695
+ static examples = [
19696
+ "<%= config.bin %> signup status",
19697
+ "<%= config.bin %> signup status user@example.com",
19698
+ "<%= config.bin %> signup status --json"
19699
+ ];
19700
+ static flags = {
19701
+ "api-base-url-1": Flags.string({
19702
+ description: "Override the primary API base URL. Internal testing only; not documented to customers.",
19703
+ env: "PRIMITIVE_API_BASE_URL_1",
19704
+ hidden: true
19705
+ }),
19706
+ json: Flags.boolean({ description: "Print pending signup status as JSON" })
19707
+ };
19708
+ async run() {
19709
+ const { args, flags } = await this.parse(SignupStatusCommand);
19710
+ runSignupStatus({
19711
+ configDir: this.config.configDir,
19712
+ email: args.email,
19713
+ flags
19714
+ });
19715
+ }
19716
+ };
18774
19717
  var SignupInteractiveCommand = class SignupInteractiveCommand extends Command {
18775
19718
  static description = "Run the full signup flow in one interactive terminal session.";
18776
19719
  static summary = "Run interactive account signup";
@@ -19031,6 +19974,7 @@ var ReplyCommand = class ReplyCommand extends Command {
19031
19974
  static examples = [
19032
19975
  "<%= config.bin %> reply --id <inbound-email-id> --body 'Thanks, got it.'",
19033
19976
  "<%= config.bin %> reply --id <inbound-email-id> --body-file ./reply.txt",
19977
+ "<%= config.bin %> reply --id <inbound-email-id> --body 'See attached.' --attachment ./report.pdf",
19034
19978
  "<%= config.bin %> reply --id <inbound-email-id> --html '<p>Thanks, got it.</p>' --wait",
19035
19979
  "<%= config.bin %> reply --id <inbound-email-id> --from 'Support <support@example.com>' --body 'Thanks!'"
19036
19980
  ];
@@ -19054,12 +19998,17 @@ var ReplyCommand = class ReplyCommand extends Command {
19054
19998
  required: true
19055
19999
  }),
19056
20000
  body: Flags.string({ description: "Plain-text reply body. Either --body or --html (or both) is required." }),
19057
- "body-file": Flags.string({ description: "Read the plain-text reply body from a UTF-8 file. Mutually exclusive with --body and --body-stdin." }),
20001
+ "body-file": Flags.string({ description: "Read the plain-text reply body from a UTF-8 file; this does not attach the file. Use --attachment for attachments. Mutually exclusive with --body and --body-stdin." }),
19058
20002
  "body-stdin": Flags.boolean({ description: "Read the plain-text reply body from stdin. Mutually exclusive with --body and --body-file. Stdin can only be consumed once." }),
19059
20003
  html: Flags.string({ description: "HTML reply body. Either --body or --html (or both) is required." }),
19060
- "html-file": Flags.string({ description: "Read the HTML reply body from a UTF-8 file. Mutually exclusive with --html and --html-stdin." }),
20004
+ "html-file": Flags.string({ description: "Read the HTML reply body from a UTF-8 file; this does not attach the file. Use --attachment for attachments. Mutually exclusive with --html and --html-stdin." }),
19061
20005
  "html-stdin": Flags.boolean({ description: "Read the HTML reply body from stdin. Mutually exclusive with --html and --html-file. Stdin can only be consumed once." }),
19062
20006
  from: Flags.string({ description: "Optional From header override. Defaults to the inbound recipient." }),
20007
+ attachment: Flags.string({
20008
+ char: "a",
20009
+ description: "Attach a file to the reply. Repeat --attachment to attach multiple files.",
20010
+ multiple: true
20011
+ }),
19063
20012
  wait: Flags.boolean({ description: "Block until the receiving MTA returns an outcome. Without --wait, the call returns once Primitive has accepted the reply for delivery." }),
19064
20013
  time: Flags.boolean({ description: TIME_FLAG_DESCRIPTION })
19065
20014
  };
@@ -19081,14 +20030,16 @@ var ReplyCommand = class ReplyCommand extends Command {
19081
20030
  apiBaseUrl2: flags["api-base-url-2"],
19082
20031
  configDir: this.config.configDir
19083
20032
  });
20033
+ const attachments = readAttachmentFiles(flags.attachment);
19084
20034
  const result = await replyToEmail({
19085
20035
  body: {
19086
20036
  ...bodies.body !== void 0 ? { body_text: bodies.body } : {},
19087
20037
  ...bodies.html !== void 0 ? { body_html: bodies.html } : {},
19088
20038
  ...flags.from !== void 0 ? { from: flags.from } : {},
20039
+ ...attachments !== void 0 ? { attachments } : {},
19089
20040
  ...flags.wait !== void 0 ? { wait: flags.wait } : {}
19090
20041
  },
19091
- client: apiClient.client,
20042
+ client: apiClient._sendClient,
19092
20043
  path: { id: flags.id },
19093
20044
  responseStyle: "fields"
19094
20045
  });
@@ -19113,39 +20064,6 @@ var ReplyCommand = class ReplyCommand extends Command {
19113
20064
  }
19114
20065
  };
19115
20066
  //#endregion
19116
- //#region src/oclif/attachments.ts
19117
- function readAttachmentBytes(path, readFile) {
19118
- try {
19119
- return Buffer.from(readFile(path));
19120
- } catch (error) {
19121
- const detail = error instanceof Error ? error.message : String(error);
19122
- throw new Errors.CLIError(`Could not read --attachment ${path}: ${detail}`, { exit: 1 });
19123
- }
19124
- }
19125
- function hasControlCharacter(value) {
19126
- return Array.from(value).some((character) => {
19127
- const code = character.charCodeAt(0);
19128
- return code <= 31 || code >= 127 && code <= 159;
19129
- });
19130
- }
19131
- function validateAttachmentFilename(path, filename) {
19132
- if (!filename) throw new Errors.CLIError(`Could not derive an attachment filename from ${path}. Pass a file path.`, { exit: 1 });
19133
- if (hasControlCharacter(filename)) throw new Errors.CLIError(`Attachment filename ${filename} contains control characters.`, { exit: 1 });
19134
- }
19135
- function readAttachmentFiles(paths, readFile = readFileSync) {
19136
- if (!paths || paths.length === 0) return void 0;
19137
- return paths.map((path) => {
19138
- const filename = basename(path);
19139
- validateAttachmentFilename(path, filename);
19140
- const bytes = readAttachmentBytes(path, readFile);
19141
- if (bytes.length === 0) throw new Errors.CLIError(`Attachment file ${path} is empty. Attachments must contain at least one byte.`, { exit: 1 });
19142
- return {
19143
- content_base64: bytes.toString("base64"),
19144
- filename
19145
- };
19146
- });
19147
- }
19148
- //#endregion
19149
20067
  //#region src/oclif/commands/send.ts
19150
20068
  var SendCommand = class SendCommand extends Command {
19151
20069
  static description = `Send an outbound email. Agent-grade shortcut for \`sending send\` with sensible defaults.
@@ -20002,6 +20920,7 @@ const COMMANDS = {
20002
20920
  "signup:confirm": SignupConfirmCommand,
20003
20921
  "signup:interactive": SignupInteractiveCommand,
20004
20922
  "signup:resend": SignupResendCommand,
20923
+ "signup:status": SignupStatusCommand,
20005
20924
  logout: LogoutCommand,
20006
20925
  whoami: WhoamiCommand,
20007
20926
  doctor: DoctorCommand,
@@ -20010,6 +20929,7 @@ const COMMANDS = {
20010
20929
  "emails:wait": EmailsWaitCommand,
20011
20930
  "domains:zone-file": DomainsZoneFileCommand,
20012
20931
  "domains:download-domain-zone-file": DomainsZoneFileCommand,
20932
+ "inbox:setup": InboxSetupCommand,
20013
20933
  "inbox:status": InboxStatusCommand,
20014
20934
  "inbox:get-inbox-status": InboxStatusCommand,
20015
20935
  "functions:init": FunctionsInitCommand,