@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.
- package/dist/api/client.d.ts +1 -0
- package/dist/api/client.js +3 -0
- package/dist/cli.js +415 -11
- package/dist/commands/auth.d.ts +4 -0
- package/dist/commands/auth.js +12 -0
- package/dist/commands/campaigns.d.ts +22 -0
- package/dist/commands/campaigns.js +25 -0
- package/dist/commands/import.d.ts +75 -0
- package/dist/commands/import.js +97 -0
- package/dist/commands/journeys.d.ts +2 -0
- package/dist/commands/platform.d.ts +8 -0
- package/dist/commands/platform.js +17 -1
- package/dist/mcp.js +4 -0
- package/dist/tools/auth.js +38 -1
- package/dist/tools/campaigns.d.ts +3 -0
- package/dist/tools/campaigns.js +78 -0
- package/dist/tools/import.d.ts +3 -0
- package/dist/tools/import.js +116 -0
- package/dist/tools/journeys.js +5 -1
- package/dist/tools/platform.js +30 -0
- package/package.json +1 -1
package/dist/api/client.d.ts
CHANGED
|
@@ -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>;
|
package/dist/api/client.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|
package/dist/commands/auth.d.ts
CHANGED
|
@@ -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>;
|
package/dist/commands/auth.js
CHANGED
|
@@ -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 =
|
|
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);
|
package/dist/tools/auth.js
CHANGED
|
@@ -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
|
|
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,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,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
|
+
}
|
package/dist/tools/journeys.js
CHANGED
|
@@ -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)"),
|
package/dist/tools/platform.js
CHANGED
|
@@ -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
|
}
|