shipmail-cli 0.6.3 → 0.6.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +27 -6
  2. package/dist/cli.js +497 -24
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -40,6 +40,7 @@ shipmail status
40
40
  # Create a domain, retrieve records, and verify DNS
41
41
  shipmail domains create example.com
42
42
  shipmail domains dns-records dom_123
43
+ shipmail domains health dom_123
43
44
  shipmail domains verify dom_123
44
45
 
45
46
  # Create a mailbox
@@ -98,12 +99,19 @@ shipmail mailboxes sandbox-inbound mbx_123 --from customer@example.com --subject
98
99
 
99
100
  # Read inbound mail
100
101
  shipmail inbox list 550e8400-e29b-41d4-a716-446655440000 --folder-role inbox --after 2025-07-20T00:00:00Z --limit 25
102
+ shipmail inbox list 550e8400-e29b-41d4-a716-446655440000 --from billing@vendor.example --subject invoice --has-attachment
101
103
  shipmail inbox message 550e8400-e29b-41d4-a716-446655440000 eml_123
104
+ # Enable API-agent hold with a mailbox-management credential.
105
+ shipmail mailboxes update 550e8400-e29b-41d4-a716-446655440000 --agent-security-hold true
106
+ # Use a separate securityReview credential to inspect and release the exact snapshot.
107
+ SHIPMAIL_API_KEY=sm_live_review_... shipmail inbox message 550e8400-e29b-41d4-a716-446655440000 eml_123
108
+ SHIPMAIL_API_KEY=sm_live_review_... shipmail inbox security-review 550e8400-e29b-41d4-a716-446655440000 eml_123 --reason "Reviewed against the customer request" --expected-content-sha256 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
102
109
  shipmail inbox threads 550e8400-e29b-41d4-a716-446655440000 --attention-state needs_reply --after 2025-07-20T00:00:00Z --limit 50
103
110
  shipmail inbox thread 550e8400-e29b-41d4-a716-446655440000 thr_123
104
111
  shipmail inbox draft 550e8400-e29b-41d4-a716-446655440000 thr_123 --expected-version 7 --text "Thanks for the note"
105
- # Apply your approval policy; stale drafts fail with 409 without delivery.
106
- shipmail inbox send-draft 550e8400-e29b-41d4-a716-446655440000 thr_123 drf_123
112
+ # Read and approve content_sha256; later edits and stale drafts fail with 409 without delivery.
113
+ shipmail inbox get-draft 550e8400-e29b-41d4-a716-446655440000 thr_123 drf_123
114
+ shipmail inbox send-draft 550e8400-e29b-41d4-a716-446655440000 thr_123 drf_123 --expected-content-sha256 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
107
115
  # Save a new message to Drafts for a human to review and send; this never sends
108
116
  shipmail inbox compose-draft 550e8400-e29b-41d4-a716-446655440000 --to user@example.com --subject "Kickoff" --text "Draft for review"
109
117
  shipmail inbox thread-attention 550e8400-e29b-41d4-a716-446655440000 thr_123 --done true --expected-version 8
@@ -124,6 +132,8 @@ shipmail newsletters create \
124
132
  --subject "What shipped in July" \
125
133
  --blocks @blocks.json
126
134
  shipmail newsletters update nws_123 --subject "Updated subject"
135
+ # Or write the body in Markdown; Shipmail converts it to blocks
136
+ shipmail newsletters update nws_123 --markdown @issue.md
127
137
  # A concurrent update exits with conflict (409); fetch the latest newsletter before retrying.
128
138
  shipmail newsletters preview nws_123
129
139
  shipmail newsletters test-send nws_123 --recipient-email owner@example.com
@@ -134,6 +144,11 @@ shipmail newsletter-assets upload ./hero.png --content-type image/png
134
144
  shipmail newsletter-assets upload ./demo.mp4 --content-type video/mp4 --thumbnail ./demo-poster.jpg
135
145
  shipmail newsletter-assets register-url https://cdn.shipmail.to/newsletter-images/org_123/hero.png
136
146
 
147
+ # Welcome new readers with a sequence of emails
148
+ shipmail audiences welcome add-step aud_123 --subject "Welcome" --markdown @welcome.md
149
+ shipmail audiences welcome add-step aud_123 --subject "Our best posts" --markdown @day-3.md --wait-minutes 4320
150
+ shipmail audiences welcome enable aud_123
151
+
137
152
  # Manage webhooks
138
153
  shipmail webhooks create \
139
154
  --url https://example.com/shipmail/webhook \
@@ -150,6 +165,11 @@ Paragraph, quote, callout, list-item, and column bodies in `--blocks` accept bar
150
165
  sanitized inline HTML. Use `<p>` and `<br>` for line breaks. Shipmail keeps `a`, `b`, `br`,
151
166
  `code`, `em`, `i`, `p`, `s`, `span`, `strong`, and `u`, with safe attributes and URLs.
152
167
 
168
+ `--markdown` (text, or `@file` to read a file) is an alternative to `--blocks` and `--body-html`.
169
+ Shipmail converts headings, paragraphs, lists, quotes, code, images, links, dividers and tables
170
+ to blocks. A callout starts with a line `:::callout` (optionally `info`, `warning` or `success`,
171
+ then a title) and ends with a line `:::`.
172
+
153
173
  `newsletter-assets upload` uses a five-minute direct storage PUT, then calls the single-use
154
174
  completion URL. Video uploads require a JPEG poster through `--thumbnail`. `newsletter-assets list`
155
175
  includes current newsletter media storage usage and a `pagination.next_cursor` value in the JSON
@@ -170,17 +190,18 @@ Use repeatable `--header "Name: value"` flags for request headers. Use `--output
170
190
  - `status`
171
191
  - `api <METHOD> <PATH>`
172
192
  - `domains list|search|create|get|verify|delete`
173
- - `mailboxes list|create|get|suspend|resume|forwarding|forwarding-add|forwarding-remove|app-passwords|folders|identities|stage-attachment|prepare-attachment-upload`
193
+ - `mailboxes list|create|get|update|suspend|resume|forwarding|forwarding-add|forwarding-remove|app-passwords|folders|identities|stage-attachment|prepare-attachment-upload`
174
194
  - `members list|get`
175
- - `inbox list|message|threads|thread|thread-state|draft|send-draft|reply-message|reply-thread|update|move`
195
+ - `inbox list|message|security-review|threads|thread|thread-state|draft|send-draft|reply-message|reply-thread|update|move`
176
196
  - `reply-scans create|status|results`
177
197
  - `messages send|list|analytics|get|reply` (test keys also support `--sandbox-outcome`)
178
198
  - `scheduled-messages list|get|update|cancel`
179
199
  - `mailboxes sandbox-inbound`
180
200
  - `webhooks list|create|update|test|deliveries|replay-delivery`
181
- - `suppressions list|remove`
201
+ - `suppressions add|list|remove`
182
202
  - `audiences list|create|get|update|delete`
183
203
  - `audiences feed get|update|rotate|revoke`
204
+ - `audiences welcome get|enable|disable|add-step|update-step|remove-step`
184
205
  - `audiences subscribers add|list|get|by-email|update|unsubscribe|resubscribe|remove`
185
206
  - `newsletter-sender-identities list`
186
207
  - `newsletter-domains list`
@@ -191,7 +212,7 @@ Member output uses the roles `owner`, `admin`, and `member`. Its `mailbox_access
191
212
  `selected`; `mailbox_ids` is empty for `all`, while selected IDs may be limited to mailboxes the
192
213
  API key can reach.
193
214
 
194
- Configure a publisher-owned Atom self URL and the number of recent issues:
215
+ Configure a publisher-owned Atom self URL and the number of recent newsletters:
195
216
 
196
217
  ```bash
197
218
  shipmail audiences feed update aud_123 \
package/dist/cli.js CHANGED
@@ -7,6 +7,7 @@ import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
7
7
  import { env as processEnv, exit, stderr, stdout } from "process";
8
8
  import { fileURLToPath } from "url";
9
9
  import {
10
+ BOOKING_LINK_STATUSES,
10
11
  CONFERENCING_PROVIDER_IDS,
11
12
  ReconciliationRequiredError,
12
13
  ShipmailClient,
@@ -649,7 +650,7 @@ async function login(options) {
649
650
  }
650
651
 
651
652
  // src/version.ts
652
- var VERSION = "0.6.3";
653
+ var VERSION = "0.6.5";
653
654
 
654
655
  // src/cli.ts
655
656
  var DEFAULT_BASE_URL = "https://shipmail.to/api/v1";
@@ -989,12 +990,27 @@ async function readNewsletterBlocks(value) {
989
990
  }
990
991
  return parsed;
991
992
  }
993
+ async function readNewsletterMarkdown(options) {
994
+ const input = getString(options, "markdown");
995
+ if (input === void 0) return void 0;
996
+ const markdown = await readTextInput(input);
997
+ if (getString(options, "blocks") !== void 0 || getString(options, "body-html") !== void 0) {
998
+ throw new CliUsageError("Use only one of --blocks, --body-html, or --markdown.");
999
+ }
1000
+ return markdown;
1001
+ }
992
1002
  function parseArchiveVisibility(options) {
993
1003
  const value = getString(options, "archive-visibility");
994
1004
  if (value === void 0) return void 0;
995
1005
  if (value === "private" || value === "public") return value;
996
1006
  throw new CliUsageError("--archive-visibility must be private or public.");
997
1007
  }
1008
+ function parsePublishTarget(options) {
1009
+ const value = getString(options, "publish-target");
1010
+ if (value === void 0) return void 0;
1011
+ if (value === "email_and_website" || value === "email" || value === "website") return value;
1012
+ throw new CliUsageError("--publish-target must be email_and_website, email or website.");
1013
+ }
998
1014
  function parseStylingMode(options) {
999
1015
  const value = getString(options, "styling-mode");
1000
1016
  if (value === void 0) return void 0;
@@ -1027,15 +1043,21 @@ async function newsletterCreateParams(options) {
1027
1043
  const blocks = blocksInput ? await readNewsletterBlocks(blocksInput) : void 0;
1028
1044
  const body_html = bodyHtmlInput ? await readTextInput(bodyHtmlInput) : void 0;
1029
1045
  const body_text = bodyTextInput ? await readTextInput(bodyTextInput) : void 0;
1030
- if (!blocks?.length && !body_html?.trim() && !body_text?.trim()) {
1031
- throw new CliUsageError("Provide at least one of --blocks, --body-html, or --body-text.");
1046
+ const content_markdown = await readNewsletterMarkdown(options);
1047
+ if (!blocks?.length && !body_html?.trim() && !body_text?.trim() && !content_markdown?.trim()) {
1048
+ throw new CliUsageError(
1049
+ "Provide at least one of --blocks, --body-html, --body-text, or --markdown."
1050
+ );
1032
1051
  }
1033
1052
  const send_window_hours = getNumber(options, "send-window-hours");
1034
1053
  const archive_visibility = parseArchiveVisibility(options);
1054
+ const publish_target = parsePublishTarget(options);
1035
1055
  const feed_entry_url = getString(options, "feed-entry-url");
1036
1056
  const styling_mode = parseStylingMode(options);
1037
1057
  const track_engagement = getBoolean(options, "track-engagement");
1058
+ const add_utm_tags = getBoolean(options, "add-utm-tags");
1038
1059
  const preview_text = getString(options, "preview-text");
1060
+ const subjectTest = subjectTestParams(options);
1039
1061
  return {
1040
1062
  audience_id,
1041
1063
  sender_identity_id,
@@ -1043,13 +1065,32 @@ async function newsletterCreateParams(options) {
1043
1065
  subject,
1044
1066
  ...preview_text !== void 0 ? { preview_text } : {},
1045
1067
  ...blocks ? { blocks } : {},
1068
+ ...content_markdown !== void 0 ? { content_markdown } : {},
1046
1069
  ...body_html !== void 0 ? { body_html } : {},
1047
1070
  ...body_text !== void 0 ? { body_text } : {},
1048
1071
  ...send_window_hours !== void 0 ? { send_window_hours } : {},
1049
1072
  ...archive_visibility !== void 0 ? { archive_visibility } : {},
1073
+ ...publish_target !== void 0 ? { publish_target } : {},
1050
1074
  ...feed_entry_url !== void 0 ? { feed_entry_url } : {},
1051
1075
  ...styling_mode !== void 0 ? { styling_mode } : {},
1052
- ...track_engagement !== void 0 ? { track_engagement } : {}
1076
+ ...track_engagement !== void 0 ? { track_engagement } : {},
1077
+ ...add_utm_tags !== void 0 ? { add_utm_tags } : {},
1078
+ ...subjectTest
1079
+ };
1080
+ }
1081
+ function subjectTestParams(options) {
1082
+ const subject_b = getString(options, "subject-b");
1083
+ const clear = getBoolean(options, "clear-subject-b") === true;
1084
+ if (subject_b !== void 0 && clear) {
1085
+ throw new CliUsageError("Use either --subject-b or --clear-subject-b.");
1086
+ }
1087
+ const subject_test_percent = getNumber(options, "subject-test-percent");
1088
+ const subject_test_hours = getNumber(options, "subject-test-hours");
1089
+ return {
1090
+ ...subject_b !== void 0 ? { subject_b } : {},
1091
+ ...clear ? { subject_b: null } : {},
1092
+ ...subject_test_percent !== void 0 ? { subject_test_percent } : {},
1093
+ ...subject_test_hours !== void 0 ? { subject_test_hours } : {}
1053
1094
  };
1054
1095
  }
1055
1096
  async function newsletterUpdateParams(options) {
@@ -1060,15 +1101,18 @@ async function newsletterUpdateParams(options) {
1060
1101
  const preview_text = getString(options, "preview-text");
1061
1102
  const send_window_hours = getNumber(options, "send-window-hours");
1062
1103
  const archive_visibility = parseArchiveVisibility(options);
1104
+ const publish_target = parsePublishTarget(options);
1063
1105
  const feed_entry_url = getString(options, "feed-entry-url");
1064
1106
  const styling_mode = parseStylingMode(options);
1065
1107
  const track_engagement = getBoolean(options, "track-engagement");
1108
+ const add_utm_tags = getBoolean(options, "add-utm-tags");
1066
1109
  const blocksInput = getString(options, "blocks");
1067
1110
  const bodyHtmlInput = getString(options, "body-html");
1068
1111
  const bodyTextInput = getString(options, "body-text");
1069
1112
  const blocks = blocksInput ? await readNewsletterBlocks(blocksInput) : void 0;
1070
1113
  const body_html = bodyHtmlInput ? await readTextInput(bodyHtmlInput) : void 0;
1071
1114
  const body_text = bodyTextInput ? await readTextInput(bodyTextInput) : void 0;
1115
+ const content_markdown = await readNewsletterMarkdown(options);
1072
1116
  const params = {
1073
1117
  ...audience_id ? { audience_id } : {},
1074
1118
  ...sender_identity_id ? { sender_identity_id } : {},
@@ -1076,13 +1120,17 @@ async function newsletterUpdateParams(options) {
1076
1120
  ...subject ? { subject } : {},
1077
1121
  ...preview_text !== void 0 ? { preview_text } : {},
1078
1122
  ...blocks ? { blocks } : {},
1123
+ ...content_markdown !== void 0 ? { content_markdown } : {},
1079
1124
  ...body_html !== void 0 ? { body_html } : {},
1080
1125
  ...body_text !== void 0 ? { body_text } : {},
1081
1126
  ...send_window_hours !== void 0 ? { send_window_hours } : {},
1082
1127
  ...archive_visibility !== void 0 ? { archive_visibility } : {},
1128
+ ...publish_target !== void 0 ? { publish_target } : {},
1083
1129
  ...feed_entry_url !== void 0 ? { feed_entry_url } : {},
1084
1130
  ...styling_mode !== void 0 ? { styling_mode } : {},
1085
- ...track_engagement !== void 0 ? { track_engagement } : {}
1131
+ ...track_engagement !== void 0 ? { track_engagement } : {},
1132
+ ...add_utm_tags !== void 0 ? { add_utm_tags } : {},
1133
+ ...subjectTestParams(options)
1086
1134
  };
1087
1135
  if (Object.keys(params).length === 0) {
1088
1136
  throw new CliUsageError("Provide at least one field to update.");
@@ -1175,11 +1223,13 @@ Usage:
1175
1223
  shipmail domains create <name>
1176
1224
  shipmail domains get <id>
1177
1225
  shipmail domains dns-records <id>
1226
+ shipmail domains health <id> [--json]
1178
1227
  shipmail domains verify <id>
1179
1228
  shipmail domains delete <id>
1180
1229
  shipmail mailboxes list [--domain-id dom] [--limit 25] [--cursor cur]
1181
1230
  shipmail mailboxes create --domain-id dom --address hello (--password value | --password-file path | --generate-password) [--display-name "Hello"]
1182
1231
  shipmail mailboxes get <id>
1232
+ shipmail mailboxes update <id> [--display-name "Ops"] [--agent-security-hold true|false]
1183
1233
  shipmail mailboxes suspend <id>
1184
1234
  shipmail mailboxes resume <id>
1185
1235
  shipmail mailboxes export <id>
@@ -1202,14 +1252,19 @@ Usage:
1202
1252
  shipmail mailboxes sandbox-inbound <id> --from sender@example.com --subject "Test" (--text "Hello" | --html "<p>Hello</p>") [--to support@example.com]
1203
1253
  shipmail members list [--limit 25] [--cursor cur]
1204
1254
  shipmail members get <id>
1205
- shipmail inbox list <mailbox-id> [--folder-role inbox] [--search text] [--after ISO] [--before ISO] [--limit 25] [--cursor cur]
1255
+ shipmail inbox list <mailbox-id> [--folder-role inbox] [--search text] [--from text] [--to text] [--subject text] [--has-attachment [true|false]] [--after ISO] [--before ISO] [--limit 25] [--cursor cur]
1206
1256
  shipmail inbox message <mailbox-id> <message-id>
1257
+ shipmail inbox security-review <mailbox-id> <message-id> --reason "Reviewed" --expected-content-sha256 HASH
1207
1258
  shipmail inbox threads <mailbox-id> [--attention-state needs_reply] [--after ISO] [--before ISO] [--limit 25] [--cursor cur]
1208
1259
  shipmail inbox thread <mailbox-id> <thread-id>
1209
1260
  shipmail inbox thread-attention <mailbox-id> <thread-id> --done true --expected-version 3
1210
1261
  shipmail inbox draft <mailbox-id> <thread-id> --expected-version 3 (--text "Hello" | --html "<p>Hello</p>") [--reply-mode reply_all]
1211
1262
  shipmail inbox compose-draft <mailbox-id> --to user@example.com [--cc ...] [--bcc ...] [--subject "Hi"] (--text "Hello" | --html "<p>Hello</p>") [--idempotency-key key] (saves a new message to Drafts; never sends)
1212
- shipmail inbox send-draft <mailbox-id> <thread-id> <draft-id>
1263
+ shipmail inbox drafts <mailbox-id> <thread-id>
1264
+ shipmail inbox get-draft <mailbox-id> <thread-id> <draft-id>
1265
+ shipmail inbox update-draft <mailbox-id> <thread-id> <draft-id> (--text "Hello" | --html "<p>Hello</p>") (the new body replaces the old one)
1266
+ shipmail inbox delete-draft <mailbox-id> <thread-id> <draft-id>
1267
+ shipmail inbox send-draft <mailbox-id> <thread-id> <draft-id> [--expected-content-sha256 hash]
1213
1268
  shipmail inbox reply-message <mailbox-id> <message-id> --to user@example.com (--text "Hello" | --html "<p>Hello</p>") [--track-reply true|false]
1214
1269
  shipmail inbox reply-thread <mailbox-id> <thread-id> --to user@example.com (--text "Hello" | --html "<p>Hello</p>") [--track-reply true|false]
1215
1270
  shipmail inbox update <mailbox-id> <message-id> [--read true] [--starred false]
@@ -1227,23 +1282,25 @@ Usage:
1227
1282
  shipmail reply-scans status <scan-id>
1228
1283
  shipmail reply-scans results <scan-id> [--limit 25] [--cursor cur]
1229
1284
  shipmail webhooks list
1230
- shipmail webhooks create --url https://example.com/hook --events message.received,message.bounced [--authorization "Bearer key"] [--standard-webhooks-secret whsec_...]
1231
- shipmail webhooks update <webhook-id> [--url https://example.com/hook] [--events message.received] [--description text] [--active true|false] [--authorization "Bearer key" | --clear-authorization] [--standard-webhooks-secret whsec_... | --clear-standard-webhooks-secret]
1285
+ shipmail webhooks create --url https://example.com/hook --events message.received,message.bounced [--authorization "Bearer key"] [--standard-webhooks-secret whsec_...] [--include-reply-text] [--authenticated-senders-only] [--allowed-sender jane@customer.com --allowed-sender customer.com]
1286
+ shipmail webhooks update <webhook-id> [--url https://example.com/hook] [--events message.received] [--description text] [--active true|false] [--authorization "Bearer key" | --clear-authorization] [--standard-webhooks-secret whsec_... | --clear-standard-webhooks-secret] [--include-reply-text true|false] [--authenticated-senders-only true|false] [--allowed-sender customer.com ... | --clear-allowed-senders]
1232
1287
  shipmail webhooks replay-delivery <webhook-id> <delivery-id> [--idempotency-key key]
1233
- shipmail suppressions list
1288
+ shipmail suppressions add <email> [--scope all|marketing]
1289
+ shipmail suppressions list [--scope all|marketing] [--limit 25] [--cursor cur]
1234
1290
  shipmail suppressions remove <email>
1235
1291
  shipmail newsletters create \\
1236
1292
  --audience-id aud_123 \\
1237
1293
  --sender-identity-id nwsid_456 \\
1238
1294
  --name "July changelog" \\
1239
1295
  --subject "What shipped in July" \\
1240
- --blocks @blocks.json \\
1241
- [--feed-entry-url https://example.com/post] [--styling-mode styled|plain] [--track-engagement true|false]
1242
- shipmail newsletters update nws_789 --subject "Updated subject" --blocks @blocks-v2.json [--feed-entry-url https://example.com/post] [--styling-mode styled|plain] [--track-engagement true|false]
1296
+ --blocks @blocks.json | --markdown @issue.md \\
1297
+ [--feed-entry-url https://example.com/post] [--styling-mode styled|plain] [--track-engagement true|false] [--add-utm-tags true|false] [--publish-target email_and_website|email|website]
1298
+ shipmail newsletters update nws_789 --subject "Updated subject" [--blocks @blocks-v2.json | --markdown @issue.md] [--feed-entry-url https://example.com/post] [--styling-mode styled|plain] [--track-engagement true|false] [--add-utm-tags true|false] [--subject-b "Second subject" | --clear-subject-b] [--subject-test-percent 1..25] [--subject-test-hours 1..72]
1243
1299
  shipmail newsletters preview <id>
1244
1300
  shipmail newsletters preflight <id>
1245
1301
  shipmail newsletters test-send <id> --recipient-email owner@example.com
1246
- shipmail newsletters schedule <id> --scheduled-at 2026-08-01T09:00:00.000Z
1302
+ shipmail newsletters schedule <id> --scheduled-at 2026-08-01T09:00:00.000Z [--limit 500]
1303
+ shipmail newsletters send-more <id> [--count 1000]
1247
1304
  shipmail newsletters cancel <id>
1248
1305
  shipmail newsletters resume <id>
1249
1306
  shipmail newsletter-assets list [--kind image|video] [--q text] [--limit 25] [--cursor cur]
@@ -1254,12 +1311,17 @@ Usage:
1254
1311
  shipmail audiences list [--limit 25] [--cursor cur]
1255
1312
  shipmail audiences create <name> --consent-source "Signup form" [--description "..."]
1256
1313
  shipmail audiences get <id>
1257
- shipmail audiences update <id> [--name "..."] [--description "..."]
1314
+ shipmail audiences update <id> [--name "..."] [--description "..."] [--cleanup true|false] [--cleanup-ignored-issues 2..100] [--cleanup-inactive-days 30..3650]
1258
1315
  shipmail audiences delete <id>
1259
1316
  shipmail audiences feed get <audience-id>
1260
1317
  shipmail audiences feed update <audience-id> [--enabled true|false] [--title "..."] [--subtitle "..."] [--site-url https://...] [--canonical-url https://...] [--author-name "..."] [--icon-url https://...] [--entry-limit 10..100] [--idempotency-key key]
1261
1318
  shipmail audiences feed rotate <audience-id> [--idempotency-key key]
1262
1319
  shipmail audiences feed revoke <audience-id> [--idempotency-key key]
1320
+ shipmail audiences welcome get <audience-id>
1321
+ shipmail audiences welcome enable|disable <audience-id> [--idempotency-key key]
1322
+ shipmail audiences welcome add-step <audience-id> --subject "Welcome" (--markdown @welcome.md | --blocks @blocks.json) [--wait-minutes 0] [--preview-text "..."] [--sender-identity-id nwsid_456]
1323
+ shipmail audiences welcome update-step <audience-id> <step-id> [--subject "..."] [--markdown @welcome.md | --blocks @blocks.json] [--wait-minutes 1440]
1324
+ shipmail audiences welcome remove-step <audience-id> <step-id>
1263
1325
  shipmail audiences subscribers add <audience-id> <email> [--name "Jane"] [--consent-source "..."]
1264
1326
  shipmail audiences subscribers list <audience-id> [--status subscribed] [--email jane@x.com] [--limit 25]
1265
1327
  shipmail audiences subscribers get <audience-id> <subscriber-id>
@@ -1279,11 +1341,27 @@ Usage:
1279
1341
  shipmail booking-pages get <id>
1280
1342
  shipmail booking-pages update <id> [--name "..."] [--conferencing-provider zoom|google_meet|none] [--active false]
1281
1343
  shipmail booking-pages delete <id>
1344
+ shipmail booking-links list [--status open|booked|expired|revoked] [--limit 25] [--cursor cur]
1345
+ shipmail booking-links create --name "Call with Sam" --mailbox hello@example.com --duration-minutes 30 --timezone Europe/Lisbon --range 2026-11-02T09:00:00Z/2026-11-02T12:00:00Z [--range ...] [--source-page bkp_...] [--max-bookings 1|unlimited] [--expires-at 2026-11-10T00:00:00Z]
1346
+ shipmail booking-links get <id>
1347
+ shipmail booking-links update <id> [--name "..."] [--range start/end|none] [--max-bookings n|unlimited] [--expires-at iso|none] [--source-page id|none]
1348
+ shipmail booking-links revoke <id>
1282
1349
 
1283
1350
  Booking page conferencing:
1284
1351
  --conferencing-provider <zoom|google_meet> Create a meeting for every booking.
1285
1352
  --conferencing-provider none Stop creating meeting links on update.
1286
1353
 
1354
+ One-off booking links:
1355
+ --range <start>/<end> A time the link offers, as two ISO instants. Repeat it, or
1356
+ separate ranges with commas. Without ranges, --source-page
1357
+ offers that booking page's weekly hours.
1358
+ --range none On update, drop the ranges and offer the source page's hours.
1359
+ --max-bookings <n|unlimited> The link stops after this many bookings. Defaults to 1.
1360
+ --expires-at <iso|none> The link stops taking bookings at this time.
1361
+ --source-page <id|none> The booking page to copy weekly hours from.
1362
+ Revoking a link stops it at once and cannot be undone. Other flags: --description,
1363
+ --buffer-minutes, --minimum-notice-minutes, --conferencing-provider, --mailbox.
1364
+
1287
1365
  Conversation IDs:
1288
1366
  <thread-id> accepts a conversation ID (thd_...) or the mail-server thread ID. Store the
1289
1367
  conversation_id every response carries. thread_id is deprecated and unchanged.
@@ -1295,14 +1373,30 @@ Member access output:
1295
1373
  Newsletter styling:
1296
1374
  --styling-mode <styled|plain> styled applies Shipmail's email theme. plain sends your HTML without injected styles, width, or centering, so the reader's email client styles it.
1297
1375
 
1376
+ Newsletter publish target:
1377
+ --publish-target <email_and_website|email|website> Where the newsletter goes. email_and_website (default) sends it, and its archive visibility decides whether it also appears on the website and feed. email never appears on the web. website is published on the audience's newsletter website and feed at the scheduled time without any email; the website must be on.
1378
+
1298
1379
  Newsletter tracking:
1299
- --track-engagement <true|false> Track opens and clicks for this issue. Off by default. Changes only while the issue is a draft or scheduled for later.
1380
+ --track-engagement <true|false> Track opens and clicks for this newsletter. Off by default. Changes only while the newsletter is a draft or scheduled for later.
1381
+ --add-utm-tags <true|false> Add utm_source, utm_medium and utm_campaign to the newsletter's links in the email and the feed. Off by default. Changes only while the newsletter is a draft or scheduled for later.
1382
+
1383
+ Newsletter subject test:
1384
+ --subject-b <text> Test a second subject. Each subject goes to --subject-test-percent of the subscribers (default 10); after --subject-test-hours (default 4) the one with more human clicks goes to the rest. Needs --track-engagement true.
1385
+ --clear-subject-b Remove the second subject.
1386
+
1387
+ Engaged first:
1388
+ --limit <n> On schedule, send only to the n most engaged subscribers. Send the rest with send-more, which takes --count or sends to everyone left.
1389
+
1390
+ List cleanup:
1391
+ --cleanup <true|false> A cold subscriber gets one last-chance email instead of a newsletter, and becomes dormant if they do not click keep within 14 days.
1392
+ --cleanup-ignored-issues <n> Tracked newsletters ignored in a row before a subscriber is cold (2 to 100, default 6).
1393
+ --cleanup-inactive-days <n> Days without a sign of life before a subscriber is cold (30 to 3650, default 180).
1300
1394
 
1301
1395
  Newsletter block prose:
1302
1396
  Paragraph, quote, callout, list-item, and column bodies accept bare text or sanitized inline HTML. Use <p> and <br> for line breaks.
1303
1397
 
1304
1398
  Newsletter feeds:
1305
- --feed-entry-url <url> Link this issue's Atom feed title to an http(s) URL instead of its Shipmail archive.
1399
+ --feed-entry-url <url> Link this newsletter's Atom feed title to an http(s) URL instead of its Shipmail archive.
1306
1400
 
1307
1401
  Sign-in:
1308
1402
  Run shipmail login once to sign in through the browser instead of using an API key.
@@ -1347,11 +1441,22 @@ function requirePositional(positional, index, label) {
1347
1441
  if (!value) throw new CliUsageError(`Missing ${label}.`);
1348
1442
  return value;
1349
1443
  }
1444
+ function inboxTextFilters(options) {
1445
+ const from = getString(options, "from");
1446
+ const to = getString(options, "to");
1447
+ const subject = getString(options, "subject");
1448
+ return {
1449
+ ...from ? { from } : {},
1450
+ ...to ? { to } : {},
1451
+ ...subject ? { subject } : {}
1452
+ };
1453
+ }
1350
1454
  function inboxListParams(options) {
1351
1455
  const params = {};
1352
1456
  const folderId = getString(options, "folder-id");
1353
1457
  const folderRole = getString(options, "folder-role");
1354
1458
  const searchText = getString(options, "search");
1459
+ const hasAttachment = getBoolean(options, "has-attachment");
1355
1460
  const cursor = getString(options, "cursor");
1356
1461
  const after = getString(options, "after");
1357
1462
  const before = getString(options, "before");
@@ -1361,6 +1466,8 @@ function inboxListParams(options) {
1361
1466
  if (folderId) params.folder_id = folderId;
1362
1467
  if (folderRole) params.folder_role = folderRole;
1363
1468
  if (searchText) params.search_text = searchText;
1469
+ Object.assign(params, inboxTextFilters(options));
1470
+ if (hasAttachment !== void 0) params.has_attachment = hasAttachment;
1364
1471
  if (cursor) params.cursor = cursor;
1365
1472
  if (after) params.after = after;
1366
1473
  if (before) params.before = before;
@@ -1395,19 +1502,39 @@ function webhookWriteOnlyUpdate(options, name) {
1395
1502
  }
1396
1503
  return clear ? null : value;
1397
1504
  }
1505
+ function webhookAllowedSenders(options) {
1506
+ const value = optionValue(options, "allowed-sender");
1507
+ if (value === void 0) return void 0;
1508
+ if (typeof value === "boolean") throw new CliUsageError("Missing value for --allowed-sender.");
1509
+ return getStringList(options, "allowed-sender");
1510
+ }
1511
+ function webhookAllowedSendersUpdate(options) {
1512
+ const senders = webhookAllowedSenders(options);
1513
+ const clear = getBoolean(options, "clear-allowed-senders") === true;
1514
+ if (senders !== void 0 && clear) {
1515
+ throw new CliUsageError("Use --allowed-sender or --clear-allowed-senders, not both.");
1516
+ }
1517
+ return clear ? null : senders;
1518
+ }
1398
1519
  function webhookUpdateParams(options) {
1399
1520
  const authorization = webhookWriteOnlyUpdate(options, "authorization");
1400
1521
  const standardWebhooksSecret = webhookWriteOnlyUpdate(options, "standard-webhooks-secret");
1401
1522
  const url = getString(options, "url");
1402
1523
  const description = getString(options, "description");
1403
1524
  const active = getBoolean(options, "active");
1525
+ const includeReplyText = getBoolean(options, "include-reply-text");
1526
+ const authenticatedSendersOnly = getBoolean(options, "authenticated-senders-only");
1527
+ const allowedSenders = webhookAllowedSendersUpdate(options);
1404
1528
  const params = {
1405
1529
  ...url !== void 0 ? { url } : {},
1406
1530
  ...optionValue(options, "events") !== void 0 ? { events: webhookEvents(options) } : {},
1407
1531
  ...description !== void 0 ? { description } : {},
1408
1532
  ...active !== void 0 ? { active } : {},
1409
1533
  ...authorization !== void 0 ? { authorization } : {},
1410
- ...standardWebhooksSecret !== void 0 ? { standard_webhooks_secret: standardWebhooksSecret } : {}
1534
+ ...standardWebhooksSecret !== void 0 ? { standard_webhooks_secret: standardWebhooksSecret } : {},
1535
+ ...includeReplyText !== void 0 ? { include_reply_text: includeReplyText } : {},
1536
+ ...authenticatedSendersOnly !== void 0 ? { authenticated_senders_only: authenticatedSendersOnly } : {},
1537
+ ...allowedSenders !== void 0 ? { allowed_senders: allowedSenders } : {}
1411
1538
  };
1412
1539
  if (Object.keys(params).length === 0) {
1413
1540
  throw new CliUsageError("Nothing to update. Pass at least one webhook option.");
@@ -1519,6 +1646,44 @@ async function replyToMessageParams(options) {
1519
1646
  } : {}
1520
1647
  };
1521
1648
  }
1649
+ var DOMAIN_HEALTH_PROVIDER_LABELS = {
1650
+ google: "Gmail",
1651
+ microsoft: "Outlook",
1652
+ yahoo: "Yahoo",
1653
+ apple: "iCloud",
1654
+ other: "Other"
1655
+ };
1656
+ function formatRecipients(count) {
1657
+ return `${count} ${count === 1 ? "recipient" : "recipients"}`;
1658
+ }
1659
+ function formatDeliveryRate(rate) {
1660
+ return rate === null ? "no results yet" : `${rate}% delivered`;
1661
+ }
1662
+ function writeDomainHealth(out, health) {
1663
+ const { dns, blocklist, delivery } = health;
1664
+ const checks = [
1665
+ ["MX", dns.mx],
1666
+ ["SPF", dns.spf],
1667
+ ["DKIM", dns.dkim],
1668
+ ["DMARC", dns.dmarc]
1669
+ ];
1670
+ const lines = [
1671
+ `${health.domain} (${health.domain_id})`,
1672
+ "",
1673
+ `DNS (checked ${dns.last_checked_at ?? "never"})`,
1674
+ ...checks.map(([name, ok]) => ` ${name.padEnd(6)}${ok ? "pass" : "fail"}`),
1675
+ "",
1676
+ `Spamhaus DBL: ${blocklist.listed ? `listed since ${blocklist.listed_since ?? "unknown"}` : "not listed"}`,
1677
+ "",
1678
+ `Delivery, last 30 days: ${formatDeliveryRate(delivery.delivery_rate)}`,
1679
+ ` ${formatRecipients(delivery.recipients)}: ${delivery.delivered} delivered, ${delivery.bounced} bounced, ${delivery.deferred} deferred, ${delivery.complained} complained`,
1680
+ ...delivery.providers.map(
1681
+ (p) => ` ${DOMAIN_HEALTH_PROVIDER_LABELS[p.provider].padEnd(8)}${formatDeliveryRate(p.delivery_rate)}, ${formatRecipients(p.recipients)}, ${p.bounced} bounced, ${p.deferred} deferred, ${p.complained} complained`
1682
+ )
1683
+ ];
1684
+ out.write(`${lines.join("\n")}
1685
+ `);
1686
+ }
1522
1687
  async function runDomains(client, parsed, out) {
1523
1688
  const subcommand = parsed.positional[1];
1524
1689
  if (subcommand === "list") {
@@ -1556,6 +1721,14 @@ async function runDomains(client, parsed, out) {
1556
1721
  );
1557
1722
  return;
1558
1723
  }
1724
+ if (subcommand === "health") {
1725
+ const health = await client.domains.getHealth(
1726
+ requirePositional(parsed.positional, 2, "domain id")
1727
+ );
1728
+ if (getBoolean(parsed.options, "json") === true) writeJson(out, health);
1729
+ else writeDomainHealth(out, health);
1730
+ return;
1731
+ }
1559
1732
  if (subcommand === "verify") {
1560
1733
  writeJson(
1561
1734
  out,
@@ -1620,6 +1793,21 @@ async function runMailboxes(client, parsed, out) {
1620
1793
  );
1621
1794
  return;
1622
1795
  }
1796
+ if (subcommand === "update") {
1797
+ const displayName = getString(parsed.options, "display-name");
1798
+ const agentSecurityHold = getBoolean(parsed.options, "agent-security-hold");
1799
+ if (displayName === void 0 && agentSecurityHold === void 0) {
1800
+ throw new CliUsageError("Provide --display-name or --agent-security-hold.");
1801
+ }
1802
+ writeJson(
1803
+ out,
1804
+ await client.mailboxes.update(requirePositional(parsed.positional, 2, "mailbox id"), {
1805
+ ...displayName !== void 0 ? { display_name: displayName } : {},
1806
+ ...agentSecurityHold !== void 0 ? { agent_security_hold: agentSecurityHold } : {}
1807
+ })
1808
+ );
1809
+ return;
1810
+ }
1623
1811
  if (subcommand === "suspend") {
1624
1812
  writeJson(
1625
1813
  out,
@@ -1887,6 +2075,21 @@ async function runInbox(client, parsed, out) {
1887
2075
  );
1888
2076
  return;
1889
2077
  }
2078
+ if (subcommand === "security-review") {
2079
+ writeJson(
2080
+ out,
2081
+ await client.mailboxes.releaseInboxMessageSecurity(
2082
+ requirePositional(parsed.positional, 2, "mailbox id"),
2083
+ requirePositional(parsed.positional, 3, "message id"),
2084
+ {
2085
+ decision: "release",
2086
+ reason: getRequiredString(parsed.options, "reason"),
2087
+ expected_content_sha256: getRequiredString(parsed.options, "expected-content-sha256")
2088
+ }
2089
+ )
2090
+ );
2091
+ return;
2092
+ }
1890
2093
  if (subcommand === "threads") {
1891
2094
  const attentionState = getString(parsed.options, "attention-state");
1892
2095
  const sortBy = getString(parsed.options, "sort-by");
@@ -1981,13 +2184,64 @@ async function runInbox(client, parsed, out) {
1981
2184
  );
1982
2185
  return;
1983
2186
  }
2187
+ if (subcommand === "drafts") {
2188
+ writeJson(
2189
+ out,
2190
+ await client.mailboxes.listInboxReplyDrafts(
2191
+ requirePositional(parsed.positional, 2, "mailbox id"),
2192
+ requirePositional(parsed.positional, 3, "thread id")
2193
+ )
2194
+ );
2195
+ return;
2196
+ }
2197
+ if (subcommand === "get-draft") {
2198
+ writeJson(
2199
+ out,
2200
+ await client.mailboxes.getInboxReplyDraft(
2201
+ requirePositional(parsed.positional, 2, "mailbox id"),
2202
+ requirePositional(parsed.positional, 3, "thread id"),
2203
+ requirePositional(parsed.positional, 4, "draft id")
2204
+ )
2205
+ );
2206
+ return;
2207
+ }
2208
+ if (subcommand === "update-draft") {
2209
+ const text = await readTextInput(getString(parsed.options, "text"));
2210
+ const html = await readTextInput(getString(parsed.options, "html"));
2211
+ if (!text && !html) throw new CliUsageError("Provide --text or --html.");
2212
+ writeJson(
2213
+ out,
2214
+ await client.mailboxes.updateInboxReplyDraft(
2215
+ requirePositional(parsed.positional, 2, "mailbox id"),
2216
+ requirePositional(parsed.positional, 3, "thread id"),
2217
+ requirePositional(parsed.positional, 4, "draft id"),
2218
+ {
2219
+ ...text ? { text } : {},
2220
+ ...html ? { html } : {}
2221
+ }
2222
+ )
2223
+ );
2224
+ return;
2225
+ }
2226
+ if (subcommand === "delete-draft") {
2227
+ const draftId = requirePositional(parsed.positional, 4, "draft id");
2228
+ await client.mailboxes.deleteInboxReplyDraft(
2229
+ requirePositional(parsed.positional, 2, "mailbox id"),
2230
+ requirePositional(parsed.positional, 3, "thread id"),
2231
+ draftId
2232
+ );
2233
+ writeJson(out, { ok: true, id: draftId });
2234
+ return;
2235
+ }
1984
2236
  if (subcommand === "send-draft") {
2237
+ const expectedContentSha256 = getString(parsed.options, "expected-content-sha256");
1985
2238
  writeJson(
1986
2239
  out,
1987
2240
  await client.mailboxes.sendInboxReplyDraft(
1988
2241
  requirePositional(parsed.positional, 2, "mailbox id"),
1989
2242
  requirePositional(parsed.positional, 3, "thread id"),
1990
- requirePositional(parsed.positional, 4, "draft id")
2243
+ requirePositional(parsed.positional, 4, "draft id"),
2244
+ expectedContentSha256 ? { expectedContentSha256 } : void 0
1991
2245
  )
1992
2246
  );
1993
2247
  return;
@@ -2209,6 +2463,9 @@ async function runWebhooks(client, parsed, out) {
2209
2463
  parsed.options,
2210
2464
  "standard-webhooks-secret"
2211
2465
  );
2466
+ const includeReplyText = getBoolean(parsed.options, "include-reply-text");
2467
+ const authenticatedSendersOnly = getBoolean(parsed.options, "authenticated-senders-only");
2468
+ const allowedSenders = webhookAllowedSenders(parsed.options);
2212
2469
  writeJson(
2213
2470
  out,
2214
2471
  await client.webhooks.create({
@@ -2216,7 +2473,10 @@ async function runWebhooks(client, parsed, out) {
2216
2473
  events: webhookEvents(parsed.options),
2217
2474
  ...description ? { description } : {},
2218
2475
  ...authorization !== void 0 ? { authorization } : {},
2219
- ...standardWebhooksSecret !== void 0 ? { standard_webhooks_secret: standardWebhooksSecret } : {}
2476
+ ...standardWebhooksSecret !== void 0 ? { standard_webhooks_secret: standardWebhooksSecret } : {},
2477
+ ...includeReplyText !== void 0 ? { include_reply_text: includeReplyText } : {},
2478
+ ...authenticatedSendersOnly !== void 0 ? { authenticated_senders_only: authenticatedSendersOnly } : {},
2479
+ ...allowedSenders !== void 0 ? { allowed_senders: allowedSenders } : {}
2220
2480
  })
2221
2481
  );
2222
2482
  return;
@@ -2276,14 +2536,27 @@ async function runMembers(client, parsed, out) {
2276
2536
  }
2277
2537
  throw new CliUsageError("Unknown members command.");
2278
2538
  }
2539
+ var SUPPRESSION_SCOPES = ["all", "marketing"];
2279
2540
  async function runSuppressions(client, parsed, out) {
2280
2541
  const subcommand = parsed.positional[1];
2542
+ if (subcommand === "add") {
2543
+ const scope = getEnum(parsed.options, "scope", SUPPRESSION_SCOPES);
2544
+ writeJson(
2545
+ out,
2546
+ await client.suppressions.create({
2547
+ email: requirePositional(parsed.positional, 2, "email"),
2548
+ ...scope ? { scope } : {}
2549
+ })
2550
+ );
2551
+ return;
2552
+ }
2281
2553
  if (subcommand === "list") {
2282
2554
  writeJson(
2283
2555
  out,
2284
2556
  await client.suppressions.list({
2285
2557
  cursor: getString(parsed.options, "cursor"),
2286
- limit: getNumber(parsed.options, "limit")
2558
+ limit: getNumber(parsed.options, "limit"),
2559
+ scope: getEnum(parsed.options, "scope", SUPPRESSION_SCOPES)
2287
2560
  })
2288
2561
  );
2289
2562
  return;
@@ -2303,7 +2576,8 @@ var SUBSCRIBER_STATUS_VALUES = [
2303
2576
  "bounced",
2304
2577
  "complained",
2305
2578
  "suppressed",
2306
- "transactional_only"
2579
+ "transactional_only",
2580
+ "dormant"
2307
2581
  ];
2308
2582
  function subscriberListParams(options) {
2309
2583
  const email = getString(options, "email");
@@ -2460,6 +2734,89 @@ async function runAudienceFeed(client, parsed, out) {
2460
2734
  }
2461
2735
  throw new CliUsageError("Unknown audiences feed command.");
2462
2736
  }
2737
+ async function welcomeStepParams(options) {
2738
+ const subject = getString(options, "subject");
2739
+ const previewText = getString(options, "preview-text");
2740
+ const waitMinutes = getNumber(options, "wait-minutes");
2741
+ const senderIdentityId = getString(options, "sender-identity-id");
2742
+ const stylingMode = parseStylingMode(options);
2743
+ const trackEngagement = getBoolean(options, "track-engagement");
2744
+ const addUtmTags = getBoolean(options, "add-utm-tags");
2745
+ const blocksInput = getString(options, "blocks");
2746
+ const blocks = blocksInput ? await readNewsletterBlocks(blocksInput) : void 0;
2747
+ const contentMarkdown = await readNewsletterMarkdown(options);
2748
+ if (waitMinutes !== void 0 && (!Number.isInteger(waitMinutes) || waitMinutes < 0)) {
2749
+ throw new CliUsageError("--wait-minutes must be a whole number of minutes, 0 or more.");
2750
+ }
2751
+ return {
2752
+ ...subject !== void 0 ? { subject } : {},
2753
+ ...previewText !== void 0 ? { preview_text: previewText } : {},
2754
+ ...blocks ? { blocks } : {},
2755
+ ...contentMarkdown !== void 0 ? { content_markdown: contentMarkdown } : {},
2756
+ ...waitMinutes !== void 0 ? { wait_minutes: waitMinutes } : {},
2757
+ ...senderIdentityId !== void 0 ? { sender_identity_id: senderIdentityId } : {},
2758
+ ...stylingMode !== void 0 ? { styling_mode: stylingMode } : {},
2759
+ ...trackEngagement !== void 0 ? { track_engagement: trackEngagement } : {},
2760
+ ...addUtmTags !== void 0 ? { add_utm_tags: addUtmTags } : {}
2761
+ };
2762
+ }
2763
+ async function runAudienceWelcome(client, parsed, out) {
2764
+ const action = parsed.positional[2];
2765
+ const audienceId = requirePositional(parsed.positional, 3, "audience id");
2766
+ if (action === "get") {
2767
+ writeJson(out, await client.audiences.welcomeSequence.get(audienceId));
2768
+ return;
2769
+ }
2770
+ const idempotencyKey = getString(parsed.options, "idempotency-key");
2771
+ const methodOptions = idempotencyKey ? { idempotencyKey } : void 0;
2772
+ if (action === "enable" || action === "disable") {
2773
+ writeJson(
2774
+ out,
2775
+ await client.audiences.welcomeSequence.update(
2776
+ audienceId,
2777
+ { enabled: action === "enable" },
2778
+ methodOptions
2779
+ )
2780
+ );
2781
+ return;
2782
+ }
2783
+ if (action === "add-step") {
2784
+ const params = await welcomeStepParams(parsed.options);
2785
+ if (!params.subject) throw new CliUsageError("--subject is required.");
2786
+ if (!params.blocks?.length && !params.content_markdown?.trim()) {
2787
+ throw new CliUsageError("Provide --blocks or --markdown.");
2788
+ }
2789
+ writeJson(
2790
+ out,
2791
+ await client.audiences.welcomeSequence.createStep(
2792
+ audienceId,
2793
+ { ...params, subject: params.subject },
2794
+ methodOptions
2795
+ )
2796
+ );
2797
+ return;
2798
+ }
2799
+ const stepId = requirePositional(parsed.positional, 4, "step id");
2800
+ if (action === "update-step") {
2801
+ const params = await welcomeStepParams(parsed.options);
2802
+ if (Object.keys(params).length === 0) {
2803
+ throw new CliUsageError("Provide at least one field to update.");
2804
+ }
2805
+ writeJson(
2806
+ out,
2807
+ await client.audiences.welcomeSequence.updateStep(audienceId, stepId, params, methodOptions)
2808
+ );
2809
+ return;
2810
+ }
2811
+ if (action === "remove-step") {
2812
+ writeJson(
2813
+ out,
2814
+ await client.audiences.welcomeSequence.deleteStep(audienceId, stepId, methodOptions)
2815
+ );
2816
+ return;
2817
+ }
2818
+ throw new CliUsageError("Unknown audiences welcome command.");
2819
+ }
2463
2820
  async function runAudiences(client, parsed, out) {
2464
2821
  const subcommand = parsed.positional[1];
2465
2822
  if (subcommand === "subscribers") {
@@ -2470,6 +2827,10 @@ async function runAudiences(client, parsed, out) {
2470
2827
  await runAudienceFeed(client, parsed, out);
2471
2828
  return;
2472
2829
  }
2830
+ if (subcommand === "welcome") {
2831
+ await runAudienceWelcome(client, parsed, out);
2832
+ return;
2833
+ }
2473
2834
  if (subcommand === "list") {
2474
2835
  writeJson(
2475
2836
  out,
@@ -2503,11 +2864,17 @@ async function runAudiences(client, parsed, out) {
2503
2864
  const id = requirePositional(parsed.positional, 2, "audience id");
2504
2865
  const name = getString(parsed.options, "name");
2505
2866
  const description = getString(parsed.options, "description");
2867
+ const cleanup_enabled = getBoolean(parsed.options, "cleanup");
2868
+ const cleanup_ignored_issues = getNumber(parsed.options, "cleanup-ignored-issues");
2869
+ const cleanup_inactive_days = getNumber(parsed.options, "cleanup-inactive-days");
2506
2870
  writeJson(
2507
2871
  out,
2508
2872
  await client.audiences.update(id, {
2509
2873
  ...name ? { name } : {},
2510
- ...description ? { description } : {}
2874
+ ...description ? { description } : {},
2875
+ ...cleanup_enabled !== void 0 ? { cleanup_enabled } : {},
2876
+ ...cleanup_ignored_issues !== void 0 ? { cleanup_ignored_issues } : {},
2877
+ ...cleanup_inactive_days !== void 0 ? { cleanup_inactive_days } : {}
2511
2878
  })
2512
2879
  );
2513
2880
  return;
@@ -2586,14 +2953,27 @@ async function runNewsletters(client, parsed, out) {
2586
2953
  return;
2587
2954
  }
2588
2955
  if (subcommand === "schedule") {
2956
+ const limit = getNumber(parsed.options, "limit");
2589
2957
  writeJson(
2590
2958
  out,
2591
2959
  await client.newsletters.schedule(requirePositional(parsed.positional, 2, "newsletter id"), {
2592
- scheduled_at: getRequiredString(parsed.options, "scheduled-at")
2960
+ scheduled_at: getRequiredString(parsed.options, "scheduled-at"),
2961
+ ...limit !== void 0 ? { limit } : {}
2593
2962
  })
2594
2963
  );
2595
2964
  return;
2596
2965
  }
2966
+ if (subcommand === "send-more") {
2967
+ const count = getNumber(parsed.options, "count");
2968
+ writeJson(
2969
+ out,
2970
+ await client.newsletters.sendToMore(
2971
+ requirePositional(parsed.positional, 2, "newsletter id"),
2972
+ count !== void 0 ? { count } : {}
2973
+ )
2974
+ );
2975
+ return;
2976
+ }
2597
2977
  if (subcommand === "cancel") {
2598
2978
  writeJson(
2599
2979
  out,
@@ -2913,6 +3293,93 @@ async function runCalendar(client, parsed, out) {
2913
3293
  }
2914
3294
  throw new CliUsageError("Unknown calendar command.");
2915
3295
  }
3296
+ function parseBookingLinkRanges(options) {
3297
+ const values = getStringList(options, "range");
3298
+ if (values.length === 0) return void 0;
3299
+ if (values.length === 1 && values[0] === "none") return null;
3300
+ return values.map((value) => {
3301
+ const [start, end, extra] = value.split("/");
3302
+ if (!start || !end || extra !== void 0) {
3303
+ throw new CliUsageError("--range must be <start>/<end>, two ISO 8601 instants.");
3304
+ }
3305
+ return { start, end };
3306
+ });
3307
+ }
3308
+ function getMaxBookings(options) {
3309
+ if (getString(options, "max-bookings") === "unlimited") return null;
3310
+ return getNumber(options, "max-bookings");
3311
+ }
3312
+ function getNullableString(options, key) {
3313
+ const value = getString(options, key);
3314
+ return value === "none" ? null : value;
3315
+ }
3316
+ async function runBookingLinks(client, parsed, out) {
3317
+ const subcommand = parsed.positional[1];
3318
+ const options = parsed.options;
3319
+ if (subcommand === "list") {
3320
+ writeJson(
3321
+ out,
3322
+ await client.bookingLinks.list({
3323
+ cursor: getString(options, "cursor"),
3324
+ limit: getNumber(options, "limit"),
3325
+ status: getEnum(options, "status", BOOKING_LINK_STATUSES)
3326
+ })
3327
+ );
3328
+ return;
3329
+ }
3330
+ if (subcommand === "create") {
3331
+ const ranges = parseBookingLinkRanges(options);
3332
+ if (ranges === null) throw new CliUsageError("--range none only applies to update.");
3333
+ const params = {
3334
+ name: getRequiredString(options, "name"),
3335
+ mailbox: getRequiredString(options, "mailbox"),
3336
+ duration_minutes: requireNumber(options, "duration-minutes"),
3337
+ timezone: getRequiredString(options, "timezone"),
3338
+ offered_ranges: ranges,
3339
+ source_page_id: getString(options, "source-page"),
3340
+ description: getString(options, "description"),
3341
+ buffer_minutes: getNumber(options, "buffer-minutes"),
3342
+ minimum_notice_minutes: getNumber(options, "minimum-notice-minutes"),
3343
+ conferencing_provider: getConferencingProvider(options),
3344
+ max_bookings: getMaxBookings(options),
3345
+ expires_at: getString(options, "expires-at")
3346
+ };
3347
+ writeJson(out, await client.bookingLinks.create(params));
3348
+ return;
3349
+ }
3350
+ if (subcommand === "get") {
3351
+ writeJson(
3352
+ out,
3353
+ await client.bookingLinks.get(requirePositional(parsed.positional, 2, "booking link id"))
3354
+ );
3355
+ return;
3356
+ }
3357
+ if (subcommand === "update") {
3358
+ const id = requirePositional(parsed.positional, 2, "booking link id");
3359
+ const params = {
3360
+ name: getString(options, "name"),
3361
+ mailbox: getString(options, "mailbox"),
3362
+ duration_minutes: getNumber(options, "duration-minutes"),
3363
+ timezone: getString(options, "timezone"),
3364
+ offered_ranges: parseBookingLinkRanges(options),
3365
+ source_page_id: getNullableString(options, "source-page"),
3366
+ description: getString(options, "description"),
3367
+ buffer_minutes: getNumber(options, "buffer-minutes"),
3368
+ minimum_notice_minutes: getNumber(options, "minimum-notice-minutes"),
3369
+ conferencing_provider: getConferencingProvider(options),
3370
+ max_bookings: getMaxBookings(options),
3371
+ expires_at: getNullableString(options, "expires-at")
3372
+ };
3373
+ writeJson(out, await client.bookingLinks.update(id, params));
3374
+ return;
3375
+ }
3376
+ if (subcommand === "revoke") {
3377
+ const id = requirePositional(parsed.positional, 2, "booking link id");
3378
+ writeJson(out, await client.bookingLinks.revoke(id));
3379
+ return;
3380
+ }
3381
+ throw new CliUsageError("Unknown booking-links command.");
3382
+ }
2916
3383
  async function runBookingPages(client, parsed, out) {
2917
3384
  const subcommand = parsed.positional[1];
2918
3385
  const options = parsed.options;
@@ -3053,6 +3520,11 @@ ${JSON.stringify(err.body, null, 2)}
3053
3520
  if (shipmailError.actionUrl) {
3054
3521
  const reason = shipmailError.reason ? ` (${shipmailError.reason})` : "";
3055
3522
  errOut.write(`Ask the organization owner to open${reason}: ${shipmailError.actionUrl}
3523
+ `);
3524
+ }
3525
+ if (shipmailError.limit) {
3526
+ const { resource, used, limit, scope } = shipmailError.limit;
3527
+ errOut.write(`Limit: ${resource} ${used} of ${limit} (${scope})
3056
3528
  `);
3057
3529
  }
3058
3530
  return;
@@ -3137,6 +3609,7 @@ async function runCli(argv, options = {}) {
3137
3609
  else if (command === "audiences") await runAudiences(client, parsed, out);
3138
3610
  else if (command === "calendar") await runCalendar(client, parsed, out);
3139
3611
  else if (command === "booking-pages") await runBookingPages(client, parsed, out);
3612
+ else if (command === "booking-links") await runBookingLinks(client, parsed, out);
3140
3613
  else throw new CliUsageError(`Unknown command: ${command}.`);
3141
3614
  return 0;
3142
3615
  } catch (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shipmail-cli",
3
- "version": "0.6.3",
3
+ "version": "0.6.5",
4
4
  "description": "Official CLI for the Shipmail API",
5
5
  "type": "module",
6
6
  "bin": {
@@ -41,7 +41,7 @@
41
41
  "access": "public"
42
42
  },
43
43
  "dependencies": {
44
- "shipmail": "0.6.4",
44
+ "shipmail": "0.6.6",
45
45
  "zod": "^4.6.5"
46
46
  },
47
47
  "devDependencies": {