@epilot/cli 0.1.130 → 0.1.132

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.
package/README.md CHANGED
@@ -29,7 +29,7 @@ npm install -g @epilot/cli
29
29
 
30
30
  <!-- usage-help -->
31
31
  ```
32
- epilot v0.1.130 — CLI for epilot APIs
32
+ epilot v0.1.132 — CLI for epilot APIs
33
33
 
34
34
  USAGE
35
35
  epilot <api> <operationId> [params...] [flags]
@@ -1438,6 +1438,70 @@
1438
1438
  }
1439
1439
  }
1440
1440
  },
1441
+ "/v1/app/{appId}/options/resolve": {
1442
+ "post": {
1443
+ "summary": "resolveOptions",
1444
+ "description": "Resolve the effective app-level options of an installation, including decrypted sensitive values (secrets). This endpoint accepts epilot internal-auth tokens exclusively (internal service calls, e.g. automation-workers or the customer portal API) — no user token, API key, or app token can call it, regardless of roles. Every call is audit-logged with the caller identity.",
1445
+ "operationId": "resolveOptions",
1446
+ "tags": [
1447
+ "App Installation"
1448
+ ],
1449
+ "parameters": [
1450
+ {
1451
+ "name": "appId",
1452
+ "in": "path",
1453
+ "required": true,
1454
+ "schema": {
1455
+ "type": "string"
1456
+ }
1457
+ }
1458
+ ],
1459
+ "requestBody": {
1460
+ "content": {
1461
+ "application/json": {
1462
+ "schema": {
1463
+ "type": "object",
1464
+ "properties": {
1465
+ "keys": {
1466
+ "type": "array",
1467
+ "items": {
1468
+ "type": "string"
1469
+ },
1470
+ "description": "Narrow the response to these option keys"
1471
+ }
1472
+ }
1473
+ }
1474
+ }
1475
+ }
1476
+ },
1477
+ "responses": {
1478
+ "200": {
1479
+ "description": "Effective options with resolved values",
1480
+ "content": {
1481
+ "application/json": {
1482
+ "schema": {
1483
+ "type": "object",
1484
+ "properties": {
1485
+ "options": {
1486
+ "type": "array",
1487
+ "items": {
1488
+ "$ref": "#/components/schemas/Options"
1489
+ }
1490
+ }
1491
+ }
1492
+ }
1493
+ }
1494
+ }
1495
+ },
1496
+ "403": {
1497
+ "description": "Caller is not authenticated with an internal-auth token"
1498
+ },
1499
+ "404": {
1500
+ "description": "App installation not found"
1501
+ }
1502
+ }
1503
+ }
1504
+ },
1441
1505
  "/v1/app-events": {
1442
1506
  "post": {
1443
1507
  "summary": "ingestEvent",
@@ -1831,6 +1895,13 @@
1831
1895
  "$ref": "#/components/schemas/FunctionDefinition"
1832
1896
  },
1833
1897
  "description": "Replaces the full set of server-side functions for this version"
1898
+ },
1899
+ "options": {
1900
+ "type": "array",
1901
+ "items": {
1902
+ "$ref": "#/components/schemas/Options"
1903
+ },
1904
+ "description": "Replaces the full set of app-level option declarations for this version.\nOption keys must be unique app-wide; `secret`-touching options are always\nsensitive and must not declare `sensitive: false`.\n"
1834
1905
  }
1835
1906
  }
1836
1907
  }
@@ -1984,6 +2055,25 @@
1984
2055
  "object"
1985
2056
  ],
1986
2057
  "description": "The type of this option. `object` declares a structured value whose fields are listed\nunder `fields`. Combine with `repeatable: true` to express a list of these objects.\n"
2058
+ },
2059
+ "sensitive": {
2060
+ "type": "boolean",
2061
+ "description": "Write-only, server-side only value — like a sensitive environment variable\n(app-level options only). Sensitive values are never serialized in any response;\nthey are resolvable only via server-side channels (API proxy injection, function\n`secrets` allowlist, the internal options/resolve endpoint). Non-sensitive values\nare readable wherever the app runs, including the end-customer browser (journey and\nportal runtime). Forced to true for `secret`-touching options. Default: false.\n"
2062
+ },
2063
+ "configured": {
2064
+ "type": "boolean",
2065
+ "readOnly": true,
2066
+ "description": "Serialized instead of `value` for sensitive options (secrets): true when a\nvalue is stored.\n"
2067
+ },
2068
+ "lifted": {
2069
+ "type": "boolean",
2070
+ "readOnly": true,
2071
+ "description": "Compatibility marker: this option is declared at app level and folded back\ninto the component's options so existing consumers (journey runtime, portal\nblocks, older services) keep working unchanged. New consumers should read\napp-level options from the installation's `options` array instead.\n"
2072
+ },
2073
+ "value_updated_at": {
2074
+ "type": "string",
2075
+ "readOnly": true,
2076
+ "description": "Timestamp of the last value change. Only returned for sensitive options\n(alongside `configured`) so installers can see when a secret was last rotated.\n"
1987
2077
  }
1988
2078
  }
1989
2079
  },
@@ -2048,7 +2138,7 @@
2048
2138
  "properties": {
2049
2139
  "component_id": {
2050
2140
  "type": "string",
2051
- "description": "ID of the component these values are for"
2141
+ "description": "ID of the component these values are for. Use the sentinel `$app` for\napp-level option values (options declared at the manifest top level).\n"
2052
2142
  },
2053
2143
  "options": {
2054
2144
  "type": "array",
@@ -2118,7 +2208,7 @@
2118
2208
  "label": {
2119
2209
  "allOf": [
2120
2210
  {
2121
- "description": null
2211
+ "description": "Human-readable display name of the function, shown to installing organizations (e.g. in the scheduled-functions summary)\n"
2122
2212
  },
2123
2213
  {
2124
2214
  "$ref": "#/components/schemas/TranslatedString"
@@ -2164,7 +2254,8 @@
2164
2254
  "items": {
2165
2255
  "type": "string"
2166
2256
  },
2167
- "description": "Keys of installation options of type secret made available to the function via input.app_options\n"
2257
+ "deprecated": true,
2258
+ "description": "DEPRECATED and ignored — functions receive the app's full effective option keyspace (sensitive values included) via input.app_options. Kept for manifest compatibility.\n"
2168
2259
  }
2169
2260
  }
2170
2261
  },
@@ -2254,6 +2345,13 @@
2254
2345
  },
2255
2346
  "description": "List of options for the app component"
2256
2347
  },
2348
+ "uses_options": {
2349
+ "type": "array",
2350
+ "items": {
2351
+ "type": "string"
2352
+ },
2353
+ "description": "Keys of app-level options this component uses. Narrows which options are\nfolded back into `options` for legacy consumers (e.g. which values a journey\nblock receives in its public args) — a scoping hint, never a security\nboundary. Stamped automatically by the component-options migration; may also\nbe declared by the app developer.\n"
2354
+ },
2257
2355
  "surfaces": {
2258
2356
  "type": "object"
2259
2357
  }
@@ -4840,6 +4938,14 @@
4840
4938
  "minItems": 0,
4841
4939
  "description": "Server-side functions of the app, including scheduled functions"
4842
4940
  },
4941
+ "options": {
4942
+ "type": "array",
4943
+ "items": {
4944
+ "$ref": "#/components/schemas/Options"
4945
+ },
4946
+ "minItems": 0,
4947
+ "description": "App-level option declarations (with sensitivity) of this version"
4948
+ },
4843
4949
  "visibility": {
4844
4950
  "type": "string",
4845
4951
  "enum": [
@@ -5077,6 +5183,14 @@
5077
5183
  },
5078
5184
  "readOnly": true
5079
5185
  },
5186
+ "options": {
5187
+ "description": "Effective app-level options of the installation: declared top-level options plus\ncomponent-level options lifted to the app keyspace, each with its configured\n`value`. Values of sensitive options (secrets) are never included — they carry\n`configured` and `value_updated_at` instead.\n",
5188
+ "type": "array",
5189
+ "items": {
5190
+ "$ref": "#/components/schemas/Options"
5191
+ },
5192
+ "readOnly": true
5193
+ },
5080
5194
  "installed_version": {
5081
5195
  "type": "string",
5082
5196
  "description": "Version of the app that is installed",
@@ -5192,6 +5306,14 @@
5192
5306
  "minItems": 0,
5193
5307
  "description": "Server-side functions of the app, including scheduled functions"
5194
5308
  },
5309
+ "options": {
5310
+ "type": "array",
5311
+ "items": {
5312
+ "$ref": "#/components/schemas/Options"
5313
+ },
5314
+ "minItems": 0,
5315
+ "description": "App-level option declarations (with sensitivity) of this version"
5316
+ },
5195
5317
  "is_beta": {
5196
5318
  "type": "boolean",
5197
5319
  "description": "Flag to indicate if the app is in beta.",
@@ -5442,6 +5564,11 @@
5442
5564
  "correlation_id": {
5443
5565
  "type": "string",
5444
5566
  "description": "Filter by correlation ID for tracing"
5567
+ },
5568
+ "search": {
5569
+ "type": "string",
5570
+ "maxLength": 200,
5571
+ "description": "Case-insensitive substring match over event details and component id"
5445
5572
  }
5446
5573
  }
5447
5574
  },
@@ -5492,6 +5619,11 @@
5492
5619
  "minimum": 1,
5493
5620
  "maximum": 1000,
5494
5621
  "default": 100
5622
+ },
5623
+ "before": {
5624
+ "type": "string",
5625
+ "format": "date-time",
5626
+ "description": "Cursor: only return events strictly older than this timestamp. Use the timestamp of the last received event to fetch the next page; more efficient than increasing page/page_size.\n"
5495
5627
  }
5496
5628
  }
5497
5629
  },
@@ -2,7 +2,7 @@
2
2
  "openapi": "3.0.2",
3
3
  "info": {
4
4
  "title": "Entity API",
5
- "version": "2.9.0",
5
+ "version": "2.10.0",
6
6
  "description": "Flexible data layer for epilot Entities.\n\nUse this API configure and access your business objects like Contacts, Opportunities and Products.\n\n[Feature Documentation](https://docs.epilot.io/docs/entities/flexible-entities)\n"
7
7
  },
8
8
  "tags": [
@@ -5072,7 +5072,8 @@
5072
5072
  },
5073
5073
  "grid_layout": {
5074
5074
  "type": "object",
5075
- "description": "Widget-grid layout for the entity-details page (builder-authored); columns/cells drive the grid geometry.",
5075
+ "deprecated": true,
5076
+ "description": "Superseded by `widget_widths`. A whole-grid layout preset that assigned each widget a width by its POSITION, so a widget that rendered nothing shifted every width after it. Neither the entity app nor the entity builder reads it any more; values stored before it was replaced are left in place rather than migrated.",
5076
5077
  "properties": {
5077
5078
  "id": {
5078
5079
  "type": "string",
@@ -5108,6 +5109,23 @@
5108
5109
  "next_best_action": true,
5109
5110
  "address_map": false
5110
5111
  }
5112
+ },
5113
+ "widget_widths": {
5114
+ "type": "object",
5115
+ "description": "Per-widget width in the entity-details widget grid, keyed by widget id (a capability widget's `component`, or `summary` for the synthesized summary card). Spans a 12-column grid: `one_third_width` = 4, `half_width` = 6, `two_third_width` = 8, `full_width` = 12. Holds only the admin's deviations from each widget's declared default: a widget absent from this map falls back to its ui_hook `default_width`, and then to `full_width`. Do not seed this map from migrations — doing so would freeze today's defaults into the schema. Replaces the positional `grid_layout` preset.",
5116
+ "additionalProperties": {
5117
+ "type": "string",
5118
+ "enum": [
5119
+ "one_third_width",
5120
+ "half_width",
5121
+ "two_third_width",
5122
+ "full_width"
5123
+ ]
5124
+ },
5125
+ "example": {
5126
+ "address_map": "full_width",
5127
+ "recent_communications": "one_third_width"
5128
+ }
5111
5129
  }
5112
5130
  }
5113
5131
  },
@@ -7577,6 +7595,17 @@
7577
7595
  "description": "Whether this widget is switched on by default for entity types that have the capability. Only meaningful on the `EntityDetailsV2:Widget` hook. Defaults to true when omitted. An admin's explicit choice in the entity builder is stored in the schema's `ui_config.widget_visibility` and wins over this.",
7578
7596
  "example": false
7579
7597
  },
7598
+ "default_width": {
7599
+ "type": "string",
7600
+ "enum": [
7601
+ "one_third_width",
7602
+ "half_width",
7603
+ "two_third_width",
7604
+ "full_width"
7605
+ ],
7606
+ "description": "How wide this widget is by default in the entity-details widget grid. Only meaningful on the `EntityDetailsV2:Widget` and `EntityDetailsV2:Header` hooks. Defaults to `full_width` when omitted — deliberately unlike the attribute layout default of `half_width`, because a full-width widget can never leave a hole in a row. An admin's explicit choice in the entity builder is stored in the schema's `ui_config.widget_widths` and wins over this.",
7607
+ "example": "half_width"
7608
+ },
7580
7609
  "pricing_tier_setting": {
7581
7610
  "type": "string",
7582
7611
  "description": "The pricing-tier settings key an organisation must have enabled to use this widget. Omit for widgets available to everyone. Resolved against `GET /v2/pricing-tiers/me` as `override_settings[key].enabled ?? settings[key].enabled`. Note this is commercial packaging enforced in the UI, not a security control — the key must also be added to the pricing tier settings list in epilot-admin-portal, or it can never be switched on for a tier.",
@@ -8964,6 +8993,39 @@
8964
8993
  "description": "Optional array of field names to include in the hydrated entity response for this node.\nWhen specified, only the requested fields plus required internal fields (_id, _schema, _org) will be returned.\nOnly applies when hydrate=true.\n"
8965
8994
  }
8966
8995
  ]
8996
+ },
8997
+ "filter": {
8998
+ "type": "array",
8999
+ "description": "Narrows this node's traversal results to entities matching every filter (AND semantics). Useful for\ndisambiguating among multiple entities reachable via the same graph edge.\n",
9000
+ "items": {
9001
+ "$ref": "#/components/schemas/GraphNodeFilter"
9002
+ }
9003
+ }
9004
+ }
9005
+ },
9006
+ "GraphNodeFilter": {
9007
+ "type": "object",
9008
+ "description": "Entities are included in this node's result only if `attribute` exactly equals the literal `value`.",
9009
+ "required": [
9010
+ "attribute",
9011
+ "value"
9012
+ ],
9013
+ "properties": {
9014
+ "attribute": {
9015
+ "type": "string",
9016
+ "description": "Entity attribute name to match against. Must be a plain attribute name, not an Elasticsearch field path.",
9017
+ "example": "order_number"
9018
+ },
9019
+ "value": {
9020
+ "allOf": [
9021
+ {
9022
+ "$ref": "#/components/schemas/SearchFilterValue"
9023
+ },
9024
+ {
9025
+ "description": "Literal value the attribute must exactly equal for the entity to be included in this node's result."
9026
+ }
9027
+ ],
9028
+ "example": "OR-113"
8967
9029
  }
8968
9030
  }
8969
9031
  },
@@ -4773,6 +4773,9 @@
4773
4773
  "conditions",
4774
4774
  "valid_from",
4775
4775
  "values",
4776
+ "_created_at",
4777
+ "_updated_at",
4778
+ "_revision",
4776
4779
  "warnings"
4777
4780
  ],
4778
4781
  "properties": {
@@ -4805,14 +4808,19 @@
4805
4808
  "$ref": "#/components/schemas/VariantValues"
4806
4809
  },
4807
4810
  "_created_at": {
4808
- "type": "string"
4811
+ "type": "string",
4812
+ "description": "When the first version was created.",
4813
+ "readOnly": true
4809
4814
  },
4810
4815
  "_updated_at": {
4811
- "type": "string"
4816
+ "type": "string",
4817
+ "description": "When the first version was last written.",
4818
+ "readOnly": true
4812
4819
  },
4813
4820
  "_revision": {
4814
4821
  "type": "number",
4815
- "description": "The revision a later write to this version must carry to be accepted. Genuinely current,\nunlike one read back later from an eventually-consistent read.\n"
4822
+ "description": "The revision a later write to this version must carry to be accepted. Genuinely current,\nunlike one read back later from an eventually-consistent read.\n",
4823
+ "readOnly": true
4816
4824
  },
4817
4825
  "warnings": {
4818
4826
  "type": "array",
@@ -4827,7 +4835,9 @@
4827
4835
  "type": "object",
4828
4836
  "required": [
4829
4837
  "code",
4830
- "message"
4838
+ "message",
4839
+ "variant_count",
4840
+ "cap"
4831
4841
  ],
4832
4842
  "properties": {
4833
4843
  "code": {
@@ -4890,7 +4900,10 @@
4890
4900
  "schema",
4891
4901
  "conditions",
4892
4902
  "valid_from",
4893
- "values"
4903
+ "values",
4904
+ "_created_at",
4905
+ "_updated_at",
4906
+ "_revision"
4894
4907
  ],
4895
4908
  "properties": {
4896
4909
  "variant_id": {
@@ -4921,14 +4934,19 @@
4921
4934
  "$ref": "#/components/schemas/VariantValues"
4922
4935
  },
4923
4936
  "_created_at": {
4924
- "type": "string"
4937
+ "type": "string",
4938
+ "description": "When this version was created.",
4939
+ "readOnly": true
4925
4940
  },
4926
4941
  "_updated_at": {
4927
- "type": "string"
4942
+ "type": "string",
4943
+ "description": "When this version was last written.",
4944
+ "readOnly": true
4928
4945
  },
4929
4946
  "_revision": {
4930
4947
  "type": "integer",
4931
4948
  "description": "The revision a write to this version must carry to be accepted. Always current: every read\nthat returns one is strongly consistent, so it is never a marker a write would be refused\nfor having read too early.\n",
4949
+ "readOnly": true,
4932
4950
  "example": 3
4933
4951
  }
4934
4952
  }
@@ -37,7 +37,7 @@
37
37
  "/v1/campaign/{campaign_id}/status": {
38
38
  "post": {
39
39
  "operationId": "changeCampaignStatus",
40
- "summary": "Change the status of a campaign",
40
+ "summary": "changeCampaignStatus",
41
41
  "description": "Change the status of a campaign to a desired status.\n\nThe status can be one of the following: active, inactive.\n\nStatus transition is accompanied by side effects, e.g., automation execution.\n",
42
42
  "tags": [
43
43
  "Campaign"
@@ -69,7 +69,7 @@
69
69
  "get": {
70
70
  "operationId": "getCampaignJobStatus",
71
71
  "description": "Get the status of a campaign's automation job",
72
- "summary": "Get the status of a campaign's automation job",
72
+ "summary": "getCampaignJobStatus",
73
73
  "tags": [
74
74
  "Campaign"
75
75
  ],
@@ -99,7 +99,7 @@
99
99
  "/v1/campaign/{campaign_id}/portals": {
100
100
  "get": {
101
101
  "operationId": "getCampaignPortals",
102
- "summary": "Get portals usage info for a campaign",
102
+ "summary": "getCampaignPortals",
103
103
  "description": "Get the list of portals and its widgets where the campaign is used.\n",
104
104
  "tags": [
105
105
  "Campaign"
@@ -130,7 +130,7 @@
130
130
  "/v1/campaign/{campaign_id}/automations:retrigger": {
131
131
  "post": {
132
132
  "operationId": "retriggerCampaignAutomations",
133
- "summary": "Retrigger automations for campaign recipients",
133
+ "summary": "retriggerCampaignAutomations",
134
134
  "description": "Retrigger automation executions for specific campaign recipients that have failed.\n\nThis endpoint starts new automation executions for the specified recipients\nusing the campaign's associated automation flow. Only recipients with\nautomation_status 'failed' will be processed. Recipients with other statuses\n(success, pending, in_progress, cancelled) will be skipped to prevent\naccidentally retriggering successful or ongoing automations.\n",
135
135
  "tags": [
136
136
  "Campaign Delivery"
@@ -174,7 +174,7 @@
174
174
  "/v1/campaign:setup": {
175
175
  "post": {
176
176
  "operationId": "setupCampaign",
177
- "summary": "Set up a campaign with related entities and configurations",
177
+ "summary": "setupCampaign",
178
178
  "description": "Creates a `campaign` entity together with its related entities and configurations in a single call.\nUsed by the campaign wizard UI, but not restricted to it.\n",
179
179
  "tags": [
180
180
  "Campaign"
@@ -210,7 +210,7 @@
210
210
  "/v1/campaign:match": {
211
211
  "post": {
212
212
  "operationId": "matchCampaigns",
213
- "summary": "Match campaigns",
213
+ "summary": "matchCampaigns",
214
214
  "description": "Match campaigns based on target entities.\n\nThis endpoint returns the list of campaigns where the provided entities are part of the target.\n",
215
215
  "tags": [
216
216
  "Campaign"
@@ -246,7 +246,7 @@
246
246
  "/v1/campaign:discover": {
247
247
  "post": {
248
248
  "operationId": "discoverCampaigns",
249
- "summary": "Discover Entity-UI Next Best Actions for an entity",
249
+ "summary": "discoverCampaigns",
250
250
  "description": "Given an entity, returns the Next Best Actions it should see on the Entity-UI channel.\n\nEnumerates the organization's **active** campaigns that carry a valid Entity-UI Next Best\nAction, live-matches each against the entity using the existing match engine, and returns\nthe matching NBAs priority-sorted (one per campaign).\n\nThis is a pure read: it writes nothing. An entity that matches no campaigns returns an\nempty list, not an error.\n",
251
251
  "tags": [
252
252
  "Campaign"
@@ -282,7 +282,7 @@
282
282
  "/v1/target:match": {
283
283
  "post": {
284
284
  "operationId": "matchTargets",
285
- "summary": "Match targets",
285
+ "summary": "matchTargets",
286
286
  "description": "Find targets from the provided list that include the provide entities.\n",
287
287
  "tags": [
288
288
  "Target"
@@ -318,7 +318,7 @@
318
318
  "/v1/target/queries": {
319
319
  "post": {
320
320
  "operationId": "getTargetQueries",
321
- "summary": "Get target queries",
321
+ "summary": "getTargetQueries",
322
322
  "description": "Transform target filters into Lucene queries for the provided target IDs.\nReturns the transformed query string for each target along with any errors encountered.\n",
323
323
  "tags": [
324
324
  "Target"
@@ -354,7 +354,7 @@
354
354
  "/v1/campaign/{campaign_id}/recipient": {
355
355
  "post": {
356
356
  "operationId": "createRecipient",
357
- "summary": "Create a recipient associated with a campaign",
357
+ "summary": "createRecipient",
358
358
  "description": "Creates a new recipient associated with a campaign.",
359
359
  "tags": [
360
360
  "Campaign Recipient"
@@ -395,7 +395,7 @@
395
395
  "/v1/campaign/{campaign_id}/recipient/{recipient_id}": {
396
396
  "patch": {
397
397
  "operationId": "updateRecipient",
398
- "summary": "Update a recipient",
398
+ "summary": "updateRecipient",
399
399
  "description": "Updates a recipient's attributes.",
400
400
  "tags": [
401
401
  "Campaign Recipient"
@@ -439,7 +439,7 @@
439
439
  "/v1/campaign/{campaign_id}/recipient/{recipient_id}/portal:status": {
440
440
  "patch": {
441
441
  "operationId": "updateRecipientPortalStatus",
442
- "summary": "Update portal status for a campaign recipient",
442
+ "summary": "updateRecipientPortalStatus",
443
443
  "description": "Updates the portal status for a specific campaign recipient.\nThe portal_status_updated_at timestamp is automatically set when the status changes.\n\nStatus transition rules:\n- From 'sent': can change to 'seen', 'dismissed', or 'clicked'\n- From 'seen': can change to 'dismissed' or 'clicked'\n- From 'dismissed' or 'clicked': cannot be changed (final states)\n",
444
444
  "tags": [
445
445
  "Campaign Recipient"
@@ -489,8 +489,8 @@
489
489
  "/v1/campaign/{campaign_id}/recipient/{recipient_id}/entity_ui:status": {
490
490
  "patch": {
491
491
  "operationId": "updateRecipientEntityUiStatus",
492
- "summary": "Update Entity-UI (Next Best Action) status for a campaign recipient",
493
- "description": "Records a Next Best Action interaction for a recipient on the Entity-UI channel.\n\nUnlike the portal channel, an NBA recipient is created lazily: the first `seen` creates\nthe recipient record (and requires `entity_schema`). `seen` is idempotent — re-viewing an\nNBA that is already seen/clicked/dismissed is a no-op success and never regresses the status.\n\nStatus transition rules:\n- `seen`: lazily creates the recipient; a no-op success if a status already exists\n- From `seen`: can change to `clicked` or `dismissed`\n- From `clicked`: can change to `dismissed`\n- From `dismissed`: cannot be changed (final state)\n\n`dismissed` and `clicked` require an existing recipient (404 otherwise, since an NBA is\nborn at `seen`) and reject invalid transitions (409).\n\nThe entity_ui_status_updated_at timestamp is automatically set when the status changes.\n",
492
+ "summary": "updateRecipientEntityUiStatus",
493
+ "description": "Records a Next Best Action interaction for a recipient on the Entity-UI channel.\n\nUnlike the portal channel, an NBA recipient is created lazily: the first `seen` creates\nthe recipient record (and requires `entity_schema`). `seen` is idempotent — re-viewing an\nNBA that is already seen/clicked/dismissed is a no-op success and never regresses the status.\n\nStatus transition rules:\n- `seen`: lazily creates the recipient; a no-op success if a status already exists\n- From `seen`: can change to `clicked` or `dismissed`\n- From `clicked`: can change to `dismissed`\n- From `dismissed`: cannot be changed via this operation — use `entity_ui:restore`\n\n`dismissed` and `clicked` require an existing recipient (404 otherwise, since an NBA is\nborn at `seen`) and reject invalid transitions (409).\n\nThe entity_ui_status_updated_at timestamp is automatically set when the status changes.\n",
494
494
  "tags": [
495
495
  "Campaign Recipient"
496
496
  ],
@@ -536,10 +536,47 @@
536
536
  }
537
537
  }
538
538
  },
539
+ "/v1/campaign/{campaign_id}/recipient/{recipient_id}/entity_ui:restore": {
540
+ "post": {
541
+ "operationId": "restoreRecipientEntityUiStatus",
542
+ "summary": "Undo a dismissal on the Entity-UI (Next Best Action) channel",
543
+ "description": "Reverses a dismissal so the Next Best Action becomes visible to the entity again. This is\nthe only way out of `dismissed` — `entity_ui:status` cannot leave that state.\n\nThe recipient's status is restored to whatever it was immediately before the dismissal\n(recorded in `entity_ui_status_before_dismiss`), so a `clicked` NBA that was dismissed\nreturns to `clicked` and does not lose its recorded click. Falls back to `seen` when no\nprevious status was recorded.\n\nScoped to the Entity-UI channel only: the recipient's portal and automation state is never\ntouched. Takes no request body — the recipient's current state fully determines the result.\n\nReturns 404 when the recipient has no Entity-UI status at all, and 409 when it has one but\nis not currently `dismissed` (there is nothing to undo).\n",
544
+ "tags": [
545
+ "Campaign Recipient"
546
+ ],
547
+ "security": [
548
+ {
549
+ "EpilotAuth": []
550
+ }
551
+ ],
552
+ "parameters": [
553
+ {
554
+ "$ref": "#/components/parameters/CampaignIdPathParam"
555
+ },
556
+ {
557
+ "$ref": "#/components/parameters/RecipientIdPathParam"
558
+ }
559
+ ],
560
+ "responses": {
561
+ "200": {
562
+ "$ref": "#/components/responses/RecipientResponse"
563
+ },
564
+ "404": {
565
+ "$ref": "#/components/responses/ClientErrorResponse"
566
+ },
567
+ "409": {
568
+ "$ref": "#/components/responses/ClientErrorResponse"
569
+ },
570
+ "500": {
571
+ "$ref": "#/components/responses/ServerErrorResponse"
572
+ }
573
+ }
574
+ }
575
+ },
539
576
  "/v1/campaign/{campaign_id}/recipients": {
540
577
  "get": {
541
578
  "operationId": "getRecipients",
542
- "summary": "Get campaign recipients",
579
+ "summary": "getRecipients",
543
580
  "description": "Get a paginated list of recipients for a campaign.",
544
581
  "tags": [
545
582
  "Campaign Recipient"
@@ -598,6 +635,14 @@
598
635
  "schema": {
599
636
  "$ref": "#/components/schemas/PortalStatus"
600
637
  }
638
+ },
639
+ {
640
+ "name": "email_status",
641
+ "in": "query",
642
+ "description": "Filter by email delivery status (e.g. bounced, delivered). The response total reflects the count for the filtered status.",
643
+ "schema": {
644
+ "$ref": "#/components/schemas/EmailStatus"
645
+ }
601
646
  }
602
647
  ],
603
648
  "responses": {
@@ -612,6 +657,37 @@
612
657
  }
613
658
  }
614
659
  }
660
+ },
661
+ "/v1/campaign/{campaign_id}/email-stats": {
662
+ "get": {
663
+ "operationId": "getEmailStats",
664
+ "summary": "getEmailStats",
665
+ "description": "Aggregate email delivery counts for a campaign, for the KPI summary on the campaign UI.\nCounts cover the email (automation) channel only; `total_emailed` is the number of\nrecipients with a recorded email status. `delivered` is derivable as\n`total_emailed - bounced - complained - failed`.\n",
666
+ "tags": [
667
+ "Campaign Recipient"
668
+ ],
669
+ "security": [
670
+ {
671
+ "EpilotAuth": []
672
+ }
673
+ ],
674
+ "parameters": [
675
+ {
676
+ "$ref": "#/components/parameters/CampaignIdPathParam"
677
+ }
678
+ ],
679
+ "responses": {
680
+ "200": {
681
+ "$ref": "#/components/responses/EmailStatsResponse"
682
+ },
683
+ "400": {
684
+ "$ref": "#/components/responses/ClientErrorResponse"
685
+ },
686
+ "500": {
687
+ "$ref": "#/components/responses/ServerErrorResponse"
688
+ }
689
+ }
690
+ }
615
691
  }
616
692
  },
617
693
  "components": {
@@ -666,6 +742,7 @@
666
742
  "CAMPAIGN_NOT_FOUND",
667
743
  "CAMPAIGN_HAS_NO_TARGET",
668
744
  "CAMPAIGN_HAS_NO_DELIVERY_METHOD",
745
+ "INVALID_NEXT_BEST_ACTION",
669
746
  "CAMPAIGN_HAS_JOB_IN_PROGRESS",
670
747
  "CAMPAIGN_HAS_UNEXPECTED_STATUS",
671
748
  "JOB_TOKEN_MISSING",
@@ -1010,6 +1087,7 @@
1010
1087
  "properties": {
1011
1088
  "category": {
1012
1089
  "type": "string",
1090
+ "maxLength": 30,
1013
1091
  "description": "Light category label shown above the title. Free-form text."
1014
1092
  },
1015
1093
  "icon": {
@@ -1031,10 +1109,12 @@
1031
1109
  },
1032
1110
  "title": {
1033
1111
  "type": "string",
1112
+ "maxLength": 60,
1034
1113
  "description": "Bold action title. Required. Supports `{{placeholders}}`."
1035
1114
  },
1036
1115
  "body": {
1037
1116
  "type": "string",
1117
+ "maxLength": 140,
1038
1118
  "description": "Optional description. Supports `{{placeholders}}` (incl. relative dates)."
1039
1119
  },
1040
1120
  "priority": {
@@ -1061,16 +1141,12 @@
1061
1141
  "enum": [
1062
1142
  "journey",
1063
1143
  "workflow",
1064
- "url"
1144
+ "flow"
1065
1145
  ]
1066
1146
  },
1067
1147
  "target": {
1068
1148
  "type": "string",
1069
- "description": "Journey id, workflow definition id, or URL, depending on `type`."
1070
- },
1071
- "label": {
1072
- "type": "string",
1073
- "description": "Optional CTA button label."
1149
+ "description": "Journey id, workflow definition id, or flow template id, depending on `type`."
1074
1150
  },
1075
1151
  "context_params": {
1076
1152
  "type": "array",
@@ -1120,6 +1196,35 @@
1120
1196
  ],
1121
1197
  "additionalProperties": false
1122
1198
  },
1199
+ "DiscoverResult": {
1200
+ "type": "object",
1201
+ "description": "One discovered Next Best Action, plus this entity's interaction state for it.",
1202
+ "properties": {
1203
+ "campaign_id": {
1204
+ "$ref": "#/components/schemas/BaseUUID"
1205
+ },
1206
+ "nba": {
1207
+ "$ref": "#/components/schemas/NextBestAction"
1208
+ },
1209
+ "status": {
1210
+ "allOf": [
1211
+ {
1212
+ "$ref": "#/components/schemas/EntityUiStatus"
1213
+ }
1214
+ ],
1215
+ "description": "The recipient's current Entity-UI status for this campaign, present only when a\nrecipient record already exists (i.e. the entity has previously seen, clicked or\ndismissed this NBA). Absent when the entity has not yet interacted with it.\n\nIn `results` this is only ever `seen` or `clicked`; in `dismissed` it is always\n`dismissed`. Lets the client skip a redundant `seen` call for NBAs already seen.\n"
1216
+ },
1217
+ "status_updated_at": {
1218
+ "type": "string",
1219
+ "format": "date-time",
1220
+ "description": "When `status` was last written. Present whenever `status` is. Lets the client show how\nlong ago an NBA was dismissed.\n"
1221
+ }
1222
+ },
1223
+ "required": [
1224
+ "campaign_id",
1225
+ "nba"
1226
+ ]
1227
+ },
1123
1228
  "MatchTargetParams": {
1124
1229
  "type": "object",
1125
1230
  "properties": {
@@ -1233,11 +1338,23 @@
1233
1338
  "clicked"
1234
1339
  ]
1235
1340
  },
1236
- "Resolution": {
1341
+ "EmailStatus": {
1237
1342
  "type": "string",
1238
- "description": "Cross-channel resolution of a campaign for a recipient. Unlike the per-channel `*_status`\nfields (where `dismissed` is channel-local), a resolution suppresses the campaign on EVERY\nchannel the 360 Entity-UI card and the portal teaser alike. Server-managed and read-only:\nnever sent by a client. Absence means unresolved.\n",
1343
+ "description": "Delivery status of the email a campaign's automation sends to a recipient. Set to `sent`\nonce the automation hands the email off, then updated asynchronously as SES notifications\narrive (`bounced` / `complained`; an SES Reject maps to `bounced`). `failed` is a send-time\nfailure (the automation execution could not send at all), distinct from an asynchronous\nbounce. `delivered` is reserved for when Delivery events are published.\n",
1239
1344
  "enum": [
1240
- "accepted"
1345
+ "sent",
1346
+ "delivered",
1347
+ "bounced",
1348
+ "complained",
1349
+ "failed"
1350
+ ]
1351
+ },
1352
+ "EmailBounceType": {
1353
+ "type": "string",
1354
+ "description": "SES bounce classification: `permanent` (hard) or `transient` (soft).",
1355
+ "enum": [
1356
+ "permanent",
1357
+ "transient"
1241
1358
  ]
1242
1359
  },
1243
1360
  "Recipient": {
@@ -1276,8 +1393,44 @@
1276
1393
  "type": "string",
1277
1394
  "format": "date-time"
1278
1395
  },
1279
- "resolution": {
1280
- "$ref": "#/components/schemas/Resolution"
1396
+ "entity_ui_status_before_dismiss": {
1397
+ "allOf": [
1398
+ {
1399
+ "$ref": "#/components/schemas/EntityUiStatus"
1400
+ }
1401
+ ],
1402
+ "description": "The Entity-UI status the recipient held immediately before it was dismissed, so\n`entity_ui:restore` can put it back without losing a recorded click.\n\nServer-managed — never send this from a client; it is ignored on write. Meaningful\n**only while** `entity_ui_status` is `dismissed`: a restore intentionally leaves the\nvalue behind rather than clearing it, so a stale value after a restore is expected and\nmust not be read.\n"
1403
+ },
1404
+ "message_entity_id": {
1405
+ "type": "string",
1406
+ "description": "The message entity id of the email sent to this recipient by the campaign's automation\n(not the SES/provider message id), used to correlate SES delivery notifications back to\nthe recipient. Server-managed.\n"
1407
+ },
1408
+ "email_status": {
1409
+ "$ref": "#/components/schemas/EmailStatus"
1410
+ },
1411
+ "email_status_updated_at": {
1412
+ "type": "string",
1413
+ "format": "date-time"
1414
+ },
1415
+ "email_bounce_type": {
1416
+ "$ref": "#/components/schemas/EmailBounceType"
1417
+ },
1418
+ "email_bounce_subtype": {
1419
+ "type": "string",
1420
+ "description": "SES bounce sub-type (e.g. `General`, `NoEmail`, `MailboxFull`)."
1421
+ },
1422
+ "email_complaint_type": {
1423
+ "type": "string",
1424
+ "description": "SES complaint feedback type (e.g. `abuse`, `fraud`), set for complaints."
1425
+ },
1426
+ "email_bounce_reason": {
1427
+ "type": "string",
1428
+ "description": "Human-readable failure reason — the SES bounce `diagnosticCode`, the complaint\nfeedback type, or a send-time error message.\n"
1429
+ },
1430
+ "email_send_error": {
1431
+ "type": "object",
1432
+ "additionalProperties": true,
1433
+ "description": "The raw SES notification (or send error) kept verbatim for a detail view."
1281
1434
  },
1282
1435
  "updated_at": {
1283
1436
  "type": "string",
@@ -1774,6 +1927,56 @@
1774
1927
  }
1775
1928
  }
1776
1929
  },
1930
+ "EmailStatsResponse": {
1931
+ "description": "Aggregate email delivery counts for a campaign.",
1932
+ "content": {
1933
+ "application/json": {
1934
+ "schema": {
1935
+ "type": "object",
1936
+ "required": [
1937
+ "total_emailed",
1938
+ "sent",
1939
+ "delivered",
1940
+ "bounced",
1941
+ "bounced_hard",
1942
+ "bounced_soft",
1943
+ "complained",
1944
+ "failed"
1945
+ ],
1946
+ "properties": {
1947
+ "total_emailed": {
1948
+ "type": "integer",
1949
+ "description": "Recipients with a recorded email status (the KPI denominator)."
1950
+ },
1951
+ "sent": {
1952
+ "type": "integer"
1953
+ },
1954
+ "delivered": {
1955
+ "type": "integer",
1956
+ "description": "Currently always 0 (Delivery events are not published yet)."
1957
+ },
1958
+ "bounced": {
1959
+ "type": "integer"
1960
+ },
1961
+ "bounced_hard": {
1962
+ "type": "integer",
1963
+ "description": "Bounces classified permanent (bounced_hard + bounced_soft = bounced)."
1964
+ },
1965
+ "bounced_soft": {
1966
+ "type": "integer",
1967
+ "description": "Bounces classified transient."
1968
+ },
1969
+ "complained": {
1970
+ "type": "integer"
1971
+ },
1972
+ "failed": {
1973
+ "type": "integer"
1974
+ }
1975
+ }
1976
+ }
1977
+ }
1978
+ }
1979
+ },
1777
1980
  "MatchCampaignsResponse": {
1778
1981
  "description": "List of campaigns where the target entities match the given entities.",
1779
1982
  "content": {
@@ -1814,39 +2017,27 @@
1814
2017
  "properties": {
1815
2018
  "hits": {
1816
2019
  "type": "number",
1817
- "description": "Number of matching NBAs."
2020
+ "description": "Number of matching NBAs. Counts `results` only — dismissed ones are excluded."
1818
2021
  },
1819
2022
  "results": {
1820
2023
  "type": "array",
1821
2024
  "description": "Matching NBAs, sorted by priority (desc); one entry per campaign.",
1822
2025
  "items": {
1823
- "type": "object",
1824
- "properties": {
1825
- "campaign_id": {
1826
- "$ref": "#/components/schemas/BaseUUID"
1827
- },
1828
- "nba": {
1829
- "$ref": "#/components/schemas/NextBestAction"
1830
- },
1831
- "status": {
1832
- "allOf": [
1833
- {
1834
- "$ref": "#/components/schemas/EntityUiStatus"
1835
- }
1836
- ],
1837
- "description": "The recipient's current Entity-UI status for this campaign, present only\nwhen a recipient record already exists (i.e. the entity has previously seen\nor clicked this NBA). Absent when the entity has not yet interacted with it.\nDismissed NBAs are filtered out server-side, so this is only ever `seen` or\n`clicked`. Lets the client skip a redundant `seen` call for NBAs already seen.\n"
1838
- }
1839
- },
1840
- "required": [
1841
- "campaign_id",
1842
- "nba"
1843
- ]
2026
+ "$ref": "#/components/schemas/DiscoverResult"
2027
+ }
2028
+ },
2029
+ "dismissed": {
2030
+ "type": "array",
2031
+ "description": "NBAs this entity has dismissed that would otherwise be in `results` — same shape,\nalso priority-sorted. Returned so the client can show the agent what it hid and\noffer to restore it, without a second round-trip.\n\nOnly currently-relevant dismissals appear: each one is matched against its\ncampaign's target exactly like a visible NBA, so a dismissal is dropped once the\ncampaign ends or the entity stops matching. Capped, so this is not a complete\ndismissal history.\n",
2032
+ "items": {
2033
+ "$ref": "#/components/schemas/DiscoverResult"
1844
2034
  }
1845
2035
  }
1846
2036
  },
1847
2037
  "required": [
1848
2038
  "hits",
1849
- "results"
2039
+ "results",
2040
+ "dismissed"
1850
2041
  ]
1851
2042
  }
1852
2043
  }
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  API_LIST
4
- } from "../chunk-4J5ZADZC.js";
4
+ } from "../chunk-4A3TISYH.js";
5
5
 
6
6
  // bin/epilot.ts
7
7
  import { runMain } from "citty";
@@ -11,7 +11,7 @@ import { defineCommand } from "citty";
11
11
  var main = defineCommand({
12
12
  meta: {
13
13
  name: "epilot",
14
- version: "0.1.130",
14
+ version: "0.1.132",
15
15
  description: "CLI for epilot APIs"
16
16
  },
17
17
  args: {
@@ -30,8 +30,8 @@ var main = defineCommand({
30
30
  auth: () => import("../auth-WMXFMPWE.js").then((m) => m.default),
31
31
  profile: () => import("../profile-OZJL5ZPT.js").then((m) => m.default),
32
32
  config: () => import("../config-DGZIMLZK.js").then((m) => m.default),
33
- completion: () => import("../completion-GFINH6YJ.js").then((m) => m.default),
34
- upgrade: () => import("../upgrade-QPOYGFSB.js").then((m) => m.default),
33
+ completion: () => import("../completion-MSAMXF3P.js").then((m) => m.default),
34
+ upgrade: () => import("../upgrade-I3HYEYCU.js").then((m) => m.default),
35
35
  "access-token": () => import("../access-token-WWE6BDJH.js").then((m) => m.default),
36
36
  address: () => import("../address-EH3C4CVB.js").then((m) => m.default),
37
37
  "address-suggestions": () => import("../address-suggestions-RRSLOBFW.js").then((m) => m.default),
@@ -134,13 +134,13 @@ process.stderr.on("error", (err) => {
134
134
  if (err.code === "EPIPE") process.exit(0);
135
135
  throw err;
136
136
  });
137
- var VERSION = true ? "0.1.130" : (await null).default.version;
137
+ var VERSION = true ? "0.1.132" : (await null).default.version;
138
138
  var reorderedArgv = hoistFlagsAfterSubcommand(process.argv.slice(2));
139
139
  process.argv = [process.argv[0], process.argv[1], ...reorderedArgv];
140
140
  var args = process.argv.slice(2);
141
141
  var completionsIdx = args.indexOf("--_completions");
142
142
  if (completionsIdx >= 0) {
143
- const { handleCompletions } = await import("../completion-GFINH6YJ.js");
143
+ const { handleCompletions } = await import("../completion-MSAMXF3P.js");
144
144
  handleCompletions(args[completionsIdx + 1], args[completionsIdx + 2]);
145
145
  process.exit(0);
146
146
  }
@@ -71,7 +71,7 @@ var API_LIST = [
71
71
  kebabName: "app",
72
72
  title: "App API",
73
73
  serverUrl: "https://app.sls.epilot.io",
74
- operationCount: 35,
74
+ operationCount: 36,
75
75
  operationIds: [
76
76
  "getPublicFacingComponent",
77
77
  "listConfigurations",
@@ -102,6 +102,7 @@ var API_LIST = [
102
102
  "patchInstallation",
103
103
  "uninstall",
104
104
  "promoteVersion",
105
+ "resolveOptions",
105
106
  "ingestEvent",
106
107
  "publicProxyGet",
107
108
  "publicProxyPost",
@@ -1370,7 +1371,7 @@ var API_LIST = [
1370
1371
  kebabName: "targeting",
1371
1372
  title: "Targeting API",
1372
1373
  serverUrl: "https://targeting.sls.epilot.io",
1373
- operationCount: 14,
1374
+ operationCount: 16,
1374
1375
  operationIds: [
1375
1376
  "changeCampaignStatus",
1376
1377
  "getCampaignJobStatus",
@@ -1385,7 +1386,9 @@ var API_LIST = [
1385
1386
  "updateRecipient",
1386
1387
  "updateRecipientPortalStatus",
1387
1388
  "updateRecipientEntityUiStatus",
1388
- "getRecipients"
1389
+ "restoreRecipientEntityUiStatus",
1390
+ "getRecipients",
1391
+ "getEmailStats"
1389
1392
  ]
1390
1393
  },
1391
1394
  {
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  API_LIST
4
- } from "./chunk-4J5ZADZC.js";
4
+ } from "./chunk-4A3TISYH.js";
5
5
  import {
6
6
  DIM,
7
7
  GREEN,
@@ -72,7 +72,7 @@ ${GREEN}${BOLD}Upgraded to @epilot/cli@${latest}${RESET}
72
72
  }
73
73
  });
74
74
  var getCurrentVersion = () => {
75
- if (true) return "0.1.130";
75
+ if (true) return "0.1.132";
76
76
  try {
77
77
  const output = execSync("npm ls -g @epilot/cli --depth=0 --json 2>/dev/null", {
78
78
  encoding: "utf-8",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@epilot/cli",
3
- "version": "0.1.130",
3
+ "version": "0.1.132",
4
4
  "description": "CLI for epilot APIs",
5
5
  "type": "module",
6
6
  "bin": {