@ascendkit/cli 0.2.6 → 0.3.1
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 +36 -9
- package/dist/cli.js +869 -474
- 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/journeys.d.ts +1 -0
- package/dist/commands/journeys.js +9 -6
- package/dist/commands/platform.d.ts +22 -2
- package/dist/commands/platform.js +211 -103
- package/dist/commands/surveys.js +5 -5
- package/dist/mcp.js +31 -3
- package/dist/tools/auth.js +32 -11
- package/dist/tools/content.js +24 -12
- package/dist/tools/email.js +47 -17
- package/dist/tools/import.js +9 -9
- package/dist/tools/journeys.js +21 -14
- package/dist/tools/platform.js +125 -10
- package/dist/tools/surveys.js +9 -9
- package/dist/utils/correlation.d.ts +11 -0
- package/dist/utils/correlation.js +67 -0
- package/dist/utils/exit.d.ts +23 -0
- package/dist/utils/exit.js +64 -0
- package/dist/utils/journey-format.d.ts +6 -0
- package/dist/utils/journey-format.js +6 -4
- package/dist/utils/redaction.d.ts +16 -0
- package/dist/utils/redaction.js +114 -0
- package/dist/utils/survey-format.js +2 -2
- package/dist/utils/telemetry.d.ts +32 -0
- package/dist/utils/telemetry.js +47 -0
- package/package.json +5 -5
|
@@ -1,8 +1,10 @@
|
|
|
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
|
+
import { exitCli } from "../utils/exit.js";
|
|
7
|
+
import { correlationHeaders } from "../utils/correlation.js";
|
|
6
8
|
const POLL_INTERVAL_MS = 2000;
|
|
7
9
|
const DEVICE_CODE_EXPIRY_MS = 300_000; // 5 minutes
|
|
8
10
|
const IGNORE_DIRS = new Set([".git", "node_modules", ".next", "dist", "build"]);
|
|
@@ -16,6 +18,14 @@ const ASCENDKIT_ENV_KEYS = [
|
|
|
16
18
|
];
|
|
17
19
|
const ASCENDKIT_BLOCK_START = "# --- AscendKit Managed Environment Variables ---";
|
|
18
20
|
const ASCENDKIT_BLOCK_END = "# --- End AscendKit Managed Environment Variables ---";
|
|
21
|
+
function normalizePromotionTier(targetTier) {
|
|
22
|
+
const normalized = targetTier.trim().toLowerCase();
|
|
23
|
+
if (normalized === "production")
|
|
24
|
+
return "prod";
|
|
25
|
+
if (normalized === "staging")
|
|
26
|
+
return "beta";
|
|
27
|
+
return normalized;
|
|
28
|
+
}
|
|
19
29
|
async function promptYesNo(question, defaultYes) {
|
|
20
30
|
const suffix = defaultYes ? "[Y/n]" : "[y/N]";
|
|
21
31
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
@@ -184,13 +194,14 @@ async function updateRuntimeEnvFile(filePath, apiUrl, publicKey, secretKey, opti
|
|
|
184
194
|
const existingPublicKey = readEnvValue(original, "ASCENDKIT_ENV_KEY");
|
|
185
195
|
const existingSecretKey = readEnvValue(original, "ASCENDKIT_SECRET_KEY");
|
|
186
196
|
const existingWebhookSecret = readEnvValue(original, "ASCENDKIT_WEBHOOK_SECRET") ?? "";
|
|
197
|
+
const incomingSecretKey = (secretKey ?? "").trim();
|
|
187
198
|
const resolvedApiUrl = apiUrl;
|
|
188
199
|
const resolvedPublicKey = preserveExistingKeys
|
|
189
200
|
? (existingPublicKey && existingPublicKey.trim() ? existingPublicKey : publicKey)
|
|
190
201
|
: publicKey;
|
|
191
202
|
const resolvedSecretKey = preserveExistingKeys
|
|
192
|
-
? (existingSecretKey && existingSecretKey.trim() ? existingSecretKey :
|
|
193
|
-
:
|
|
203
|
+
? (existingSecretKey && existingSecretKey.trim() ? existingSecretKey : incomingSecretKey)
|
|
204
|
+
: (incomingSecretKey || existingSecretKey || "");
|
|
194
205
|
const resolvedWebhookSecret = options?.webhookSecret ?? existingWebhookSecret;
|
|
195
206
|
return rewriteAscendKitEnvBlock(filePath, {
|
|
196
207
|
NEXT_PUBLIC_ASCENDKIT_API_URL: resolvedApiUrl,
|
|
@@ -201,6 +212,124 @@ async function updateRuntimeEnvFile(filePath, apiUrl, publicKey, secretKey, opti
|
|
|
201
212
|
ASCENDKIT_WEBHOOK_SECRET: resolvedWebhookSecret,
|
|
202
213
|
});
|
|
203
214
|
}
|
|
215
|
+
async function syncLocalEnvFiles(apiUrl, publicKey, secretKey) {
|
|
216
|
+
const cwd = process.cwd();
|
|
217
|
+
const folders = await findEnvFolders(cwd);
|
|
218
|
+
const updatedFiles = [];
|
|
219
|
+
for (const folder of folders) {
|
|
220
|
+
for (const filePath of folder.examples) {
|
|
221
|
+
const changed = await updateEnvExampleFile(filePath);
|
|
222
|
+
if (changed)
|
|
223
|
+
updatedFiles.push(path.relative(cwd, filePath));
|
|
224
|
+
}
|
|
225
|
+
for (const filePath of folder.runtime) {
|
|
226
|
+
const changed = await updateRuntimeEnvFile(filePath, apiUrl, publicKey, secretKey);
|
|
227
|
+
if (changed)
|
|
228
|
+
updatedFiles.push(path.relative(cwd, filePath));
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return updatedFiles;
|
|
232
|
+
}
|
|
233
|
+
function saveActiveEnvironmentContext(result) {
|
|
234
|
+
saveEnvContext({
|
|
235
|
+
publicKey: result.environment.publicKey,
|
|
236
|
+
projectId: result.project.id,
|
|
237
|
+
projectName: result.project.name,
|
|
238
|
+
environmentId: result.environment.id,
|
|
239
|
+
environmentName: result.environment.name,
|
|
240
|
+
tier: result.environment.tier,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
async function activateEnvironment(auth, result, options) {
|
|
244
|
+
const promptForFileUpdates = options?.promptForFileUpdates ?? true;
|
|
245
|
+
const shouldSetupWebhook = options?.setupWebhook ?? true;
|
|
246
|
+
const logOutput = options?.logOutput ?? true;
|
|
247
|
+
saveActiveEnvironmentContext(result);
|
|
248
|
+
if (logOutput) {
|
|
249
|
+
console.log(` → Project: ${result.project.name}`);
|
|
250
|
+
console.log(` → Environment: ${result.environment.name}`);
|
|
251
|
+
console.log(` → Role: ${result.role}`);
|
|
252
|
+
}
|
|
253
|
+
const cwd = process.cwd();
|
|
254
|
+
const folders = await findEnvFolders(cwd);
|
|
255
|
+
const updatedFiles = [];
|
|
256
|
+
const consentedRuntimeFolders = [];
|
|
257
|
+
if (folders.length > 0) {
|
|
258
|
+
if (logOutput) {
|
|
259
|
+
console.log(`\nFound env files in ${folders.length} folder(s).`);
|
|
260
|
+
}
|
|
261
|
+
for (const folder of folders) {
|
|
262
|
+
let update = true;
|
|
263
|
+
if (promptForFileUpdates) {
|
|
264
|
+
const fileNames = [...folder.examples, ...folder.runtime]
|
|
265
|
+
.map(f => path.basename(f)).join(", ");
|
|
266
|
+
update = await promptYesNo(` ${folder.label} (${fileNames}) — update?`, true);
|
|
267
|
+
}
|
|
268
|
+
if (!update)
|
|
269
|
+
continue;
|
|
270
|
+
for (const filePath of folder.examples) {
|
|
271
|
+
const changed = await updateEnvExampleFile(filePath);
|
|
272
|
+
if (changed)
|
|
273
|
+
updatedFiles.push(path.relative(cwd, filePath));
|
|
274
|
+
}
|
|
275
|
+
if (folder.runtime.length > 0) {
|
|
276
|
+
consentedRuntimeFolders.push(folder);
|
|
277
|
+
for (const filePath of folder.runtime) {
|
|
278
|
+
const changed = await updateRuntimeEnvFile(filePath, auth.apiUrl, result.environment.publicKey, result.environment.secretKey);
|
|
279
|
+
if (changed)
|
|
280
|
+
updatedFiles.push(path.relative(cwd, filePath));
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
if (logOutput) {
|
|
286
|
+
console.log("\nUpdated files:");
|
|
287
|
+
if (updatedFiles.length === 0) {
|
|
288
|
+
console.log(" (none)");
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
for (const filePath of updatedFiles)
|
|
292
|
+
console.log(` - ${filePath}`);
|
|
293
|
+
}
|
|
294
|
+
if (!result.environment.secretKey) {
|
|
295
|
+
console.log("\nNote: your role does not have access to the environment secret key.");
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
if (!shouldSetupWebhook) {
|
|
299
|
+
return { updatedFiles, webhookSecret: null };
|
|
300
|
+
}
|
|
301
|
+
const webhookSecret = await setupWebhook(auth, result.environment.publicKey);
|
|
302
|
+
if (webhookSecret) {
|
|
303
|
+
if (consentedRuntimeFolders.length > 0) {
|
|
304
|
+
for (const folder of consentedRuntimeFolders) {
|
|
305
|
+
for (const filePath of folder.runtime) {
|
|
306
|
+
await updateRuntimeEnvFile(filePath, auth.apiUrl, result.environment.publicKey, result.environment.secretKey, { preserveExistingKeys: true, webhookSecret });
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
if (logOutput) {
|
|
310
|
+
console.log("\nWebhook secret written to .env file(s).");
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
else if (logOutput) {
|
|
314
|
+
console.log(`\nAdd this to your environment:`);
|
|
315
|
+
console.log(` ASCENDKIT_WEBHOOK_SECRET=${webhookSecret}`);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
if (logOutput) {
|
|
319
|
+
const remainingSteps = [];
|
|
320
|
+
if (!webhookSecret) {
|
|
321
|
+
remainingSteps.push("Set up a webhook endpoint (run ascendkit set-env again, or use the portal).");
|
|
322
|
+
}
|
|
323
|
+
remainingSteps.push("Ensure frontend and backend are using keys from the same AscendKit environment.");
|
|
324
|
+
if (remainingSteps.length > 0) {
|
|
325
|
+
console.log("\nWhat you still need to do:");
|
|
326
|
+
for (let i = 0; i < remainingSteps.length; i++) {
|
|
327
|
+
console.log(` ${i + 1}. ${remainingSteps[i]}`);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return { updatedFiles, webhookSecret };
|
|
332
|
+
}
|
|
204
333
|
function generateDeviceCode() {
|
|
205
334
|
const chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; // no I, O, 0, 1 for readability
|
|
206
335
|
let code = "";
|
|
@@ -212,7 +341,10 @@ function generateDeviceCode() {
|
|
|
212
341
|
return code;
|
|
213
342
|
}
|
|
214
343
|
async function apiRequest(apiUrl, method, path, body, token, publicKey) {
|
|
215
|
-
const headers = {
|
|
344
|
+
const headers = {
|
|
345
|
+
"Content-Type": "application/json",
|
|
346
|
+
...correlationHeaders(),
|
|
347
|
+
};
|
|
216
348
|
if (token)
|
|
217
349
|
headers["Authorization"] = `Bearer ${token}`;
|
|
218
350
|
if (publicKey)
|
|
@@ -223,6 +355,28 @@ async function apiRequest(apiUrl, method, path, body, token, publicKey) {
|
|
|
223
355
|
const response = await fetch(`${apiUrl}${path}`, init);
|
|
224
356
|
if (!response.ok) {
|
|
225
357
|
const text = await response.text();
|
|
358
|
+
if (!text) {
|
|
359
|
+
throw new Error(`API error ${response.status}: ${response.statusText}`);
|
|
360
|
+
}
|
|
361
|
+
try {
|
|
362
|
+
const parsed = JSON.parse(text);
|
|
363
|
+
if (typeof parsed.error === "string") {
|
|
364
|
+
throw new Error(parsed.error);
|
|
365
|
+
}
|
|
366
|
+
if (typeof parsed.detail === "string") {
|
|
367
|
+
throw new Error(parsed.detail);
|
|
368
|
+
}
|
|
369
|
+
if (parsed.detail
|
|
370
|
+
&& typeof parsed.detail === "object"
|
|
371
|
+
&& typeof parsed.detail.message === "string") {
|
|
372
|
+
throw new Error(parsed.detail.message);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
catch (error) {
|
|
376
|
+
if (error instanceof Error && error.message !== text) {
|
|
377
|
+
throw error;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
226
380
|
throw new Error(`API error ${response.status}: ${text}`);
|
|
227
381
|
}
|
|
228
382
|
const json = await response.json();
|
|
@@ -322,7 +476,8 @@ function requireAuth() {
|
|
|
322
476
|
const auth = loadAuth();
|
|
323
477
|
if (!auth?.token) {
|
|
324
478
|
console.error("Not initialized. Run: ascendkit init");
|
|
325
|
-
|
|
479
|
+
exitCli(1);
|
|
480
|
+
throw new Error("unreachable"); // exitCli terminates the process
|
|
326
481
|
}
|
|
327
482
|
return auth;
|
|
328
483
|
}
|
|
@@ -330,6 +485,14 @@ export async function listProjects() {
|
|
|
330
485
|
const auth = requireAuth();
|
|
331
486
|
return apiRequest(auth.apiUrl, "GET", "/api/platform/projects", undefined, auth.token);
|
|
332
487
|
}
|
|
488
|
+
export async function showProject(projectId) {
|
|
489
|
+
const projects = await listProjects();
|
|
490
|
+
const project = projects.find((item) => item.id === projectId);
|
|
491
|
+
if (!project) {
|
|
492
|
+
throw new Error(`Project ${projectId} not found.`);
|
|
493
|
+
}
|
|
494
|
+
return project;
|
|
495
|
+
}
|
|
333
496
|
export async function createProject(name, description, enabledServices) {
|
|
334
497
|
const auth = requireAuth();
|
|
335
498
|
return apiRequest(auth.apiUrl, "POST", "/api/platform/projects", {
|
|
@@ -349,26 +512,7 @@ export async function useEnvironment(tier, projectId) {
|
|
|
349
512
|
if (!env) {
|
|
350
513
|
throw new Error(`No ${tier} environment found for project ${projectId}. Available: ${envs.map(e => e.tier).join(", ")}`);
|
|
351
514
|
}
|
|
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}`);
|
|
515
|
+
await setEnv(env.publicKey);
|
|
372
516
|
}
|
|
373
517
|
async function setupWebhook(auth, publicKey) {
|
|
374
518
|
// Never run webhook setup in non-interactive mode — selecting a webhook
|
|
@@ -434,82 +578,11 @@ async function setupWebhook(auth, publicKey) {
|
|
|
434
578
|
export async function setEnv(publicKey) {
|
|
435
579
|
const auth = requireAuth();
|
|
436
580
|
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,
|
|
581
|
+
await activateEnvironment(auth, result, {
|
|
582
|
+
promptForFileUpdates: true,
|
|
583
|
+
setupWebhook: true,
|
|
584
|
+
logOutput: true,
|
|
444
585
|
});
|
|
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
586
|
}
|
|
514
587
|
export async function mcpLogin(client, params) {
|
|
515
588
|
const data = await client.publicRequest("POST", "/api/platform/auth/token", {
|
|
@@ -517,8 +590,20 @@ export async function mcpLogin(client, params) {
|
|
|
517
590
|
password: params.password,
|
|
518
591
|
});
|
|
519
592
|
client.configurePlatform(data.token);
|
|
593
|
+
saveAuth({ token: data.token, apiUrl: client.currentApiUrl });
|
|
594
|
+
ensureGitignore();
|
|
520
595
|
return data;
|
|
521
596
|
}
|
|
597
|
+
export async function mcpSetEnvironmentByPublicKey(client, publicKey) {
|
|
598
|
+
const result = await client.platformRequest("GET", `/api/platform/resolve-key?pk=${encodeURIComponent(publicKey)}`);
|
|
599
|
+
client.configure(result.environment.publicKey);
|
|
600
|
+
const { updatedFiles } = await activateEnvironment({ token: "__mcp__", apiUrl: client.currentApiUrl }, result, {
|
|
601
|
+
promptForFileUpdates: false,
|
|
602
|
+
setupWebhook: false,
|
|
603
|
+
logOutput: false,
|
|
604
|
+
});
|
|
605
|
+
return { ...result, updatedFiles };
|
|
606
|
+
}
|
|
522
607
|
export async function mcpListProjects(client) {
|
|
523
608
|
return client.platformRequest("GET", "/api/platform/projects");
|
|
524
609
|
}
|
|
@@ -546,7 +631,7 @@ export async function updateEnvironment(projectId, envId, name, description) {
|
|
|
546
631
|
}
|
|
547
632
|
export async function promoteEnvironment(environmentId, targetTier) {
|
|
548
633
|
const auth = requireAuth();
|
|
549
|
-
return apiRequest(auth.apiUrl, "POST", `/api/platform/environments/${environmentId}/promote`, { targetTier, dryRun: false }, auth.token);
|
|
634
|
+
return apiRequest(auth.apiUrl, "POST", `/api/platform/environments/${environmentId}/promote`, { targetTier: normalizePromotionTier(targetTier), dryRun: false }, auth.token);
|
|
550
635
|
}
|
|
551
636
|
export async function mcpUpdateEnvironment(client, params) {
|
|
552
637
|
const body = {};
|
|
@@ -557,10 +642,33 @@ export async function mcpUpdateEnvironment(client, params) {
|
|
|
557
642
|
return client.platformRequest("PATCH", `/api/platform/projects/${params.projectId}/environments/${params.environmentId}`, body);
|
|
558
643
|
}
|
|
559
644
|
export async function mcpPromoteEnvironment(client, params) {
|
|
560
|
-
return client.platformRequest("POST", `/api/platform/environments/${params.environmentId}/promote`, { targetTier: params.targetTier, dryRun: false });
|
|
645
|
+
return client.platformRequest("POST", `/api/platform/environments/${params.environmentId}/promote`, { targetTier: normalizePromotionTier(params.targetTier), dryRun: false });
|
|
646
|
+
}
|
|
647
|
+
function resolveActiveEnvironmentIds(params) {
|
|
648
|
+
if (params?.projectId && params?.envId) {
|
|
649
|
+
return { projectId: params.projectId, envId: params.envId };
|
|
650
|
+
}
|
|
651
|
+
const ctx = loadEnvContext();
|
|
652
|
+
if (!ctx?.projectId || !ctx.environmentId) {
|
|
653
|
+
throw new Error("No active environment found. Use ascendkit_set_env or ascendkit set-env first.");
|
|
654
|
+
}
|
|
655
|
+
return {
|
|
656
|
+
projectId: params?.projectId ?? ctx.projectId,
|
|
657
|
+
envId: params?.envId ?? ctx.environmentId,
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
export async function mcpGetEnvironment(client, params) {
|
|
661
|
+
const target = resolveActiveEnvironmentIds(params);
|
|
662
|
+
const envs = await client.platformRequest("GET", `/api/platform/projects/${target.projectId}/environments`);
|
|
663
|
+
const env = envs.find((item) => item.id === target.envId);
|
|
664
|
+
if (!env) {
|
|
665
|
+
throw new Error(`Environment ${target.envId} not found in project ${target.projectId}.`);
|
|
666
|
+
}
|
|
667
|
+
return env;
|
|
561
668
|
}
|
|
562
669
|
export async function mcpUpdateEnvironmentVariables(client, params) {
|
|
563
|
-
|
|
670
|
+
const target = resolveActiveEnvironmentIds(params);
|
|
671
|
+
return client.platformRequest("PATCH", `/api/platform/projects/${target.projectId}/environments/${target.envId}`, { variables: params.variables });
|
|
564
672
|
}
|
|
565
673
|
export async function getEnvironment(projectId, envId) {
|
|
566
674
|
const auth = requireAuth();
|
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
|
@@ -13,20 +13,48 @@ import { registerWebhookTools } from "./tools/webhooks.js";
|
|
|
13
13
|
import { registerCampaignTools } from "./tools/campaigns.js";
|
|
14
14
|
import { registerImportTools } from "./tools/import.js";
|
|
15
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";
|
|
16
19
|
const client = new AscendKitClient({
|
|
17
20
|
apiUrl: process.env.ASCENDKIT_API_URL ?? DEFAULT_API_URL,
|
|
18
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
|
+
}
|
|
19
30
|
const server = new McpServer({
|
|
20
31
|
name: "ascendkit",
|
|
21
|
-
version:
|
|
32
|
+
version: CLI_VERSION,
|
|
22
33
|
});
|
|
23
|
-
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.", {
|
|
24
35
|
publicKey: z.string().describe("Your project's public key (pk_dev_..., pk_beta_..., or pk_prod_...)"),
|
|
25
36
|
apiUrl: z.string().optional().describe("AscendKit API URL (default: https://api.ascendkit.dev)"),
|
|
26
37
|
}, async (params) => {
|
|
27
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
|
+
}
|
|
28
56
|
return {
|
|
29
|
-
content: [{ type: "text", text: `Configured for
|
|
57
|
+
content: [{ type: "text", text: `Configured for environment ${params.publicKey}` }],
|
|
30
58
|
};
|
|
31
59
|
});
|
|
32
60
|
registerPlatformTools(server, client);
|
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,17 +39,24 @@ 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)"),
|
|
@@ -87,23 +108,23 @@ export function registerAuthTools(server, client) {
|
|
|
87
108
|
}],
|
|
88
109
|
};
|
|
89
110
|
});
|
|
90
|
-
server.tool("
|
|
111
|
+
server.tool("auth_user_list", "List all project users (end-users who signed up via the SDK)", {}, async () => {
|
|
91
112
|
const data = await auth.listUsers(client);
|
|
92
113
|
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
93
114
|
});
|
|
94
|
-
server.tool("
|
|
115
|
+
server.tool("auth_user_remove", "Deactivate a user (soft delete). The user record is preserved but marked inactive.", {
|
|
95
116
|
userId: z.string().describe("User ID (usr_ prefixed)"),
|
|
96
117
|
}, async (params) => {
|
|
97
118
|
const data = await auth.deleteUser(client, params.userId);
|
|
98
119
|
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
99
120
|
});
|
|
100
|
-
server.tool("
|
|
121
|
+
server.tool("auth_user_bulk_remove", "Bulk deactivate users (soft delete). All specified users are marked inactive.", {
|
|
101
122
|
userIds: z.array(z.string()).min(1).max(100).describe("Array of user IDs (usr_ prefixed)"),
|
|
102
123
|
}, async (params) => {
|
|
103
124
|
const data = await auth.bulkDeleteUsers(client, params.userIds);
|
|
104
125
|
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
105
126
|
});
|
|
106
|
-
server.tool("
|
|
127
|
+
server.tool("auth_user_reactivate", "Reactivate a previously deactivated user.", {
|
|
107
128
|
userId: z.string().describe("User ID (usr_ prefixed)"),
|
|
108
129
|
}, async (params) => {
|
|
109
130
|
const data = await auth.reactivateUser(client, params.userId);
|
package/dist/tools/content.js
CHANGED
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import * as content from "../commands/content.js";
|
|
3
|
+
function formatTemplateSummary(data) {
|
|
4
|
+
const lines = [`Template: ${data.name} (${data.id})`];
|
|
5
|
+
if (data.slug)
|
|
6
|
+
lines.push(`Slug: ${data.slug}`);
|
|
7
|
+
lines.push(`Subject: ${data.subject ?? "-"}`);
|
|
8
|
+
if (data.currentVersion != null)
|
|
9
|
+
lines.push(`Version: ${data.currentVersion}`);
|
|
10
|
+
if (Array.isArray(data.variables) && data.variables.length > 0) {
|
|
11
|
+
lines.push(`Variables: ${data.variables.join(", ")}`);
|
|
12
|
+
}
|
|
13
|
+
return lines.join("\n");
|
|
14
|
+
}
|
|
3
15
|
export function registerContentTools(server, client) {
|
|
4
|
-
server.tool("
|
|
16
|
+
server.tool("template_create", "Create a new email content template with HTML and plain-text versions. Supports {{variable}} placeholders.", {
|
|
5
17
|
name: z.string().describe("Template name, e.g. 'welcome-email'"),
|
|
6
18
|
subject: z.string().describe("Email subject line"),
|
|
7
19
|
bodyHtml: z.string().describe("HTML email body. Use {{variable}} for placeholders"),
|
|
@@ -10,9 +22,9 @@ export function registerContentTools(server, client) {
|
|
|
10
22
|
description: z.string().optional().describe("Template description"),
|
|
11
23
|
}, async (params) => {
|
|
12
24
|
const data = await content.createTemplate(client, params);
|
|
13
|
-
return { content: [{ type: "text", text:
|
|
25
|
+
return { content: [{ type: "text", text: formatTemplateSummary(data) }] };
|
|
14
26
|
});
|
|
15
|
-
server.tool("
|
|
27
|
+
server.tool("template_list", "List all content templates for the current project", {
|
|
16
28
|
query: z.string().optional().describe("Search templates by name, slug, or description"),
|
|
17
29
|
isSystem: z.boolean().optional().describe("Filter: true for system templates, false for custom"),
|
|
18
30
|
}, async (params) => {
|
|
@@ -22,13 +34,13 @@ export function registerContentTools(server, client) {
|
|
|
22
34
|
const data = await content.listTemplates(client, listParams);
|
|
23
35
|
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
24
36
|
});
|
|
25
|
-
server.tool("
|
|
37
|
+
server.tool("template_show", "Get a content template by ID, including current version body and variables", {
|
|
26
38
|
templateId: z.string().describe("Template ID (tpl_ prefixed)"),
|
|
27
39
|
}, async (params) => {
|
|
28
40
|
const data = await content.getTemplate(client, params.templateId);
|
|
29
|
-
return { content: [{ type: "text", text:
|
|
41
|
+
return { content: [{ type: "text", text: formatTemplateSummary(data) }] };
|
|
30
42
|
});
|
|
31
|
-
server.tool("
|
|
43
|
+
server.tool("template_update", "Update a content template. Creates a new immutable version — previous versions are preserved for result tracking.", {
|
|
32
44
|
templateId: z.string().describe("Template ID (tpl_ prefixed)"),
|
|
33
45
|
subject: z.string().optional().describe("New email subject line"),
|
|
34
46
|
bodyHtml: z.string().optional().describe("New HTML email body"),
|
|
@@ -40,21 +52,21 @@ export function registerContentTools(server, client) {
|
|
|
40
52
|
}, async (params) => {
|
|
41
53
|
const { templateId, ...rest } = params;
|
|
42
54
|
const data = await content.updateTemplate(client, templateId, rest);
|
|
43
|
-
return { content: [{ type: "text", text:
|
|
55
|
+
return { content: [{ type: "text", text: formatTemplateSummary(data) }] };
|
|
44
56
|
});
|
|
45
|
-
server.tool("
|
|
57
|
+
server.tool("template_remove", "Delete a content template and all its versions", {
|
|
46
58
|
templateId: z.string().describe("Template ID (tpl_ prefixed)"),
|
|
47
59
|
}, async (params) => {
|
|
48
|
-
|
|
49
|
-
return { content: [{ type: "text", text:
|
|
60
|
+
await content.deleteTemplate(client, params.templateId);
|
|
61
|
+
return { content: [{ type: "text", text: `Removed template ${params.templateId}.` }] };
|
|
50
62
|
});
|
|
51
|
-
server.tool("
|
|
63
|
+
server.tool("template_version_list", "List all versions of a content template (newest first)", {
|
|
52
64
|
templateId: z.string().describe("Template ID (tpl_ prefixed)"),
|
|
53
65
|
}, async (params) => {
|
|
54
66
|
const data = await content.listVersions(client, params.templateId);
|
|
55
67
|
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
56
68
|
});
|
|
57
|
-
server.tool("
|
|
69
|
+
server.tool("template_version_show", "Get a specific version of a content template", {
|
|
58
70
|
templateId: z.string().describe("Template ID (tpl_ prefixed)"),
|
|
59
71
|
versionNumber: z.number().describe("Version number to retrieve"),
|
|
60
72
|
}, async (params) => {
|