@ascendkit/cli 0.2.0 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -32,6 +32,7 @@ export declare class AscendKitClient {
32
32
  delete<T = unknown>(path: string): Promise<T>;
33
33
  /** Write operation requiring both public key and platform auth. */
34
34
  managedRequest<T = unknown>(method: string, path: string, body?: unknown): Promise<T>;
35
+ managedGet<T = unknown>(path: string): Promise<T>;
35
36
  managedPut<T = unknown>(path: string, body?: unknown): Promise<T>;
36
37
  managedPost<T = unknown>(path: string, body?: unknown): Promise<T>;
37
38
  managedDelete<T = unknown>(path: string): Promise<T>;
@@ -179,6 +179,9 @@ export class AscendKitClient {
179
179
  const json = (await response.json());
180
180
  return json.data;
181
181
  }
182
+ managedGet(path) {
183
+ return this.managedRequest("GET", path);
184
+ }
182
185
  managedPut(path, body) {
183
186
  return this.managedRequest("PUT", path, body);
184
187
  }
package/dist/cli.js CHANGED
@@ -11,6 +11,8 @@ import * as platform from "./commands/platform.js";
11
11
  import * as journeys from "./commands/journeys.js";
12
12
  import * as email from "./commands/email.js";
13
13
  import * as webhooks from "./commands/webhooks.js";
14
+ import * as campaigns from "./commands/campaigns.js";
15
+ import * as importCmd from "./commands/import.js";
14
16
  import { parseDelay } from "./utils/duration.js";
15
17
  const require = createRequire(import.meta.url);
16
18
  const { version: CLI_VERSION } = require("../package.json");
@@ -32,6 +34,8 @@ Services:
32
34
  journey Lifecycle journeys, nodes, transitions
33
35
  email Email settings, domain verification, DNS
34
36
  webhook Webhook endpoints and testing
37
+ campaign Email campaigns, scheduling, analytics
38
+ import Import users from external auth providers
35
39
 
36
40
  Project Management:
37
41
  projects List and create projects
@@ -89,8 +93,8 @@ Commands:
89
93
  journey archive <journey-id>
90
94
  journey analytics <journey-id>
91
95
  journey list-nodes <journey-id>
92
- journey add-node <journey-id> --name <node-name> [--action <json>] [--terminal <true|false>]
93
- journey edit-node <journey-id> <node-name> [--action <json>] [--terminal <true|false>]
96
+ journey add-node <journey-id> --name <node-name> [--action <json>] [--email-id <email>] [--terminal <true|false>]
97
+ journey edit-node <journey-id> <node-name> [--action <json>] [--email-id <email>] [--terminal <true|false>]
94
98
  journey remove-node <journey-id> <node-name>
95
99
  journey list-transitions <journey-id> [--from <node-name>] [--to <node-name>]
96
100
  journey add-transition <journey-id> --from <node-name> --to <node-name> --trigger <json> [--priority <n>] [--name <transition-name>]
@@ -117,13 +121,54 @@ Commands:
117
121
  webhook update <webhook-id> [--url <url>] [--events <e1,e2,...>] [--status <active|inactive>]
118
122
  webhook delete <webhook-id>
119
123
  webhook test <webhook-id> [--event <event-type>]`,
124
+ campaign: `Usage: ascendkit campaign <command>
125
+
126
+ Commands:
127
+ campaign create --name <name> --template <template-id> --audience <json> [--scheduled-at <datetime>]
128
+ campaign list [--status <draft|scheduled|sending|sent|failed|cancelled>]
129
+ campaign show <campaign-id>
130
+ campaign update <campaign-id> [--name <name>] [--template <template-id>] [--audience <json>] [--scheduled-at <datetime>]
131
+ campaign preview <campaign-id>
132
+ campaign schedule <campaign-id> --at <datetime>
133
+ campaign cancel <campaign-id>
134
+ campaign analytics <campaign-id>
135
+
136
+ Notes:
137
+ - --audience is a JSON filter object, e.g. '{"tags":{"$in":["premium"]}}'
138
+ - --scheduled-at / --at accepts ISO 8601 datetime, e.g. 2026-03-15T10:00:00Z
139
+ - cancel deletes a draft/failed campaign or cancels a scheduled/sending campaign`,
120
140
  env: `Usage: ascendkit env <command>
121
141
 
122
142
  Commands:
123
143
  env list --project <project-id>
124
144
  env use <tier> --project <project-id>
125
145
  env update <env-id> --project <project-id> [--name <name>] [--description <desc>]
126
- env promote <env-id> --target <tier>`,
146
+ env promote <env-id> --target <tier>
147
+ env set-var <key> <value>
148
+ env unset-var <key>
149
+ env list-vars`,
150
+ import: `Usage: ascendkit import <source> [options]
151
+
152
+ Sources:
153
+ clerk Import users from Clerk
154
+
155
+ Commands:
156
+ import clerk --api-key <key> [options]
157
+ import clerk --file <path> [options]
158
+ import create-migration-journey [--from-identity <email>]
159
+
160
+ Options:
161
+ --api-key <key> Clerk secret API key (fetches users from Clerk API)
162
+ --file <path> Path to Clerk dashboard export (JSON)
163
+ --instance-url <url> Custom Clerk API URL (default: https://api.clerk.com)
164
+ --execute Run the import for real (default is dry-run preview)
165
+ --users Import users (included by default; use to select only users)
166
+ --settings Import auth settings / OAuth providers (included by default)
167
+ --from-identity <email> Email identity for migration journey emails
168
+
169
+ By default, import runs in dry-run mode and includes both users and settings.
170
+ Pass --execute to apply changes. Pass --users or --settings alone to select
171
+ only that phase (e.g. --users --execute imports only users, not settings).`,
127
172
  projects: `Usage: ascendkit projects <command>
128
173
 
129
174
  Commands:
@@ -292,7 +337,11 @@ async function run() {
292
337
  process.exit(1);
293
338
  }
294
339
  try {
295
- output(await platform.createProject(flags.name, flags.description, flags.services?.split(",")));
340
+ const proj = await platform.createProject(flags.name, flags.description, flags.services?.split(","));
341
+ const env = proj.environment;
342
+ console.log(`Project created: ${proj.id}`);
343
+ if (env)
344
+ console.log(`Environment: ${env.publicKey}`);
296
345
  }
297
346
  catch (err) {
298
347
  let message = err instanceof Error ? err.message : String(err);
@@ -355,12 +404,163 @@ async function run() {
355
404
  case "webhook":
356
405
  await runWebhook(client, action, args.slice(2));
357
406
  break;
407
+ case "campaign":
408
+ await runCampaign(client, action, args.slice(2));
409
+ break;
410
+ case "import":
411
+ await runImport(client, action, args.slice(2));
412
+ break;
358
413
  default:
359
414
  console.error(`Unknown command: ${domain}`);
360
415
  console.error('Run "ascendkit --help" for usage');
361
416
  process.exit(1);
362
417
  }
363
418
  }
419
+ async function runImport(client, source, rest) {
420
+ const flags = parseFlags(rest);
421
+ if (source === "create-migration-journey") {
422
+ const result = await importCmd.instantiateMigrationJourney(client, flags["from-identity"]);
423
+ output(result);
424
+ console.log("\nNext steps:");
425
+ console.log(" ascendkit journey list — review created journeys");
426
+ console.log(" ascendkit templates list — review migration email templates");
427
+ console.log(" ascendkit journey activate <journey-id> — activate when ready");
428
+ return;
429
+ }
430
+ if (source !== "clerk") {
431
+ console.error(`Unsupported import source: ${source}`);
432
+ console.error("Supported sources: clerk");
433
+ console.error('Run "ascendkit help import" for usage');
434
+ process.exit(1);
435
+ }
436
+ const execute = flags.execute === "true" || flags.execute === "";
437
+ const dryRun = !execute;
438
+ const hasUsers = flags.users !== undefined;
439
+ const hasSettings = flags.settings !== undefined;
440
+ // If neither --users nor --settings is passed, both default to true.
441
+ // If either is passed, only the specified phases run.
442
+ const importUsers = (!hasUsers && !hasSettings) || hasUsers;
443
+ const importSettings = (!hasUsers && !hasSettings) || hasSettings;
444
+ const apiKey = flags["api-key"];
445
+ const filePath = flags.file;
446
+ if (!apiKey && !filePath) {
447
+ console.error("Usage: ascendkit import clerk --api-key <key> | --file <path> [--execute]");
448
+ process.exit(1);
449
+ }
450
+ let clerkUsers = [];
451
+ if (filePath) {
452
+ console.log(`Reading Clerk export from ${filePath}...`);
453
+ const rawUsers = importCmd.parseClerkExport(filePath);
454
+ clerkUsers = rawUsers.map(importCmd.transformClerkUser);
455
+ console.log(`Parsed ${clerkUsers.length} users from file.`);
456
+ }
457
+ else {
458
+ console.log("Fetching users from Clerk API...");
459
+ const rawUsers = await importCmd.fetchClerkUsers(apiKey, flags["instance-url"]);
460
+ clerkUsers = rawUsers.map(importCmd.transformClerkUser);
461
+ console.log(`Fetched ${clerkUsers.length} users from Clerk.`);
462
+ }
463
+ if (clerkUsers.length === 0 && importUsers) {
464
+ console.log("No users to import.");
465
+ return;
466
+ }
467
+ if (dryRun) {
468
+ console.log("\n--- DRY RUN (pass --execute to apply changes) ---");
469
+ }
470
+ // Batch in chunks of 500 (backend max)
471
+ const batchSize = 500;
472
+ let totalImported = 0;
473
+ const allDuplicates = [];
474
+ const allErrors = [];
475
+ const allWarnings = [];
476
+ if (importUsers) {
477
+ for (let i = 0; i < clerkUsers.length; i += batchSize) {
478
+ const batch = clerkUsers.slice(i, i + batchSize);
479
+ const batchNum = Math.floor(i / batchSize) + 1;
480
+ const totalBatches = Math.ceil(clerkUsers.length / batchSize);
481
+ if (totalBatches > 1) {
482
+ console.log(`\nBatch ${batchNum}/${totalBatches} (${batch.length} users)...`);
483
+ }
484
+ const result = await importCmd.importUsers(client, {
485
+ source: "clerk",
486
+ users: batch,
487
+ dryRun,
488
+ });
489
+ totalImported += result.imported;
490
+ allDuplicates.push(...result.duplicates);
491
+ allErrors.push(...result.errors);
492
+ allWarnings.push(...result.warnings);
493
+ }
494
+ }
495
+ else {
496
+ console.log("Skipping user import (--settings only).");
497
+ }
498
+ if (importSettings) {
499
+ const derived = importCmd.deriveSettingsFromUsers(clerkUsers);
500
+ // --providers override lets the user pick exactly which SSO to enable
501
+ const providerOverride = flags.providers;
502
+ const ssoProviders = providerOverride
503
+ ? providerOverride.split(",").map((s) => s.trim()).filter(Boolean)
504
+ : derived.providers;
505
+ const providers = [];
506
+ if (derived.hasCredentials)
507
+ providers.push("credentials");
508
+ providers.push(...ssoProviders);
509
+ console.log("\nAuth settings from user data:");
510
+ console.log(` Credentials (email/password): ${derived.hasCredentials ? "yes" : "no"}`);
511
+ console.log(` SSO providers: ${ssoProviders.length > 0 ? ssoProviders.join(", ") : "none"}`);
512
+ if (providerOverride) {
513
+ console.log(` (overridden via --providers)`);
514
+ }
515
+ if (providers.length > 0) {
516
+ const settingsPayload = {
517
+ source: "clerk",
518
+ users: [],
519
+ authSettings: { enabledProviders: providers },
520
+ dryRun,
521
+ };
522
+ const settingsResult = await importCmd.importUsers(client, settingsPayload);
523
+ allWarnings.push(...settingsResult.warnings);
524
+ if (!dryRun) {
525
+ console.log(`\nAuth settings applied: ${providers.join(", ")}`);
526
+ console.log("Configure OAuth secrets in the portal under Auth → OAuth settings.");
527
+ }
528
+ else {
529
+ console.log(`\nWill enable: ${providers.join(", ")}`);
530
+ console.log("Use --providers linkedin to pick specific SSO providers.");
531
+ }
532
+ }
533
+ }
534
+ else {
535
+ console.log("Skipping auth settings import (--users only).");
536
+ }
537
+ console.log(`\n--- Import Summary ---`);
538
+ if (importUsers) {
539
+ console.log(`Imported: ${totalImported}`);
540
+ if (allDuplicates.length > 0) {
541
+ console.log(`Duplicates: ${allDuplicates.length}`);
542
+ }
543
+ }
544
+ if (allErrors.length > 0) {
545
+ console.log(`Errors: ${allErrors.length}`);
546
+ for (const err of allErrors.slice(0, 10)) {
547
+ console.log(` - ${err.email}: ${err.reason}`);
548
+ }
549
+ if (allErrors.length > 10) {
550
+ console.log(` ... and ${allErrors.length - 10} more`);
551
+ }
552
+ }
553
+ for (const warning of allWarnings) {
554
+ console.log(`Warning: ${warning}`);
555
+ }
556
+ if (dryRun) {
557
+ console.log("\nThis was a dry run. Pass --execute to apply changes.");
558
+ }
559
+ else if (totalImported > 0) {
560
+ console.log("\nTo set up migration emails, run:\n" +
561
+ " ascendkit import create-migration-journey");
562
+ }
563
+ }
364
564
  async function runEnv(action, rest) {
365
565
  const flags = parseFlags(rest);
366
566
  switch (action) {
@@ -447,9 +647,69 @@ async function runEnv(action, rest) {
447
647
  }
448
648
  break;
449
649
  }
650
+ case "set-var": {
651
+ const key = rest[0];
652
+ const value = rest[1];
653
+ if (!key || value === undefined) {
654
+ console.error("Usage: ascendkit env set-var <key> <value>");
655
+ process.exit(1);
656
+ }
657
+ const ctx = loadEnvContext();
658
+ if (!ctx) {
659
+ console.error("No environment set. Run: ascendkit env use <tier> --project <project-id>");
660
+ process.exit(1);
661
+ }
662
+ const current = await platform.getEnvironment(ctx.projectId, ctx.environmentId);
663
+ const vars = { ...(current.variables ?? {}) };
664
+ vars[key] = value;
665
+ await platform.updateEnvironmentVariables(ctx.projectId, ctx.environmentId, vars);
666
+ console.log(`Set ${key}=${value}`);
667
+ break;
668
+ }
669
+ case "unset-var": {
670
+ const key = rest[0];
671
+ if (!key) {
672
+ console.error("Usage: ascendkit env unset-var <key>");
673
+ process.exit(1);
674
+ }
675
+ const ctx = loadEnvContext();
676
+ if (!ctx) {
677
+ console.error("No environment set. Run: ascendkit env use <tier> --project <project-id>");
678
+ process.exit(1);
679
+ }
680
+ const current = await platform.getEnvironment(ctx.projectId, ctx.environmentId);
681
+ const vars = { ...(current.variables ?? {}) };
682
+ if (!(key in vars)) {
683
+ console.error(`Variable "${key}" not found.`);
684
+ process.exit(1);
685
+ }
686
+ delete vars[key];
687
+ await platform.updateEnvironmentVariables(ctx.projectId, ctx.environmentId, vars);
688
+ console.log(`Unset ${key}`);
689
+ break;
690
+ }
691
+ case "list-vars": {
692
+ const ctx = loadEnvContext();
693
+ if (!ctx) {
694
+ console.error("No environment set. Run: ascendkit env use <tier> --project <project-id>");
695
+ process.exit(1);
696
+ }
697
+ const current = await platform.getEnvironment(ctx.projectId, ctx.environmentId);
698
+ const vars = current.variables ?? {};
699
+ const entries = Object.entries(vars);
700
+ if (entries.length === 0) {
701
+ console.log("No variables set.");
702
+ }
703
+ else {
704
+ for (const [k, v] of entries) {
705
+ console.log(`${k}=${v || "(empty)"}`);
706
+ }
707
+ }
708
+ break;
709
+ }
450
710
  default:
451
711
  console.error(`Unknown env command: ${action}`);
452
- console.error("Usage: ascendkit env list|use|update|promote");
712
+ console.error("Usage: ascendkit env list|use|update|promote|set-var|unset-var|list-vars");
453
713
  process.exit(1);
454
714
  }
455
715
  }
@@ -472,7 +732,14 @@ async function runAuth(client, action, rest) {
472
732
  }
473
733
  if (flags["session-duration"])
474
734
  params.sessionDuration = flags["session-duration"];
475
- output(await auth.updateSettings(client, params));
735
+ const s = await auth.updateSettings(client, params);
736
+ console.log(`Providers: ${Array.isArray(s.providers) ? s.providers.join(", ") : "none"}`);
737
+ const f = s.features ?? {};
738
+ const enabled = Object.entries(f).filter(([, v]) => v).map(([k]) => k);
739
+ if (enabled.length)
740
+ console.log(`Features: ${enabled.join(", ")}`);
741
+ if (s.sessionDuration)
742
+ console.log(`Session: ${s.sessionDuration}`);
476
743
  }
477
744
  else {
478
745
  output(await auth.getSettings(client));
@@ -960,12 +1227,19 @@ async function runJourney(client, action, rest) {
960
1227
  break;
961
1228
  case "add-node": {
962
1229
  if (!rest[0] || !flags.name) {
963
- console.error("Usage: ascendkit journey add-node <journey-id> --name <node-name> [--action <json>] [--terminal <true|false>]");
1230
+ console.error("Usage: ascendkit journey add-node <journey-id> --name <node-name> [--action <json>] [--email-id <email>] [--terminal <true|false>]");
964
1231
  process.exit(1);
965
1232
  }
966
1233
  const params = { name: flags.name };
967
1234
  if (flags.action)
968
1235
  params.action = JSON.parse(flags.action);
1236
+ if (flags["email-id"]) {
1237
+ if (!params.action || params.action.type !== "send_email") {
1238
+ console.error("--email-id requires a send_email action (use --action '{\"type\": \"send_email\", \"templateSlug\": \"...\"}')");
1239
+ process.exit(1);
1240
+ }
1241
+ params.action.fromIdentityEmail = flags["email-id"];
1242
+ }
969
1243
  if (flags.terminal)
970
1244
  params.terminal = flags.terminal === "true";
971
1245
  output(await journeys.addNode(client, rest[0], params));
@@ -973,12 +1247,28 @@ async function runJourney(client, action, rest) {
973
1247
  }
974
1248
  case "edit-node": {
975
1249
  if (!rest[0] || !rest[1]) {
976
- console.error("Usage: ascendkit journey edit-node <journey-id> <node-name> [--action <json>] [--terminal <true|false>]");
1250
+ console.error("Usage: ascendkit journey edit-node <journey-id> <node-name> [--action <json>] [--email-id <email>] [--terminal <true|false>]");
977
1251
  process.exit(1);
978
1252
  }
979
1253
  const params = {};
980
1254
  if (flags.action)
981
1255
  params.action = JSON.parse(flags.action);
1256
+ if (flags["email-id"]) {
1257
+ if (!params.action) {
1258
+ const nodesData = await journeys.listNodes(client, rest[0]);
1259
+ const current = nodesData.nodes?.find((n) => n.name === rest[1]);
1260
+ if (!current?.action || current.action.type !== "send_email") {
1261
+ console.error("--email-id can only be set on send_email nodes");
1262
+ process.exit(1);
1263
+ }
1264
+ params.action = current.action;
1265
+ }
1266
+ else if (params.action.type !== "send_email") {
1267
+ console.error("--email-id requires a send_email action");
1268
+ process.exit(1);
1269
+ }
1270
+ params.action.fromIdentityEmail = flags["email-id"];
1271
+ }
982
1272
  if (flags.terminal)
983
1273
  params.terminal = flags.terminal === "true";
984
1274
  output(await journeys.editNode(client, rest[0], rest[1], params));
@@ -1089,17 +1379,22 @@ async function runWebhook(client, action, rest) {
1089
1379
  }
1090
1380
  output(await webhooks.getWebhook(client, rest[0]));
1091
1381
  break;
1092
- case "update":
1382
+ case "update": {
1093
1383
  if (!rest[0]) {
1094
1384
  console.error("Usage: ascendkit webhook update <webhook-id> [--flags]");
1095
1385
  process.exit(1);
1096
1386
  }
1097
- output(await webhooks.updateWebhook(client, rest[0], {
1387
+ const updated = await webhooks.updateWebhook(client, rest[0], {
1098
1388
  url: flags.url,
1099
1389
  events: flags.events ? flags.events.split(",") : undefined,
1100
1390
  status: flags.status,
1101
- }));
1391
+ });
1392
+ console.log(`Updated: ${updated.id}`);
1393
+ console.log(`URL: ${updated.url}`);
1394
+ console.log(`Status: ${updated.status}`);
1395
+ console.log(`Events: ${Array.isArray(updated.events) ? updated.events.join(", ") : updated.events}`);
1102
1396
  break;
1397
+ }
1103
1398
  case "delete":
1104
1399
  if (!rest[0]) {
1105
1400
  console.error("Usage: ascendkit webhook delete <webhook-id>");
@@ -1120,6 +1415,115 @@ async function runWebhook(client, action, rest) {
1120
1415
  process.exit(1);
1121
1416
  }
1122
1417
  }
1418
+ async function runCampaign(client, action, rest) {
1419
+ const flags = parseFlags(rest);
1420
+ if (!action) {
1421
+ console.log(HELP_SECTION.campaign);
1422
+ return;
1423
+ }
1424
+ switch (action) {
1425
+ case "create":
1426
+ if (!flags.name || !flags.template || !flags.audience) {
1427
+ console.error("Usage: ascendkit campaign create --name <name> --template <template-id> --audience <json> [--scheduled-at <datetime>]");
1428
+ process.exit(1);
1429
+ }
1430
+ let createFilter;
1431
+ try {
1432
+ createFilter = JSON.parse(flags.audience);
1433
+ }
1434
+ catch {
1435
+ console.error("Invalid JSON for --audience flag. Provide a valid JSON object.");
1436
+ process.exit(1);
1437
+ }
1438
+ output(await campaigns.createCampaign(client, {
1439
+ name: flags.name,
1440
+ templateId: flags.template,
1441
+ audienceFilter: createFilter,
1442
+ scheduledAt: flags["scheduled-at"],
1443
+ }));
1444
+ break;
1445
+ case "list": {
1446
+ const items = await campaigns.listCampaigns(client, flags.status);
1447
+ table(items, [
1448
+ { key: "id", label: "ID" },
1449
+ { key: "name", label: "Name", width: 30 },
1450
+ { key: "status", label: "Status" },
1451
+ { key: "scheduledAt", label: "Scheduled", width: 22 },
1452
+ { key: "templateId", label: "Template" },
1453
+ ]);
1454
+ break;
1455
+ }
1456
+ case "show":
1457
+ case "get":
1458
+ if (!rest[0]) {
1459
+ console.error("Usage: ascendkit campaign show <campaign-id>");
1460
+ process.exit(1);
1461
+ }
1462
+ output(await campaigns.getCampaign(client, rest[0]));
1463
+ break;
1464
+ case "update":
1465
+ if (!rest[0]) {
1466
+ console.error("Usage: ascendkit campaign update <campaign-id> [--flags]");
1467
+ process.exit(1);
1468
+ }
1469
+ let updateFilter;
1470
+ if (flags.audience) {
1471
+ try {
1472
+ updateFilter = JSON.parse(flags.audience);
1473
+ }
1474
+ catch {
1475
+ console.error("Invalid JSON for --audience flag. Provide a valid JSON object.");
1476
+ process.exit(1);
1477
+ }
1478
+ }
1479
+ output(await campaigns.updateCampaign(client, rest[0], {
1480
+ name: flags.name,
1481
+ templateId: flags.template,
1482
+ audienceFilter: updateFilter,
1483
+ scheduledAt: flags["scheduled-at"],
1484
+ }));
1485
+ break;
1486
+ case "preview":
1487
+ if (!rest[0]) {
1488
+ console.error("Usage: ascendkit campaign preview <campaign-id>");
1489
+ process.exit(1);
1490
+ }
1491
+ {
1492
+ const detail = await campaigns.getCampaign(client, rest[0]);
1493
+ if (!detail?.audienceFilter) {
1494
+ console.error("Campaign has no audience filter set.");
1495
+ process.exit(1);
1496
+ }
1497
+ output(await campaigns.previewAudience(client, detail.audienceFilter));
1498
+ }
1499
+ break;
1500
+ case "schedule":
1501
+ if (!rest[0] || !flags.at) {
1502
+ console.error("Usage: ascendkit campaign schedule <campaign-id> --at <datetime>");
1503
+ process.exit(1);
1504
+ }
1505
+ output(await campaigns.updateCampaign(client, rest[0], { scheduledAt: flags.at }));
1506
+ break;
1507
+ case "cancel":
1508
+ if (!rest[0]) {
1509
+ console.error("Usage: ascendkit campaign cancel <campaign-id>");
1510
+ process.exit(1);
1511
+ }
1512
+ output(await campaigns.deleteCampaign(client, rest[0]));
1513
+ break;
1514
+ case "analytics":
1515
+ if (!rest[0]) {
1516
+ console.error("Usage: ascendkit campaign analytics <campaign-id>");
1517
+ process.exit(1);
1518
+ }
1519
+ output(await campaigns.getCampaignAnalytics(client, rest[0]));
1520
+ break;
1521
+ default:
1522
+ console.error(`Unknown campaign command: ${action}`);
1523
+ console.error('Run "ascendkit campaign --help" for usage');
1524
+ process.exit(1);
1525
+ }
1526
+ }
1123
1527
  async function runEmail(client, action, rest) {
1124
1528
  const flags = parseFlags(rest);
1125
1529
  switch (action) {
@@ -13,5 +13,9 @@ export declare function getSettings(client: AscendKitClient): Promise<unknown>;
13
13
  export declare function updateSettings(client: AscendKitClient, params: UpdateSettingsParams): Promise<unknown>;
14
14
  export declare function updateProviders(client: AscendKitClient, providers: string[]): Promise<unknown>;
15
15
  export declare function updateOAuthCredentials(client: AscendKitClient, provider: string, clientId: string, clientSecret: string, callbackUrl?: string): Promise<unknown>;
16
+ export declare function deleteOAuthCredentials(client: AscendKitClient, provider: string): Promise<unknown>;
16
17
  export declare function getOAuthSetupUrl(portalUrl: string, provider: string, publicKey?: string): string;
17
18
  export declare function listUsers(client: AscendKitClient): Promise<unknown>;
19
+ export declare function deleteUser(client: AscendKitClient, userId: string): Promise<unknown>;
20
+ export declare function bulkDeleteUsers(client: AscendKitClient, userIds: string[]): Promise<unknown>;
21
+ export declare function reactivateUser(client: AscendKitClient, userId: string): Promise<unknown>;
@@ -20,6 +20,9 @@ export async function updateOAuthCredentials(client, provider, clientId, clientS
20
20
  body.callbackUrl = callbackUrl;
21
21
  return client.managedPut(`/api/auth/settings/oauth/${provider}`, body);
22
22
  }
23
+ export async function deleteOAuthCredentials(client, provider) {
24
+ return client.managedDelete(`/api/auth/settings/oauth/${provider}`);
25
+ }
23
26
  export function getOAuthSetupUrl(portalUrl, provider, publicKey) {
24
27
  const base = `${portalUrl}/settings/oauth/${provider}`;
25
28
  return publicKey ? `${base}?pk=${encodeURIComponent(publicKey)}` : base;
@@ -27,3 +30,12 @@ export function getOAuthSetupUrl(portalUrl, provider, publicKey) {
27
30
  export async function listUsers(client) {
28
31
  return client.managedRequest("GET", "/api/users");
29
32
  }
33
+ export async function deleteUser(client, userId) {
34
+ return client.managedDelete(`/api/users/${userId}`);
35
+ }
36
+ export async function bulkDeleteUsers(client, userIds) {
37
+ return client.managedPost("/api/users/bulk-deactivate", { userIds });
38
+ }
39
+ export async function reactivateUser(client, userId) {
40
+ return client.managedPost(`/api/users/${userId}/reactivate`);
41
+ }
@@ -0,0 +1,22 @@
1
+ import { AscendKitClient } from "../api/client.js";
2
+ export interface CreateCampaignParams {
3
+ name: string;
4
+ templateId: string;
5
+ audienceFilter: Record<string, unknown>;
6
+ scheduledAt?: string;
7
+ fromIdentityEmail?: string;
8
+ }
9
+ export interface UpdateCampaignParams {
10
+ name?: string;
11
+ templateId?: string;
12
+ audienceFilter?: Record<string, unknown>;
13
+ scheduledAt?: string;
14
+ fromIdentityEmail?: string;
15
+ }
16
+ export declare function createCampaign(client: AscendKitClient, params: CreateCampaignParams): Promise<unknown>;
17
+ export declare function listCampaigns(client: AscendKitClient, status?: string, limit?: number): Promise<unknown>;
18
+ export declare function getCampaign(client: AscendKitClient, campaignId: string): Promise<unknown>;
19
+ export declare function updateCampaign(client: AscendKitClient, campaignId: string, params: UpdateCampaignParams): Promise<unknown>;
20
+ export declare function deleteCampaign(client: AscendKitClient, campaignId: string): Promise<unknown>;
21
+ export declare function previewAudience(client: AscendKitClient, audienceFilter: Record<string, unknown>): Promise<unknown>;
22
+ export declare function getCampaignAnalytics(client: AscendKitClient, campaignId: string): Promise<unknown>;
@@ -0,0 +1,25 @@
1
+ export async function createCampaign(client, params) {
2
+ return client.managedPost("/api/v1/campaigns", params);
3
+ }
4
+ export async function listCampaigns(client, status, limit = 200) {
5
+ const params = new URLSearchParams();
6
+ if (status)
7
+ params.set("status", status);
8
+ params.set("limit", String(limit));
9
+ return client.managedGet(`/api/v1/campaigns?${params.toString()}`);
10
+ }
11
+ export async function getCampaign(client, campaignId) {
12
+ return client.managedGet(`/api/v1/campaigns/${campaignId}`);
13
+ }
14
+ export async function updateCampaign(client, campaignId, params) {
15
+ return client.managedPut(`/api/v1/campaigns/${campaignId}`, params);
16
+ }
17
+ export async function deleteCampaign(client, campaignId) {
18
+ return client.managedDelete(`/api/v1/campaigns/${campaignId}`);
19
+ }
20
+ export async function previewAudience(client, audienceFilter) {
21
+ return client.managedPost("/api/v1/campaigns/preview", { audienceFilter });
22
+ }
23
+ export async function getCampaignAnalytics(client, campaignId) {
24
+ return client.managedGet(`/api/v1/campaigns/${campaignId}/analytics`);
25
+ }
@@ -0,0 +1,75 @@
1
+ import { AscendKitClient } from "../api/client.js";
2
+ export interface ImportUserRecord {
3
+ sourceId: string;
4
+ email: string;
5
+ name?: string;
6
+ image?: string;
7
+ emailVerified?: boolean;
8
+ hadPassword?: boolean;
9
+ oauthProviders?: Array<{
10
+ providerId: string;
11
+ accountId: string;
12
+ }>;
13
+ metadata?: Record<string, unknown>;
14
+ tags?: string[];
15
+ createdAt?: string;
16
+ lastLoginAt?: string;
17
+ }
18
+ export interface ImportUsersPayload {
19
+ source: string;
20
+ users: ImportUserRecord[];
21
+ authSettings?: {
22
+ enabledProviders?: string[];
23
+ oauthClientIds?: Record<string, string>;
24
+ };
25
+ dryRun?: boolean;
26
+ }
27
+ export interface ImportResult {
28
+ imported: number;
29
+ duplicates: Array<{
30
+ email: string;
31
+ sourceId: string;
32
+ }>;
33
+ errors: Array<{
34
+ email: string;
35
+ sourceId: string;
36
+ reason: string;
37
+ }>;
38
+ warnings: string[];
39
+ }
40
+ export declare function importUsers(client: AscendKitClient, payload: ImportUsersPayload): Promise<ImportResult>;
41
+ export declare function instantiateMigrationJourney(client: AscendKitClient, fromIdentityEmail?: string): Promise<unknown>;
42
+ interface ClerkUser {
43
+ id: string;
44
+ email_addresses?: Array<{
45
+ id: string;
46
+ email_address: string;
47
+ verification?: {
48
+ status: string;
49
+ };
50
+ }>;
51
+ primary_email_address_id?: string;
52
+ first_name?: string;
53
+ last_name?: string;
54
+ image_url?: string;
55
+ password_enabled?: boolean;
56
+ external_accounts?: Array<{
57
+ id: string;
58
+ provider: string;
59
+ provider_user_id: string;
60
+ }>;
61
+ public_metadata?: Record<string, unknown>;
62
+ private_metadata?: Record<string, unknown>;
63
+ created_at?: number;
64
+ last_sign_in_at?: number;
65
+ last_active_at?: number;
66
+ }
67
+ export declare function transformClerkUser(clerk: ClerkUser): ImportUserRecord;
68
+ export declare function fetchClerkUsers(apiKey: string, instanceUrl?: string): Promise<ClerkUser[]>;
69
+ export interface ClerkDerivedSettings {
70
+ hasCredentials: boolean;
71
+ providers: string[];
72
+ }
73
+ export declare function deriveSettingsFromUsers(users: ImportUserRecord[]): ClerkDerivedSettings;
74
+ export declare function parseClerkExport(filePath: string): ClerkUser[];
75
+ export {};
@@ -0,0 +1,97 @@
1
+ import { readFileSync } from "fs";
2
+ export async function importUsers(client, payload) {
3
+ return client.managedPost("/api/import/users", payload);
4
+ }
5
+ export async function instantiateMigrationJourney(client, fromIdentityEmail) {
6
+ return client.managedPost("/api/import/instantiate-migration-journey", {
7
+ fromIdentityEmail: fromIdentityEmail ?? null,
8
+ });
9
+ }
10
+ function clerkProviderToAscendKit(provider) {
11
+ const map = {
12
+ oauth_google: "google",
13
+ oauth_github: "github",
14
+ oauth_linkedin: "linkedin",
15
+ oauth_linkedin_oidc: "linkedin",
16
+ };
17
+ return map[provider] ?? provider.replace(/^oauth_/, "");
18
+ }
19
+ export function transformClerkUser(clerk) {
20
+ const primaryEmail = clerk.email_addresses?.find((e) => e.id === clerk.primary_email_address_id);
21
+ const email = primaryEmail?.email_address ?? clerk.email_addresses?.[0]?.email_address ?? "";
22
+ const emailVerified = primaryEmail?.verification?.status === "verified";
23
+ const name = [clerk.first_name, clerk.last_name].filter(Boolean).join(" ") || undefined;
24
+ const oauthProviders = (clerk.external_accounts ?? []).map((ext) => ({
25
+ providerId: clerkProviderToAscendKit(ext.provider),
26
+ accountId: ext.provider_user_id,
27
+ }));
28
+ // Clerk timestamps are Unix ms
29
+ const createdAt = clerk.created_at
30
+ ? new Date(clerk.created_at).toISOString()
31
+ : undefined;
32
+ const lastLoginAt = clerk.last_sign_in_at
33
+ ? new Date(clerk.last_sign_in_at).toISOString()
34
+ : undefined;
35
+ return {
36
+ sourceId: clerk.id,
37
+ email,
38
+ name,
39
+ image: clerk.image_url || undefined,
40
+ emailVerified,
41
+ hadPassword: clerk.password_enabled ?? false,
42
+ oauthProviders,
43
+ metadata: clerk.public_metadata ?? undefined,
44
+ createdAt,
45
+ lastLoginAt,
46
+ };
47
+ }
48
+ export async function fetchClerkUsers(apiKey, instanceUrl) {
49
+ const baseUrl = instanceUrl ?? "https://api.clerk.com";
50
+ const allUsers = [];
51
+ let offset = 0;
52
+ const limit = 100;
53
+ while (true) {
54
+ const url = `${baseUrl}/v1/users?limit=${limit}&offset=${offset}`;
55
+ const response = await fetch(url, {
56
+ headers: {
57
+ Authorization: `Bearer ${apiKey}`,
58
+ "Content-Type": "application/json",
59
+ },
60
+ });
61
+ if (!response.ok) {
62
+ const text = await response.text();
63
+ throw new Error(`Clerk API error ${response.status}: ${text}`);
64
+ }
65
+ const users = (await response.json());
66
+ if (users.length === 0)
67
+ break;
68
+ allUsers.push(...users);
69
+ offset += users.length;
70
+ if (users.length < limit)
71
+ break;
72
+ }
73
+ return allUsers;
74
+ }
75
+ export function deriveSettingsFromUsers(users) {
76
+ let hasCredentials = false;
77
+ const providers = new Set();
78
+ for (const u of users) {
79
+ if (u.hadPassword)
80
+ hasCredentials = true;
81
+ for (const p of u.oauthProviders ?? []) {
82
+ providers.add(p.providerId);
83
+ }
84
+ }
85
+ return { hasCredentials, providers: [...providers].sort() };
86
+ }
87
+ export function parseClerkExport(filePath) {
88
+ const raw = readFileSync(filePath, "utf-8");
89
+ const parsed = JSON.parse(raw);
90
+ if (Array.isArray(parsed)) {
91
+ return parsed;
92
+ }
93
+ if (parsed.users && Array.isArray(parsed.users)) {
94
+ return parsed.users;
95
+ }
96
+ throw new Error("Unrecognized Clerk export format. Expected a JSON array of users or an object with a 'users' key.");
97
+ }
@@ -41,6 +41,7 @@ export interface AddNodeParams {
41
41
  surveySlug?: string;
42
42
  tagName?: string;
43
43
  stageName?: string;
44
+ fromIdentityEmail?: string;
44
45
  };
45
46
  terminal?: boolean;
46
47
  }
@@ -51,6 +52,7 @@ export interface EditNodeParams {
51
52
  surveySlug?: string;
52
53
  tagName?: string;
53
54
  stageName?: string;
55
+ fromIdentityEmail?: string;
54
56
  };
55
57
  terminal?: boolean;
56
58
  }
@@ -47,3 +47,11 @@ export declare function mcpPromoteEnvironment(client: AscendKitClient, params: {
47
47
  environmentId: string;
48
48
  targetTier: string;
49
49
  }): Promise<unknown>;
50
+ export interface McpUpdateEnvironmentVariablesParams {
51
+ projectId: string;
52
+ envId: string;
53
+ variables: Record<string, string>;
54
+ }
55
+ export declare function mcpUpdateEnvironmentVariables(client: AscendKitClient, params: McpUpdateEnvironmentVariablesParams): Promise<unknown>;
56
+ export declare function getEnvironment(projectId: string, envId: string): Promise<Record<string, unknown>>;
57
+ export declare function updateEnvironmentVariables(projectId: string, envId: string, variables: Record<string, string>): Promise<unknown>;
@@ -184,7 +184,7 @@ async function updateRuntimeEnvFile(filePath, apiUrl, publicKey, secretKey, opti
184
184
  const existingPublicKey = readEnvValue(original, "ASCENDKIT_ENV_KEY");
185
185
  const existingSecretKey = readEnvValue(original, "ASCENDKIT_SECRET_KEY");
186
186
  const existingWebhookSecret = readEnvValue(original, "ASCENDKIT_WEBHOOK_SECRET") ?? "";
187
- const resolvedApiUrl = existingApiUrl ?? apiUrl;
187
+ const resolvedApiUrl = apiUrl;
188
188
  const resolvedPublicKey = preserveExistingKeys
189
189
  ? (existingPublicKey && existingPublicKey.trim() ? existingPublicKey : publicKey)
190
190
  : publicKey;
@@ -559,3 +559,19 @@ export async function mcpUpdateEnvironment(client, params) {
559
559
  export async function mcpPromoteEnvironment(client, params) {
560
560
  return client.platformRequest("POST", `/api/platform/environments/${params.environmentId}/promote`, { targetTier: params.targetTier, dryRun: false });
561
561
  }
562
+ export async function mcpUpdateEnvironmentVariables(client, params) {
563
+ return client.platformRequest("PATCH", `/api/platform/projects/${params.projectId}/environments/${params.envId}`, { variables: params.variables });
564
+ }
565
+ export async function getEnvironment(projectId, envId) {
566
+ const auth = requireAuth();
567
+ const envs = await apiRequest(auth.apiUrl, "GET", `/api/platform/projects/${projectId}/environments`, undefined, auth.token);
568
+ const env = envs.find(e => e.id === envId);
569
+ if (!env) {
570
+ throw new Error(`Environment ${envId} not found in project ${projectId}.`);
571
+ }
572
+ return env;
573
+ }
574
+ export async function updateEnvironmentVariables(projectId, envId, variables) {
575
+ const auth = requireAuth();
576
+ return apiRequest(auth.apiUrl, "PATCH", `/api/platform/projects/${projectId}/environments/${envId}`, { variables }, auth.token);
577
+ }
package/dist/mcp.js CHANGED
@@ -10,6 +10,8 @@ import { registerPlatformTools } from "./tools/platform.js";
10
10
  import { registerEmailTools } from "./tools/email.js";
11
11
  import { registerJourneyTools } from "./tools/journeys.js";
12
12
  import { registerWebhookTools } from "./tools/webhooks.js";
13
+ import { registerCampaignTools } from "./tools/campaigns.js";
14
+ import { registerImportTools } from "./tools/import.js";
13
15
  import { DEFAULT_API_URL } from "./constants.js";
14
16
  const client = new AscendKitClient({
15
17
  apiUrl: process.env.ASCENDKIT_API_URL ?? DEFAULT_API_URL,
@@ -34,6 +36,8 @@ registerSurveyTools(server, client);
34
36
  registerEmailTools(server, client);
35
37
  registerJourneyTools(server, client);
36
38
  registerWebhookTools(server, client);
39
+ registerCampaignTools(server, client);
40
+ registerImportTools(server, client);
37
41
  async function main() {
38
42
  const transport = new StdioServerTransport();
39
43
  await server.connect(transport);
@@ -35,12 +35,31 @@ export function registerAuthTools(server, client) {
35
35
  const data = await auth.updateProviders(client, params.providers);
36
36
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
37
37
  });
38
- server.tool("auth_setup_oauth", "Configure OAuth credentials for a provider. Pass clientId and clientSecret to set credentials directly, or omit them to get a portal URL for browser-based entry. Note: credentials in tool args may appear in logs — use the portal URL for sensitive environments.", {
38
+ server.tool("auth_setup_oauth", "Configure OAuth credentials for a provider. Pass clientId and clientSecret for custom credentials, or useProxy to revert to AscendKit managed credentials. Omit all to get a portal URL for browser-based entry.", {
39
39
  provider: z.string().describe('OAuth provider name, e.g. "google", "github"'),
40
40
  clientId: z.string().optional().describe("OAuth client ID (to set credentials directly)"),
41
41
  clientSecret: z.string().optional().describe("OAuth client secret (to set credentials directly)"),
42
42
  callbackUrl: z.string().optional().describe("Auth callback URL for this provider"),
43
+ useProxy: z.boolean().optional().describe("Clear custom credentials and use AscendKit managed proxy credentials"),
43
44
  }, async (params) => {
45
+ if (params.useProxy && (params.clientId || params.clientSecret)) {
46
+ return {
47
+ content: [{
48
+ type: "text",
49
+ text: "Cannot use useProxy with custom credentials. Either set useProxy to clear credentials, or provide clientId and clientSecret.",
50
+ }],
51
+ isError: true,
52
+ };
53
+ }
54
+ if (params.useProxy) {
55
+ const data = await auth.deleteOAuthCredentials(client, params.provider);
56
+ return {
57
+ content: [{
58
+ type: "text",
59
+ text: `Cleared custom OAuth credentials for ${params.provider}. Now using AscendKit managed proxy credentials.\n\n${JSON.stringify(data, null, 2)}`,
60
+ }],
61
+ };
62
+ }
44
63
  if ((params.clientId && !params.clientSecret) || (!params.clientId && params.clientSecret)) {
45
64
  return {
46
65
  content: [{
@@ -72,4 +91,22 @@ export function registerAuthTools(server, client) {
72
91
  const data = await auth.listUsers(client);
73
92
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
74
93
  });
94
+ server.tool("auth_delete_user", "Deactivate a user (soft delete). The user record is preserved but marked inactive.", {
95
+ userId: z.string().describe("User ID (usr_ prefixed)"),
96
+ }, async (params) => {
97
+ const data = await auth.deleteUser(client, params.userId);
98
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
99
+ });
100
+ server.tool("auth_bulk_delete_users", "Bulk deactivate users (soft delete). All specified users are marked inactive.", {
101
+ userIds: z.array(z.string()).min(1).max(100).describe("Array of user IDs (usr_ prefixed)"),
102
+ }, async (params) => {
103
+ const data = await auth.bulkDeleteUsers(client, params.userIds);
104
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
105
+ });
106
+ server.tool("auth_reactivate_user", "Reactivate a previously deactivated user.", {
107
+ userId: z.string().describe("User ID (usr_ prefixed)"),
108
+ }, async (params) => {
109
+ const data = await auth.reactivateUser(client, params.userId);
110
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
111
+ });
75
112
  }
@@ -0,0 +1,3 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { AscendKitClient } from "../api/client.js";
3
+ export declare function registerCampaignTools(server: McpServer, client: AscendKitClient): void;
@@ -0,0 +1,78 @@
1
+ import { z } from "zod";
2
+ import * as campaigns from "../commands/campaigns.js";
3
+ export function registerCampaignTools(server, client) {
4
+ server.tool("campaign_create", "Create a new email campaign targeting users that match an audience filter. Campaigns start as drafts; set scheduledAt to schedule for future delivery. Campaign lifecycle: create → preview audience → schedule → sending → sent.", {
5
+ name: z.string().describe("Campaign name, e.g. 'March Newsletter'"),
6
+ templateId: z.string().describe("Email template ID (tpl_ prefixed) to use for the campaign"),
7
+ audienceFilter: z
8
+ .record(z.unknown())
9
+ .describe("Filter object to select target users (e.g. { tags: { $in: ['premium'] } }). Allowed fields: createdAt, emailVerified, lastLoginAt, tags, stage, name, email, waitlistStatus, metadata.*"),
10
+ scheduledAt: z
11
+ .string()
12
+ .optional()
13
+ .describe("ISO 8601 datetime to schedule sending (omit to keep as draft)"),
14
+ fromIdentityEmail: z
15
+ .string()
16
+ .optional()
17
+ .describe("Verified email identity to use as sender. Falls back to environment default."),
18
+ }, async (params) => {
19
+ const data = await campaigns.createCampaign(client, params);
20
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
21
+ });
22
+ server.tool("campaign_list", "List campaigns for the current project. Optionally filter by status: draft, scheduled, sending, sent, failed, or cancelled.", {
23
+ status: z
24
+ .enum(["draft", "scheduled", "sending", "sent", "failed", "cancelled"])
25
+ .optional()
26
+ .describe("Filter by campaign status"),
27
+ }, async (params) => {
28
+ const data = await campaigns.listCampaigns(client, params.status);
29
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
30
+ });
31
+ server.tool("campaign_get", "Get full details of a campaign including its status, template, audience filter, and schedule.", {
32
+ campaignId: z.string().describe("Campaign ID (cmp_ prefixed)"),
33
+ }, async (params) => {
34
+ const data = await campaigns.getCampaign(client, params.campaignId);
35
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
36
+ });
37
+ server.tool("campaign_update", "Update a draft, scheduled, or failed campaign. You can change the name, template, audience filter, or schedule. Only provided fields are updated.", {
38
+ campaignId: z.string().describe("Campaign ID (cmp_ prefixed)"),
39
+ name: z.string().optional().describe("New campaign name"),
40
+ templateId: z.string().optional().describe("New email template ID (tpl_ prefixed)"),
41
+ audienceFilter: z
42
+ .record(z.unknown())
43
+ .optional()
44
+ .describe("New audience filter object"),
45
+ scheduledAt: z
46
+ .string()
47
+ .optional()
48
+ .describe("New scheduled send time (ISO 8601 datetime)"),
49
+ fromIdentityEmail: z
50
+ .string()
51
+ .optional()
52
+ .describe("Verified email identity to use as sender. Falls back to environment default."),
53
+ }, async (params) => {
54
+ const { campaignId, ...rest } = params;
55
+ const data = await campaigns.updateCampaign(client, campaignId, rest);
56
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
57
+ });
58
+ server.tool("campaign_delete", "Delete a draft or failed campaign, or cancel a scheduled/sending campaign. Sent campaigns cannot be deleted.", {
59
+ campaignId: z.string().describe("Campaign ID (cmp_ prefixed)"),
60
+ }, async (params) => {
61
+ const data = await campaigns.deleteCampaign(client, params.campaignId);
62
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
63
+ });
64
+ server.tool("campaign_preview_audience", "Preview how many users match an audience filter before creating or updating a campaign. Returns the count and a sample of matching users.", {
65
+ audienceFilter: z
66
+ .record(z.unknown())
67
+ .describe("Filter object to preview (e.g. { tags: { $in: ['premium'] } }). Allowed fields: createdAt, emailVerified, lastLoginAt, tags, stage, name, email, waitlistStatus, metadata.*"),
68
+ }, async (params) => {
69
+ const data = await campaigns.previewAudience(client, params.audienceFilter);
70
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
71
+ });
72
+ server.tool("campaign_analytics", "Get campaign performance analytics: delivery stats (sent, failed, bounced), engagement metrics (opened, clicked), and calculated rates.", {
73
+ campaignId: z.string().describe("Campaign ID (cmp_ prefixed)"),
74
+ }, async (params) => {
75
+ const data = await campaigns.getCampaignAnalytics(client, params.campaignId);
76
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
77
+ });
78
+ }
@@ -0,0 +1,3 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { AscendKitClient } from "../api/client.js";
3
+ export declare function registerImportTools(server: McpServer, client: AscendKitClient): void;
@@ -0,0 +1,116 @@
1
+ import { z } from "zod";
2
+ import * as importCmd from "../commands/import.js";
3
+ export function registerImportTools(server, client) {
4
+ server.tool("auth_import_clerk", "Import users from Clerk into the current AscendKit environment. Fetches all users from the Clerk API, transforms them to AscendKit format, and pushes them to the import endpoint. Tags users by auth type: import:needs-password-reset or import:social-only. Defaults to dry-run — pass execute=true to apply changes.", {
5
+ clerkApiKey: z
6
+ .string()
7
+ .describe("Clerk secret API key (sk_live_... or sk_test_...)"),
8
+ clerkInstanceUrl: z
9
+ .string()
10
+ .optional()
11
+ .describe("Custom Clerk API URL (default: https://api.clerk.com)"),
12
+ execute: z
13
+ .boolean()
14
+ .optional()
15
+ .describe("Set to true to apply changes. Default is dry-run (preview only)."),
16
+ importUsers: z
17
+ .boolean()
18
+ .optional()
19
+ .describe("Import users (default: true)"),
20
+ importSettings: z
21
+ .boolean()
22
+ .optional()
23
+ .describe("Import auth settings — OAuth provider config (default: true)"),
24
+ }, async (params) => {
25
+ try {
26
+ const dryRun = !(params.execute ?? false);
27
+ const shouldImportUsers = params.importUsers ?? true;
28
+ const shouldImportSettings = params.importSettings ?? true;
29
+ const rawUsers = await importCmd.fetchClerkUsers(params.clerkApiKey, params.clerkInstanceUrl);
30
+ const users = rawUsers.map(importCmd.transformClerkUser);
31
+ if (users.length === 0) {
32
+ return {
33
+ content: [{ type: "text", text: "No users found in Clerk." }],
34
+ };
35
+ }
36
+ let totalImported = 0;
37
+ let totalDuplicates = 0;
38
+ const allErrors = [];
39
+ const allWarnings = [];
40
+ // Import users in batches of 500
41
+ if (shouldImportUsers) {
42
+ const batchSize = 500;
43
+ for (let i = 0; i < users.length; i += batchSize) {
44
+ const batch = users.slice(i, i + batchSize);
45
+ const result = await importCmd.importUsers(client, {
46
+ source: "clerk",
47
+ users: batch,
48
+ dryRun,
49
+ });
50
+ totalImported += result.imported;
51
+ totalDuplicates += result.duplicates.length;
52
+ allErrors.push(...result.errors);
53
+ allWarnings.push(...result.warnings);
54
+ }
55
+ }
56
+ // Import auth settings (OAuth provider config)
57
+ if (shouldImportSettings) {
58
+ const providers = new Set();
59
+ for (const u of users) {
60
+ for (const p of u.oauthProviders ?? []) {
61
+ providers.add(p.providerId);
62
+ }
63
+ }
64
+ if (providers.size > 0) {
65
+ const settingsResult = await importCmd.importUsers(client, {
66
+ source: "clerk",
67
+ users: [],
68
+ authSettings: { enabledProviders: [...providers] },
69
+ dryRun,
70
+ });
71
+ allWarnings.push(...settingsResult.warnings);
72
+ }
73
+ }
74
+ const summary = {
75
+ fetched: users.length,
76
+ dryRun,
77
+ importUsers: shouldImportUsers,
78
+ importSettings: shouldImportSettings,
79
+ };
80
+ if (shouldImportUsers) {
81
+ summary.imported = totalImported;
82
+ summary.duplicates = totalDuplicates;
83
+ }
84
+ if (allErrors.length > 0)
85
+ summary.errors = allErrors;
86
+ if (allWarnings.length > 0)
87
+ summary.warnings = allWarnings;
88
+ let text = JSON.stringify(summary, null, 2);
89
+ if (dryRun) {
90
+ text += "\n\nThis was a dry run. Pass execute=true to apply changes.";
91
+ }
92
+ else if (totalImported > 0) {
93
+ text += "\n\nTo set up migration emails, use the journey_create_migration tool.";
94
+ }
95
+ return { content: [{ type: "text", text }] };
96
+ }
97
+ catch (err) {
98
+ return {
99
+ content: [{
100
+ type: "text",
101
+ text: `Import failed: ${err instanceof Error ? err.message : String(err)}`,
102
+ }],
103
+ isError: true,
104
+ };
105
+ }
106
+ });
107
+ server.tool("journey_create_migration", "Create pre-built migration email templates and draft journeys for notifying imported users. Creates 5 email templates (announcement, go-live, reminder, password reset, password reset reminder) and 2 journeys: announcement cadence (all users, Day 0/3/7) and password reset cadence (credential users, Day 1/4). Idempotent — skips already-existing templates/journeys.", {
108
+ fromIdentityEmail: z
109
+ .string()
110
+ .optional()
111
+ .describe("Verified email identity to use as sender for migration emails"),
112
+ }, async (params) => {
113
+ const data = await importCmd.instantiateMigrationJourney(client, params.fromIdentityEmail);
114
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
115
+ });
116
+ }
@@ -14,7 +14,7 @@ export function registerJourneyTools(server, client) {
14
14
  nodes: z
15
15
  .record(z.record(z.unknown()))
16
16
  .optional()
17
- .describe("Map of node names to definitions. Each node has 'action' ({type, templateSlug?, surveySlug?, tagName?, stageName?}) and optional 'terminal' (boolean). If omitted, the entry node is auto-created as a 'none' action placeholder."),
17
+ .describe("Map of node names to definitions. Each node has 'action' ({type, templateSlug?, surveySlug?, tagName?, stageName?, variables?}) and optional 'terminal' (boolean). If omitted, the entry node is auto-created as a 'none' action placeholder."),
18
18
  transitions: z
19
19
  .array(z.record(z.unknown()))
20
20
  .optional()
@@ -138,6 +138,8 @@ export function registerJourneyTools(server, client) {
138
138
  surveySlug: z.string().optional().describe("Survey slug to include in email (for send_email)"),
139
139
  tagName: z.string().optional().describe("Tag to add (for tag_user)"),
140
140
  stageName: z.string().optional().describe("Stage to set (for advance_stage)"),
141
+ fromIdentityEmail: z.string().optional().describe("Verified email identity to use when this node sends email"),
142
+ variables: z.record(z.string()).optional().describe("Custom template variables for this node's email (e.g. {loginUrl: 'https://...', resetUrl: 'https://...'})"),
141
143
  })
142
144
  .optional()
143
145
  .describe("Action to execute when a user enters this node. Defaults to {type: 'none'}."),
@@ -161,6 +163,8 @@ export function registerJourneyTools(server, client) {
161
163
  surveySlug: z.string().optional().describe("Survey slug to include in email"),
162
164
  tagName: z.string().optional().describe("Tag to add"),
163
165
  stageName: z.string().optional().describe("Stage to set"),
166
+ fromIdentityEmail: z.string().optional().describe("Verified email identity to use when this node sends email"),
167
+ variables: z.record(z.string()).optional().describe("Custom template variables for this node's email (e.g. {loginUrl: 'https://...', resetUrl: 'https://...'})"),
164
168
  })
165
169
  .optional()
166
170
  .describe("New action definition (replaces the entire action)"),
@@ -172,4 +172,34 @@ export function registerPlatformTools(server, client) {
172
172
  };
173
173
  }
174
174
  });
175
+ server.tool("platform_update_environment_variables", "Set environment variables for a project environment. Pass the full variables dict — it replaces all existing variables.", {
176
+ projectId: z.string().describe("Project ID (prj_ prefixed)"),
177
+ envId: z.string().describe("Environment ID (env_ prefixed)"),
178
+ variables: z.record(z.string()).describe("Key-value map of environment variables"),
179
+ }, async (params) => {
180
+ try {
181
+ const data = await platform.mcpUpdateEnvironmentVariables(client, params);
182
+ return {
183
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
184
+ };
185
+ }
186
+ catch (err) {
187
+ let message = err instanceof Error ? err.message : String(err);
188
+ const jsonMatch = message.match(/\{.*\}/s);
189
+ if (jsonMatch) {
190
+ try {
191
+ const parsed = JSON.parse(jsonMatch[0]);
192
+ if (parsed.error)
193
+ message = parsed.error;
194
+ else if (parsed.detail)
195
+ message = parsed.detail;
196
+ }
197
+ catch { /* use raw message */ }
198
+ }
199
+ return {
200
+ content: [{ type: "text", text: message }],
201
+ isError: true,
202
+ };
203
+ }
204
+ });
175
205
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ascendkit/cli",
3
- "version": "0.2.0",
3
+ "version": "0.2.6",
4
4
  "description": "AscendKit CLI and MCP server",
5
5
  "author": "ascendkit.dev",
6
6
  "license": "MIT",