@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.
@@ -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 : secretKey)
193
- : secretKey;
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 = { "Content-Type": "application/json" };
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
- process.exit(1);
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
- // Fetch project name for echo-back
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
- saveEnvContext({
438
- publicKey: result.environment.publicKey,
439
- projectId: result.project.id,
440
- projectName: result.project.name,
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
- return client.platformRequest("PATCH", `/api/platform/projects/${params.projectId}/environments/${params.envId}`, { variables: params.variables });
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();
@@ -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.get("/api/surveys");
5
+ return client.managedGet("/api/surveys");
6
6
  }
7
7
  export async function getSurvey(client, surveyId) {
8
- return client.get(`/api/surveys/${surveyId}`);
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.get(`/api/surveys/${surveyId}/invitations`);
20
+ return client.managedGet(`/api/surveys/${surveyId}/invitations`);
21
21
  }
22
22
  export async function getAnalytics(client, surveyId) {
23
- return client.get(`/api/surveys/${surveyId}/analytics`);
23
+ return client.managedGet(`/api/surveys/${surveyId}/analytics`);
24
24
  }
25
25
  export async function listQuestions(client, surveyId) {
26
- return client.get(`/api/surveys/${surveyId}/questions`);
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: "0.1.0",
32
+ version: CLI_VERSION,
22
33
  });
23
- server.tool("ascendkit_configure", "Configure AscendKit with your project's public key. Must be called before using any other AscendKit tools.", {
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 project ${params.publicKey}` }],
57
+ content: [{ type: "text", text: `Configured for environment ${params.publicKey}` }],
30
58
  };
31
59
  });
32
60
  registerPlatformTools(server, client);
@@ -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("auth_get_settings", "Get authentication settings for the current project (enabled providers, features, OAuth configs)", {}, async () => {
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: JSON.stringify(data, null, 2) }] };
20
+ return { content: [{ type: "text", text: formatAuthSettings(data) }] };
7
21
  });
8
- server.tool("auth_update_settings", "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.", {
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: JSON.stringify(data, null, 2) }] };
42
+ return { content: [{ type: "text", text: formatAuthSettings(data) }] };
29
43
  });
30
- server.tool("auth_update_providers", "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.", {
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: JSON.stringify(data, null, 2) }] };
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("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.", {
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("auth_list_users", "List all project users (end-users who signed up via the SDK)", {}, async () => {
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("auth_delete_user", "Deactivate a user (soft delete). The user record is preserved but marked inactive.", {
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("auth_bulk_delete_users", "Bulk deactivate users (soft delete). All specified users are marked inactive.", {
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("auth_reactivate_user", "Reactivate a previously deactivated user.", {
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);
@@ -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("content_create_template", "Create a new email content template with HTML and plain-text versions. Supports {{variable}} placeholders.", {
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: JSON.stringify(data, null, 2) }] };
25
+ return { content: [{ type: "text", text: formatTemplateSummary(data) }] };
14
26
  });
15
- server.tool("content_list_templates", "List all content templates for the current project", {
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("content_get_template", "Get a content template by ID, including current version body and variables", {
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: JSON.stringify(data, null, 2) }] };
41
+ return { content: [{ type: "text", text: formatTemplateSummary(data) }] };
30
42
  });
31
- server.tool("content_update_template", "Update a content template. Creates a new immutable version — previous versions are preserved for result tracking.", {
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: JSON.stringify(data, null, 2) }] };
55
+ return { content: [{ type: "text", text: formatTemplateSummary(data) }] };
44
56
  });
45
- server.tool("content_delete_template", "Delete a content template and all its versions", {
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
- const data = await content.deleteTemplate(client, params.templateId);
49
- return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
60
+ await content.deleteTemplate(client, params.templateId);
61
+ return { content: [{ type: "text", text: `Removed template ${params.templateId}.` }] };
50
62
  });
51
- server.tool("content_list_versions", "List all versions of a content template (newest first)", {
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("content_get_version", "Get a specific version of a content template", {
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) => {