@ascendkit/cli 0.2.0 → 0.3.0
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 +2 -0
- package/dist/api/client.js +34 -8
- package/dist/cli.js +1047 -314
- 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/content.js +4 -4
- package/dist/commands/email.d.ts +62 -6
- package/dist/commands/email.js +26 -17
- package/dist/commands/import.d.ts +75 -0
- package/dist/commands/import.js +97 -0
- package/dist/commands/journeys.d.ts +3 -0
- package/dist/commands/journeys.js +9 -6
- package/dist/commands/platform.d.ts +28 -0
- package/dist/commands/platform.js +219 -101
- package/dist/commands/surveys.js +5 -5
- package/dist/mcp.js +35 -3
- package/dist/tools/auth.js +66 -8
- package/dist/tools/campaigns.d.ts +3 -0
- package/dist/tools/campaigns.js +78 -0
- package/dist/tools/content.js +24 -12
- package/dist/tools/email.js +47 -17
- package/dist/tools/import.d.ts +3 -0
- package/dist/tools/import.js +116 -0
- package/dist/tools/journeys.js +25 -14
- package/dist/tools/platform.js +151 -6
- package/dist/tools/surveys.js +9 -9
- package/dist/utils/journey-format.d.ts +6 -0
- package/dist/utils/journey-format.js +6 -4
- package/dist/utils/survey-format.js +2 -2
- package/package.json +5 -5
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { hostname, platform as osPlatform, release } from "node:os";
|
|
2
2
|
import { readdir, readFile, writeFile } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { loadAuth, saveAuth, deleteAuth, saveEnvContext, ensureGitignore } from "../utils/credentials.js";
|
|
4
|
+
import { loadAuth, loadEnvContext, saveAuth, deleteAuth, saveEnvContext, ensureGitignore, } from "../utils/credentials.js";
|
|
5
5
|
import { DEFAULT_API_URL, DEFAULT_PORTAL_URL } from "../constants.js";
|
|
6
6
|
const POLL_INTERVAL_MS = 2000;
|
|
7
7
|
const DEVICE_CODE_EXPIRY_MS = 300_000; // 5 minutes
|
|
@@ -16,6 +16,14 @@ const ASCENDKIT_ENV_KEYS = [
|
|
|
16
16
|
];
|
|
17
17
|
const ASCENDKIT_BLOCK_START = "# --- AscendKit Managed Environment Variables ---";
|
|
18
18
|
const ASCENDKIT_BLOCK_END = "# --- End AscendKit Managed Environment Variables ---";
|
|
19
|
+
function normalizePromotionTier(targetTier) {
|
|
20
|
+
const normalized = targetTier.trim().toLowerCase();
|
|
21
|
+
if (normalized === "production")
|
|
22
|
+
return "prod";
|
|
23
|
+
if (normalized === "staging")
|
|
24
|
+
return "beta";
|
|
25
|
+
return normalized;
|
|
26
|
+
}
|
|
19
27
|
async function promptYesNo(question, defaultYes) {
|
|
20
28
|
const suffix = defaultYes ? "[Y/n]" : "[y/N]";
|
|
21
29
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
@@ -184,13 +192,14 @@ async function updateRuntimeEnvFile(filePath, apiUrl, publicKey, secretKey, opti
|
|
|
184
192
|
const existingPublicKey = readEnvValue(original, "ASCENDKIT_ENV_KEY");
|
|
185
193
|
const existingSecretKey = readEnvValue(original, "ASCENDKIT_SECRET_KEY");
|
|
186
194
|
const existingWebhookSecret = readEnvValue(original, "ASCENDKIT_WEBHOOK_SECRET") ?? "";
|
|
187
|
-
const
|
|
195
|
+
const incomingSecretKey = (secretKey ?? "").trim();
|
|
196
|
+
const resolvedApiUrl = apiUrl;
|
|
188
197
|
const resolvedPublicKey = preserveExistingKeys
|
|
189
198
|
? (existingPublicKey && existingPublicKey.trim() ? existingPublicKey : publicKey)
|
|
190
199
|
: publicKey;
|
|
191
200
|
const resolvedSecretKey = preserveExistingKeys
|
|
192
|
-
? (existingSecretKey && existingSecretKey.trim() ? existingSecretKey :
|
|
193
|
-
:
|
|
201
|
+
? (existingSecretKey && existingSecretKey.trim() ? existingSecretKey : incomingSecretKey)
|
|
202
|
+
: (incomingSecretKey || existingSecretKey || "");
|
|
194
203
|
const resolvedWebhookSecret = options?.webhookSecret ?? existingWebhookSecret;
|
|
195
204
|
return rewriteAscendKitEnvBlock(filePath, {
|
|
196
205
|
NEXT_PUBLIC_ASCENDKIT_API_URL: resolvedApiUrl,
|
|
@@ -201,6 +210,124 @@ async function updateRuntimeEnvFile(filePath, apiUrl, publicKey, secretKey, opti
|
|
|
201
210
|
ASCENDKIT_WEBHOOK_SECRET: resolvedWebhookSecret,
|
|
202
211
|
});
|
|
203
212
|
}
|
|
213
|
+
async function syncLocalEnvFiles(apiUrl, publicKey, secretKey) {
|
|
214
|
+
const cwd = process.cwd();
|
|
215
|
+
const folders = await findEnvFolders(cwd);
|
|
216
|
+
const updatedFiles = [];
|
|
217
|
+
for (const folder of folders) {
|
|
218
|
+
for (const filePath of folder.examples) {
|
|
219
|
+
const changed = await updateEnvExampleFile(filePath);
|
|
220
|
+
if (changed)
|
|
221
|
+
updatedFiles.push(path.relative(cwd, filePath));
|
|
222
|
+
}
|
|
223
|
+
for (const filePath of folder.runtime) {
|
|
224
|
+
const changed = await updateRuntimeEnvFile(filePath, apiUrl, publicKey, secretKey);
|
|
225
|
+
if (changed)
|
|
226
|
+
updatedFiles.push(path.relative(cwd, filePath));
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return updatedFiles;
|
|
230
|
+
}
|
|
231
|
+
function saveActiveEnvironmentContext(result) {
|
|
232
|
+
saveEnvContext({
|
|
233
|
+
publicKey: result.environment.publicKey,
|
|
234
|
+
projectId: result.project.id,
|
|
235
|
+
projectName: result.project.name,
|
|
236
|
+
environmentId: result.environment.id,
|
|
237
|
+
environmentName: result.environment.name,
|
|
238
|
+
tier: result.environment.tier,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
async function activateEnvironment(auth, result, options) {
|
|
242
|
+
const promptForFileUpdates = options?.promptForFileUpdates ?? true;
|
|
243
|
+
const shouldSetupWebhook = options?.setupWebhook ?? true;
|
|
244
|
+
const logOutput = options?.logOutput ?? true;
|
|
245
|
+
saveActiveEnvironmentContext(result);
|
|
246
|
+
if (logOutput) {
|
|
247
|
+
console.log(` → Project: ${result.project.name}`);
|
|
248
|
+
console.log(` → Environment: ${result.environment.name}`);
|
|
249
|
+
console.log(` → Role: ${result.role}`);
|
|
250
|
+
}
|
|
251
|
+
const cwd = process.cwd();
|
|
252
|
+
const folders = await findEnvFolders(cwd);
|
|
253
|
+
const updatedFiles = [];
|
|
254
|
+
const consentedRuntimeFolders = [];
|
|
255
|
+
if (folders.length > 0) {
|
|
256
|
+
if (logOutput) {
|
|
257
|
+
console.log(`\nFound env files in ${folders.length} folder(s).`);
|
|
258
|
+
}
|
|
259
|
+
for (const folder of folders) {
|
|
260
|
+
let update = true;
|
|
261
|
+
if (promptForFileUpdates) {
|
|
262
|
+
const fileNames = [...folder.examples, ...folder.runtime]
|
|
263
|
+
.map(f => path.basename(f)).join(", ");
|
|
264
|
+
update = await promptYesNo(` ${folder.label} (${fileNames}) — update?`, true);
|
|
265
|
+
}
|
|
266
|
+
if (!update)
|
|
267
|
+
continue;
|
|
268
|
+
for (const filePath of folder.examples) {
|
|
269
|
+
const changed = await updateEnvExampleFile(filePath);
|
|
270
|
+
if (changed)
|
|
271
|
+
updatedFiles.push(path.relative(cwd, filePath));
|
|
272
|
+
}
|
|
273
|
+
if (folder.runtime.length > 0) {
|
|
274
|
+
consentedRuntimeFolders.push(folder);
|
|
275
|
+
for (const filePath of folder.runtime) {
|
|
276
|
+
const changed = await updateRuntimeEnvFile(filePath, auth.apiUrl, result.environment.publicKey, result.environment.secretKey);
|
|
277
|
+
if (changed)
|
|
278
|
+
updatedFiles.push(path.relative(cwd, filePath));
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
if (logOutput) {
|
|
284
|
+
console.log("\nUpdated files:");
|
|
285
|
+
if (updatedFiles.length === 0) {
|
|
286
|
+
console.log(" (none)");
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
for (const filePath of updatedFiles)
|
|
290
|
+
console.log(` - ${filePath}`);
|
|
291
|
+
}
|
|
292
|
+
if (!result.environment.secretKey) {
|
|
293
|
+
console.log("\nNote: your role does not have access to the environment secret key.");
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
if (!shouldSetupWebhook) {
|
|
297
|
+
return { updatedFiles, webhookSecret: null };
|
|
298
|
+
}
|
|
299
|
+
const webhookSecret = await setupWebhook(auth, result.environment.publicKey);
|
|
300
|
+
if (webhookSecret) {
|
|
301
|
+
if (consentedRuntimeFolders.length > 0) {
|
|
302
|
+
for (const folder of consentedRuntimeFolders) {
|
|
303
|
+
for (const filePath of folder.runtime) {
|
|
304
|
+
await updateRuntimeEnvFile(filePath, auth.apiUrl, result.environment.publicKey, result.environment.secretKey, { preserveExistingKeys: true, webhookSecret });
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
if (logOutput) {
|
|
308
|
+
console.log("\nWebhook secret written to .env file(s).");
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
else if (logOutput) {
|
|
312
|
+
console.log(`\nAdd this to your environment:`);
|
|
313
|
+
console.log(` ASCENDKIT_WEBHOOK_SECRET=${webhookSecret}`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
if (logOutput) {
|
|
317
|
+
const remainingSteps = [];
|
|
318
|
+
if (!webhookSecret) {
|
|
319
|
+
remainingSteps.push("Set up a webhook endpoint (run ascendkit set-env again, or use the portal).");
|
|
320
|
+
}
|
|
321
|
+
remainingSteps.push("Ensure frontend and backend are using keys from the same AscendKit environment.");
|
|
322
|
+
if (remainingSteps.length > 0) {
|
|
323
|
+
console.log("\nWhat you still need to do:");
|
|
324
|
+
for (let i = 0; i < remainingSteps.length; i++) {
|
|
325
|
+
console.log(` ${i + 1}. ${remainingSteps[i]}`);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return { updatedFiles, webhookSecret };
|
|
330
|
+
}
|
|
204
331
|
function generateDeviceCode() {
|
|
205
332
|
const chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; // no I, O, 0, 1 for readability
|
|
206
333
|
let code = "";
|
|
@@ -223,6 +350,28 @@ async function apiRequest(apiUrl, method, path, body, token, publicKey) {
|
|
|
223
350
|
const response = await fetch(`${apiUrl}${path}`, init);
|
|
224
351
|
if (!response.ok) {
|
|
225
352
|
const text = await response.text();
|
|
353
|
+
if (!text) {
|
|
354
|
+
throw new Error(`API error ${response.status}: ${response.statusText}`);
|
|
355
|
+
}
|
|
356
|
+
try {
|
|
357
|
+
const parsed = JSON.parse(text);
|
|
358
|
+
if (typeof parsed.error === "string") {
|
|
359
|
+
throw new Error(parsed.error);
|
|
360
|
+
}
|
|
361
|
+
if (typeof parsed.detail === "string") {
|
|
362
|
+
throw new Error(parsed.detail);
|
|
363
|
+
}
|
|
364
|
+
if (parsed.detail
|
|
365
|
+
&& typeof parsed.detail === "object"
|
|
366
|
+
&& typeof parsed.detail.message === "string") {
|
|
367
|
+
throw new Error(parsed.detail.message);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
catch (error) {
|
|
371
|
+
if (error instanceof Error && error.message !== text) {
|
|
372
|
+
throw error;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
226
375
|
throw new Error(`API error ${response.status}: ${text}`);
|
|
227
376
|
}
|
|
228
377
|
const json = await response.json();
|
|
@@ -330,6 +479,14 @@ export async function listProjects() {
|
|
|
330
479
|
const auth = requireAuth();
|
|
331
480
|
return apiRequest(auth.apiUrl, "GET", "/api/platform/projects", undefined, auth.token);
|
|
332
481
|
}
|
|
482
|
+
export async function showProject(projectId) {
|
|
483
|
+
const projects = await listProjects();
|
|
484
|
+
const project = projects.find((item) => item.id === projectId);
|
|
485
|
+
if (!project) {
|
|
486
|
+
throw new Error(`Project ${projectId} not found.`);
|
|
487
|
+
}
|
|
488
|
+
return project;
|
|
489
|
+
}
|
|
333
490
|
export async function createProject(name, description, enabledServices) {
|
|
334
491
|
const auth = requireAuth();
|
|
335
492
|
return apiRequest(auth.apiUrl, "POST", "/api/platform/projects", {
|
|
@@ -349,26 +506,7 @@ export async function useEnvironment(tier, projectId) {
|
|
|
349
506
|
if (!env) {
|
|
350
507
|
throw new Error(`No ${tier} environment found for project ${projectId}. Available: ${envs.map(e => e.tier).join(", ")}`);
|
|
351
508
|
}
|
|
352
|
-
|
|
353
|
-
let projectName = projectId;
|
|
354
|
-
try {
|
|
355
|
-
const projects = await apiRequest(auth.apiUrl, "GET", "/api/platform/projects", undefined, auth.token);
|
|
356
|
-
const project = projects.find(p => p.id === projectId);
|
|
357
|
-
if (project)
|
|
358
|
-
projectName = project.name;
|
|
359
|
-
}
|
|
360
|
-
catch { /* non-critical */ }
|
|
361
|
-
saveEnvContext({
|
|
362
|
-
publicKey: env.publicKey,
|
|
363
|
-
projectId,
|
|
364
|
-
projectName,
|
|
365
|
-
environmentId: env.id,
|
|
366
|
-
environmentName: env.name,
|
|
367
|
-
tier: env.tier,
|
|
368
|
-
});
|
|
369
|
-
console.log(`Active environment: ${env.name} (${env.tier})`);
|
|
370
|
-
console.log(`Project: ${projectName}`);
|
|
371
|
-
console.log(`Public key: ${env.publicKey}`);
|
|
509
|
+
await setEnv(env.publicKey);
|
|
372
510
|
}
|
|
373
511
|
async function setupWebhook(auth, publicKey) {
|
|
374
512
|
// Never run webhook setup in non-interactive mode — selecting a webhook
|
|
@@ -434,82 +572,11 @@ async function setupWebhook(auth, publicKey) {
|
|
|
434
572
|
export async function setEnv(publicKey) {
|
|
435
573
|
const auth = requireAuth();
|
|
436
574
|
const result = await apiRequest(auth.apiUrl, "GET", `/api/platform/resolve-key?pk=${encodeURIComponent(publicKey)}`, undefined, auth.token);
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
environmentId: result.environment.id,
|
|
442
|
-
environmentName: result.environment.name,
|
|
443
|
-
tier: result.environment.tier,
|
|
575
|
+
await activateEnvironment(auth, result, {
|
|
576
|
+
promptForFileUpdates: true,
|
|
577
|
+
setupWebhook: true,
|
|
578
|
+
logOutput: true,
|
|
444
579
|
});
|
|
445
|
-
console.log(` → Project: ${result.project.name}`);
|
|
446
|
-
console.log(` → Environment: ${result.environment.name}`);
|
|
447
|
-
console.log(` → Role: ${result.role}`);
|
|
448
|
-
const cwd = process.cwd();
|
|
449
|
-
const folders = await findEnvFolders(cwd);
|
|
450
|
-
const updatedFiles = [];
|
|
451
|
-
const consentedRuntimeFolders = [];
|
|
452
|
-
if (folders.length > 0) {
|
|
453
|
-
console.log(`\nFound env files in ${folders.length} folder(s).`);
|
|
454
|
-
for (const folder of folders) {
|
|
455
|
-
const fileNames = [...folder.examples, ...folder.runtime]
|
|
456
|
-
.map(f => path.basename(f)).join(", ");
|
|
457
|
-
const update = await promptYesNo(` ${folder.label} (${fileNames}) — update?`, true);
|
|
458
|
-
if (!update)
|
|
459
|
-
continue;
|
|
460
|
-
for (const filePath of folder.examples) {
|
|
461
|
-
const changed = await updateEnvExampleFile(filePath);
|
|
462
|
-
if (changed)
|
|
463
|
-
updatedFiles.push(path.relative(cwd, filePath));
|
|
464
|
-
}
|
|
465
|
-
if (folder.runtime.length > 0) {
|
|
466
|
-
consentedRuntimeFolders.push(folder);
|
|
467
|
-
for (const filePath of folder.runtime) {
|
|
468
|
-
const changed = await updateRuntimeEnvFile(filePath, auth.apiUrl, result.environment.publicKey, result.environment.secretKey ?? "");
|
|
469
|
-
if (changed)
|
|
470
|
-
updatedFiles.push(path.relative(cwd, filePath));
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
console.log("\nUpdated files:");
|
|
476
|
-
if (updatedFiles.length === 0) {
|
|
477
|
-
console.log(" (none)");
|
|
478
|
-
}
|
|
479
|
-
else {
|
|
480
|
-
for (const filePath of updatedFiles)
|
|
481
|
-
console.log(` - ${filePath}`);
|
|
482
|
-
}
|
|
483
|
-
if (!result.environment.secretKey) {
|
|
484
|
-
console.log("\nNote: your role does not have access to the environment secret key.");
|
|
485
|
-
}
|
|
486
|
-
// --- Webhook setup ---
|
|
487
|
-
const webhookSecret = await setupWebhook(auth, result.environment.publicKey);
|
|
488
|
-
if (webhookSecret) {
|
|
489
|
-
if (consentedRuntimeFolders.length > 0) {
|
|
490
|
-
for (const folder of consentedRuntimeFolders) {
|
|
491
|
-
for (const filePath of folder.runtime) {
|
|
492
|
-
await updateRuntimeEnvFile(filePath, auth.apiUrl, result.environment.publicKey, result.environment.secretKey ?? "", { preserveExistingKeys: true, webhookSecret });
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
console.log("\nWebhook secret written to .env file(s).");
|
|
496
|
-
}
|
|
497
|
-
else {
|
|
498
|
-
console.log(`\nAdd this to your environment:`);
|
|
499
|
-
console.log(` ASCENDKIT_WEBHOOK_SECRET=${webhookSecret}`);
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
const remainingSteps = [];
|
|
503
|
-
if (!webhookSecret) {
|
|
504
|
-
remainingSteps.push("Set up a webhook endpoint (run ascendkit set-env again, or use the portal).");
|
|
505
|
-
}
|
|
506
|
-
remainingSteps.push("Ensure frontend and backend are using keys from the same AscendKit environment.");
|
|
507
|
-
if (remainingSteps.length > 0) {
|
|
508
|
-
console.log("\nWhat you still need to do:");
|
|
509
|
-
for (let i = 0; i < remainingSteps.length; i++) {
|
|
510
|
-
console.log(` ${i + 1}. ${remainingSteps[i]}`);
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
580
|
}
|
|
514
581
|
export async function mcpLogin(client, params) {
|
|
515
582
|
const data = await client.publicRequest("POST", "/api/platform/auth/token", {
|
|
@@ -517,8 +584,20 @@ export async function mcpLogin(client, params) {
|
|
|
517
584
|
password: params.password,
|
|
518
585
|
});
|
|
519
586
|
client.configurePlatform(data.token);
|
|
587
|
+
saveAuth({ token: data.token, apiUrl: client.currentApiUrl });
|
|
588
|
+
ensureGitignore();
|
|
520
589
|
return data;
|
|
521
590
|
}
|
|
591
|
+
export async function mcpSetEnvironmentByPublicKey(client, publicKey) {
|
|
592
|
+
const result = await client.platformRequest("GET", `/api/platform/resolve-key?pk=${encodeURIComponent(publicKey)}`);
|
|
593
|
+
client.configure(result.environment.publicKey);
|
|
594
|
+
const { updatedFiles } = await activateEnvironment({ token: "__mcp__", apiUrl: client.currentApiUrl }, result, {
|
|
595
|
+
promptForFileUpdates: false,
|
|
596
|
+
setupWebhook: false,
|
|
597
|
+
logOutput: false,
|
|
598
|
+
});
|
|
599
|
+
return { ...result, updatedFiles };
|
|
600
|
+
}
|
|
522
601
|
export async function mcpListProjects(client) {
|
|
523
602
|
return client.platformRequest("GET", "/api/platform/projects");
|
|
524
603
|
}
|
|
@@ -546,7 +625,7 @@ export async function updateEnvironment(projectId, envId, name, description) {
|
|
|
546
625
|
}
|
|
547
626
|
export async function promoteEnvironment(environmentId, targetTier) {
|
|
548
627
|
const auth = requireAuth();
|
|
549
|
-
return apiRequest(auth.apiUrl, "POST", `/api/platform/environments/${environmentId}/promote`, { targetTier, dryRun: false }, auth.token);
|
|
628
|
+
return apiRequest(auth.apiUrl, "POST", `/api/platform/environments/${environmentId}/promote`, { targetTier: normalizePromotionTier(targetTier), dryRun: false }, auth.token);
|
|
550
629
|
}
|
|
551
630
|
export async function mcpUpdateEnvironment(client, params) {
|
|
552
631
|
const body = {};
|
|
@@ -557,5 +636,44 @@ export async function mcpUpdateEnvironment(client, params) {
|
|
|
557
636
|
return client.platformRequest("PATCH", `/api/platform/projects/${params.projectId}/environments/${params.environmentId}`, body);
|
|
558
637
|
}
|
|
559
638
|
export async function mcpPromoteEnvironment(client, params) {
|
|
560
|
-
return client.platformRequest("POST", `/api/platform/environments/${params.environmentId}/promote`, { targetTier: params.targetTier, dryRun: false });
|
|
639
|
+
return client.platformRequest("POST", `/api/platform/environments/${params.environmentId}/promote`, { targetTier: normalizePromotionTier(params.targetTier), dryRun: false });
|
|
640
|
+
}
|
|
641
|
+
function resolveActiveEnvironmentIds(params) {
|
|
642
|
+
if (params?.projectId && params?.envId) {
|
|
643
|
+
return { projectId: params.projectId, envId: params.envId };
|
|
644
|
+
}
|
|
645
|
+
const ctx = loadEnvContext();
|
|
646
|
+
if (!ctx?.projectId || !ctx.environmentId) {
|
|
647
|
+
throw new Error("No active environment found. Use ascendkit_set_env or ascendkit set-env first.");
|
|
648
|
+
}
|
|
649
|
+
return {
|
|
650
|
+
projectId: params?.projectId ?? ctx.projectId,
|
|
651
|
+
envId: params?.envId ?? ctx.environmentId,
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
export async function mcpGetEnvironment(client, params) {
|
|
655
|
+
const target = resolveActiveEnvironmentIds(params);
|
|
656
|
+
const envs = await client.platformRequest("GET", `/api/platform/projects/${target.projectId}/environments`);
|
|
657
|
+
const env = envs.find((item) => item.id === target.envId);
|
|
658
|
+
if (!env) {
|
|
659
|
+
throw new Error(`Environment ${target.envId} not found in project ${target.projectId}.`);
|
|
660
|
+
}
|
|
661
|
+
return env;
|
|
662
|
+
}
|
|
663
|
+
export async function mcpUpdateEnvironmentVariables(client, params) {
|
|
664
|
+
const target = resolveActiveEnvironmentIds(params);
|
|
665
|
+
return client.platformRequest("PATCH", `/api/platform/projects/${target.projectId}/environments/${target.envId}`, { variables: params.variables });
|
|
666
|
+
}
|
|
667
|
+
export async function getEnvironment(projectId, envId) {
|
|
668
|
+
const auth = requireAuth();
|
|
669
|
+
const envs = await apiRequest(auth.apiUrl, "GET", `/api/platform/projects/${projectId}/environments`, undefined, auth.token);
|
|
670
|
+
const env = envs.find(e => e.id === envId);
|
|
671
|
+
if (!env) {
|
|
672
|
+
throw new Error(`Environment ${envId} not found in project ${projectId}.`);
|
|
673
|
+
}
|
|
674
|
+
return env;
|
|
675
|
+
}
|
|
676
|
+
export async function updateEnvironmentVariables(projectId, envId, variables) {
|
|
677
|
+
const auth = requireAuth();
|
|
678
|
+
return apiRequest(auth.apiUrl, "PATCH", `/api/platform/projects/${projectId}/environments/${envId}`, { variables }, auth.token);
|
|
561
679
|
}
|
package/dist/commands/surveys.js
CHANGED
|
@@ -2,10 +2,10 @@ export async function createSurvey(client, params) {
|
|
|
2
2
|
return client.managedPost("/api/surveys", params);
|
|
3
3
|
}
|
|
4
4
|
export async function listSurveys(client) {
|
|
5
|
-
return client.
|
|
5
|
+
return client.managedGet("/api/surveys");
|
|
6
6
|
}
|
|
7
7
|
export async function getSurvey(client, surveyId) {
|
|
8
|
-
return client.
|
|
8
|
+
return client.managedGet(`/api/surveys/${surveyId}`);
|
|
9
9
|
}
|
|
10
10
|
export async function updateSurvey(client, surveyId, params) {
|
|
11
11
|
return client.managedPut(`/api/surveys/${surveyId}`, params);
|
|
@@ -17,13 +17,13 @@ export async function distributeSurvey(client, surveyId, userIds) {
|
|
|
17
17
|
return client.managedPost(`/api/surveys/${surveyId}/distribute`, { userIds });
|
|
18
18
|
}
|
|
19
19
|
export async function listInvitations(client, surveyId) {
|
|
20
|
-
return client.
|
|
20
|
+
return client.managedGet(`/api/surveys/${surveyId}/invitations`);
|
|
21
21
|
}
|
|
22
22
|
export async function getAnalytics(client, surveyId) {
|
|
23
|
-
return client.
|
|
23
|
+
return client.managedGet(`/api/surveys/${surveyId}/analytics`);
|
|
24
24
|
}
|
|
25
25
|
export async function listQuestions(client, surveyId) {
|
|
26
|
-
return client.
|
|
26
|
+
return client.managedGet(`/api/surveys/${surveyId}/questions`);
|
|
27
27
|
}
|
|
28
28
|
export async function addQuestion(client, surveyId, params) {
|
|
29
29
|
return client.managedPost(`/api/surveys/${surveyId}/questions`, params);
|
package/dist/mcp.js
CHANGED
|
@@ -10,21 +10,51 @@ 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";
|
|
16
|
+
import { loadAuth, loadEnvContext } from "./utils/credentials.js";
|
|
17
|
+
import * as platformCommands from "./commands/platform.js";
|
|
18
|
+
import { CLI_VERSION } from "./api/client.js";
|
|
14
19
|
const client = new AscendKitClient({
|
|
15
20
|
apiUrl: process.env.ASCENDKIT_API_URL ?? DEFAULT_API_URL,
|
|
16
21
|
});
|
|
22
|
+
const persistedAuth = loadAuth();
|
|
23
|
+
if (persistedAuth?.token) {
|
|
24
|
+
client.configurePlatform(persistedAuth.token, persistedAuth.apiUrl);
|
|
25
|
+
}
|
|
26
|
+
const persistedEnv = loadEnvContext();
|
|
27
|
+
if (persistedEnv?.publicKey) {
|
|
28
|
+
client.configure(persistedEnv.publicKey, persistedAuth?.apiUrl ?? process.env.ASCENDKIT_API_URL ?? DEFAULT_API_URL);
|
|
29
|
+
}
|
|
17
30
|
const server = new McpServer({
|
|
18
31
|
name: "ascendkit",
|
|
19
|
-
version:
|
|
32
|
+
version: CLI_VERSION,
|
|
20
33
|
});
|
|
21
|
-
server.tool("ascendkit_configure", "Configure AscendKit with your project's public key.
|
|
34
|
+
server.tool("ascendkit_configure", "Configure AscendKit with your project's public key. If platform auth is available, this also persists the shared .ascendkit environment context used by both MCP and CLI.", {
|
|
22
35
|
publicKey: z.string().describe("Your project's public key (pk_dev_..., pk_beta_..., or pk_prod_...)"),
|
|
23
36
|
apiUrl: z.string().optional().describe("AscendKit API URL (default: https://api.ascendkit.dev)"),
|
|
24
37
|
}, async (params) => {
|
|
25
38
|
client.configure(params.publicKey, params.apiUrl);
|
|
39
|
+
if (client.platformConfigured) {
|
|
40
|
+
try {
|
|
41
|
+
const data = await platformCommands.mcpSetEnvironmentByPublicKey(client, params.publicKey);
|
|
42
|
+
return {
|
|
43
|
+
content: [{
|
|
44
|
+
type: "text",
|
|
45
|
+
text: `Active environment: ${data.environment.name} (${data.environment.tier})\n` +
|
|
46
|
+
`Project: ${data.project.name}\n` +
|
|
47
|
+
`Public key: ${data.environment.publicKey}\n` +
|
|
48
|
+
`Updated env files: ${Array.isArray(data.updatedFiles) && data.updatedFiles.length > 0 ? data.updatedFiles.join(", ") : "(none)"}`,
|
|
49
|
+
}],
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// Fall back to in-memory configure if resolve/persist fails.
|
|
54
|
+
}
|
|
55
|
+
}
|
|
26
56
|
return {
|
|
27
|
-
content: [{ type: "text", text: `Configured for
|
|
57
|
+
content: [{ type: "text", text: `Configured for environment ${params.publicKey}` }],
|
|
28
58
|
};
|
|
29
59
|
});
|
|
30
60
|
registerPlatformTools(server, client);
|
|
@@ -34,6 +64,8 @@ registerSurveyTools(server, client);
|
|
|
34
64
|
registerEmailTools(server, client);
|
|
35
65
|
registerJourneyTools(server, client);
|
|
36
66
|
registerWebhookTools(server, client);
|
|
67
|
+
registerCampaignTools(server, client);
|
|
68
|
+
registerImportTools(server, client);
|
|
37
69
|
async function main() {
|
|
38
70
|
const transport = new StdioServerTransport();
|
|
39
71
|
await server.connect(transport);
|
package/dist/tools/auth.js
CHANGED
|
@@ -1,11 +1,25 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import * as auth from "../commands/auth.js";
|
|
3
|
+
function formatAuthSettings(data) {
|
|
4
|
+
const providers = Array.isArray(data.providers) ? data.providers : [];
|
|
5
|
+
const features = data.features ?? {};
|
|
6
|
+
const lines = [
|
|
7
|
+
`Providers: ${providers.length > 0 ? providers.join(", ") : "none"}`,
|
|
8
|
+
`Email verification: ${features.emailVerification ? "on" : "off"}`,
|
|
9
|
+
`Waitlist: ${features.waitlist ? "on" : "off"}`,
|
|
10
|
+
`Password reset: ${features.passwordReset ? "on" : "off"}`,
|
|
11
|
+
`Require username: ${features.requireUsername ? "on" : "off"}`,
|
|
12
|
+
];
|
|
13
|
+
if (data.sessionDuration)
|
|
14
|
+
lines.push(`Session: ${data.sessionDuration}`);
|
|
15
|
+
return lines.join("\n");
|
|
16
|
+
}
|
|
3
17
|
export function registerAuthTools(server, client) {
|
|
4
|
-
server.tool("
|
|
18
|
+
server.tool("auth_show", "Get authentication settings for the current project (enabled providers, features, OAuth configs)", {}, async () => {
|
|
5
19
|
const data = await auth.getSettings(client);
|
|
6
|
-
return { content: [{ type: "text", text:
|
|
20
|
+
return { content: [{ type: "text", text: formatAuthSettings(data) }] };
|
|
7
21
|
});
|
|
8
|
-
server.tool("
|
|
22
|
+
server.tool("auth_update", "Update authentication settings: providers, features, session duration. Credentials and magic-link are mutually exclusive. Social-only login is valid. emailVerification, passwordReset, requireUsername only apply when credentials is active; waitlist applies to all modes.", {
|
|
9
23
|
providers: z
|
|
10
24
|
.array(z.string())
|
|
11
25
|
.optional()
|
|
@@ -25,22 +39,48 @@ export function registerAuthTools(server, client) {
|
|
|
25
39
|
.describe('Session duration, e.g. "7d", "24h"'),
|
|
26
40
|
}, async (params) => {
|
|
27
41
|
const data = await auth.updateSettings(client, params);
|
|
28
|
-
return { content: [{ type: "text", text:
|
|
42
|
+
return { content: [{ type: "text", text: formatAuthSettings(data) }] };
|
|
29
43
|
});
|
|
30
|
-
server.tool("
|
|
44
|
+
server.tool("auth_provider_set", "Set which auth providers are enabled for the project. Credentials and magic-link are mutually exclusive. Social-only login (e.g. [\"google\"]) is valid. At least one provider required.", {
|
|
31
45
|
providers: z
|
|
32
46
|
.array(z.string())
|
|
33
47
|
.describe('List of provider names, e.g. ["credentials", "google", "github"]'),
|
|
34
48
|
}, async (params) => {
|
|
35
49
|
const data = await auth.updateProviders(client, params.providers);
|
|
36
|
-
return { content: [{ type: "text", text:
|
|
50
|
+
return { content: [{ type: "text", text: formatAuthSettings(data) }] };
|
|
51
|
+
});
|
|
52
|
+
server.tool("auth_provider_list", "List enabled auth providers for the current project.", {}, async () => {
|
|
53
|
+
const data = await auth.getSettings(client);
|
|
54
|
+
const providers = Array.isArray(data.providers) ? data.providers : [];
|
|
55
|
+
return {
|
|
56
|
+
content: [{ type: "text", text: providers.length > 0 ? providers.join("\n") : "No providers configured." }],
|
|
57
|
+
};
|
|
37
58
|
});
|
|
38
|
-
server.tool("
|
|
59
|
+
server.tool("auth_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
60
|
provider: z.string().describe('OAuth provider name, e.g. "google", "github"'),
|
|
40
61
|
clientId: z.string().optional().describe("OAuth client ID (to set credentials directly)"),
|
|
41
62
|
clientSecret: z.string().optional().describe("OAuth client secret (to set credentials directly)"),
|
|
42
63
|
callbackUrl: z.string().optional().describe("Auth callback URL for this provider"),
|
|
64
|
+
useProxy: z.boolean().optional().describe("Clear custom credentials and use AscendKit managed proxy credentials"),
|
|
43
65
|
}, async (params) => {
|
|
66
|
+
if (params.useProxy && (params.clientId || params.clientSecret)) {
|
|
67
|
+
return {
|
|
68
|
+
content: [{
|
|
69
|
+
type: "text",
|
|
70
|
+
text: "Cannot use useProxy with custom credentials. Either set useProxy to clear credentials, or provide clientId and clientSecret.",
|
|
71
|
+
}],
|
|
72
|
+
isError: true,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
if (params.useProxy) {
|
|
76
|
+
const data = await auth.deleteOAuthCredentials(client, params.provider);
|
|
77
|
+
return {
|
|
78
|
+
content: [{
|
|
79
|
+
type: "text",
|
|
80
|
+
text: `Cleared custom OAuth credentials for ${params.provider}. Now using AscendKit managed proxy credentials.\n\n${JSON.stringify(data, null, 2)}`,
|
|
81
|
+
}],
|
|
82
|
+
};
|
|
83
|
+
}
|
|
44
84
|
if ((params.clientId && !params.clientSecret) || (!params.clientId && params.clientSecret)) {
|
|
45
85
|
return {
|
|
46
86
|
content: [{
|
|
@@ -68,8 +108,26 @@ export function registerAuthTools(server, client) {
|
|
|
68
108
|
}],
|
|
69
109
|
};
|
|
70
110
|
});
|
|
71
|
-
server.tool("
|
|
111
|
+
server.tool("auth_user_list", "List all project users (end-users who signed up via the SDK)", {}, async () => {
|
|
72
112
|
const data = await auth.listUsers(client);
|
|
73
113
|
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
74
114
|
});
|
|
115
|
+
server.tool("auth_user_remove", "Deactivate a user (soft delete). The user record is preserved but marked inactive.", {
|
|
116
|
+
userId: z.string().describe("User ID (usr_ prefixed)"),
|
|
117
|
+
}, async (params) => {
|
|
118
|
+
const data = await auth.deleteUser(client, params.userId);
|
|
119
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
120
|
+
});
|
|
121
|
+
server.tool("auth_user_bulk_remove", "Bulk deactivate users (soft delete). All specified users are marked inactive.", {
|
|
122
|
+
userIds: z.array(z.string()).min(1).max(100).describe("Array of user IDs (usr_ prefixed)"),
|
|
123
|
+
}, async (params) => {
|
|
124
|
+
const data = await auth.bulkDeleteUsers(client, params.userIds);
|
|
125
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
126
|
+
});
|
|
127
|
+
server.tool("auth_user_reactivate", "Reactivate a previously deactivated user.", {
|
|
128
|
+
userId: z.string().describe("User ID (usr_ prefixed)"),
|
|
129
|
+
}, async (params) => {
|
|
130
|
+
const data = await auth.reactivateUser(client, params.userId);
|
|
131
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
132
|
+
});
|
|
75
133
|
}
|