@ascendkit/cli 0.3.2 → 0.3.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -20,6 +20,7 @@ import { redactArgs } from "./utils/redaction.js";
20
20
  import { captureTelemetry } from "./utils/telemetry.js";
21
21
  import { hostname as osHostname, platform as osPlatform } from "node:os";
22
22
  import { formatJourneyAnalytics, formatJourneyWithGuidance, formatNodeList, formatSingleNode, formatSingleTransition, formatTransitionList, } from "./utils/journey-format.js";
23
+ import { formatDistributionResult, formatQuestionList, formatSingleQuestion, } from "./utils/survey-format.js";
23
24
  const require = createRequire(import.meta.url);
24
25
  const { version: CLI_VERSION } = require("../package.json");
25
26
  const HELP = `ascendkit v${CLI_VERSION} - AscendKit CLI
@@ -68,10 +69,10 @@ Commands:
68
69
  template: `Usage: ascendkit template <command>
69
70
 
70
71
  Commands:
71
- template create --name <name> --subject <subject> --body-html <html> --body-text <text> [--slug <slug>] [--description <description>]
72
+ template create --name <name> --subject <subject> --body-html <html> --body-text <text> [--slug <slug>] [--description <description>] [--category <marketing|transactional>]
72
73
  template list [--query <search>] [--system true|--custom true]
73
74
  template show <template-id>
74
- template update <template-id> [--subject <subject>] [--body-html <html>] [--body-text <text>] [--change-note <note>]
75
+ template update <template-id> [--subject <subject>] [--body-html <html>] [--body-text <text>] [--change-note <note>] [--category <marketing|transactional>]
75
76
  template remove <template-id>
76
77
  template version list <template-id>
77
78
  template version show <template-id> <n>`,
@@ -102,16 +103,16 @@ Commands:
102
103
  journey update <journey-id> [--name <name>] [--nodes <json>] [--transitions <json>] [--description <description>] [--entry-event <event>] [--entry-node <node>] [--entry-conditions <json>] [--re-entry-policy <skip|restart>]
103
104
  journey remove <journey-id>
104
105
  journey activate <journey-id>
105
- journey pause <journey-id>
106
+ journey pause <journey-id> [--yes]
106
107
  journey resume <journey-id>
107
108
  journey archive <journey-id>
108
109
  journey analytics <journey-id>
109
110
  journey node list <journey-id>
110
- journey node add <journey-id> --name <node-name> [--action <json>] [--email-id <email>] [--terminal <true|false>]
111
+ journey node add <journey-id> --name <node-name> [--action <json>] [--email-id <email>] [--terminal <true|false>] [--quiet]
111
112
  journey node update <journey-id> <node-name> [--action <json>] [--email-id <email>] [--terminal <true|false>]
112
113
  journey node remove <journey-id> <node-name>
113
114
  journey transition list <journey-id> [--from <node-name>] [--to <node-name>]
114
- journey transition add <journey-id> --from <node-name> --to <node-name> --trigger <json> [--priority <n>] [--name <transition-name>]
115
+ journey transition add <journey-id> --from <node-name> --to <node-name> --trigger <json> [--priority <n>] [--name <transition-name>] [--quiet]
115
116
  journey transition update <journey-id> <transition-name> [--trigger <json>] [--priority <n>]
116
117
  journey transition remove <journey-id> <transition-name>`,
117
118
  "email-identity": `Usage: ascendkit email-identity <command>
@@ -123,6 +124,7 @@ Commands:
123
124
  email-identity remove-domain
124
125
  email-identity list
125
126
  email-identity add <email> [--display-name <name>]
127
+ email-identity update <email> --display-name <name>
126
128
  email-identity resend <email>
127
129
  email-identity set-default <email> [--display-name <name>]
128
130
  email-identity remove <email>
@@ -140,10 +142,10 @@ Commands:
140
142
  campaign: `Usage: ascendkit campaign <command>
141
143
 
142
144
  Commands:
143
- campaign create --name <name> --template <template-id> --audience <json> [--scheduled-at <datetime>]
145
+ campaign create --name <name> --template <template-id> --audience <json> [--from <email>] [--scheduled-at <datetime>]
144
146
  campaign list [--status <draft|scheduled|sending|sent|failed|cancelled>]
145
147
  campaign show <campaign-id>
146
- campaign update <campaign-id> [--name <name>] [--template <template-id>] [--audience <json>] [--scheduled-at <datetime>]
148
+ campaign update <campaign-id> [--name <name>] [--template <template-id>] [--audience <json>] [--from <email>] [--scheduled-at <datetime>]
147
149
  campaign preview <campaign-id>
148
150
  campaign schedule <campaign-id> --at <datetime>
149
151
  campaign cancel <campaign-id>
@@ -151,6 +153,7 @@ Commands:
151
153
 
152
154
  Notes:
153
155
  - --audience is a JSON filter object, e.g. '{"tags":{"$in":["premium"]}}'
156
+ - --from specifies a verified email identity to send from (defaults to environment default)
154
157
  - --scheduled-at / --at accepts ISO 8601 datetime, e.g. 2026-03-15T10:00:00Z
155
158
  - cancel deletes a draft/failed campaign or cancels a scheduled/sending campaign`,
156
159
  environment: `Usage: ascendkit environment <command>
@@ -291,6 +294,8 @@ function printTemplateSummary(data, opts) {
291
294
  console.log(`Template: ${data.name} (${data.id})`);
292
295
  if (data.slug)
293
296
  console.log(`Slug: ${data.slug}`);
297
+ if (data.category)
298
+ console.log(`Category: ${data.category}`);
294
299
  console.log(`Subject: ${data.subject ?? "-"}`);
295
300
  if (data.currentVersion != null)
296
301
  console.log(`Version: ${data.currentVersion}`);
@@ -407,6 +412,92 @@ function printProjectSummary(data) {
407
412
  console.log(`Environment: ${data.environment.publicKey}`);
408
413
  }
409
414
  }
415
+ function printCampaignSummary(data) {
416
+ console.log(`Campaign: ${data.name} (${data.id})`);
417
+ if (data.templateName)
418
+ console.log(`Template: ${data.templateName}`);
419
+ console.log(`Status: ${data.status} | Recipients: ${data.totalRecipients ?? 0}`);
420
+ if (data.audienceSummary)
421
+ console.log(`Audience: ${data.audienceSummary}`);
422
+ if (data.scheduledAt)
423
+ console.log(`Scheduled: ${data.scheduledAt}`);
424
+ if (data.sentAt)
425
+ console.log(`Sent: ${data.sentAt}`);
426
+ if (data.fromIdentityEmail)
427
+ console.log(`From: ${data.fromIdentityEmail}`);
428
+ }
429
+ function printCampaignAnalytics(data) {
430
+ const sent = data.sent ?? 0;
431
+ const opened = data.opened ?? 0;
432
+ const clicked = data.clicked ?? 0;
433
+ const bounced = data.bounced ?? 0;
434
+ const rates = data.rates ?? {};
435
+ const fmtRate = (r) => r != null ? ` (${Math.round(r * 100)}%)` : "";
436
+ console.log(`Sent: ${sent} | Opened: ${opened}${fmtRate(rates.openRate)} | Clicked: ${clicked}${fmtRate(rates.clickRate)} | Bounced: ${bounced}`);
437
+ }
438
+ function printAudiencePreview(data) {
439
+ const count = data.count ?? data.total ?? 0;
440
+ console.log(`Matched ${count} recipient(s)`);
441
+ }
442
+ function printWebhookSummary(data, opts) {
443
+ console.log(`Webhook: ${data.id}`);
444
+ const events = Array.isArray(data.events) && data.events.length > 0 ? data.events.join(", ") : "all";
445
+ console.log(`URL: ${data.url} | Status: ${data.status}`);
446
+ console.log(`Events: ${events}`);
447
+ if (opts?.showSecret && data.secret) {
448
+ console.log(`Secret: ${data.secret} (save this — it won't be shown again)`);
449
+ }
450
+ }
451
+ function printWebhookTestResult(data) {
452
+ if (data.success) {
453
+ console.log(`✓ Delivered: HTTP ${data.statusCode}`);
454
+ }
455
+ else if (data.statusCode) {
456
+ console.log(`✗ Failed: HTTP ${data.statusCode}`);
457
+ }
458
+ else {
459
+ console.log(`✗ Failed`);
460
+ }
461
+ if (data.error)
462
+ console.log(data.error);
463
+ }
464
+ function printTemplateVersionSummary(data) {
465
+ const note = data.changeNote || "no note";
466
+ console.log(`Version ${data.versionNumber} — ${note}`);
467
+ console.log(`Subject: ${data.subject ?? "—"}`);
468
+ if (Array.isArray(data.variables) && data.variables.length > 0) {
469
+ console.log(`Variables: ${data.variables.join(", ")}`);
470
+ }
471
+ if (data.createdAt) {
472
+ console.log(`Created: ${data.createdAt}`);
473
+ }
474
+ if (data.bodyHtml) {
475
+ console.log(`\n--- HTML Body ---\n${data.bodyHtml}`);
476
+ }
477
+ if (data.bodyText) {
478
+ console.log(`\n--- Plain Text Body ---\n${data.bodyText}`);
479
+ }
480
+ }
481
+ function printSurveyAnalytics(data) {
482
+ const funnel = data.funnel ?? {};
483
+ const invited = funnel.sent ?? data.totalInvited ?? 0;
484
+ const responded = funnel.submitted ?? data.totalResponded ?? 0;
485
+ const rate = funnel.completionRate ?? data.completionRate ?? 0;
486
+ console.log(`Responses: ${responded}/${invited} (${rate}%)`);
487
+ if (data.npsScore != null)
488
+ console.log(`NPS: ${data.npsScore}`);
489
+ if (data.csatAverage != null)
490
+ console.log(`CSAT: ${data.csatAverage}`);
491
+ }
492
+ function printSurveyInvitations(data) {
493
+ const rows = Array.isArray(data) ? data : [];
494
+ table(rows, [
495
+ { key: "id", label: "ID", width: 20 },
496
+ { key: "userId", label: "User", width: 20 },
497
+ { key: "status", label: "Status", width: 12 },
498
+ { key: "sentAt", label: "Sent At", width: 22 },
499
+ ]);
500
+ }
410
501
  function normalizeJourneyRows(data) {
411
502
  if (Array.isArray(data)) {
412
503
  return data;
@@ -442,6 +533,16 @@ function table(rows, columns) {
442
533
  console.log(line);
443
534
  }
444
535
  }
536
+ function normalizeTemplateCategoryFlag(value) {
537
+ if (value === undefined)
538
+ return undefined;
539
+ if (typeof value != "string")
540
+ return undefined;
541
+ const normalized = value.trim().toLowerCase();
542
+ if (normalized === "marketing" || normalized === "transactional")
543
+ return normalized;
544
+ return undefined;
545
+ }
445
546
  async function run() {
446
547
  installGlobalHandlers();
447
548
  const args = process.argv.slice(2);
@@ -506,6 +607,14 @@ async function run() {
506
607
  }
507
608
  return;
508
609
  }
610
+ // --help anywhere after the action (e.g. ascendkit template update <id> --help)
611
+ if (args.slice(2).some(a => a === "--help" || a === "-h")) {
612
+ if (!printSectionHelp(domain)) {
613
+ console.error(`Unknown command section: ${domain}`);
614
+ return await exitCli(1);
615
+ }
616
+ return;
617
+ }
509
618
  // Platform commands (don't need environment key)
510
619
  switch (domain) {
511
620
  case "init": {
@@ -857,7 +966,7 @@ async function runEnvironment(action, rest) {
857
966
  try {
858
967
  const result = await platform.updateEnvironment(ctx.projectId, envId, name, description);
859
968
  console.log("Environment updated:");
860
- console.log(JSON.stringify(result, null, 2));
969
+ printEnvironmentSummary(result);
861
970
  }
862
971
  catch (err) {
863
972
  let message = err instanceof Error ? err.message : String(err);
@@ -1087,6 +1196,16 @@ async function runAuth(client, action, rest) {
1087
1196
  }
1088
1197
  async function runContent(client, action, rest) {
1089
1198
  const flags = parseFlags(rest);
1199
+ // Accept --html/--text as aliases for --body-html/--body-text
1200
+ if (!flags["body-html"] && flags.html)
1201
+ flags["body-html"] = flags.html;
1202
+ if (!flags["body-text"] && flags.text)
1203
+ flags["body-text"] = flags.text;
1204
+ const category = normalizeTemplateCategoryFlag(flags.category);
1205
+ if (flags.category && !category) {
1206
+ console.error("Invalid --category. Use marketing or transactional.");
1207
+ return await exitCli(1);
1208
+ }
1090
1209
  if (!action) {
1091
1210
  console.log(HELP_SECTION.template);
1092
1211
  return;
@@ -1097,13 +1216,14 @@ async function runContent(client, action, rest) {
1097
1216
  switch (normalizedAction) {
1098
1217
  case "create":
1099
1218
  if (!flags.name || !flags.subject || !flags["body-html"] || !flags["body-text"]) {
1100
- console.error("Usage: ascendkit template create --name <n> --subject <s> --body-html <h> --body-text <t> [--slug <slug>] [--description <desc>]");
1219
+ console.error("Usage: ascendkit template create --name <n> --subject <s> --body-html <h> --body-text <t> [--slug <slug>] [--description <desc>] [--category <marketing|transactional>]");
1101
1220
  return await exitCli(1);
1102
1221
  }
1103
1222
  printTemplateSummary(await content.createTemplate(client, {
1104
1223
  name: flags.name, subject: flags.subject,
1105
1224
  bodyHtml: flags["body-html"], bodyText: flags["body-text"],
1106
1225
  slug: flags.slug, description: flags.description,
1226
+ category,
1107
1227
  }));
1108
1228
  break;
1109
1229
  case "list": {
@@ -1119,6 +1239,7 @@ async function runContent(client, action, rest) {
1119
1239
  { key: "id", label: "ID" },
1120
1240
  { key: "name", label: "Name", width: 30 },
1121
1241
  { key: "slug", label: "Slug", width: 25 },
1242
+ { key: "category", label: "Category", width: 14 },
1122
1243
  { key: "subject", label: "Subject", width: 30 },
1123
1244
  ]);
1124
1245
  break;
@@ -1140,6 +1261,7 @@ async function runContent(client, action, rest) {
1140
1261
  bodyHtml: flags["body-html"],
1141
1262
  bodyText: flags["body-text"],
1142
1263
  changeNote: flags["change-note"],
1264
+ category,
1143
1265
  }));
1144
1266
  break;
1145
1267
  case "remove":
@@ -1172,7 +1294,7 @@ async function runContent(client, action, rest) {
1172
1294
  console.error("Usage: ascendkit template version show <template-id> <n>");
1173
1295
  return await exitCli(1);
1174
1296
  }
1175
- output(await content.getVersion(client, templateId, parseInt(versionNumber, 10)));
1297
+ printTemplateVersionSummary(await content.getVersion(client, templateId, parseInt(versionNumber, 10)));
1176
1298
  }
1177
1299
  break;
1178
1300
  default:
@@ -1240,7 +1362,7 @@ async function runSurvey(client, action, rest) {
1240
1362
  console.error("Usage: ascendkit survey distribute <survey-id> --users <usr_id1,usr_id2,...>");
1241
1363
  return await exitCli(1);
1242
1364
  }
1243
- output(await surveys.distributeSurvey(client, rest[0], flags.users.split(",")));
1365
+ console.log(formatDistributionResult(await surveys.distributeSurvey(client, rest[0], flags.users.split(","))));
1244
1366
  break;
1245
1367
  case "invitations":
1246
1368
  case "invitation":
@@ -1250,7 +1372,7 @@ async function runSurvey(client, action, rest) {
1250
1372
  console.error("Usage: ascendkit survey invitation list <survey-id>");
1251
1373
  return await exitCli(1);
1252
1374
  }
1253
- output(await surveys.listInvitations(client, surveyId));
1375
+ printSurveyInvitations(await surveys.listInvitations(client, surveyId));
1254
1376
  }
1255
1377
  else {
1256
1378
  console.error(`Unknown survey invitation command: ${rest[0]}`);
@@ -1262,7 +1384,7 @@ async function runSurvey(client, action, rest) {
1262
1384
  console.error("Usage: ascendkit survey analytics <survey-id>");
1263
1385
  return await exitCli(1);
1264
1386
  }
1265
- output(await surveys.getAnalytics(client, rest[0]));
1387
+ printSurveyAnalytics(await surveys.getAnalytics(client, rest[0]));
1266
1388
  break;
1267
1389
  case "export-definition":
1268
1390
  case "import-definition":
@@ -1327,7 +1449,7 @@ async function runSurvey(client, action, rest) {
1327
1449
  return await exitCli(1);
1328
1450
  }
1329
1451
  if (questionAction === "list") {
1330
- output(await surveys.listQuestions(client, surveyId));
1452
+ console.log(formatQuestionList(await surveys.listQuestions(client, surveyId)));
1331
1453
  }
1332
1454
  else if (questionAction === "add") {
1333
1455
  if (!flags.type || !flags.title) {
@@ -1343,13 +1465,17 @@ async function runSurvey(client, action, rest) {
1343
1465
  params.choices = flags.choices.split(",");
1344
1466
  if (flags.position != null)
1345
1467
  params.position = Number(flags.position);
1346
- output(await surveys.addQuestion(client, surveyId, params));
1468
+ console.log(formatSingleQuestion(await surveys.addQuestion(client, surveyId, params), "Added"));
1347
1469
  }
1348
1470
  else if (questionAction === "update") {
1349
1471
  if (!questionName) {
1350
1472
  console.error("Usage: ascendkit survey question update <survey-id> <question-name> [--title <title>] [--required <true|false>] [--choices <c1,c2,...>]");
1351
1473
  return await exitCli(1);
1352
1474
  }
1475
+ if (flags.type) {
1476
+ console.error("Error: question type cannot be changed on an existing question. Remove and re-add it to change the type.");
1477
+ return await exitCli(1);
1478
+ }
1353
1479
  const params = {};
1354
1480
  if (flags.title)
1355
1481
  params.title = flags.title;
@@ -1357,21 +1483,23 @@ async function runSurvey(client, action, rest) {
1357
1483
  params.isRequired = flags.required === "true";
1358
1484
  if (flags.choices)
1359
1485
  params.choices = flags.choices.split(",");
1360
- output(await surveys.editQuestion(client, surveyId, questionName, params));
1486
+ console.log(formatSingleQuestion(await surveys.editQuestion(client, surveyId, questionName, params), "Updated"));
1361
1487
  }
1362
1488
  else if (questionAction === "remove") {
1363
1489
  if (!questionName) {
1364
1490
  console.error("Usage: ascendkit survey question remove <survey-id> <question-name>");
1365
1491
  return await exitCli(1);
1366
1492
  }
1367
- output(await surveys.removeQuestion(client, surveyId, questionName));
1493
+ await surveys.removeQuestion(client, surveyId, questionName);
1494
+ console.log("Question removed.");
1368
1495
  }
1369
1496
  else if (questionAction === "reorder") {
1370
1497
  if (!flags.order) {
1371
1498
  console.error("Usage: ascendkit survey question reorder <survey-id> --order <name1,name2,...>");
1372
1499
  return await exitCli(1);
1373
1500
  }
1374
- output(await surveys.reorderQuestions(client, surveyId, flags.order.split(",")));
1501
+ await surveys.reorderQuestions(client, surveyId, flags.order.split(","));
1502
+ console.log("Questions reordered.");
1375
1503
  }
1376
1504
  else {
1377
1505
  console.error(`Unknown survey question command: ${questionAction}`);
@@ -1565,13 +1693,36 @@ async function runJourney(client, action, rest) {
1565
1693
  }
1566
1694
  console.log(formatJourneyWithGuidance(await journeys.activateJourney(client, rest[0])));
1567
1695
  break;
1568
- case "pause":
1696
+ case "pause": {
1569
1697
  if (!rest[0]) {
1570
- console.error("Usage: ascendkit journey pause <journey-id>");
1698
+ console.error("Usage: ascendkit journey pause <journey-id> [--yes]");
1571
1699
  return await exitCli(1);
1572
1700
  }
1701
+ const journeyForPause = await journeys.getJourney(client, rest[0]);
1702
+ const activeUsers = journeyForPause?.stats?.currentlyActive ?? 0;
1703
+ if (activeUsers > 0 && !flags.yes) {
1704
+ if (process.stdin.isTTY && process.stdout.isTTY) {
1705
+ const { createInterface } = await import("node:readline/promises");
1706
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
1707
+ try {
1708
+ const answer = (await rl.question(`${activeUsers} user(s) are currently active in this journey. Actions will be queued while paused. Proceed? [y/N] `)).trim().toLowerCase();
1709
+ if (answer !== "y" && answer !== "yes") {
1710
+ console.log("Cancelled.");
1711
+ return;
1712
+ }
1713
+ }
1714
+ finally {
1715
+ rl.close();
1716
+ }
1717
+ }
1718
+ else {
1719
+ console.error(`Warning: ${activeUsers} user(s) are currently active. Pass --yes to skip confirmation.`);
1720
+ return await exitCli(1);
1721
+ }
1722
+ }
1573
1723
  console.log(formatJourneyWithGuidance(await journeys.pauseJourney(client, rest[0])));
1574
1724
  break;
1725
+ }
1575
1726
  case "resume":
1576
1727
  if (!rest[0]) {
1577
1728
  console.error("Usage: ascendkit journey resume <journey-id>");
@@ -1617,7 +1768,7 @@ async function runJourney(client, action, rest) {
1617
1768
  }
1618
1769
  if (flags.terminal)
1619
1770
  params.terminal = flags.terminal === "true";
1620
- console.log(formatSingleNode(await journeys.addNode(client, rest[0], params), "Added", flags.name));
1771
+ console.log(formatSingleNode(await journeys.addNode(client, rest[0], params), "Added", flags.name, { suppressWarnings: !!flags.quiet }));
1621
1772
  break;
1622
1773
  }
1623
1774
  case "edit-node": {
@@ -1692,7 +1843,7 @@ async function runJourney(client, action, rest) {
1692
1843
  if (flags.name)
1693
1844
  params.name = flags.name;
1694
1845
  const transitionName = flags.name || `${flags.from}-to-${flags.to}`;
1695
- console.log(formatSingleTransition(await journeys.addTransition(client, rest[0], params), "Added", transitionName));
1846
+ console.log(formatSingleTransition(await journeys.addTransition(client, rest[0], params), "Added", transitionName, { suppressWarnings: !!flags.quiet }));
1696
1847
  break;
1697
1848
  }
1698
1849
  case "edit-transition": {
@@ -1735,10 +1886,10 @@ async function runWebhook(client, action, rest) {
1735
1886
  console.error("Usage: ascendkit webhook create --url <url> [--events <e1,e2,...>]");
1736
1887
  return await exitCli(1);
1737
1888
  }
1738
- output(await webhooks.createWebhook(client, {
1889
+ printWebhookSummary(await webhooks.createWebhook(client, {
1739
1890
  url: flags.url,
1740
1891
  events: flags.events ? flags.events.split(",") : undefined,
1741
- }));
1892
+ }), { showSecret: true });
1742
1893
  break;
1743
1894
  case "list":
1744
1895
  table(await webhooks.listWebhooks(client), [
@@ -1753,7 +1904,7 @@ async function runWebhook(client, action, rest) {
1753
1904
  console.error("Usage: ascendkit webhook get <webhook-id>");
1754
1905
  return await exitCli(1);
1755
1906
  }
1756
- output(await webhooks.getWebhook(client, rest[0]));
1907
+ printWebhookSummary(await webhooks.getWebhook(client, rest[0]));
1757
1908
  break;
1758
1909
  case "update": {
1759
1910
  if (!rest[0]) {
@@ -1776,14 +1927,15 @@ async function runWebhook(client, action, rest) {
1776
1927
  console.error("Usage: ascendkit webhook delete <webhook-id>");
1777
1928
  return await exitCli(1);
1778
1929
  }
1779
- output(await webhooks.deleteWebhook(client, rest[0]));
1930
+ await webhooks.deleteWebhook(client, rest[0]);
1931
+ console.log("Webhook deleted.");
1780
1932
  break;
1781
1933
  case "test":
1782
1934
  if (!rest[0]) {
1783
1935
  console.error("Usage: ascendkit webhook test <webhook-id> [--event <event-type>]");
1784
1936
  return await exitCli(1);
1785
1937
  }
1786
- output(await webhooks.testWebhook(client, rest[0], flags.event));
1938
+ printWebhookTestResult(await webhooks.testWebhook(client, rest[0], flags.event));
1787
1939
  break;
1788
1940
  default:
1789
1941
  console.error(`Unknown webhook command: ${action}`);
@@ -1800,7 +1952,7 @@ async function runCampaign(client, action, rest) {
1800
1952
  switch (action) {
1801
1953
  case "create": {
1802
1954
  if (!flags.name || !flags.template || !flags.audience) {
1803
- console.error("Usage: ascendkit campaign create --name <name> --template <template-id> --audience <json> [--scheduled-at <datetime>]");
1955
+ console.error("Usage: ascendkit campaign create --name <name> --template <template-id> --audience <json> [--from <email>] [--scheduled-at <datetime>]");
1804
1956
  return await exitCli(1);
1805
1957
  }
1806
1958
  let createFilter;
@@ -1811,10 +1963,11 @@ async function runCampaign(client, action, rest) {
1811
1963
  console.error("Invalid JSON for --audience flag. Provide a valid JSON object.");
1812
1964
  return await exitCli(1);
1813
1965
  }
1814
- output(await campaigns.createCampaign(client, {
1966
+ printCampaignSummary(await campaigns.createCampaign(client, {
1815
1967
  name: flags.name,
1816
1968
  templateId: flags.template,
1817
1969
  audienceFilter: createFilter,
1970
+ fromIdentityEmail: flags.from,
1818
1971
  scheduledAt: flags["scheduled-at"],
1819
1972
  }));
1820
1973
  break;
@@ -1836,7 +1989,7 @@ async function runCampaign(client, action, rest) {
1836
1989
  console.error("Usage: ascendkit campaign show <campaign-id>");
1837
1990
  return await exitCli(1);
1838
1991
  }
1839
- output(await campaigns.getCampaign(client, rest[0]));
1992
+ printCampaignSummary(await campaigns.getCampaign(client, rest[0]));
1840
1993
  break;
1841
1994
  case "update": {
1842
1995
  if (!rest[0]) {
@@ -1853,10 +2006,11 @@ async function runCampaign(client, action, rest) {
1853
2006
  return await exitCli(1);
1854
2007
  }
1855
2008
  }
1856
- output(await campaigns.updateCampaign(client, rest[0], {
2009
+ printCampaignSummary(await campaigns.updateCampaign(client, rest[0], {
1857
2010
  name: flags.name,
1858
2011
  templateId: flags.template,
1859
2012
  audienceFilter: updateFilter,
2013
+ fromIdentityEmail: flags.from,
1860
2014
  scheduledAt: flags["scheduled-at"],
1861
2015
  }));
1862
2016
  break;
@@ -1871,7 +2025,7 @@ async function runCampaign(client, action, rest) {
1871
2025
  console.error("Campaign has no audience filter set.");
1872
2026
  return await exitCli(1);
1873
2027
  }
1874
- output(await campaigns.previewAudience(client, detail.audienceFilter));
2028
+ printAudiencePreview(await campaigns.previewAudience(client, detail.audienceFilter));
1875
2029
  break;
1876
2030
  }
1877
2031
  case "schedule":
@@ -1879,21 +2033,22 @@ async function runCampaign(client, action, rest) {
1879
2033
  console.error("Usage: ascendkit campaign schedule <campaign-id> --at <datetime>");
1880
2034
  return await exitCli(1);
1881
2035
  }
1882
- output(await campaigns.updateCampaign(client, rest[0], { scheduledAt: flags.at }));
2036
+ printCampaignSummary(await campaigns.updateCampaign(client, rest[0], { scheduledAt: flags.at }));
1883
2037
  break;
1884
2038
  case "cancel":
1885
2039
  if (!rest[0]) {
1886
2040
  console.error("Usage: ascendkit campaign cancel <campaign-id>");
1887
2041
  return await exitCli(1);
1888
2042
  }
1889
- output(await campaigns.deleteCampaign(client, rest[0]));
2043
+ await campaigns.deleteCampaign(client, rest[0]);
2044
+ console.log("Campaign cancelled.");
1890
2045
  break;
1891
2046
  case "analytics":
1892
2047
  if (!rest[0]) {
1893
2048
  console.error("Usage: ascendkit campaign analytics <campaign-id>");
1894
2049
  return await exitCli(1);
1895
2050
  }
1896
- output(await campaigns.getCampaignAnalytics(client, rest[0]));
2051
+ printCampaignAnalytics(await campaigns.getCampaignAnalytics(client, rest[0]));
1897
2052
  break;
1898
2053
  default:
1899
2054
  console.error(`Unknown campaign command: ${action}`);
@@ -1975,6 +2130,19 @@ async function runEmail(client, action, rest) {
1975
2130
  console.log("Verification email sent.");
1976
2131
  break;
1977
2132
  }
2133
+ case "update": {
2134
+ const identityEmail = rest[0];
2135
+ if (!identityEmail || !flags["display-name"]) {
2136
+ console.error("Usage: ascendkit email-identity update <email> --display-name <name>");
2137
+ return await exitCli(1);
2138
+ }
2139
+ const updated = await email.updateIdentity(client, {
2140
+ email: identityEmail,
2141
+ displayName: flags["display-name"],
2142
+ });
2143
+ printEmailIdentities(updated.identities ?? []);
2144
+ break;
2145
+ }
1978
2146
  case "resend": {
1979
2147
  const identityEmail = rest[0];
1980
2148
  if (!identityEmail) {
@@ -6,12 +6,14 @@ export interface CreateTemplateParams {
6
6
  bodyText: string;
7
7
  slug?: string;
8
8
  description?: string;
9
+ category?: "marketing" | "transactional";
9
10
  }
10
11
  export interface UpdateTemplateParams {
11
12
  subject?: string;
12
13
  bodyHtml?: string;
13
14
  bodyText?: string;
14
15
  changeNote?: string;
16
+ category?: "marketing" | "transactional";
15
17
  }
16
18
  export declare function createTemplate(client: AscendKitClient, params: CreateTemplateParams): Promise<unknown>;
17
19
  export declare function listTemplates(client: AscendKitClient, params?: {
@@ -80,6 +80,10 @@ export declare function createIdentity(client: AscendKitClient, identity: {
80
80
  displayName?: string;
81
81
  }): Promise<EmailIdentitiesResponse>;
82
82
  export declare function resendIdentityVerification(client: AscendKitClient, email: string): Promise<EmailIdentitiesResponse>;
83
+ export declare function updateIdentity(client: AscendKitClient, identity: {
84
+ email: string;
85
+ displayName: string;
86
+ }): Promise<EmailIdentitiesResponse>;
83
87
  export declare function setDefaultIdentity(client: AscendKitClient, identity: {
84
88
  email: string;
85
89
  displayName?: string;
@@ -37,6 +37,9 @@ export async function createIdentity(client, identity) {
37
37
  export async function resendIdentityVerification(client, email) {
38
38
  return client.managedPost(`/api/email/settings/identities/${encodeURIComponent(email)}/resend`, {});
39
39
  }
40
+ export async function updateIdentity(client, identity) {
41
+ return client.managedPost(`/api/email/settings/identities/${encodeURIComponent(identity.email)}/update`, { displayName: identity.displayName });
42
+ }
40
43
  export async function setDefaultIdentity(client, identity) {
41
44
  return client.managedPost(`/api/email/settings/identities/${encodeURIComponent(identity.email)}/default`, { displayName: identity.displayName ?? "" });
42
45
  }
@@ -64,6 +64,13 @@ export function registerEmailTools(server, client) {
64
64
  const data = await email.resendIdentityVerification(client, params.email);
65
65
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
66
66
  });
67
+ server.tool("email_identity_update", "Update the display name of a sender identity", {
68
+ email: z.string().describe("Sender identity email"),
69
+ displayName: z.string().describe("New display name"),
70
+ }, async (params) => {
71
+ const data = await email.updateIdentity(client, params);
72
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
73
+ });
67
74
  server.tool("email_identity_set_default", "Set the default sender identity for this environment", {
68
75
  email: z.string().describe("Sender identity email"),
69
76
  displayName: z.string().optional().describe("Optional updated display name"),
@@ -22,6 +22,7 @@ interface JourneyData {
22
22
  terminal?: boolean;
23
23
  }>;
24
24
  transitions?: Array<{
25
+ name?: string;
25
26
  from?: string;
26
27
  from_?: string;
27
28
  to?: string;
@@ -75,7 +76,9 @@ interface AnalyticsData {
75
76
  exited?: number;
76
77
  };
77
78
  }
78
- export declare function formatJourneyWithGuidance(journey: JourneyData): string;
79
+ export declare function formatJourneyWithGuidance(journey: JourneyData, opts?: {
80
+ suppressWarnings?: boolean;
81
+ }): string;
79
82
  export declare function formatJourneyList(data: {
80
83
  journeys: JourneyData[];
81
84
  total: number;
@@ -109,10 +112,14 @@ interface TransitionListItem {
109
112
  export declare function formatNodeList(data: {
110
113
  nodes: NodeListItem[];
111
114
  }): string;
112
- export declare function formatSingleNode(data: JourneyData, action: string, nodeName: string): string;
115
+ export declare function formatSingleNode(data: JourneyData, action: string, nodeName: string, opts?: {
116
+ suppressWarnings?: boolean;
117
+ }): string;
113
118
  export declare function formatTransitionList(data: {
114
119
  transitions: TransitionListItem[];
115
120
  }): string;
116
- export declare function formatSingleTransition(data: JourneyData, action: string, transitionName: string): string;
121
+ export declare function formatSingleTransition(data: JourneyData, action: string, transitionName: string, opts?: {
122
+ suppressWarnings?: boolean;
123
+ }): string;
117
124
  export declare function formatJourneyAnalytics(data: AnalyticsData): string;
118
125
  export {};
@@ -14,7 +14,7 @@ function nodeCount(journey) {
14
14
  function transitionCount(journey) {
15
15
  return (journey.transitions || []).length;
16
16
  }
17
- export function formatJourneyWithGuidance(journey) {
17
+ export function formatJourneyWithGuidance(journey, opts) {
18
18
  const nodes = nodeCount(journey);
19
19
  const transitions = transitionCount(journey);
20
20
  const stats = journey.stats;
@@ -50,11 +50,16 @@ export function formatJourneyWithGuidance(journey) {
50
50
  else {
51
51
  desc = trigger?.event || "event";
52
52
  }
53
- lines.push(` ${from} ${t.to} [${desc}]`);
53
+ const id = t.name ? ` (${t.name})` : "";
54
+ lines.push(` ${from} → ${t.to} [${desc}]${id}`);
54
55
  }
55
56
  }
56
- // Warnings
57
- if (journey.warnings && journey.warnings.length > 0) {
57
+ // Warnings — suppressed on paused journeys (mid-construction) or when opts.suppressWarnings is set
58
+ const showWarnings = !opts?.suppressWarnings &&
59
+ journey.status !== "paused" &&
60
+ journey.warnings &&
61
+ journey.warnings.length > 0;
62
+ if (showWarnings) {
58
63
  lines.push("");
59
64
  lines.push("Warnings:");
60
65
  for (const w of journey.warnings) {
@@ -146,8 +151,8 @@ export function formatNodeList(data) {
146
151
  }
147
152
  return lines.join("\n");
148
153
  }
149
- export function formatSingleNode(data, action, nodeName) {
150
- return `${action} node '${nodeName}'.\n\n${formatJourneyWithGuidance(data)}`;
154
+ export function formatSingleNode(data, action, nodeName, opts) {
155
+ return `${action} node '${nodeName}'.\n\n${formatJourneyWithGuidance(data, opts)}`;
151
156
  }
152
157
  export function formatTransitionList(data) {
153
158
  const transitions = data.transitions || [];
@@ -162,8 +167,8 @@ export function formatTransitionList(data) {
162
167
  }
163
168
  return lines.join("\n");
164
169
  }
165
- export function formatSingleTransition(data, action, transitionName) {
166
- return `${action} transition '${transitionName}'.\n\n${formatJourneyWithGuidance(data)}`;
170
+ export function formatSingleTransition(data, action, transitionName, opts) {
171
+ return `${action} transition '${transitionName}'.\n\n${formatJourneyWithGuidance(data, opts)}`;
167
172
  }
168
173
  export function formatJourneyAnalytics(data) {
169
174
  const { journey, nodes, transitions, terminalDistribution, totals } = data;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ascendkit/cli",
3
- "version": "0.3.2",
3
+ "version": "0.3.9",
4
4
  "description": "AscendKit CLI and MCP server",
5
5
  "author": "ascendkit.dev",
6
6
  "license": "MIT",