shipmail-cli 0.6.4 → 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 +26 -6
  2. package/dist/cli.js +449 -20
  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
@@ -100,11 +101,17 @@ shipmail mailboxes sandbox-inbound mbx_123 --from customer@example.com --subject
100
101
  shipmail inbox list 550e8400-e29b-41d4-a716-446655440000 --folder-role inbox --after 2025-07-20T00:00:00Z --limit 25
101
102
  shipmail inbox list 550e8400-e29b-41d4-a716-446655440000 --from billing@vendor.example --subject invoice --has-attachment
102
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
103
109
  shipmail inbox threads 550e8400-e29b-41d4-a716-446655440000 --attention-state needs_reply --after 2025-07-20T00:00:00Z --limit 50
104
110
  shipmail inbox thread 550e8400-e29b-41d4-a716-446655440000 thr_123
105
111
  shipmail inbox draft 550e8400-e29b-41d4-a716-446655440000 thr_123 --expected-version 7 --text "Thanks for the note"
106
- # Apply your approval policy; stale drafts fail with 409 without delivery.
107
- 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
108
115
  # Save a new message to Drafts for a human to review and send; this never sends
109
116
  shipmail inbox compose-draft 550e8400-e29b-41d4-a716-446655440000 --to user@example.com --subject "Kickoff" --text "Draft for review"
110
117
  shipmail inbox thread-attention 550e8400-e29b-41d4-a716-446655440000 thr_123 --done true --expected-version 8
@@ -125,6 +132,8 @@ shipmail newsletters create \
125
132
  --subject "What shipped in July" \
126
133
  --blocks @blocks.json
127
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
128
137
  # A concurrent update exits with conflict (409); fetch the latest newsletter before retrying.
129
138
  shipmail newsletters preview nws_123
130
139
  shipmail newsletters test-send nws_123 --recipient-email owner@example.com
@@ -135,6 +144,11 @@ shipmail newsletter-assets upload ./hero.png --content-type image/png
135
144
  shipmail newsletter-assets upload ./demo.mp4 --content-type video/mp4 --thumbnail ./demo-poster.jpg
136
145
  shipmail newsletter-assets register-url https://cdn.shipmail.to/newsletter-images/org_123/hero.png
137
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
+
138
152
  # Manage webhooks
139
153
  shipmail webhooks create \
140
154
  --url https://example.com/shipmail/webhook \
@@ -151,6 +165,11 @@ Paragraph, quote, callout, list-item, and column bodies in `--blocks` accept bar
151
165
  sanitized inline HTML. Use `<p>` and `<br>` for line breaks. Shipmail keeps `a`, `b`, `br`,
152
166
  `code`, `em`, `i`, `p`, `s`, `span`, `strong`, and `u`, with safe attributes and URLs.
153
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
+
154
173
  `newsletter-assets upload` uses a five-minute direct storage PUT, then calls the single-use
155
174
  completion URL. Video uploads require a JPEG poster through `--thumbnail`. `newsletter-assets list`
156
175
  includes current newsletter media storage usage and a `pagination.next_cursor` value in the JSON
@@ -171,17 +190,18 @@ Use repeatable `--header "Name: value"` flags for request headers. Use `--output
171
190
  - `status`
172
191
  - `api <METHOD> <PATH>`
173
192
  - `domains list|search|create|get|verify|delete`
174
- - `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`
175
194
  - `members list|get`
176
- - `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`
177
196
  - `reply-scans create|status|results`
178
197
  - `messages send|list|analytics|get|reply` (test keys also support `--sandbox-outcome`)
179
198
  - `scheduled-messages list|get|update|cancel`
180
199
  - `mailboxes sandbox-inbound`
181
200
  - `webhooks list|create|update|test|deliveries|replay-delivery`
182
- - `suppressions list|remove`
201
+ - `suppressions add|list|remove`
183
202
  - `audiences list|create|get|update|delete`
184
203
  - `audiences feed get|update|rotate|revoke`
204
+ - `audiences welcome get|enable|disable|add-step|update-step|remove-step`
185
205
  - `audiences subscribers add|list|get|by-email|update|unsubscribe|resubscribe|remove`
186
206
  - `newsletter-sender-identities list`
187
207
  - `newsletter-domains list`
@@ -192,7 +212,7 @@ Member output uses the roles `owner`, `admin`, and `member`. Its `mailbox_access
192
212
  `selected`; `mailbox_ids` is empty for `all`, while selected IDs may be limited to mailboxes the
193
213
  API key can reach.
194
214
 
195
- 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:
196
216
 
197
217
  ```bash
198
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.4";
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,16 +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");
1038
1058
  const add_utm_tags = getBoolean(options, "add-utm-tags");
1039
1059
  const preview_text = getString(options, "preview-text");
1060
+ const subjectTest = subjectTestParams(options);
1040
1061
  return {
1041
1062
  audience_id,
1042
1063
  sender_identity_id,
@@ -1044,14 +1065,32 @@ async function newsletterCreateParams(options) {
1044
1065
  subject,
1045
1066
  ...preview_text !== void 0 ? { preview_text } : {},
1046
1067
  ...blocks ? { blocks } : {},
1068
+ ...content_markdown !== void 0 ? { content_markdown } : {},
1047
1069
  ...body_html !== void 0 ? { body_html } : {},
1048
1070
  ...body_text !== void 0 ? { body_text } : {},
1049
1071
  ...send_window_hours !== void 0 ? { send_window_hours } : {},
1050
1072
  ...archive_visibility !== void 0 ? { archive_visibility } : {},
1073
+ ...publish_target !== void 0 ? { publish_target } : {},
1051
1074
  ...feed_entry_url !== void 0 ? { feed_entry_url } : {},
1052
1075
  ...styling_mode !== void 0 ? { styling_mode } : {},
1053
1076
  ...track_engagement !== void 0 ? { track_engagement } : {},
1054
- ...add_utm_tags !== void 0 ? { add_utm_tags } : {}
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 } : {}
1055
1094
  };
1056
1095
  }
1057
1096
  async function newsletterUpdateParams(options) {
@@ -1062,6 +1101,7 @@ async function newsletterUpdateParams(options) {
1062
1101
  const preview_text = getString(options, "preview-text");
1063
1102
  const send_window_hours = getNumber(options, "send-window-hours");
1064
1103
  const archive_visibility = parseArchiveVisibility(options);
1104
+ const publish_target = parsePublishTarget(options);
1065
1105
  const feed_entry_url = getString(options, "feed-entry-url");
1066
1106
  const styling_mode = parseStylingMode(options);
1067
1107
  const track_engagement = getBoolean(options, "track-engagement");
@@ -1072,6 +1112,7 @@ async function newsletterUpdateParams(options) {
1072
1112
  const blocks = blocksInput ? await readNewsletterBlocks(blocksInput) : void 0;
1073
1113
  const body_html = bodyHtmlInput ? await readTextInput(bodyHtmlInput) : void 0;
1074
1114
  const body_text = bodyTextInput ? await readTextInput(bodyTextInput) : void 0;
1115
+ const content_markdown = await readNewsletterMarkdown(options);
1075
1116
  const params = {
1076
1117
  ...audience_id ? { audience_id } : {},
1077
1118
  ...sender_identity_id ? { sender_identity_id } : {},
@@ -1079,14 +1120,17 @@ async function newsletterUpdateParams(options) {
1079
1120
  ...subject ? { subject } : {},
1080
1121
  ...preview_text !== void 0 ? { preview_text } : {},
1081
1122
  ...blocks ? { blocks } : {},
1123
+ ...content_markdown !== void 0 ? { content_markdown } : {},
1082
1124
  ...body_html !== void 0 ? { body_html } : {},
1083
1125
  ...body_text !== void 0 ? { body_text } : {},
1084
1126
  ...send_window_hours !== void 0 ? { send_window_hours } : {},
1085
1127
  ...archive_visibility !== void 0 ? { archive_visibility } : {},
1128
+ ...publish_target !== void 0 ? { publish_target } : {},
1086
1129
  ...feed_entry_url !== void 0 ? { feed_entry_url } : {},
1087
1130
  ...styling_mode !== void 0 ? { styling_mode } : {},
1088
1131
  ...track_engagement !== void 0 ? { track_engagement } : {},
1089
- ...add_utm_tags !== void 0 ? { add_utm_tags } : {}
1132
+ ...add_utm_tags !== void 0 ? { add_utm_tags } : {},
1133
+ ...subjectTestParams(options)
1090
1134
  };
1091
1135
  if (Object.keys(params).length === 0) {
1092
1136
  throw new CliUsageError("Provide at least one field to update.");
@@ -1179,11 +1223,13 @@ Usage:
1179
1223
  shipmail domains create <name>
1180
1224
  shipmail domains get <id>
1181
1225
  shipmail domains dns-records <id>
1226
+ shipmail domains health <id> [--json]
1182
1227
  shipmail domains verify <id>
1183
1228
  shipmail domains delete <id>
1184
1229
  shipmail mailboxes list [--domain-id dom] [--limit 25] [--cursor cur]
1185
1230
  shipmail mailboxes create --domain-id dom --address hello (--password value | --password-file path | --generate-password) [--display-name "Hello"]
1186
1231
  shipmail mailboxes get <id>
1232
+ shipmail mailboxes update <id> [--display-name "Ops"] [--agent-security-hold true|false]
1187
1233
  shipmail mailboxes suspend <id>
1188
1234
  shipmail mailboxes resume <id>
1189
1235
  shipmail mailboxes export <id>
@@ -1208,12 +1254,17 @@ Usage:
1208
1254
  shipmail members get <id>
1209
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]
1210
1256
  shipmail inbox message <mailbox-id> <message-id>
1257
+ shipmail inbox security-review <mailbox-id> <message-id> --reason "Reviewed" --expected-content-sha256 HASH
1211
1258
  shipmail inbox threads <mailbox-id> [--attention-state needs_reply] [--after ISO] [--before ISO] [--limit 25] [--cursor cur]
1212
1259
  shipmail inbox thread <mailbox-id> <thread-id>
1213
1260
  shipmail inbox thread-attention <mailbox-id> <thread-id> --done true --expected-version 3
1214
1261
  shipmail inbox draft <mailbox-id> <thread-id> --expected-version 3 (--text "Hello" | --html "<p>Hello</p>") [--reply-mode reply_all]
1215
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)
1216
- 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]
1217
1268
  shipmail inbox reply-message <mailbox-id> <message-id> --to user@example.com (--text "Hello" | --html "<p>Hello</p>") [--track-reply true|false]
1218
1269
  shipmail inbox reply-thread <mailbox-id> <thread-id> --to user@example.com (--text "Hello" | --html "<p>Hello</p>") [--track-reply true|false]
1219
1270
  shipmail inbox update <mailbox-id> <message-id> [--read true] [--starred false]
@@ -1234,20 +1285,22 @@ Usage:
1234
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]
1235
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]
1236
1287
  shipmail webhooks replay-delivery <webhook-id> <delivery-id> [--idempotency-key key]
1237
- shipmail suppressions list
1288
+ shipmail suppressions add <email> [--scope all|marketing]
1289
+ shipmail suppressions list [--scope all|marketing] [--limit 25] [--cursor cur]
1238
1290
  shipmail suppressions remove <email>
1239
1291
  shipmail newsletters create \\
1240
1292
  --audience-id aud_123 \\
1241
1293
  --sender-identity-id nwsid_456 \\
1242
1294
  --name "July changelog" \\
1243
1295
  --subject "What shipped in July" \\
1244
- --blocks @blocks.json \\
1245
- [--feed-entry-url https://example.com/post] [--styling-mode styled|plain] [--track-engagement true|false] [--add-utm-tags true|false]
1246
- 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] [--add-utm-tags 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]
1247
1299
  shipmail newsletters preview <id>
1248
1300
  shipmail newsletters preflight <id>
1249
1301
  shipmail newsletters test-send <id> --recipient-email owner@example.com
1250
- 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]
1251
1304
  shipmail newsletters cancel <id>
1252
1305
  shipmail newsletters resume <id>
1253
1306
  shipmail newsletter-assets list [--kind image|video] [--q text] [--limit 25] [--cursor cur]
@@ -1258,12 +1311,17 @@ Usage:
1258
1311
  shipmail audiences list [--limit 25] [--cursor cur]
1259
1312
  shipmail audiences create <name> --consent-source "Signup form" [--description "..."]
1260
1313
  shipmail audiences get <id>
1261
- 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]
1262
1315
  shipmail audiences delete <id>
1263
1316
  shipmail audiences feed get <audience-id>
1264
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]
1265
1318
  shipmail audiences feed rotate <audience-id> [--idempotency-key key]
1266
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>
1267
1325
  shipmail audiences subscribers add <audience-id> <email> [--name "Jane"] [--consent-source "..."]
1268
1326
  shipmail audiences subscribers list <audience-id> [--status subscribed] [--email jane@x.com] [--limit 25]
1269
1327
  shipmail audiences subscribers get <audience-id> <subscriber-id>
@@ -1283,11 +1341,27 @@ Usage:
1283
1341
  shipmail booking-pages get <id>
1284
1342
  shipmail booking-pages update <id> [--name "..."] [--conferencing-provider zoom|google_meet|none] [--active false]
1285
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>
1286
1349
 
1287
1350
  Booking page conferencing:
1288
1351
  --conferencing-provider <zoom|google_meet> Create a meeting for every booking.
1289
1352
  --conferencing-provider none Stop creating meeting links on update.
1290
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
+
1291
1365
  Conversation IDs:
1292
1366
  <thread-id> accepts a conversation ID (thd_...) or the mail-server thread ID. Store the
1293
1367
  conversation_id every response carries. thread_id is deprecated and unchanged.
@@ -1299,15 +1373,30 @@ Member access output:
1299
1373
  Newsletter styling:
1300
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.
1301
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
+
1302
1379
  Newsletter tracking:
1303
- --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.
1304
- --add-utm-tags <true|false> Add utm_source, utm_medium and utm_campaign to the issue's links in the email and the feed. 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).
1305
1394
 
1306
1395
  Newsletter block prose:
1307
1396
  Paragraph, quote, callout, list-item, and column bodies accept bare text or sanitized inline HTML. Use <p> and <br> for line breaks.
1308
1397
 
1309
1398
  Newsletter feeds:
1310
- --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.
1311
1400
 
1312
1401
  Sign-in:
1313
1402
  Run shipmail login once to sign in through the browser instead of using an API key.
@@ -1557,6 +1646,44 @@ async function replyToMessageParams(options) {
1557
1646
  } : {}
1558
1647
  };
1559
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
+ }
1560
1687
  async function runDomains(client, parsed, out) {
1561
1688
  const subcommand = parsed.positional[1];
1562
1689
  if (subcommand === "list") {
@@ -1594,6 +1721,14 @@ async function runDomains(client, parsed, out) {
1594
1721
  );
1595
1722
  return;
1596
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
+ }
1597
1732
  if (subcommand === "verify") {
1598
1733
  writeJson(
1599
1734
  out,
@@ -1658,6 +1793,21 @@ async function runMailboxes(client, parsed, out) {
1658
1793
  );
1659
1794
  return;
1660
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
+ }
1661
1811
  if (subcommand === "suspend") {
1662
1812
  writeJson(
1663
1813
  out,
@@ -1925,6 +2075,21 @@ async function runInbox(client, parsed, out) {
1925
2075
  );
1926
2076
  return;
1927
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
+ }
1928
2093
  if (subcommand === "threads") {
1929
2094
  const attentionState = getString(parsed.options, "attention-state");
1930
2095
  const sortBy = getString(parsed.options, "sort-by");
@@ -2019,13 +2184,64 @@ async function runInbox(client, parsed, out) {
2019
2184
  );
2020
2185
  return;
2021
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
+ }
2022
2236
  if (subcommand === "send-draft") {
2237
+ const expectedContentSha256 = getString(parsed.options, "expected-content-sha256");
2023
2238
  writeJson(
2024
2239
  out,
2025
2240
  await client.mailboxes.sendInboxReplyDraft(
2026
2241
  requirePositional(parsed.positional, 2, "mailbox id"),
2027
2242
  requirePositional(parsed.positional, 3, "thread id"),
2028
- requirePositional(parsed.positional, 4, "draft id")
2243
+ requirePositional(parsed.positional, 4, "draft id"),
2244
+ expectedContentSha256 ? { expectedContentSha256 } : void 0
2029
2245
  )
2030
2246
  );
2031
2247
  return;
@@ -2320,14 +2536,27 @@ async function runMembers(client, parsed, out) {
2320
2536
  }
2321
2537
  throw new CliUsageError("Unknown members command.");
2322
2538
  }
2539
+ var SUPPRESSION_SCOPES = ["all", "marketing"];
2323
2540
  async function runSuppressions(client, parsed, out) {
2324
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
+ }
2325
2553
  if (subcommand === "list") {
2326
2554
  writeJson(
2327
2555
  out,
2328
2556
  await client.suppressions.list({
2329
2557
  cursor: getString(parsed.options, "cursor"),
2330
- limit: getNumber(parsed.options, "limit")
2558
+ limit: getNumber(parsed.options, "limit"),
2559
+ scope: getEnum(parsed.options, "scope", SUPPRESSION_SCOPES)
2331
2560
  })
2332
2561
  );
2333
2562
  return;
@@ -2347,7 +2576,8 @@ var SUBSCRIBER_STATUS_VALUES = [
2347
2576
  "bounced",
2348
2577
  "complained",
2349
2578
  "suppressed",
2350
- "transactional_only"
2579
+ "transactional_only",
2580
+ "dormant"
2351
2581
  ];
2352
2582
  function subscriberListParams(options) {
2353
2583
  const email = getString(options, "email");
@@ -2504,6 +2734,89 @@ async function runAudienceFeed(client, parsed, out) {
2504
2734
  }
2505
2735
  throw new CliUsageError("Unknown audiences feed command.");
2506
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
+ }
2507
2820
  async function runAudiences(client, parsed, out) {
2508
2821
  const subcommand = parsed.positional[1];
2509
2822
  if (subcommand === "subscribers") {
@@ -2514,6 +2827,10 @@ async function runAudiences(client, parsed, out) {
2514
2827
  await runAudienceFeed(client, parsed, out);
2515
2828
  return;
2516
2829
  }
2830
+ if (subcommand === "welcome") {
2831
+ await runAudienceWelcome(client, parsed, out);
2832
+ return;
2833
+ }
2517
2834
  if (subcommand === "list") {
2518
2835
  writeJson(
2519
2836
  out,
@@ -2547,11 +2864,17 @@ async function runAudiences(client, parsed, out) {
2547
2864
  const id = requirePositional(parsed.positional, 2, "audience id");
2548
2865
  const name = getString(parsed.options, "name");
2549
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");
2550
2870
  writeJson(
2551
2871
  out,
2552
2872
  await client.audiences.update(id, {
2553
2873
  ...name ? { name } : {},
2554
- ...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 } : {}
2555
2878
  })
2556
2879
  );
2557
2880
  return;
@@ -2630,14 +2953,27 @@ async function runNewsletters(client, parsed, out) {
2630
2953
  return;
2631
2954
  }
2632
2955
  if (subcommand === "schedule") {
2956
+ const limit = getNumber(parsed.options, "limit");
2633
2957
  writeJson(
2634
2958
  out,
2635
2959
  await client.newsletters.schedule(requirePositional(parsed.positional, 2, "newsletter id"), {
2636
- scheduled_at: getRequiredString(parsed.options, "scheduled-at")
2960
+ scheduled_at: getRequiredString(parsed.options, "scheduled-at"),
2961
+ ...limit !== void 0 ? { limit } : {}
2637
2962
  })
2638
2963
  );
2639
2964
  return;
2640
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
+ }
2641
2977
  if (subcommand === "cancel") {
2642
2978
  writeJson(
2643
2979
  out,
@@ -2957,6 +3293,93 @@ async function runCalendar(client, parsed, out) {
2957
3293
  }
2958
3294
  throw new CliUsageError("Unknown calendar command.");
2959
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
+ }
2960
3383
  async function runBookingPages(client, parsed, out) {
2961
3384
  const subcommand = parsed.positional[1];
2962
3385
  const options = parsed.options;
@@ -3097,6 +3520,11 @@ ${JSON.stringify(err.body, null, 2)}
3097
3520
  if (shipmailError.actionUrl) {
3098
3521
  const reason = shipmailError.reason ? ` (${shipmailError.reason})` : "";
3099
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})
3100
3528
  `);
3101
3529
  }
3102
3530
  return;
@@ -3181,6 +3609,7 @@ async function runCli(argv, options = {}) {
3181
3609
  else if (command === "audiences") await runAudiences(client, parsed, out);
3182
3610
  else if (command === "calendar") await runCalendar(client, parsed, out);
3183
3611
  else if (command === "booking-pages") await runBookingPages(client, parsed, out);
3612
+ else if (command === "booking-links") await runBookingLinks(client, parsed, out);
3184
3613
  else throw new CliUsageError(`Unknown command: ${command}.`);
3185
3614
  return 0;
3186
3615
  } catch (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shipmail-cli",
3
- "version": "0.6.4",
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.5",
44
+ "shipmail": "0.6.6",
45
45
  "zod": "^4.6.5"
46
46
  },
47
47
  "devDependencies": {