@zanfia/cli 0.3.52 → 0.3.63

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 (2) hide show
  1. package/dist/index.js +576 -21
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -8343,6 +8343,9 @@ function createMockClient() {
8343
8343
  async cancelEmailCampaign() {
8344
8344
  throw new ApiError(501, "email campaigns are not available in mock mode");
8345
8345
  },
8346
+ async resendEmailCampaign() {
8347
+ throw new ApiError(501, "email campaigns are not available in mock mode");
8348
+ },
8346
8349
  async listCampaignRecipients() {
8347
8350
  throw new ApiError(501, "email campaigns are not available in mock mode");
8348
8351
  },
@@ -8538,6 +8541,15 @@ function createMockClient() {
8538
8541
  async copyNewsletterIssues() {
8539
8542
  throw new ApiError(501, "newsletter content is not available in mock mode");
8540
8543
  },
8544
+ async sendNewsletterIssue() {
8545
+ throw new ApiError(501, "newsletter content is not available in mock mode");
8546
+ },
8547
+ async cancelNewsletterIssueSend() {
8548
+ throw new ApiError(501, "newsletter content is not available in mock mode");
8549
+ },
8550
+ async getNewsletterIssueSendReport() {
8551
+ throw new ApiError(501, "newsletter content is not available in mock mode");
8552
+ },
8541
8553
  async getKbContent() {
8542
8554
  throw new ApiError(501, "knowledge-base content is not available in mock mode");
8543
8555
  },
@@ -8577,6 +8589,21 @@ function createMockClient() {
8577
8589
  async getMediaTranscript() {
8578
8590
  throw new ApiError(501, "media transcripts are not available in mock mode");
8579
8591
  },
8592
+ async listMediaCaptions() {
8593
+ throw new ApiError(501, "video captions are not available in mock mode");
8594
+ },
8595
+ async uploadMediaCaption() {
8596
+ throw new ApiError(501, "video captions are not available in mock mode");
8597
+ },
8598
+ async getMediaCaption() {
8599
+ throw new ApiError(501, "video captions are not available in mock mode");
8600
+ },
8601
+ async deleteMediaCaption() {
8602
+ throw new ApiError(501, "video captions are not available in mock mode");
8603
+ },
8604
+ async regenerateMediaCaption() {
8605
+ throw new ApiError(501, "video captions are not available in mock mode");
8606
+ },
8580
8607
  async generateAIImage() {
8581
8608
  throw new ApiError(501, "AI image generation is not available in mock mode");
8582
8609
  },
@@ -8706,6 +8733,12 @@ function createMockClient() {
8706
8733
  async listSignupFormSubmissions() {
8707
8734
  return { submissions: [] };
8708
8735
  },
8736
+ async listLiveRooms() {
8737
+ return { rooms: [] };
8738
+ },
8739
+ async getLiveRoomAttendance() {
8740
+ throw new ApiError(501, "live rooms are not available in mock mode");
8741
+ },
8709
8742
  async call(method, pathTemplate) {
8710
8743
  throw new ApiError(501, `${method} ${pathTemplate} is not available in mock mode`);
8711
8744
  },
@@ -9502,6 +9535,7 @@ function createHttpClient(config) {
9502
9535
  scheduledAt !== void 0 ? { scheduledAt } : {}
9503
9536
  ),
9504
9537
  cancelEmailCampaign: (id) => request("POST", `/campaigns/${encodeURIComponent(id)}/cancel`),
9538
+ resendEmailCampaign: (id, target) => request("POST", `/campaigns/${encodeURIComponent(id)}/resend`, target ? { target } : {}),
9505
9539
  listCampaignRecipients: (id, options) => {
9506
9540
  const params = new URLSearchParams();
9507
9541
  if (options?.status) params.set("status", options.status);
@@ -9661,6 +9695,19 @@ function createHttpClient(config) {
9661
9695
  `/products/${encodeURIComponent(productId)}/newsletter/issues/${encodeURIComponent(issueId)}/draft`
9662
9696
  ),
9663
9697
  copyNewsletterIssues: (productId, input) => request("POST", `/products/${encodeURIComponent(productId)}/newsletter/issues/copy`, input),
9698
+ sendNewsletterIssue: (productId, issueId, input) => request(
9699
+ "POST",
9700
+ `/products/${encodeURIComponent(productId)}/newsletter/issues/${encodeURIComponent(issueId)}/send`,
9701
+ input
9702
+ ),
9703
+ cancelNewsletterIssueSend: (productId, issueId) => request(
9704
+ "POST",
9705
+ `/products/${encodeURIComponent(productId)}/newsletter/issues/${encodeURIComponent(issueId)}/send/cancel`
9706
+ ),
9707
+ getNewsletterIssueSendReport: (productId, issueId) => request(
9708
+ "GET",
9709
+ `/products/${encodeURIComponent(productId)}/newsletter/issues/${encodeURIComponent(issueId)}/send/report`
9710
+ ),
9664
9711
  // Knowledge-base product content (addressed by product id).
9665
9712
  getKbContent: (productId) => request("GET", `/products/${encodeURIComponent(productId)}/kb`),
9666
9713
  getKbArticle: (productId, articleId) => request(
@@ -9701,6 +9748,17 @@ function createHttpClient(config) {
9701
9748
  "GET",
9702
9749
  `/media/${encodeURIComponent(mediaId)}/transcript${includeWords ? "?includeWords=true" : ""}`
9703
9750
  ),
9751
+ listMediaCaptions: (mediaId) => request("GET", `/media/${encodeURIComponent(mediaId)}/captions`),
9752
+ uploadMediaCaption: (mediaId, input) => request("POST", `/media/${encodeURIComponent(mediaId)}/captions`, input),
9753
+ getMediaCaption: (mediaId, srclang) => request(
9754
+ "GET",
9755
+ `/media/${encodeURIComponent(mediaId)}/captions/${encodeURIComponent(srclang)}`
9756
+ ),
9757
+ deleteMediaCaption: (mediaId, srclang) => request(
9758
+ "DELETE",
9759
+ `/media/${encodeURIComponent(mediaId)}/captions/${encodeURIComponent(srclang)}`
9760
+ ),
9761
+ regenerateMediaCaption: (mediaId) => request("POST", `/media/${encodeURIComponent(mediaId)}/captions/regenerate`, {}),
9704
9762
  // AI image generation + on-demand transcription (both spend credits / minutes).
9705
9763
  generateAIImage: (input) => request("POST", "/media/ai-images", input),
9706
9764
  transcribeMedia: (mediaId, input) => request("POST", `/media/${encodeURIComponent(mediaId)}/transcribe`, input),
@@ -9840,6 +9898,12 @@ function createHttpClient(config) {
9840
9898
  "GET",
9841
9899
  `/forms/${encodeURIComponent(id)}/submissions${limit !== void 0 ? `?limit=${limit}` : ""}`
9842
9900
  ),
9901
+ // Live rooms live on the Workspace API (api-v1), read only.
9902
+ listLiveRooms: () => request("GET", "/live-rooms"),
9903
+ getLiveRoomAttendance: (roomId, session) => request(
9904
+ "GET",
9905
+ `/live-rooms/${encodeURIComponent(roomId)}/attendance${session !== void 0 ? `?session=${encodeURIComponent(session)}` : ""}`
9906
+ ),
9843
9907
  // Descriptor-generated commands (certificates, …) — Workspace API (api-v1).
9844
9908
  call: (method, pathTemplate, params, query, body) => request(method, `${fillPathTemplate(pathTemplate, params)}${toQuery(query)}`, body),
9845
9909
  // Referral / affiliate programs live on the Workspace API (api-v1).
@@ -9946,6 +10010,14 @@ function parseNumberOption(flag, format) {
9946
10010
  return n;
9947
10011
  };
9948
10012
  }
10013
+ var WORKFLOW_KINDS = ["transactional", "marketing"];
10014
+ function parseWorkflowKind(value) {
10015
+ const kind = value.trim().toLowerCase();
10016
+ if (!WORKFLOW_KINDS.includes(kind)) {
10017
+ throw new Error(`Invalid --kind: ${value} (expected ${WORKFLOW_KINDS.join(" or ")})`);
10018
+ }
10019
+ return kind;
10020
+ }
9949
10021
  function parseCommaList(flag) {
9950
10022
  return (value) => {
9951
10023
  const items = [
@@ -10349,9 +10421,133 @@ function registerCertificatesCommands(program2) {
10349
10421
  });
10350
10422
  }
10351
10423
 
10424
+ // src/commands/_generated/blog.ts
10425
+ var listBlogPostsColumns = ["id", "slug", "title", "status", "publishedAt", "updatedAt"];
10426
+ function registerBlogCommands(program2) {
10427
+ const blog = program2.command("blog");
10428
+ blog.description(
10429
+ "The workspace blog (Your site \u2192 Blog): Markdown posts served at /blog on the store host; drafts are visible to the team only"
10430
+ );
10431
+ blog.command("list").summary("List blog posts").description(
10432
+ "Every post of the workspace blog (drafts and published), newest change first: id, slug (`/blog/{slug}` on the store host), title, excerpt, cover, tags, status and dates. No body - use get-blog-post for the Markdown. The id source for every other blog tool."
10433
+ ).action(async (_options, command) => {
10434
+ const flags = getGlobalFlags(command);
10435
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
10436
+ const params = {};
10437
+ const result = await client.call("GET", "/blog-posts", params, void 0, void 0);
10438
+ const rows = tableRowsOf(result, "posts");
10439
+ emit(flags, result ?? {}, () => printTable(rows, listBlogPostsColumns));
10440
+ });
10441
+ blog.command("create").summary("Create a blog post").description(
10442
+ "Creates a blog post as a DRAFT (or published with `publish: true` when it has a body). The body is Markdown. A slug the caller sets must be free (409) and not `feed`; a slug derived from the title takes the next free numeric suffix. Returns the full post. Requires the blog early access on the workspace (403 otherwise)."
10443
+ ).requiredOption("--title <value>", "Post title (max 200 characters).").option(
10444
+ "--slug <value>",
10445
+ "Derived from the title when omitted; a taken derived slug gets a numeric suffix."
10446
+ ).option("--content <value>", "The body as Markdown (headings, lists, links, images by URL).").option(
10447
+ "--excerpt <value>",
10448
+ "Short plain-text summary (list, RSS, meta description); derived from the body on publish when empty."
10449
+ ).option("--cover <value>", "Cover image URL (Media Library).").option("--tags <a,b,...>", "Lowercase labels, at most 10.", parseCommaList("--tags")).option("--seo <json|file>", "seo", parseJsonObjectFileOrInline("--seo")).option("--publish", "Publish right away (needs a non-empty body); default false = draft.").option("--no-publish", "Send `publish: false` explicitly.").action(async (options, command) => {
10450
+ const flags = getGlobalFlags(command);
10451
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
10452
+ const params = {};
10453
+ const payload = {
10454
+ title: options.title,
10455
+ slug: options.slug,
10456
+ content: options.content,
10457
+ excerpt: options.excerpt,
10458
+ coverImageUrl: options.cover,
10459
+ tags: options.tags,
10460
+ seo: options.seo,
10461
+ publish: options.publish
10462
+ };
10463
+ const result = await client.call("POST", "/blog-posts", params, void 0, payload);
10464
+ emit(flags, result ?? {}, () => printJson(result ?? {}));
10465
+ });
10466
+ blog.command("get").summary("Blog post detail").description(
10467
+ "One blog post with its Markdown body and SEO overrides (title, description, noindex). 404 when unknown."
10468
+ ).argument("<id>", "Blog post id (`post_\u2026`) from list-blog-posts.").action(async (id, _options, command) => {
10469
+ const flags = getGlobalFlags(command);
10470
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
10471
+ const params = {
10472
+ postId: id
10473
+ };
10474
+ const result = await client.call("GET", "/blog-posts/:postId", params, void 0, void 0);
10475
+ emit(flags, result ?? {}, () => printJson(result ?? {}));
10476
+ });
10477
+ blog.command("update").summary("Update a blog post").description(
10478
+ "Partial update of a blog post: title, slug (must be free, 409), Markdown body, excerpt, cover (null removes it), tags, SEO (whole section replaced). A published post keeps serving the new content at once. Returns the full post."
10479
+ ).argument("<id>", "Blog post id (`post_\u2026`) from list-blog-posts.").option("--title <value>", "title").option(
10480
+ "--slug <value>",
10481
+ "Public path segment (`/blog/{slug}`): lowercase letters, digits and hyphens."
10482
+ ).option("--content <value>", "The body as Markdown (headings, lists, links, images by URL).").option(
10483
+ "--excerpt <value>",
10484
+ "Short plain-text summary (list, RSS, meta description); derived from the body on publish when empty."
10485
+ ).option("--cover <value>", "Cover image URL; null removes the cover.").option("--tags <a,b,...>", "Lowercase labels, at most 10.", parseCommaList("--tags")).option(
10486
+ "--seo <json|file>",
10487
+ "Replaces the whole SEO section.",
10488
+ parseJsonObjectFileOrInline("--seo")
10489
+ ).action(async (id, options, command) => {
10490
+ const flags = getGlobalFlags(command);
10491
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
10492
+ const params = {
10493
+ postId: id
10494
+ };
10495
+ const payload = {
10496
+ title: options.title,
10497
+ slug: options.slug,
10498
+ content: options.content,
10499
+ excerpt: options.excerpt,
10500
+ coverImageUrl: options.cover,
10501
+ tags: options.tags,
10502
+ seo: options.seo
10503
+ };
10504
+ const result = await client.call("PATCH", "/blog-posts/:postId", params, void 0, payload);
10505
+ emit(flags, result ?? {}, () => printJson(result ?? {}));
10506
+ });
10507
+ blog.command("publish").summary("Publish or unpublish a blog post").description(
10508
+ "Publishes a post at `/blog/{slug}` (needs a non-empty body, 400 otherwise; the excerpt is derived from the body when empty) or takes it back to draft. The first publication date is the post's date and survives unpublish / republish. Reversible. Returns the full post."
10509
+ ).argument("<id>", "Blog post id (`post_\u2026`) from list-blog-posts.").option("--publish", "true = publish, false = back to draft.").action(async (id, options, command) => {
10510
+ const flags = getGlobalFlags(command);
10511
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
10512
+ const params = {
10513
+ postId: id
10514
+ };
10515
+ const payload = {
10516
+ publish: options.publish
10517
+ };
10518
+ const result = await client.call(
10519
+ "POST",
10520
+ "/blog-posts/:postId/publish",
10521
+ params,
10522
+ void 0,
10523
+ payload
10524
+ );
10525
+ emit(flags, result ?? {}, () => printJson(result ?? {}));
10526
+ });
10527
+ blog.command("delete").summary("Delete a blog post").description(
10528
+ "Deletes a blog post for good; a published one disappears from `/blog` at once. No undo - prefer unpublishing."
10529
+ ).argument("<id>", "Blog post id (`post_\u2026`) from list-blog-posts.").option("--confirm", "required \u2014 acknowledge that you want to deletes the post for good").action(async (id, options, command) => {
10530
+ const flags = getGlobalFlags(command);
10531
+ requireConfirm(options.confirm, "deletes the post for good");
10532
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
10533
+ const params = {
10534
+ postId: id
10535
+ };
10536
+ const result = await client.call(
10537
+ "DELETE",
10538
+ "/blog-posts/:postId",
10539
+ params,
10540
+ void 0,
10541
+ void 0
10542
+ );
10543
+ emit(flags, result ?? {}, () => printJson(result ?? {}));
10544
+ });
10545
+ }
10546
+
10352
10547
  // src/commands/_generated/index.ts
10353
10548
  function registerGeneratedCommands(program2) {
10354
10549
  registerCertificatesCommands(program2);
10550
+ registerBlogCommands(program2);
10355
10551
  }
10356
10552
 
10357
10553
  // src/commands/ads/index.ts
@@ -12140,13 +12336,15 @@ function resolveSender(options) {
12140
12336
  };
12141
12337
  }
12142
12338
  function resolveTheme(options) {
12143
- if (options.backgroundColor === void 0 && options.contentBackgroundColor === void 0 && options.linkColor === void 0) {
12339
+ if (options.backgroundColor === void 0 && options.contentBackgroundColor === void 0 && options.linkColor === void 0 && options.buttonColor === void 0 && options.buttonTextColor === void 0) {
12144
12340
  return void 0;
12145
12341
  }
12146
12342
  return {
12147
12343
  ...options.backgroundColor !== void 0 ? { backgroundColor: options.backgroundColor } : {},
12148
12344
  ...options.contentBackgroundColor !== void 0 ? { contentBackgroundColor: options.contentBackgroundColor } : {},
12149
- ...options.linkColor !== void 0 ? { linkColor: options.linkColor } : {}
12345
+ ...options.linkColor !== void 0 ? { linkColor: options.linkColor } : {},
12346
+ ...options.buttonColor !== void 0 ? { buttonColor: options.buttonColor } : {},
12347
+ ...options.buttonTextColor !== void 0 ? { buttonTextColor: options.buttonTextColor } : {}
12150
12348
  };
12151
12349
  }
12152
12350
  function resolveAudience(options) {
@@ -12259,7 +12457,10 @@ function registerCampaignsCommands(program2) {
12259
12457
  ).option("--template-id <id>", "look template the Markdown body renders into").option("--from-name <name>", "From display name").option("--reply-to <email>", "Reply-To address (requires --from-name)").option(
12260
12458
  "--from-email <email>",
12261
12459
  "custom From on the workspace's verified sending domain (requires --from-name); omit for the platform sender"
12262
- ).option("--segment <segmentId>", "send to a saved audience segment (re-resolved at send time)").option("--all-subscribed", "send to every subscribed client (the default audience)").option("--background-color <hex>", "theme: page background behind the content column").option("--content-background-color <hex>", "theme: content column (card) background").option("--link-color <hex>", "theme: link color inside the content column").action(
12460
+ ).option("--segment <segmentId>", "send to a saved audience segment (re-resolved at send time)").option("--all-subscribed", "send to every subscribed client (the default audience)").option("--background-color <hex>", "theme: page background behind the content column").option("--content-background-color <hex>", "theme: content column (card) background").option("--link-color <hex>", "theme: link color inside the content column").option(
12461
+ "--button-color <hex>",
12462
+ "theme: background of buttons without their own color (wins over the look default)"
12463
+ ).option("--button-text-color <hex>", "theme: text color of buttons without their own color").action(
12263
12464
  async (options, command) => {
12264
12465
  const flags = getGlobalFlags(command);
12265
12466
  const contentMarkdown = resolveContent(options);
@@ -12287,7 +12488,10 @@ function registerCampaignsCommands(program2) {
12287
12488
  });
12288
12489
  }
12289
12490
  );
12290
- campaigns.command("update").description("Update a DRAFT campaign (campaigns past draft are immutable except cancel)").argument("<id>", "campaign id").option("--name <name>", "new internal label").option("--subject <subject>", "new email subject line").option("--preheader <text>", "new preview text").option("--content <markdown>", "new campaign body as Markdown (replaces the whole body)").option("--content-file <path>", "read the new campaign body (Markdown) from a file").option("--html-file <path>", "read the new HTML document of a designed e-mail from a file").option("--template-id <id>", "look template for the body (`none` = the default shell)").option("--from-name <name>", "new From display name").option("--reply-to <email>", "new Reply-To address (requires --from-name)").option("--from-email <email>", "new custom From address (requires --from-name)").option("--segment <segmentId>", "switch the audience to a saved segment").option("--all-subscribed", "switch the audience to every subscribed client").option("--background-color <hex>", "theme: page background behind the content column").option("--content-background-color <hex>", "theme: content column (card) background").option("--link-color <hex>", "theme: link color inside the content column").action(
12491
+ campaigns.command("update").description("Update a DRAFT campaign (campaigns past draft are immutable except cancel)").argument("<id>", "campaign id").option("--name <name>", "new internal label").option("--subject <subject>", "new email subject line").option("--preheader <text>", "new preview text").option("--content <markdown>", "new campaign body as Markdown (replaces the whole body)").option("--content-file <path>", "read the new campaign body (Markdown) from a file").option("--html-file <path>", "read the new HTML document of a designed e-mail from a file").option("--template-id <id>", "look template for the body (`none` = the default shell)").option("--from-name <name>", "new From display name").option("--reply-to <email>", "new Reply-To address (requires --from-name)").option("--from-email <email>", "new custom From address (requires --from-name)").option("--segment <segmentId>", "switch the audience to a saved segment").option("--all-subscribed", "switch the audience to every subscribed client").option("--background-color <hex>", "theme: page background behind the content column").option("--content-background-color <hex>", "theme: content column (card) background").option("--link-color <hex>", "theme: link color inside the content column").option(
12492
+ "--button-color <hex>",
12493
+ "theme: background of buttons without their own color (wins over the look default)"
12494
+ ).option("--button-text-color <hex>", "theme: text color of buttons without their own color").action(
12291
12495
  async (id, options, command) => {
12292
12496
  const flags = getGlobalFlags(command);
12293
12497
  const input = {};
@@ -12374,6 +12578,28 @@ function registerCampaignsCommands(program2) {
12374
12578
  const { stats } = result.campaign;
12375
12579
  process.stdout.write(
12376
12580
  `Cancelled campaign ${result.campaign.id} \u2014 ${stats.sent} recipient${stats.sent === 1 ? " was" : "s were"} already sent to before the cancel.
12581
+ `
12582
+ );
12583
+ });
12584
+ });
12585
+ campaigns.command("resend").description(
12586
+ "Follow up a SENT campaign: builds a segment over its non-openers (or non-clickers) and creates a DRAFT copy aimed at it - nothing is sent; review and `campaigns send` the draft"
12587
+ ).argument("<id>", "campaign id (must be sent)").option("--target <target>", "not-opened (default) | not-clicked").action(async (id, options, command) => {
12588
+ const flags = getGlobalFlags(command);
12589
+ let target;
12590
+ if (options.target !== void 0) {
12591
+ if (options.target !== "not-opened" && options.target !== "not-clicked") {
12592
+ process.stderr.write("--target must be not-opened or not-clicked\n");
12593
+ process.exitCode = 1;
12594
+ return;
12595
+ }
12596
+ target = options.target;
12597
+ }
12598
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
12599
+ const result = await client.resendEmailCampaign(id, target);
12600
+ emit(flags, result, () => {
12601
+ process.stdout.write(
12602
+ `Draft ${result.campaign.id} "${result.campaign.name}" created for segment "${result.segment.name}" (${result.segment.id}${result.segmentReused ? ", reused" : ""}). Nothing was sent - review it, then \`campaigns send <id> --confirm\`.
12377
12603
  `
12378
12604
  );
12379
12605
  });
@@ -13836,6 +14062,7 @@ Next page: --cursor ${page.nextCursor}
13836
14062
  let created = 0;
13837
14063
  let existing = 0;
13838
14064
  let invalid = 0;
14065
+ let overLimit = 0;
13839
14066
  for (let offset = 0; offset < contacts.length; offset += IMPORT_CHUNK_SIZE) {
13840
14067
  const chunk2 = contacts.slice(offset, offset + IMPORT_CHUNK_SIZE);
13841
14068
  if (!flags.json && contacts.length > IMPORT_CHUNK_SIZE) {
@@ -13849,13 +14076,18 @@ Next page: --cursor ${page.nextCursor}
13849
14076
  created += page.created;
13850
14077
  existing += page.existing;
13851
14078
  invalid += page.invalid;
14079
+ overLimit += page.overLimit ?? 0;
13852
14080
  }
13853
- const summary = { results, created, existing, invalid };
14081
+ const summary = { results, created, existing, invalid, overLimit };
13854
14082
  emit(flags, summary, () => {
13855
14083
  process.stdout.write(
13856
- `Imported ${contacts.length} row${contacts.length === 1 ? "" : "s"} from ${csvFile}: ${created} created, ${existing} already existed, ${invalid} invalid.
13857
- `
14084
+ `Imported ${contacts.length} row${contacts.length === 1 ? "" : "s"} from ${csvFile}: ${created} created, ${existing} already existed, ${invalid} invalid` + (overLimit > 0 ? `, ${overLimit} over the plan's contacts limit` : "") + ".\n"
13858
14085
  );
14086
+ if (overLimit > 0) {
14087
+ process.stdout.write(
14088
+ "The plan's contacts limit is reached \u2014 upgrade the plan to import the remaining rows.\n"
14089
+ );
14090
+ }
13859
14091
  const problems = results.filter((r) => r.status !== "created" && r.status !== "exists");
13860
14092
  if (problems.length > 0) {
13861
14093
  process.stdout.write("\nRows that were not imported:\n");
@@ -14945,11 +15177,11 @@ Next page: --cursor ${result.nextCursor}
14945
15177
  member.command("mute").description("Mute (timeout) a member for N minutes (1..40320 = 28 days)").argument("<communityId>", "community id").argument("<memberId>", "member id (wcid)").requiredOption("--minutes <n>", "mute duration in minutes (1..40320)").action(
14946
15178
  async (communityId, memberId, options, command) => {
14947
15179
  const flags = getGlobalFlags(command);
14948
- const minutes = Number.parseInt(options.minutes, 10);
14949
- if (!Number.isInteger(minutes) || minutes < 1 || minutes > 40320) {
15180
+ const minutes2 = Number.parseInt(options.minutes, 10);
15181
+ if (!Number.isInteger(minutes2) || minutes2 < 1 || minutes2 > 40320) {
14950
15182
  throw new Error(`Invalid --minutes: ${options.minutes} (expected an integer 1..40320).`);
14951
15183
  }
14952
- const result = await clientFor(flags).muteCommunityMember(communityId, memberId, minutes);
15184
+ const result = await clientFor(flags).muteCommunityMember(communityId, memberId, minutes2);
14953
15185
  emit(flags, result, () => {
14954
15186
  process.stdout.write(`Muted member ${memberId} until ${result.mutedUntil}.
14955
15187
  `);
@@ -17285,6 +17517,10 @@ function printSignupFormDetail(form) {
17285
17517
  `);
17286
17518
  process.stdout.write(` Redirect: ${form.redirectUrl ?? "(success state on the form)"}
17287
17519
  `);
17520
+ process.stdout.write(
17521
+ ` After confirmation: ${form.confirmationRedirectUrl ?? "(platform confirmation page)"}
17522
+ `
17523
+ );
17288
17524
  if (form.confirmationEmail) {
17289
17525
  process.stdout.write(
17290
17526
  ` Confirmation email overrides: subject ${form.confirmationEmail.subject ?? "(default)"}, intro ${form.confirmationEmail.intro ?? "(default)"}
@@ -17342,6 +17578,9 @@ async function buildFormUpdateInput(client, formId, options) {
17342
17578
  input.optInMode = parseChoice2(options.optIn, OPT_IN_MODES, "--opt-in");
17343
17579
  }
17344
17580
  if (options.redirect !== void 0) input.redirectUrl = orNull3(options.redirect);
17581
+ if (options.confirmationRedirect !== void 0) {
17582
+ input.confirmationRedirectUrl = orNull3(options.confirmationRedirect);
17583
+ }
17345
17584
  let current;
17346
17585
  const fetchCurrent = async () => {
17347
17586
  current ??= await client.getSignupForm(formId);
@@ -17404,7 +17643,10 @@ function registerFormsCommands(program2) {
17404
17643
  });
17405
17644
  forms.command("create").description(
17406
17645
  "Create a standalone signup form (double opt-in unless --opt-in single). The full confirmation-email copy is set with `forms update --json`"
17407
- ).requiredOption("--name <name>", "internal form name").option("--tags <ids>", "comma-separated client-tag ids applied on (confirmed) submission").option("--opt-in <mode>", `opt-in mode (${OPT_IN_MODES.join(" | ")}; default double)`).option("--redirect <url>", "thank-you redirect after a successful submit").option("--header <text>", "config: header text shown above the form").option("--description <text>", "config: description under the header").option("--button-text <text>", "config: submit button label").option("--success-message <text>", "config: message shown after a successful submit").option("--variant <variant>", `config: layout variant (${FORM_VARIANTS.join(" | ")})`).option("--phone <mode>", `config: phone number field (${PHONE_FIELD_MODES.join(" | ")})`).option(
17646
+ ).requiredOption("--name <name>", "internal form name").option("--tags <ids>", "comma-separated client-tag ids applied on (confirmed) submission").option("--opt-in <mode>", `opt-in mode (${OPT_IN_MODES.join(" | ")}; default double)`).option("--redirect <url>", "thank-you redirect after a successful submit").option(
17647
+ "--confirmation-redirect <url>",
17648
+ "where the confirmation e-mail click lands (double opt-in): lead magnet, OTO, calendar"
17649
+ ).option("--header <text>", "config: header text shown above the form").option("--description <text>", "config: description under the header").option("--button-text <text>", "config: submit button label").option("--success-message <text>", "config: message shown after a successful submit").option("--variant <variant>", `config: layout variant (${FORM_VARIANTS.join(" | ")})`).option("--phone <mode>", `config: phone number field (${PHONE_FIELD_MODES.join(" | ")})`).option(
17408
17650
  "--sms-consent <on|off>",
17409
17651
  "config: marketing-SMS consent checkbox (needs the phone field)"
17410
17652
  ).option(
@@ -17422,7 +17664,8 @@ function registerFormsCommands(program2) {
17422
17664
  tagIds: options.tags !== void 0 ? parseCsv2(options.tags) : void 0,
17423
17665
  config,
17424
17666
  ...optInMode !== void 0 ? { optInMode } : {},
17425
- ...options.redirect !== void 0 ? { redirectUrl: options.redirect } : {}
17667
+ ...options.redirect !== void 0 ? { redirectUrl: options.redirect } : {},
17668
+ ...options.confirmationRedirect !== void 0 ? { confirmationRedirectUrl: options.confirmationRedirect } : {}
17426
17669
  });
17427
17670
  emit(flags, result, () => {
17428
17671
  process.stdout.write(
@@ -17434,7 +17677,10 @@ function registerFormsCommands(program2) {
17434
17677
  );
17435
17678
  forms.command("update").description(
17436
17679
  "Update a standalone form. The embed config is REPLACED IN FULL by the API \u2014 flat config flags are merged over the current config for you; a --json `config` object is sent as the full replacement (flat flags still win). `none` clears nullable fields."
17437
- ).argument("<id>", "form id").option("--name <name>", "internal form name").option("--tags <ids>", "comma-separated client-tag ids (full replace; empty string clears)").option("--opt-in <mode>", `opt-in mode (${OPT_IN_MODES.join(" | ")})`).option("--redirect <url|none>", "thank-you redirect after submit; `none` clears it").option("--confirmation-subject <text>", "double-opt-in confirmation email subject override").option("--confirmation-intro <text>", "double-opt-in confirmation email intro override").option("--clear-confirmation-email", "restore the workspace default confirmation copy").option("--header <text>", "config: header text shown above the form").option("--description <text>", "config: description under the header").option("--button-text <text>", "config: submit button label").option("--success-message <text>", "config: message shown after a successful submit").option("--variant <variant>", `config: layout variant (${FORM_VARIANTS.join(" | ")})`).option("--phone <mode>", `config: phone number field (${PHONE_FIELD_MODES.join(" | ")})`).option(
17680
+ ).argument("<id>", "form id").option("--name <name>", "internal form name").option("--tags <ids>", "comma-separated client-tag ids (full replace; empty string clears)").option("--opt-in <mode>", `opt-in mode (${OPT_IN_MODES.join(" | ")})`).option("--redirect <url|none>", "thank-you redirect after submit; `none` clears it").option(
17681
+ "--confirmation-redirect <url|none>",
17682
+ "where the confirmation e-mail click lands (double opt-in); `none` restores the platform page"
17683
+ ).option("--confirmation-subject <text>", "double-opt-in confirmation email subject override").option("--confirmation-intro <text>", "double-opt-in confirmation email intro override").option("--clear-confirmation-email", "restore the workspace default confirmation copy").option("--header <text>", "config: header text shown above the form").option("--description <text>", "config: description under the header").option("--button-text <text>", "config: submit button label").option("--success-message <text>", "config: message shown after a successful submit").option("--variant <variant>", `config: layout variant (${FORM_VARIANTS.join(" | ")})`).option("--phone <mode>", `config: phone number field (${PHONE_FIELD_MODES.join(" | ")})`).option(
17438
17684
  "--sms-consent <on|off>",
17439
17685
  "config: marketing-SMS consent checkbox (needs the phone field)"
17440
17686
  ).option(
@@ -19018,6 +19264,78 @@ join again to refresh it.
19018
19264
  });
19019
19265
  }
19020
19266
 
19267
+ // src/commands/liveRooms/index.ts
19268
+ var minutes = (seconds) => `${Math.round(seconds / 60)} min`;
19269
+ var percent = (part, whole) => whole > 0 ? `${Math.round(part / whole * 100)}%` : "-";
19270
+ function describeSchedule(room) {
19271
+ if (!room.schedule) return "fixed";
19272
+ const until = room.schedule.until ? ` until ${room.schedule.until}` : "";
19273
+ return `every ${room.schedule.everyMinutes} min${until}`;
19274
+ }
19275
+ function registerLiveRoomsCommands(program2) {
19276
+ const liveRooms = program2.command("live-rooms").description(
19277
+ "Live rooms (webinar rooms, incl. rolling autowebinar sessions) \u2014 list and attendance; rooms are managed in the dashboard"
19278
+ );
19279
+ liveRooms.command("list").description("List live rooms (latest start first)").action(async (_, command) => {
19280
+ const flags = getGlobalFlags(command);
19281
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
19282
+ const result = await client.listLiveRooms();
19283
+ emit(flags, result, () => {
19284
+ printTable(
19285
+ result.rooms.map((room) => ({
19286
+ id: room.id,
19287
+ title: room.title,
19288
+ source: room.source,
19289
+ status: room.endedAt ? "ended" : room.status,
19290
+ startAt: room.startAt,
19291
+ schedule: describeSchedule(room),
19292
+ access: room.access.everyone ? "everyone" : "restricted"
19293
+ })),
19294
+ ["id", "title", "source", "status", "startAt", "schedule", "access"]
19295
+ );
19296
+ });
19297
+ });
19298
+ liveRooms.command("attendance").description("A room's attendance per session; --session adds that session's viewers").argument("<roomId>", "live room id (from live-rooms list)").option("--session <iso>", "ISO start of one session to list its attendees").action(async (roomId, options, command) => {
19299
+ const flags = getGlobalFlags(command);
19300
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
19301
+ const result = await client.getLiveRoomAttendance(roomId, options.session);
19302
+ emit(flags, result, () => {
19303
+ printTable(
19304
+ result.sessions.map((session) => ({
19305
+ session: session.sessionStartAt,
19306
+ attendees: session.attendees,
19307
+ identified: session.identified,
19308
+ avgWatched: `${minutes(session.averageWatchedSeconds)} (${percent(
19309
+ session.averageWatchedSeconds,
19310
+ session.durationSeconds
19311
+ )})`,
19312
+ toEnd: `${session.watchedToEnd} (${percent(session.watchedToEnd, session.attendees)})`,
19313
+ sawOffer: session.sawOffer,
19314
+ clickedOffer: session.clickedOffer
19315
+ })),
19316
+ ["session", "attendees", "identified", "avgWatched", "toEnd", "sawOffer", "clickedOffer"]
19317
+ );
19318
+ if (options.session !== void 0) {
19319
+ process.stdout.write(`
19320
+ Viewers of ${options.session}:
19321
+ `);
19322
+ printTable(
19323
+ result.attendees.map((attendee) => ({
19324
+ who: attendee.displayName ?? attendee.email ?? attendee.attendee,
19325
+ email: attendee.email ?? "",
19326
+ clientId: attendee.clientId ?? "",
19327
+ watched: minutes(attendee.watchedSeconds),
19328
+ reached: minutes(attendee.maxPositionSeconds),
19329
+ firstSeenAt: attendee.firstSeenAt,
19330
+ offer: attendee.purchasedAt ? "bought" : attendee.offerClickedAt ? "clicked" : attendee.offerSeenAt ? "saw" : ""
19331
+ })),
19332
+ ["who", "email", "clientId", "watched", "reached", "firstSeenAt", "offer"]
19333
+ );
19334
+ }
19335
+ });
19336
+ });
19337
+ }
19338
+
19021
19339
  // src/commands/media/index.ts
19022
19340
  import { existsSync, readFileSync as readFileSync17, writeFileSync as writeFileSync4 } from "fs";
19023
19341
  import { readFile, stat as stat3 } from "fs/promises";
@@ -22419,6 +22737,100 @@ function registerMediaCommands(program2) {
22419
22737
  });
22420
22738
  }
22421
22739
  );
22740
+ const captions = media.command("captions").description("Caption (subtitle) tracks of a video - one per language");
22741
+ captions.command("list").description("List a video's caption tracks (language, source, status)").argument("<mediaId>", "video media item id").action(async (mediaId, _options, command) => {
22742
+ const flags = getGlobalFlags(command);
22743
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
22744
+ const result = await client.listMediaCaptions(mediaId);
22745
+ emit(flags, result, () => {
22746
+ if (result.tracks.length === 0) {
22747
+ process.stdout.write(`No caption tracks on ${mediaId}.
22748
+ `);
22749
+ return;
22750
+ }
22751
+ printTable(
22752
+ result.tracks.map((track) => ({
22753
+ lang: track.srclang,
22754
+ label: track.label,
22755
+ source: track.source,
22756
+ status: track.recorded ? track.status : "not recorded",
22757
+ updated: track.updatedAt ?? ""
22758
+ })),
22759
+ ["lang", "label", "source", "status", "updated"]
22760
+ );
22761
+ });
22762
+ });
22763
+ captions.command("upload").description(
22764
+ "Add or replace the subtitles in one language from a .vtt or .srt file (UTF-8, up to 2 MB). The transcript's own language is refused - edit the transcript instead."
22765
+ ).argument("<mediaId>", "video media item id").requiredOption("--lang <code>", "language code of the track, e.g. en, de, pt-br").requiredOption("--file <path>", ".vtt or .srt file").option("--label <label>", "track name in the player's menu (default: the language name)").action(
22766
+ async (mediaId, options, command) => {
22767
+ const flags = getGlobalFlags(command);
22768
+ if (!existsSync(options.file)) {
22769
+ process.stderr.write(`File not found: ${options.file}
22770
+ `);
22771
+ process.exitCode = 1;
22772
+ return;
22773
+ }
22774
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
22775
+ const result = await client.uploadMediaCaption(mediaId, {
22776
+ srclang: options.lang,
22777
+ content: readFileSync17(options.file, "utf8"),
22778
+ ...options.label ? { label: options.label } : {}
22779
+ });
22780
+ emit(flags, result, () => {
22781
+ process.stdout.write(
22782
+ result.track.status === "failed" ? `Stored the ${result.track.srclang} file, but the player refused it - run the same command again in a moment.
22783
+ ` : `Captions ${result.track.srclang} (${result.track.label}) are live on ${mediaId}.
22784
+ `
22785
+ );
22786
+ });
22787
+ }
22788
+ );
22789
+ captions.command("download").description("Print (or save) one caption track as WebVTT").argument("<mediaId>", "video media item id").requiredOption("--lang <code>", "language code of the track").option("--out <file>", "write the .vtt to a file instead of stdout").action(async (mediaId, options, command) => {
22790
+ const flags = getGlobalFlags(command);
22791
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
22792
+ const result = await client.getMediaCaption(mediaId, options.lang);
22793
+ if (options.out) {
22794
+ writeFileSync4(options.out, result.content, "utf8");
22795
+ emit(flags, { mediaId, srclang: result.srclang, out: options.out }, () => {
22796
+ process.stdout.write(`Wrote the ${result.srclang} captions to ${options.out}.
22797
+ `);
22798
+ });
22799
+ return;
22800
+ }
22801
+ emit(flags, result, () => {
22802
+ process.stdout.write(result.content);
22803
+ });
22804
+ });
22805
+ captions.command("delete").description("Remove an uploaded caption track (it disappears from the player)").argument("<mediaId>", "video media item id").requiredOption("--lang <code>", "language code of the track").option("--confirm", "required: removes the track from the live player").action(
22806
+ async (mediaId, options, command) => {
22807
+ const flags = getGlobalFlags(command);
22808
+ if (!options.confirm) {
22809
+ process.stderr.write(
22810
+ "Refusing to delete without --confirm (the track disappears from the player).\n"
22811
+ );
22812
+ process.exitCode = 1;
22813
+ return;
22814
+ }
22815
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
22816
+ const result = await client.deleteMediaCaption(mediaId, options.lang);
22817
+ emit(flags, result, () => {
22818
+ process.stdout.write(`Removed the ${result.srclang} captions from ${mediaId}.
22819
+ `);
22820
+ });
22821
+ }
22822
+ );
22823
+ captions.command("regenerate").description("Push the transcript's caption track to the player again").argument("<mediaId>", "video media item id").action(async (mediaId, _options, command) => {
22824
+ const flags = getGlobalFlags(command);
22825
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
22826
+ const result = await client.regenerateMediaCaption(mediaId);
22827
+ emit(flags, result, () => {
22828
+ process.stdout.write(
22829
+ result.track.status === "failed" ? "The player refused the transcript captions - try again in a moment.\n" : `Transcript captions (${result.track.srclang}) pushed to the player.
22830
+ `
22831
+ );
22832
+ });
22833
+ });
22422
22834
  media.command("transcript").description(
22423
22835
  "Print a video transcript (plain text, or full JSON with --words / --json). Videos without a transcript yet report so; non-video media is a 404."
22424
22836
  ).argument("<mediaId>", "video media item id").option("--words", "include word-level timings (large payload; implies JSON output)").option("--out <file>", "write the transcript to a file instead of stdout").action(
@@ -22595,6 +23007,7 @@ var ACCESS_STRATEGIES = [
22595
23007
  "from_subscribe_date"
22596
23008
  ];
22597
23009
  var DOUBLE_OPT_INS = ["none", "external"];
23010
+ var DELIVERIES = ["zanfia", "integration"];
22598
23011
  function assertOneOf2(value, allowed, flag) {
22599
23012
  if (!allowed.includes(value)) {
22600
23013
  throw new Error(`Invalid ${flag}: ${value}. Use one of: ${allowed.join(", ")}`);
@@ -22652,9 +23065,10 @@ function registerNewslettersCommands(program2) {
22652
23065
  slug: issue.slug,
22653
23066
  status: issue.status,
22654
23067
  landing: issue.landingPageVisibility ?? "",
23068
+ send: issue.sendStatus ?? "",
22655
23069
  createdAt: issue.createdAt
22656
23070
  })),
22657
- ["id", "subject", "slug", "status", "landing", "createdAt"]
23071
+ ["id", "subject", "slug", "status", "landing", "send", "createdAt"]
22658
23072
  );
22659
23073
  if (page.nextCursor) {
22660
23074
  process.stdout.write(`
@@ -22838,6 +23252,90 @@ Next: \`newsletters issues draft save ${productId} ${issue.id} --content <markdo
22838
23252
  });
22839
23253
  }
22840
23254
  );
23255
+ issues.command("send").description(
23256
+ "\u26A0\uFE0F SENDS the issue from Zanfia to every subscriber of the newsletter product \u2014 REAL bulk e-mail, IRREVERSIBLE once delivered (`issues cancel` only stops the remaining sends). Sending is publishing: a staged draft is promoted first. The newsletter must send from Zanfia (`settings update --delivery zanfia`). Requires --confirm. Omit --at to send NOW; --at schedules (max 30 days out)."
23257
+ ).argument("<productId>", "newsletter product id").argument("<issueId>", "issue id").option(
23258
+ "--at <isoTimestamp>",
23259
+ "schedule the send (ISO, max 30 days out) instead of sending now"
23260
+ ).option("--confirm", "required \u2014 acknowledge this e-mails the subscribers for real").action(
23261
+ async (productId, issueId, options, command) => {
23262
+ const flags = getGlobalFlags(command);
23263
+ if (!options.confirm) {
23264
+ throw new Error(
23265
+ `Refusing to send without --confirm. \`newsletters issues send\` e-mails EVERY subscriber of the newsletter for real and cannot be undone once mail is delivered. Re-run with --confirm to proceed (check the issue first with \`newsletters issues list --product ${productId}\`).`
23266
+ );
23267
+ }
23268
+ if (options.at !== void 0 && Number.isNaN(Date.parse(options.at))) {
23269
+ throw new Error(
23270
+ `Invalid --at timestamp: ${options.at} (expected ISO, e.g. 2026-07-15T09:00:00Z)`
23271
+ );
23272
+ }
23273
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
23274
+ const result = await client.sendNewsletterIssue(productId, issueId, {
23275
+ ...options.at !== void 0 ? { scheduledAt: options.at } : {}
23276
+ });
23277
+ emit(flags, result, () => {
23278
+ const { send } = result;
23279
+ if (send.scheduledAt) {
23280
+ process.stdout.write(
23281
+ `Issue ${result.issueId} scheduled for ${send.scheduledAt} \u2014 stop it before then with \`newsletters issues cancel ${productId} ${issueId} --confirm\`.
23282
+ `
23283
+ );
23284
+ } else {
23285
+ process.stdout.write(
23286
+ `Issue ${result.issueId} is sending now (status: ${send.status}). \`newsletters issues cancel\` can only stop the remaining sends.
23287
+ `
23288
+ );
23289
+ }
23290
+ });
23291
+ }
23292
+ );
23293
+ issues.command("cancel").description(
23294
+ "Cancel a scheduled or in-flight issue send \u2014 remaining sends stop; delivered mail is out. Requires --confirm."
23295
+ ).argument("<productId>", "newsletter product id").argument("<issueId>", "issue id").option("--confirm", "required \u2014 acknowledge the send is stopped").action(
23296
+ async (productId, issueId, options, command) => {
23297
+ const flags = getGlobalFlags(command);
23298
+ if (!options.confirm) {
23299
+ throw new Error(
23300
+ "Refusing to cancel without --confirm. Re-run with --confirm to un-queue the scheduled send or stop the one in flight."
23301
+ );
23302
+ }
23303
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
23304
+ const result = await client.cancelNewsletterIssueSend(productId, issueId);
23305
+ emit(flags, result, () => {
23306
+ process.stdout.write(
23307
+ result.send ? `Stopped the send of issue ${result.issueId} (status: ${result.send.status}).
23308
+ ` : `Un-queued the send of issue ${result.issueId}; it is a draft again.
23309
+ `
23310
+ );
23311
+ });
23312
+ }
23313
+ );
23314
+ issues.command("report").description("Progress and delivery / engagement stats of an issue\u2019s native send").argument("<productId>", "newsletter product id").argument("<issueId>", "issue id").action(async (productId, issueId, command) => {
23315
+ const flags = getGlobalFlags(command);
23316
+ const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
23317
+ const result = await client.getNewsletterIssueSendReport(productId, issueId);
23318
+ emit(flags, result, () => {
23319
+ if (!result.send) {
23320
+ process.stdout.write(`Issue ${result.issueId} was never sent from Zanfia.
23321
+ `);
23322
+ return;
23323
+ }
23324
+ const { send, report } = result;
23325
+ process.stdout.write(
23326
+ `Issue ${result.issueId}: ${send.status}` + (send.scheduledAt ? ` (scheduled for ${send.scheduledAt})` : "") + (send.recipientCount !== null ? `, ${send.recipientCount} recipients` : "") + "\n"
23327
+ );
23328
+ if (report) {
23329
+ printTable(
23330
+ Object.entries(report.stats).map(([stat4, value]) => ({
23331
+ stat: stat4,
23332
+ value: String(value ?? "")
23333
+ })),
23334
+ ["stat", "value"]
23335
+ );
23336
+ }
23337
+ });
23338
+ });
22841
23339
  issues.command("delete").description(
22842
23340
  "\u26A0\uFE0F PERMANENTLY delete an archived issue \u2014 a HARD delete with no undo. Requires --confirm."
22843
23341
  ).argument("<issueId>", "archived issue id").requiredOption("--product <id>", "newsletter product id").option("--confirm", "required \u2014 acknowledge the issue is removed permanently").action(
@@ -22856,9 +23354,11 @@ Next: \`newsletters issues draft save ${productId} ${issue.id} --content <markdo
22856
23354
  });
22857
23355
  }
22858
23356
  );
22859
- const settings = newsletters.command("settings").description("Edit newsletter settings (archive, archive inbox, double opt-in, client tags)");
23357
+ const settings = newsletters.command("settings").description(
23358
+ "Edit newsletter settings (archive, archive inbox, double opt-in, client tags, delivery, sender)"
23359
+ );
22860
23360
  settings.command("update").description(
22861
- "Patch newsletter settings. Archive flags merge per-field; --archive-inbox, --double-opt-in and --client-tag replace their whole block. At least one change is required."
23361
+ "Patch newsletter settings. Archive and sender flags merge per-field; --archive-inbox, --double-opt-in and --client-tag replace their whole block. At least one change is required."
22862
23362
  ).requiredOption("--product <id>", "newsletter product id").option("--archive-enabled <on|off>", "archive: enable/disable the public archive").option("--archive-access <strategy>", `archive: ${ACCESS_STRATEGIES.join("|")}`).option(
22863
23363
  "--publication-delay <days|none>",
22864
23364
  'archive: days before a new issue goes public; "none" = immediately'
@@ -22876,9 +23376,18 @@ Next: \`newsletters issues draft save ${productId} ${issue.id} --content <markdo
22876
23376
  "tag applied to new subscribers (repeat the flag; replaces the list)",
22877
23377
  collectRepeatable4,
22878
23378
  []
22879
- ).action(
23379
+ ).option(
23380
+ "--delivery <mode>",
23381
+ `who sends the issues: ${DELIVERIES.join("|")} (zanfia = native sending from the product)`
23382
+ ).option(
23383
+ "--from-name <name>",
23384
+ 'sender: From display name of native sends ("" = workspace default)'
23385
+ ).option("--reply-to <email>", 'sender: Reply-To of native sends ("" = workspace default)').option("--clear-sender", "sender: drop both defaults (workspace defaults apply)").action(
22880
23386
  async (options, command) => {
22881
23387
  const flags = getGlobalFlags(command);
23388
+ if (options.clearSender && (options.fromName !== void 0 || options.replyTo !== void 0)) {
23389
+ throw new Error("Use either --clear-sender or --from-name/--reply-to, not both.");
23390
+ }
22882
23391
  const archive = {};
22883
23392
  if (options.archiveEnabled !== void 0) {
22884
23393
  archive.enabled = parseBool2(options.archiveEnabled, "--archive-enabled");
@@ -22922,6 +23431,17 @@ Next: \`newsletters issues draft save ${productId} ${issue.id} --content <markdo
22922
23431
  input.doubleOptIn = assertOneOf2(options.doubleOptIn, DOUBLE_OPT_INS, "--double-opt-in");
22923
23432
  }
22924
23433
  if (options.clientTag.length > 0) input.clientTags = options.clientTag;
23434
+ if (options.delivery !== void 0) {
23435
+ input.delivery = assertOneOf2(options.delivery, DELIVERIES, "--delivery");
23436
+ }
23437
+ if (options.clearSender) {
23438
+ input.sender = null;
23439
+ } else if (options.fromName !== void 0 || options.replyTo !== void 0) {
23440
+ input.sender = {
23441
+ ...options.fromName !== void 0 ? { fromName: options.fromName || null } : {},
23442
+ ...options.replyTo !== void 0 ? { replyTo: options.replyTo || null } : {}
23443
+ };
23444
+ }
22925
23445
  if (Object.keys(input).length === 0) {
22926
23446
  throw new Error(
22927
23447
  "Nothing to update \u2014 pass at least one settings flag (see `newsletters settings update --help`)."
@@ -26091,6 +26611,12 @@ function resolveConditions(options) {
26091
26611
  }
26092
26612
  conditions.push({ field: "emailSubscription", operator: "is", status });
26093
26613
  }
26614
+ if (options.notOpened !== void 0) {
26615
+ conditions.push({ field: "campaign", operator: "not-opened", campaignId: options.notOpened });
26616
+ }
26617
+ if (options.notClicked !== void 0) {
26618
+ conditions.push({ field: "campaign", operator: "not-clicked", campaignId: options.notClicked });
26619
+ }
26094
26620
  return conditions;
26095
26621
  }
26096
26622
  function withConditionOptions(command) {
@@ -26140,6 +26666,12 @@ function withConditionOptions(command) {
26140
26666
  ).option("--spent-lte <minorUnits>", "lifetime spend at most this (minor units)").option(
26141
26667
  "--subscription <status>",
26142
26668
  `marketing-email subscription state is (${SUBSCRIPTION_STATUSES2.join(" | ")})`
26669
+ ).option(
26670
+ "--not-opened <campaignId>",
26671
+ "received this SENT campaign and never opened it (the resend-to-non-openers audience)"
26672
+ ).option(
26673
+ "--not-clicked <campaignId>",
26674
+ "received this SENT campaign and never clicked a link in it"
26143
26675
  ).option(
26144
26676
  "--json-file <path>",
26145
26677
  "read the conditions as a JSON array from a file instead of flags"
@@ -26166,6 +26698,8 @@ function describeCondition(condition) {
26166
26698
  return `sms consent is ${condition.status}`;
26167
26699
  case "phoneNumber":
26168
26700
  return condition.operator === "has" ? "has a phone number" : "has no phone number";
26701
+ case "campaign":
26702
+ return `${condition.operator.replace("-", " ")} campaign ${condition.campaignId}`;
26169
26703
  }
26170
26704
  }
26171
26705
  function registerSegmentsCommands(program2) {
@@ -28474,6 +29008,10 @@ function registerWorkflowAuthoringCommands(workflows) {
28474
29008
  ).option(
28475
29009
  "--trigger <eventType>",
28476
29010
  "business event that starts it, e.g. product.ProductPurchased (see `workflows list` for examples)"
29011
+ ).option(
29012
+ "--kind <kind>",
29013
+ "transactional (default for purchase/access triggers: no unsubscribe link, reaches everyone) or marketing (unsubscribe link + headers, e-mail/SMS steps skipped for unsubscribed contacts); omitted = derived from the triggers at publish",
29014
+ parseWorkflowKind
28477
29015
  ).action(
28478
29016
  async (options, command) => {
28479
29017
  const flags = getGlobalFlags(command);
@@ -28481,6 +29019,7 @@ function registerWorkflowAuthoringCommands(workflows) {
28481
29019
  if (options.description !== void 0) input.description = options.description;
28482
29020
  if (options.product !== void 0) input.productIds = options.product;
28483
29021
  if (options.trigger !== void 0) input.triggerEventType = options.trigger;
29022
+ if (options.kind !== void 0) input.kind = options.kind;
28484
29023
  const client = createApiClient({ apiKey: flags.apiKey, verbose: flags.verbose });
28485
29024
  const result = await client.createAutomation(input);
28486
29025
  emit(flags, result, () => {
@@ -28535,6 +29074,10 @@ function registerWorkflowAuthoringCommands(workflows) {
28535
29074
  "--product <ids>",
28536
29075
  "also replace the product scope (comma-separated ids)",
28537
29076
  parseCommaList("--product")
29077
+ ).option(
29078
+ "--kind <kind>",
29079
+ "also set the kind: transactional or marketing (see `workflows create --help`)",
29080
+ parseWorkflowKind
28538
29081
  ).action(
28539
29082
  async (automationId, options, command) => {
28540
29083
  const flags = getGlobalFlags(command);
@@ -28543,7 +29086,8 @@ function registerWorkflowAuthoringCommands(workflows) {
28543
29086
  draft: options.json,
28544
29087
  ...options.name !== void 0 ? { name: options.name } : {},
28545
29088
  ...options.description !== void 0 ? { description: options.description } : {},
28546
- ...options.product !== void 0 ? { productIds: options.product } : {}
29089
+ ...options.product !== void 0 ? { productIds: options.product } : {},
29090
+ ...options.kind !== void 0 ? { kind: options.kind } : {}
28547
29091
  }).catch(withGraphHint);
28548
29092
  emit(flags, result, () => {
28549
29093
  if (result.issues.length > 0) {
@@ -28656,12 +29200,13 @@ function registerWorkflowsCommands(program2) {
28656
29200
  id: w.id,
28657
29201
  name: w.name,
28658
29202
  status: w.status,
29203
+ kind: w.kind,
28659
29204
  triggers: w.triggers.join(", "),
28660
29205
  products: formatProductScope(w),
28661
29206
  runsThisMonth: w.runsThisMonth,
28662
29207
  lastRun: w.lastRunAt ?? ""
28663
29208
  })),
28664
- ["id", "name", "status", "triggers", "products", "runsThisMonth", "lastRun"]
29209
+ ["id", "name", "status", "kind", "triggers", "products", "runsThisMonth", "lastRun"]
28665
29210
  );
28666
29211
  });
28667
29212
  });
@@ -28797,6 +29342,8 @@ function printWorkflowSummary(workflow) {
28797
29342
  if (workflow.description) process.stdout.write(` Description: ${workflow.description}
28798
29343
  `);
28799
29344
  process.stdout.write(` Status: ${workflow.status}
29345
+ `);
29346
+ process.stdout.write(` Kind: ${workflow.kind}
28800
29347
  `);
28801
29348
  process.stdout.write(` Triggers: ${workflow.triggers.join(", ")}
28802
29349
  `);
@@ -28833,6 +29380,7 @@ var GRAPH_ACTION_LABELS = {
28833
29380
  createCommunityPost: "Create community post",
28834
29381
  updateClientTags: "Update client tags",
28835
29382
  setEmailSubscription: "Set email subscription",
29383
+ openClientDeadline: "Open deadline window",
28836
29384
  requestTestimonial: "Request testimonial"
28837
29385
  };
28838
29386
  var GRAPH_NODE_KINDS = /* @__PURE__ */ new Set([
@@ -29069,6 +29617,12 @@ function describeAction(node, templateNames) {
29069
29617
  const sub = isRecord2(node.config.emailSubscription) ? node.config.emailSubscription : {};
29070
29618
  return `${label} \u2014 ${optionalString(sub.status) ?? "?"}`;
29071
29619
  }
29620
+ case "openClientDeadline": {
29621
+ const deadline = isRecord2(node.config.clientDeadline) ? node.config.clientDeadline : {};
29622
+ const amount = typeof deadline.amount === "number" ? String(deadline.amount) : "?";
29623
+ const endTime = optionalString(deadline.endTime);
29624
+ return `${label} \u2014 ${optionalString(deadline.key) ?? "?"} for ${amount} ${optionalString(deadline.unit) ?? "?"}${endTime ? `, closes at ${endTime}` : ""}`;
29625
+ }
29072
29626
  case "requestTestimonial":
29073
29627
  return label;
29074
29628
  }
@@ -29275,8 +29829,8 @@ function applyShortHelp(program2) {
29275
29829
 
29276
29830
  // src/program.ts
29277
29831
  function resolveCliVersion() {
29278
- if ("0.3.52".length > 0) {
29279
- return "0.3.52";
29832
+ if ("0.3.63".length > 0) {
29833
+ return "0.3.63";
29280
29834
  }
29281
29835
  try {
29282
29836
  const raw = readFileSync28(new URL("../package.json", import.meta.url), "utf8");
@@ -29337,6 +29891,7 @@ Signing in \u2014 two roles, two consent pages:
29337
29891
  registerSmsCampaignsCommands(program2);
29338
29892
  registerSegmentsCommands(program2);
29339
29893
  registerFormsCommands(program2);
29894
+ registerLiveRoomsCommands(program2);
29340
29895
  registerTestimonialsCommands(program2);
29341
29896
  registerBrandCommands(program2);
29342
29897
  registerTeamCommands(program2);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zanfia/cli",
3
- "version": "0.3.52",
3
+ "version": "0.3.63",
4
4
  "private": false,
5
5
  "description": "Zanfia CLI — thin HTTP client over the Zanfia Public API",
6
6
  "type": "module",