shipmail-cli 0.6.4 → 0.6.6

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 +32 -6
  2. package/dist/cli.js +452 -20
  3. package/package.json +6 -3
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 \
@@ -201,3 +221,9 @@ shipmail audiences feed update aud_123 \
201
221
  ```
202
222
 
203
223
  Run `shipmail --help` for flags and examples.
224
+
225
+ ## Links
226
+
227
+ - [CLI guide](https://shipmail.to/docs/cli)
228
+ - [Source](https://github.com/shipmail-to/shipmail-node/tree/main/packages/shipmail-cli)
229
+ - [Issues](https://github.com/shipmail-to/shipmail-node/issues)
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,
@@ -350,6 +351,7 @@ var OAuthError = class extends Error {
350
351
  this.code = code;
351
352
  this.name = "OAuthError";
352
353
  }
354
+ code;
353
355
  };
354
356
  function trimSlash(value) {
355
357
  return value.replace(/\/+$/, "");
@@ -649,7 +651,7 @@ async function login(options) {
649
651
  }
650
652
 
651
653
  // src/version.ts
652
- var VERSION = "0.6.4";
654
+ var VERSION = "0.6.6";
653
655
 
654
656
  // src/cli.ts
655
657
  var DEFAULT_BASE_URL = "https://shipmail.to/api/v1";
@@ -685,6 +687,8 @@ var CliHttpError = class extends Error {
685
687
  this.body = body;
686
688
  this.name = "CliHttpError";
687
689
  }
690
+ status;
691
+ body;
688
692
  };
689
693
  function parseCliArgs(argv) {
690
694
  const positional = [];
@@ -989,12 +993,27 @@ async function readNewsletterBlocks(value) {
989
993
  }
990
994
  return parsed;
991
995
  }
996
+ async function readNewsletterMarkdown(options) {
997
+ const input = getString(options, "markdown");
998
+ if (input === void 0) return void 0;
999
+ const markdown = await readTextInput(input);
1000
+ if (getString(options, "blocks") !== void 0 || getString(options, "body-html") !== void 0) {
1001
+ throw new CliUsageError("Use only one of --blocks, --body-html, or --markdown.");
1002
+ }
1003
+ return markdown;
1004
+ }
992
1005
  function parseArchiveVisibility(options) {
993
1006
  const value = getString(options, "archive-visibility");
994
1007
  if (value === void 0) return void 0;
995
1008
  if (value === "private" || value === "public") return value;
996
1009
  throw new CliUsageError("--archive-visibility must be private or public.");
997
1010
  }
1011
+ function parsePublishTarget(options) {
1012
+ const value = getString(options, "publish-target");
1013
+ if (value === void 0) return void 0;
1014
+ if (value === "email_and_website" || value === "email" || value === "website") return value;
1015
+ throw new CliUsageError("--publish-target must be email_and_website, email or website.");
1016
+ }
998
1017
  function parseStylingMode(options) {
999
1018
  const value = getString(options, "styling-mode");
1000
1019
  if (value === void 0) return void 0;
@@ -1027,16 +1046,21 @@ async function newsletterCreateParams(options) {
1027
1046
  const blocks = blocksInput ? await readNewsletterBlocks(blocksInput) : void 0;
1028
1047
  const body_html = bodyHtmlInput ? await readTextInput(bodyHtmlInput) : void 0;
1029
1048
  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.");
1049
+ const content_markdown = await readNewsletterMarkdown(options);
1050
+ if (!blocks?.length && !body_html?.trim() && !body_text?.trim() && !content_markdown?.trim()) {
1051
+ throw new CliUsageError(
1052
+ "Provide at least one of --blocks, --body-html, --body-text, or --markdown."
1053
+ );
1032
1054
  }
1033
1055
  const send_window_hours = getNumber(options, "send-window-hours");
1034
1056
  const archive_visibility = parseArchiveVisibility(options);
1057
+ const publish_target = parsePublishTarget(options);
1035
1058
  const feed_entry_url = getString(options, "feed-entry-url");
1036
1059
  const styling_mode = parseStylingMode(options);
1037
1060
  const track_engagement = getBoolean(options, "track-engagement");
1038
1061
  const add_utm_tags = getBoolean(options, "add-utm-tags");
1039
1062
  const preview_text = getString(options, "preview-text");
1063
+ const subjectTest = subjectTestParams(options);
1040
1064
  return {
1041
1065
  audience_id,
1042
1066
  sender_identity_id,
@@ -1044,14 +1068,32 @@ async function newsletterCreateParams(options) {
1044
1068
  subject,
1045
1069
  ...preview_text !== void 0 ? { preview_text } : {},
1046
1070
  ...blocks ? { blocks } : {},
1071
+ ...content_markdown !== void 0 ? { content_markdown } : {},
1047
1072
  ...body_html !== void 0 ? { body_html } : {},
1048
1073
  ...body_text !== void 0 ? { body_text } : {},
1049
1074
  ...send_window_hours !== void 0 ? { send_window_hours } : {},
1050
1075
  ...archive_visibility !== void 0 ? { archive_visibility } : {},
1076
+ ...publish_target !== void 0 ? { publish_target } : {},
1051
1077
  ...feed_entry_url !== void 0 ? { feed_entry_url } : {},
1052
1078
  ...styling_mode !== void 0 ? { styling_mode } : {},
1053
1079
  ...track_engagement !== void 0 ? { track_engagement } : {},
1054
- ...add_utm_tags !== void 0 ? { add_utm_tags } : {}
1080
+ ...add_utm_tags !== void 0 ? { add_utm_tags } : {},
1081
+ ...subjectTest
1082
+ };
1083
+ }
1084
+ function subjectTestParams(options) {
1085
+ const subject_b = getString(options, "subject-b");
1086
+ const clear = getBoolean(options, "clear-subject-b") === true;
1087
+ if (subject_b !== void 0 && clear) {
1088
+ throw new CliUsageError("Use either --subject-b or --clear-subject-b.");
1089
+ }
1090
+ const subject_test_percent = getNumber(options, "subject-test-percent");
1091
+ const subject_test_hours = getNumber(options, "subject-test-hours");
1092
+ return {
1093
+ ...subject_b !== void 0 ? { subject_b } : {},
1094
+ ...clear ? { subject_b: null } : {},
1095
+ ...subject_test_percent !== void 0 ? { subject_test_percent } : {},
1096
+ ...subject_test_hours !== void 0 ? { subject_test_hours } : {}
1055
1097
  };
1056
1098
  }
1057
1099
  async function newsletterUpdateParams(options) {
@@ -1062,6 +1104,7 @@ async function newsletterUpdateParams(options) {
1062
1104
  const preview_text = getString(options, "preview-text");
1063
1105
  const send_window_hours = getNumber(options, "send-window-hours");
1064
1106
  const archive_visibility = parseArchiveVisibility(options);
1107
+ const publish_target = parsePublishTarget(options);
1065
1108
  const feed_entry_url = getString(options, "feed-entry-url");
1066
1109
  const styling_mode = parseStylingMode(options);
1067
1110
  const track_engagement = getBoolean(options, "track-engagement");
@@ -1072,6 +1115,7 @@ async function newsletterUpdateParams(options) {
1072
1115
  const blocks = blocksInput ? await readNewsletterBlocks(blocksInput) : void 0;
1073
1116
  const body_html = bodyHtmlInput ? await readTextInput(bodyHtmlInput) : void 0;
1074
1117
  const body_text = bodyTextInput ? await readTextInput(bodyTextInput) : void 0;
1118
+ const content_markdown = await readNewsletterMarkdown(options);
1075
1119
  const params = {
1076
1120
  ...audience_id ? { audience_id } : {},
1077
1121
  ...sender_identity_id ? { sender_identity_id } : {},
@@ -1079,14 +1123,17 @@ async function newsletterUpdateParams(options) {
1079
1123
  ...subject ? { subject } : {},
1080
1124
  ...preview_text !== void 0 ? { preview_text } : {},
1081
1125
  ...blocks ? { blocks } : {},
1126
+ ...content_markdown !== void 0 ? { content_markdown } : {},
1082
1127
  ...body_html !== void 0 ? { body_html } : {},
1083
1128
  ...body_text !== void 0 ? { body_text } : {},
1084
1129
  ...send_window_hours !== void 0 ? { send_window_hours } : {},
1085
1130
  ...archive_visibility !== void 0 ? { archive_visibility } : {},
1131
+ ...publish_target !== void 0 ? { publish_target } : {},
1086
1132
  ...feed_entry_url !== void 0 ? { feed_entry_url } : {},
1087
1133
  ...styling_mode !== void 0 ? { styling_mode } : {},
1088
1134
  ...track_engagement !== void 0 ? { track_engagement } : {},
1089
- ...add_utm_tags !== void 0 ? { add_utm_tags } : {}
1135
+ ...add_utm_tags !== void 0 ? { add_utm_tags } : {},
1136
+ ...subjectTestParams(options)
1090
1137
  };
1091
1138
  if (Object.keys(params).length === 0) {
1092
1139
  throw new CliUsageError("Provide at least one field to update.");
@@ -1179,11 +1226,13 @@ Usage:
1179
1226
  shipmail domains create <name>
1180
1227
  shipmail domains get <id>
1181
1228
  shipmail domains dns-records <id>
1229
+ shipmail domains health <id> [--json]
1182
1230
  shipmail domains verify <id>
1183
1231
  shipmail domains delete <id>
1184
1232
  shipmail mailboxes list [--domain-id dom] [--limit 25] [--cursor cur]
1185
1233
  shipmail mailboxes create --domain-id dom --address hello (--password value | --password-file path | --generate-password) [--display-name "Hello"]
1186
1234
  shipmail mailboxes get <id>
1235
+ shipmail mailboxes update <id> [--display-name "Ops"] [--agent-security-hold true|false]
1187
1236
  shipmail mailboxes suspend <id>
1188
1237
  shipmail mailboxes resume <id>
1189
1238
  shipmail mailboxes export <id>
@@ -1208,12 +1257,17 @@ Usage:
1208
1257
  shipmail members get <id>
1209
1258
  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
1259
  shipmail inbox message <mailbox-id> <message-id>
1260
+ shipmail inbox security-review <mailbox-id> <message-id> --reason "Reviewed" --expected-content-sha256 HASH
1211
1261
  shipmail inbox threads <mailbox-id> [--attention-state needs_reply] [--after ISO] [--before ISO] [--limit 25] [--cursor cur]
1212
1262
  shipmail inbox thread <mailbox-id> <thread-id>
1213
1263
  shipmail inbox thread-attention <mailbox-id> <thread-id> --done true --expected-version 3
1214
1264
  shipmail inbox draft <mailbox-id> <thread-id> --expected-version 3 (--text "Hello" | --html "<p>Hello</p>") [--reply-mode reply_all]
1215
1265
  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>
1266
+ shipmail inbox drafts <mailbox-id> <thread-id>
1267
+ shipmail inbox get-draft <mailbox-id> <thread-id> <draft-id>
1268
+ shipmail inbox update-draft <mailbox-id> <thread-id> <draft-id> (--text "Hello" | --html "<p>Hello</p>") (the new body replaces the old one)
1269
+ shipmail inbox delete-draft <mailbox-id> <thread-id> <draft-id>
1270
+ shipmail inbox send-draft <mailbox-id> <thread-id> <draft-id> [--expected-content-sha256 hash]
1217
1271
  shipmail inbox reply-message <mailbox-id> <message-id> --to user@example.com (--text "Hello" | --html "<p>Hello</p>") [--track-reply true|false]
1218
1272
  shipmail inbox reply-thread <mailbox-id> <thread-id> --to user@example.com (--text "Hello" | --html "<p>Hello</p>") [--track-reply true|false]
1219
1273
  shipmail inbox update <mailbox-id> <message-id> [--read true] [--starred false]
@@ -1234,20 +1288,22 @@ Usage:
1234
1288
  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
1289
  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
1290
  shipmail webhooks replay-delivery <webhook-id> <delivery-id> [--idempotency-key key]
1237
- shipmail suppressions list
1291
+ shipmail suppressions add <email> [--scope all|marketing]
1292
+ shipmail suppressions list [--scope all|marketing] [--limit 25] [--cursor cur]
1238
1293
  shipmail suppressions remove <email>
1239
1294
  shipmail newsletters create \\
1240
1295
  --audience-id aud_123 \\
1241
1296
  --sender-identity-id nwsid_456 \\
1242
1297
  --name "July changelog" \\
1243
1298
  --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]
1299
+ --blocks @blocks.json | --markdown @issue.md \\
1300
+ [--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]
1301
+ 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
1302
  shipmail newsletters preview <id>
1248
1303
  shipmail newsletters preflight <id>
1249
1304
  shipmail newsletters test-send <id> --recipient-email owner@example.com
1250
- shipmail newsletters schedule <id> --scheduled-at 2026-08-01T09:00:00.000Z
1305
+ shipmail newsletters schedule <id> --scheduled-at 2026-08-01T09:00:00.000Z [--limit 500]
1306
+ shipmail newsletters send-more <id> [--count 1000]
1251
1307
  shipmail newsletters cancel <id>
1252
1308
  shipmail newsletters resume <id>
1253
1309
  shipmail newsletter-assets list [--kind image|video] [--q text] [--limit 25] [--cursor cur]
@@ -1258,12 +1314,17 @@ Usage:
1258
1314
  shipmail audiences list [--limit 25] [--cursor cur]
1259
1315
  shipmail audiences create <name> --consent-source "Signup form" [--description "..."]
1260
1316
  shipmail audiences get <id>
1261
- shipmail audiences update <id> [--name "..."] [--description "..."]
1317
+ shipmail audiences update <id> [--name "..."] [--description "..."] [--cleanup true|false] [--cleanup-ignored-issues 2..100] [--cleanup-inactive-days 30..3650]
1262
1318
  shipmail audiences delete <id>
1263
1319
  shipmail audiences feed get <audience-id>
1264
1320
  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
1321
  shipmail audiences feed rotate <audience-id> [--idempotency-key key]
1266
1322
  shipmail audiences feed revoke <audience-id> [--idempotency-key key]
1323
+ shipmail audiences welcome get <audience-id>
1324
+ shipmail audiences welcome enable|disable <audience-id> [--idempotency-key key]
1325
+ 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]
1326
+ shipmail audiences welcome update-step <audience-id> <step-id> [--subject "..."] [--markdown @welcome.md | --blocks @blocks.json] [--wait-minutes 1440]
1327
+ shipmail audiences welcome remove-step <audience-id> <step-id>
1267
1328
  shipmail audiences subscribers add <audience-id> <email> [--name "Jane"] [--consent-source "..."]
1268
1329
  shipmail audiences subscribers list <audience-id> [--status subscribed] [--email jane@x.com] [--limit 25]
1269
1330
  shipmail audiences subscribers get <audience-id> <subscriber-id>
@@ -1283,11 +1344,27 @@ Usage:
1283
1344
  shipmail booking-pages get <id>
1284
1345
  shipmail booking-pages update <id> [--name "..."] [--conferencing-provider zoom|google_meet|none] [--active false]
1285
1346
  shipmail booking-pages delete <id>
1347
+ shipmail booking-links list [--status open|booked|expired|revoked] [--limit 25] [--cursor cur]
1348
+ 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]
1349
+ shipmail booking-links get <id>
1350
+ shipmail booking-links update <id> [--name "..."] [--range start/end|none] [--max-bookings n|unlimited] [--expires-at iso|none] [--source-page id|none]
1351
+ shipmail booking-links revoke <id>
1286
1352
 
1287
1353
  Booking page conferencing:
1288
1354
  --conferencing-provider <zoom|google_meet> Create a meeting for every booking.
1289
1355
  --conferencing-provider none Stop creating meeting links on update.
1290
1356
 
1357
+ One-off booking links:
1358
+ --range <start>/<end> A time the link offers, as two ISO instants. Repeat it, or
1359
+ separate ranges with commas. Without ranges, --source-page
1360
+ offers that booking page's weekly hours.
1361
+ --range none On update, drop the ranges and offer the source page's hours.
1362
+ --max-bookings <n|unlimited> The link stops after this many bookings. Defaults to 1.
1363
+ --expires-at <iso|none> The link stops taking bookings at this time.
1364
+ --source-page <id|none> The booking page to copy weekly hours from.
1365
+ Revoking a link stops it at once and cannot be undone. Other flags: --description,
1366
+ --buffer-minutes, --minimum-notice-minutes, --conferencing-provider, --mailbox.
1367
+
1291
1368
  Conversation IDs:
1292
1369
  <thread-id> accepts a conversation ID (thd_...) or the mail-server thread ID. Store the
1293
1370
  conversation_id every response carries. thread_id is deprecated and unchanged.
@@ -1299,15 +1376,30 @@ Member access output:
1299
1376
  Newsletter styling:
1300
1377
  --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
1378
 
1379
+ Newsletter publish target:
1380
+ --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.
1381
+
1302
1382
  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.
1383
+ --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.
1384
+ --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.
1385
+
1386
+ Newsletter subject test:
1387
+ --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.
1388
+ --clear-subject-b Remove the second subject.
1389
+
1390
+ Engaged first:
1391
+ --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.
1392
+
1393
+ List cleanup:
1394
+ --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.
1395
+ --cleanup-ignored-issues <n> Tracked newsletters ignored in a row before a subscriber is cold (2 to 100, default 6).
1396
+ --cleanup-inactive-days <n> Days without a sign of life before a subscriber is cold (30 to 3650, default 180).
1305
1397
 
1306
1398
  Newsletter block prose:
1307
1399
  Paragraph, quote, callout, list-item, and column bodies accept bare text or sanitized inline HTML. Use <p> and <br> for line breaks.
1308
1400
 
1309
1401
  Newsletter feeds:
1310
- --feed-entry-url <url> Link this issue's Atom feed title to an http(s) URL instead of its Shipmail archive.
1402
+ --feed-entry-url <url> Link this newsletter's Atom feed title to an http(s) URL instead of its Shipmail archive.
1311
1403
 
1312
1404
  Sign-in:
1313
1405
  Run shipmail login once to sign in through the browser instead of using an API key.
@@ -1557,6 +1649,44 @@ async function replyToMessageParams(options) {
1557
1649
  } : {}
1558
1650
  };
1559
1651
  }
1652
+ var DOMAIN_HEALTH_PROVIDER_LABELS = {
1653
+ google: "Gmail",
1654
+ microsoft: "Outlook",
1655
+ yahoo: "Yahoo",
1656
+ apple: "iCloud",
1657
+ other: "Other"
1658
+ };
1659
+ function formatRecipients(count) {
1660
+ return `${count} ${count === 1 ? "recipient" : "recipients"}`;
1661
+ }
1662
+ function formatDeliveryRate(rate) {
1663
+ return rate === null ? "no results yet" : `${rate}% delivered`;
1664
+ }
1665
+ function writeDomainHealth(out, health) {
1666
+ const { dns, blocklist, delivery } = health;
1667
+ const checks = [
1668
+ ["MX", dns.mx],
1669
+ ["SPF", dns.spf],
1670
+ ["DKIM", dns.dkim],
1671
+ ["DMARC", dns.dmarc]
1672
+ ];
1673
+ const lines = [
1674
+ `${health.domain} (${health.domain_id})`,
1675
+ "",
1676
+ `DNS (checked ${dns.last_checked_at ?? "never"})`,
1677
+ ...checks.map(([name, ok]) => ` ${name.padEnd(6)}${ok ? "pass" : "fail"}`),
1678
+ "",
1679
+ `Spamhaus DBL: ${blocklist.listed ? `listed since ${blocklist.listed_since ?? "unknown"}` : "not listed"}`,
1680
+ "",
1681
+ `Delivery, last 30 days: ${formatDeliveryRate(delivery.delivery_rate)}`,
1682
+ ` ${formatRecipients(delivery.recipients)}: ${delivery.delivered} delivered, ${delivery.bounced} bounced, ${delivery.deferred} deferred, ${delivery.complained} complained`,
1683
+ ...delivery.providers.map(
1684
+ (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`
1685
+ )
1686
+ ];
1687
+ out.write(`${lines.join("\n")}
1688
+ `);
1689
+ }
1560
1690
  async function runDomains(client, parsed, out) {
1561
1691
  const subcommand = parsed.positional[1];
1562
1692
  if (subcommand === "list") {
@@ -1594,6 +1724,14 @@ async function runDomains(client, parsed, out) {
1594
1724
  );
1595
1725
  return;
1596
1726
  }
1727
+ if (subcommand === "health") {
1728
+ const health = await client.domains.getHealth(
1729
+ requirePositional(parsed.positional, 2, "domain id")
1730
+ );
1731
+ if (getBoolean(parsed.options, "json") === true) writeJson(out, health);
1732
+ else writeDomainHealth(out, health);
1733
+ return;
1734
+ }
1597
1735
  if (subcommand === "verify") {
1598
1736
  writeJson(
1599
1737
  out,
@@ -1658,6 +1796,21 @@ async function runMailboxes(client, parsed, out) {
1658
1796
  );
1659
1797
  return;
1660
1798
  }
1799
+ if (subcommand === "update") {
1800
+ const displayName = getString(parsed.options, "display-name");
1801
+ const agentSecurityHold = getBoolean(parsed.options, "agent-security-hold");
1802
+ if (displayName === void 0 && agentSecurityHold === void 0) {
1803
+ throw new CliUsageError("Provide --display-name or --agent-security-hold.");
1804
+ }
1805
+ writeJson(
1806
+ out,
1807
+ await client.mailboxes.update(requirePositional(parsed.positional, 2, "mailbox id"), {
1808
+ ...displayName !== void 0 ? { display_name: displayName } : {},
1809
+ ...agentSecurityHold !== void 0 ? { agent_security_hold: agentSecurityHold } : {}
1810
+ })
1811
+ );
1812
+ return;
1813
+ }
1661
1814
  if (subcommand === "suspend") {
1662
1815
  writeJson(
1663
1816
  out,
@@ -1925,6 +2078,21 @@ async function runInbox(client, parsed, out) {
1925
2078
  );
1926
2079
  return;
1927
2080
  }
2081
+ if (subcommand === "security-review") {
2082
+ writeJson(
2083
+ out,
2084
+ await client.mailboxes.releaseInboxMessageSecurity(
2085
+ requirePositional(parsed.positional, 2, "mailbox id"),
2086
+ requirePositional(parsed.positional, 3, "message id"),
2087
+ {
2088
+ decision: "release",
2089
+ reason: getRequiredString(parsed.options, "reason"),
2090
+ expected_content_sha256: getRequiredString(parsed.options, "expected-content-sha256")
2091
+ }
2092
+ )
2093
+ );
2094
+ return;
2095
+ }
1928
2096
  if (subcommand === "threads") {
1929
2097
  const attentionState = getString(parsed.options, "attention-state");
1930
2098
  const sortBy = getString(parsed.options, "sort-by");
@@ -2019,13 +2187,64 @@ async function runInbox(client, parsed, out) {
2019
2187
  );
2020
2188
  return;
2021
2189
  }
2190
+ if (subcommand === "drafts") {
2191
+ writeJson(
2192
+ out,
2193
+ await client.mailboxes.listInboxReplyDrafts(
2194
+ requirePositional(parsed.positional, 2, "mailbox id"),
2195
+ requirePositional(parsed.positional, 3, "thread id")
2196
+ )
2197
+ );
2198
+ return;
2199
+ }
2200
+ if (subcommand === "get-draft") {
2201
+ writeJson(
2202
+ out,
2203
+ await client.mailboxes.getInboxReplyDraft(
2204
+ requirePositional(parsed.positional, 2, "mailbox id"),
2205
+ requirePositional(parsed.positional, 3, "thread id"),
2206
+ requirePositional(parsed.positional, 4, "draft id")
2207
+ )
2208
+ );
2209
+ return;
2210
+ }
2211
+ if (subcommand === "update-draft") {
2212
+ const text = await readTextInput(getString(parsed.options, "text"));
2213
+ const html = await readTextInput(getString(parsed.options, "html"));
2214
+ if (!text && !html) throw new CliUsageError("Provide --text or --html.");
2215
+ writeJson(
2216
+ out,
2217
+ await client.mailboxes.updateInboxReplyDraft(
2218
+ requirePositional(parsed.positional, 2, "mailbox id"),
2219
+ requirePositional(parsed.positional, 3, "thread id"),
2220
+ requirePositional(parsed.positional, 4, "draft id"),
2221
+ {
2222
+ ...text ? { text } : {},
2223
+ ...html ? { html } : {}
2224
+ }
2225
+ )
2226
+ );
2227
+ return;
2228
+ }
2229
+ if (subcommand === "delete-draft") {
2230
+ const draftId = requirePositional(parsed.positional, 4, "draft id");
2231
+ await client.mailboxes.deleteInboxReplyDraft(
2232
+ requirePositional(parsed.positional, 2, "mailbox id"),
2233
+ requirePositional(parsed.positional, 3, "thread id"),
2234
+ draftId
2235
+ );
2236
+ writeJson(out, { ok: true, id: draftId });
2237
+ return;
2238
+ }
2022
2239
  if (subcommand === "send-draft") {
2240
+ const expectedContentSha256 = getString(parsed.options, "expected-content-sha256");
2023
2241
  writeJson(
2024
2242
  out,
2025
2243
  await client.mailboxes.sendInboxReplyDraft(
2026
2244
  requirePositional(parsed.positional, 2, "mailbox id"),
2027
2245
  requirePositional(parsed.positional, 3, "thread id"),
2028
- requirePositional(parsed.positional, 4, "draft id")
2246
+ requirePositional(parsed.positional, 4, "draft id"),
2247
+ expectedContentSha256 ? { expectedContentSha256 } : void 0
2029
2248
  )
2030
2249
  );
2031
2250
  return;
@@ -2320,14 +2539,27 @@ async function runMembers(client, parsed, out) {
2320
2539
  }
2321
2540
  throw new CliUsageError("Unknown members command.");
2322
2541
  }
2542
+ var SUPPRESSION_SCOPES = ["all", "marketing"];
2323
2543
  async function runSuppressions(client, parsed, out) {
2324
2544
  const subcommand = parsed.positional[1];
2545
+ if (subcommand === "add") {
2546
+ const scope = getEnum(parsed.options, "scope", SUPPRESSION_SCOPES);
2547
+ writeJson(
2548
+ out,
2549
+ await client.suppressions.create({
2550
+ email: requirePositional(parsed.positional, 2, "email"),
2551
+ ...scope ? { scope } : {}
2552
+ })
2553
+ );
2554
+ return;
2555
+ }
2325
2556
  if (subcommand === "list") {
2326
2557
  writeJson(
2327
2558
  out,
2328
2559
  await client.suppressions.list({
2329
2560
  cursor: getString(parsed.options, "cursor"),
2330
- limit: getNumber(parsed.options, "limit")
2561
+ limit: getNumber(parsed.options, "limit"),
2562
+ scope: getEnum(parsed.options, "scope", SUPPRESSION_SCOPES)
2331
2563
  })
2332
2564
  );
2333
2565
  return;
@@ -2347,7 +2579,8 @@ var SUBSCRIBER_STATUS_VALUES = [
2347
2579
  "bounced",
2348
2580
  "complained",
2349
2581
  "suppressed",
2350
- "transactional_only"
2582
+ "transactional_only",
2583
+ "dormant"
2351
2584
  ];
2352
2585
  function subscriberListParams(options) {
2353
2586
  const email = getString(options, "email");
@@ -2504,6 +2737,89 @@ async function runAudienceFeed(client, parsed, out) {
2504
2737
  }
2505
2738
  throw new CliUsageError("Unknown audiences feed command.");
2506
2739
  }
2740
+ async function welcomeStepParams(options) {
2741
+ const subject = getString(options, "subject");
2742
+ const previewText = getString(options, "preview-text");
2743
+ const waitMinutes = getNumber(options, "wait-minutes");
2744
+ const senderIdentityId = getString(options, "sender-identity-id");
2745
+ const stylingMode = parseStylingMode(options);
2746
+ const trackEngagement = getBoolean(options, "track-engagement");
2747
+ const addUtmTags = getBoolean(options, "add-utm-tags");
2748
+ const blocksInput = getString(options, "blocks");
2749
+ const blocks = blocksInput ? await readNewsletterBlocks(blocksInput) : void 0;
2750
+ const contentMarkdown = await readNewsletterMarkdown(options);
2751
+ if (waitMinutes !== void 0 && (!Number.isInteger(waitMinutes) || waitMinutes < 0)) {
2752
+ throw new CliUsageError("--wait-minutes must be a whole number of minutes, 0 or more.");
2753
+ }
2754
+ return {
2755
+ ...subject !== void 0 ? { subject } : {},
2756
+ ...previewText !== void 0 ? { preview_text: previewText } : {},
2757
+ ...blocks ? { blocks } : {},
2758
+ ...contentMarkdown !== void 0 ? { content_markdown: contentMarkdown } : {},
2759
+ ...waitMinutes !== void 0 ? { wait_minutes: waitMinutes } : {},
2760
+ ...senderIdentityId !== void 0 ? { sender_identity_id: senderIdentityId } : {},
2761
+ ...stylingMode !== void 0 ? { styling_mode: stylingMode } : {},
2762
+ ...trackEngagement !== void 0 ? { track_engagement: trackEngagement } : {},
2763
+ ...addUtmTags !== void 0 ? { add_utm_tags: addUtmTags } : {}
2764
+ };
2765
+ }
2766
+ async function runAudienceWelcome(client, parsed, out) {
2767
+ const action = parsed.positional[2];
2768
+ const audienceId = requirePositional(parsed.positional, 3, "audience id");
2769
+ if (action === "get") {
2770
+ writeJson(out, await client.audiences.welcomeSequence.get(audienceId));
2771
+ return;
2772
+ }
2773
+ const idempotencyKey = getString(parsed.options, "idempotency-key");
2774
+ const methodOptions = idempotencyKey ? { idempotencyKey } : void 0;
2775
+ if (action === "enable" || action === "disable") {
2776
+ writeJson(
2777
+ out,
2778
+ await client.audiences.welcomeSequence.update(
2779
+ audienceId,
2780
+ { enabled: action === "enable" },
2781
+ methodOptions
2782
+ )
2783
+ );
2784
+ return;
2785
+ }
2786
+ if (action === "add-step") {
2787
+ const params = await welcomeStepParams(parsed.options);
2788
+ if (!params.subject) throw new CliUsageError("--subject is required.");
2789
+ if (!params.blocks?.length && !params.content_markdown?.trim()) {
2790
+ throw new CliUsageError("Provide --blocks or --markdown.");
2791
+ }
2792
+ writeJson(
2793
+ out,
2794
+ await client.audiences.welcomeSequence.createStep(
2795
+ audienceId,
2796
+ { ...params, subject: params.subject },
2797
+ methodOptions
2798
+ )
2799
+ );
2800
+ return;
2801
+ }
2802
+ const stepId = requirePositional(parsed.positional, 4, "step id");
2803
+ if (action === "update-step") {
2804
+ const params = await welcomeStepParams(parsed.options);
2805
+ if (Object.keys(params).length === 0) {
2806
+ throw new CliUsageError("Provide at least one field to update.");
2807
+ }
2808
+ writeJson(
2809
+ out,
2810
+ await client.audiences.welcomeSequence.updateStep(audienceId, stepId, params, methodOptions)
2811
+ );
2812
+ return;
2813
+ }
2814
+ if (action === "remove-step") {
2815
+ writeJson(
2816
+ out,
2817
+ await client.audiences.welcomeSequence.deleteStep(audienceId, stepId, methodOptions)
2818
+ );
2819
+ return;
2820
+ }
2821
+ throw new CliUsageError("Unknown audiences welcome command.");
2822
+ }
2507
2823
  async function runAudiences(client, parsed, out) {
2508
2824
  const subcommand = parsed.positional[1];
2509
2825
  if (subcommand === "subscribers") {
@@ -2514,6 +2830,10 @@ async function runAudiences(client, parsed, out) {
2514
2830
  await runAudienceFeed(client, parsed, out);
2515
2831
  return;
2516
2832
  }
2833
+ if (subcommand === "welcome") {
2834
+ await runAudienceWelcome(client, parsed, out);
2835
+ return;
2836
+ }
2517
2837
  if (subcommand === "list") {
2518
2838
  writeJson(
2519
2839
  out,
@@ -2547,11 +2867,17 @@ async function runAudiences(client, parsed, out) {
2547
2867
  const id = requirePositional(parsed.positional, 2, "audience id");
2548
2868
  const name = getString(parsed.options, "name");
2549
2869
  const description = getString(parsed.options, "description");
2870
+ const cleanup_enabled = getBoolean(parsed.options, "cleanup");
2871
+ const cleanup_ignored_issues = getNumber(parsed.options, "cleanup-ignored-issues");
2872
+ const cleanup_inactive_days = getNumber(parsed.options, "cleanup-inactive-days");
2550
2873
  writeJson(
2551
2874
  out,
2552
2875
  await client.audiences.update(id, {
2553
2876
  ...name ? { name } : {},
2554
- ...description ? { description } : {}
2877
+ ...description ? { description } : {},
2878
+ ...cleanup_enabled !== void 0 ? { cleanup_enabled } : {},
2879
+ ...cleanup_ignored_issues !== void 0 ? { cleanup_ignored_issues } : {},
2880
+ ...cleanup_inactive_days !== void 0 ? { cleanup_inactive_days } : {}
2555
2881
  })
2556
2882
  );
2557
2883
  return;
@@ -2630,14 +2956,27 @@ async function runNewsletters(client, parsed, out) {
2630
2956
  return;
2631
2957
  }
2632
2958
  if (subcommand === "schedule") {
2959
+ const limit = getNumber(parsed.options, "limit");
2633
2960
  writeJson(
2634
2961
  out,
2635
2962
  await client.newsletters.schedule(requirePositional(parsed.positional, 2, "newsletter id"), {
2636
- scheduled_at: getRequiredString(parsed.options, "scheduled-at")
2963
+ scheduled_at: getRequiredString(parsed.options, "scheduled-at"),
2964
+ ...limit !== void 0 ? { limit } : {}
2637
2965
  })
2638
2966
  );
2639
2967
  return;
2640
2968
  }
2969
+ if (subcommand === "send-more") {
2970
+ const count = getNumber(parsed.options, "count");
2971
+ writeJson(
2972
+ out,
2973
+ await client.newsletters.sendToMore(
2974
+ requirePositional(parsed.positional, 2, "newsletter id"),
2975
+ count !== void 0 ? { count } : {}
2976
+ )
2977
+ );
2978
+ return;
2979
+ }
2641
2980
  if (subcommand === "cancel") {
2642
2981
  writeJson(
2643
2982
  out,
@@ -2957,6 +3296,93 @@ async function runCalendar(client, parsed, out) {
2957
3296
  }
2958
3297
  throw new CliUsageError("Unknown calendar command.");
2959
3298
  }
3299
+ function parseBookingLinkRanges(options) {
3300
+ const values = getStringList(options, "range");
3301
+ if (values.length === 0) return void 0;
3302
+ if (values.length === 1 && values[0] === "none") return null;
3303
+ return values.map((value) => {
3304
+ const [start, end, extra] = value.split("/");
3305
+ if (!start || !end || extra !== void 0) {
3306
+ throw new CliUsageError("--range must be <start>/<end>, two ISO 8601 instants.");
3307
+ }
3308
+ return { start, end };
3309
+ });
3310
+ }
3311
+ function getMaxBookings(options) {
3312
+ if (getString(options, "max-bookings") === "unlimited") return null;
3313
+ return getNumber(options, "max-bookings");
3314
+ }
3315
+ function getNullableString(options, key) {
3316
+ const value = getString(options, key);
3317
+ return value === "none" ? null : value;
3318
+ }
3319
+ async function runBookingLinks(client, parsed, out) {
3320
+ const subcommand = parsed.positional[1];
3321
+ const options = parsed.options;
3322
+ if (subcommand === "list") {
3323
+ writeJson(
3324
+ out,
3325
+ await client.bookingLinks.list({
3326
+ cursor: getString(options, "cursor"),
3327
+ limit: getNumber(options, "limit"),
3328
+ status: getEnum(options, "status", BOOKING_LINK_STATUSES)
3329
+ })
3330
+ );
3331
+ return;
3332
+ }
3333
+ if (subcommand === "create") {
3334
+ const ranges = parseBookingLinkRanges(options);
3335
+ if (ranges === null) throw new CliUsageError("--range none only applies to update.");
3336
+ const params = {
3337
+ name: getRequiredString(options, "name"),
3338
+ mailbox: getRequiredString(options, "mailbox"),
3339
+ duration_minutes: requireNumber(options, "duration-minutes"),
3340
+ timezone: getRequiredString(options, "timezone"),
3341
+ offered_ranges: ranges,
3342
+ source_page_id: getString(options, "source-page"),
3343
+ description: getString(options, "description"),
3344
+ buffer_minutes: getNumber(options, "buffer-minutes"),
3345
+ minimum_notice_minutes: getNumber(options, "minimum-notice-minutes"),
3346
+ conferencing_provider: getConferencingProvider(options),
3347
+ max_bookings: getMaxBookings(options),
3348
+ expires_at: getString(options, "expires-at")
3349
+ };
3350
+ writeJson(out, await client.bookingLinks.create(params));
3351
+ return;
3352
+ }
3353
+ if (subcommand === "get") {
3354
+ writeJson(
3355
+ out,
3356
+ await client.bookingLinks.get(requirePositional(parsed.positional, 2, "booking link id"))
3357
+ );
3358
+ return;
3359
+ }
3360
+ if (subcommand === "update") {
3361
+ const id = requirePositional(parsed.positional, 2, "booking link id");
3362
+ const params = {
3363
+ name: getString(options, "name"),
3364
+ mailbox: getString(options, "mailbox"),
3365
+ duration_minutes: getNumber(options, "duration-minutes"),
3366
+ timezone: getString(options, "timezone"),
3367
+ offered_ranges: parseBookingLinkRanges(options),
3368
+ source_page_id: getNullableString(options, "source-page"),
3369
+ description: getString(options, "description"),
3370
+ buffer_minutes: getNumber(options, "buffer-minutes"),
3371
+ minimum_notice_minutes: getNumber(options, "minimum-notice-minutes"),
3372
+ conferencing_provider: getConferencingProvider(options),
3373
+ max_bookings: getMaxBookings(options),
3374
+ expires_at: getNullableString(options, "expires-at")
3375
+ };
3376
+ writeJson(out, await client.bookingLinks.update(id, params));
3377
+ return;
3378
+ }
3379
+ if (subcommand === "revoke") {
3380
+ const id = requirePositional(parsed.positional, 2, "booking link id");
3381
+ writeJson(out, await client.bookingLinks.revoke(id));
3382
+ return;
3383
+ }
3384
+ throw new CliUsageError("Unknown booking-links command.");
3385
+ }
2960
3386
  async function runBookingPages(client, parsed, out) {
2961
3387
  const subcommand = parsed.positional[1];
2962
3388
  const options = parsed.options;
@@ -3097,6 +3523,11 @@ ${JSON.stringify(err.body, null, 2)}
3097
3523
  if (shipmailError.actionUrl) {
3098
3524
  const reason = shipmailError.reason ? ` (${shipmailError.reason})` : "";
3099
3525
  errOut.write(`Ask the organization owner to open${reason}: ${shipmailError.actionUrl}
3526
+ `);
3527
+ }
3528
+ if (shipmailError.limit) {
3529
+ const { resource, used, limit, scope } = shipmailError.limit;
3530
+ errOut.write(`Limit: ${resource} ${used} of ${limit} (${scope})
3100
3531
  `);
3101
3532
  }
3102
3533
  return;
@@ -3181,6 +3612,7 @@ async function runCli(argv, options = {}) {
3181
3612
  else if (command === "audiences") await runAudiences(client, parsed, out);
3182
3613
  else if (command === "calendar") await runCalendar(client, parsed, out);
3183
3614
  else if (command === "booking-pages") await runBookingPages(client, parsed, out);
3615
+ else if (command === "booking-links") await runBookingLinks(client, parsed, out);
3184
3616
  else throw new CliUsageError(`Unknown command: ${command}.`);
3185
3617
  return 0;
3186
3618
  } 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.6",
4
4
  "description": "Official CLI for the Shipmail API",
5
5
  "type": "module",
6
6
  "bin": {
@@ -31,9 +31,12 @@
31
31
  "homepage": "https://shipmail.to/docs/cli",
32
32
  "repository": {
33
33
  "type": "git",
34
- "url": "git+https://github.com/shipmail-to/Shipmail.git",
34
+ "url": "git+https://github.com/shipmail-to/shipmail-node.git",
35
35
  "directory": "packages/shipmail-cli"
36
36
  },
37
+ "bugs": {
38
+ "url": "https://github.com/shipmail-to/shipmail-node/issues"
39
+ },
37
40
  "engines": {
38
41
  "node": ">=18"
39
42
  },
@@ -41,7 +44,7 @@
41
44
  "access": "public"
42
45
  },
43
46
  "dependencies": {
44
- "shipmail": "0.6.5",
47
+ "shipmail": "0.6.7",
45
48
  "zod": "^4.6.5"
46
49
  },
47
50
  "devDependencies": {