@iterable/mcp 1.2.2 → 1.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/keys-cli.js CHANGED
@@ -6,21 +6,385 @@ import { readFileSync } from "fs";
6
6
  import inquirer from "inquirer";
7
7
  import path from "path";
8
8
  import { fileURLToPath } from "url";
9
+ import { COMMAND_NAME, KEYS_COMMAND_TABLE } from "./utils/command-info.js";
9
10
  const { dirname, join } = path;
10
11
  import { getSpinner, loadUi } from "./utils/cli-env.js";
12
+ import { promptForIterableBaseUrl } from "./utils/endpoint-prompt.js";
11
13
  import { getKeyStorageMessage } from "./utils/formatting.js";
12
14
  import { promptForApiKey } from "./utils/password-prompt.js";
15
+ import { sanitizeString } from "./utils/sanitize.js";
13
16
  // Get package version
14
17
  const packageJson = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), "..", "package.json"), "utf-8"));
18
+ /**
19
+ * Display key details including permissions
20
+ *
21
+ * @param meta - The key metadata to display
22
+ * @param ui - UI utilities (chalk, formatKeyValue, linkColor)
23
+ */
24
+ export function displayKeyDetails(meta, ui) {
25
+ const { chalk, formatKeyValue, linkColor } = ui;
26
+ console.log(formatKeyValue("Name", meta.name, chalk.white.bold));
27
+ console.log(formatKeyValue("ID", meta.id, chalk.gray));
28
+ console.log(formatKeyValue("Endpoint", meta.baseUrl, linkColor()));
29
+ const pii = meta.env?.ITERABLE_USER_PII === "true"
30
+ ? chalk.green("Enabled")
31
+ : chalk.gray("Disabled");
32
+ const writes = meta.env?.ITERABLE_ENABLE_WRITES === "true"
33
+ ? chalk.green("Enabled")
34
+ : chalk.gray("Disabled");
35
+ const sends = meta.env?.ITERABLE_ENABLE_SENDS === "true"
36
+ ? chalk.green("Enabled")
37
+ : chalk.gray("Disabled");
38
+ console.log(formatKeyValue("User PII", pii));
39
+ console.log(formatKeyValue("Writes", writes));
40
+ console.log(formatKeyValue("Sends", sends));
41
+ }
42
+ /**
43
+ * Find a key by ID or name, with helpful error messages and suggestions
44
+ *
45
+ * @param keyManager - The key manager instance
46
+ * @param idOrName - The key ID or name to find (if undefined/empty, shows usage and exits)
47
+ * @param commandName - The command name for usage messages (e.g., "update", "delete")
48
+ * @param ui - UI utilities (chalk, showError, showInfo)
49
+ * @returns The found key metadata
50
+ * @throws Exits process if key not found or missing
51
+ */
52
+ async function findKeyOrExit(keyManager, idOrName, commandName, ui) {
53
+ const { chalk, showError, showInfo } = ui;
54
+ if (!idOrName) {
55
+ console.log();
56
+ showError("Missing key name or ID");
57
+ console.log();
58
+ console.log(chalk.white.bold(" USAGE"));
59
+ console.log(chalk.white(` ${COMMAND_NAME} keys ${commandName} <name-or-id>`));
60
+ console.log();
61
+ console.log(chalk.white.bold(" EXAMPLE"));
62
+ console.log(chalk.cyan(` ${COMMAND_NAME} keys ${commandName} production`));
63
+ console.log();
64
+ showInfo("If your key name has spaces, wrap it in quotes");
65
+ console.log(chalk.gray(` Example: ${COMMAND_NAME} keys ${commandName} "My Prod Key"`));
66
+ console.log();
67
+ process.exit(1);
68
+ }
69
+ const keys = await keyManager.listKeys();
70
+ const key = keys.find((k) => k.id === idOrName || k.name === idOrName);
71
+ if (!key) {
72
+ showError(`Key not found: ${idOrName}`);
73
+ showInfo(`Run '${COMMAND_NAME} keys list' to view all keys`);
74
+ process.exit(1);
75
+ }
76
+ return key;
77
+ }
78
+ /**
79
+ * Interactive flow to save (add or update) an API key
80
+ *
81
+ * Handles the complete flow including:
82
+ * - Prompting for key details
83
+ * - Saving the key
84
+ * - Optionally activating new keys
85
+ * - Showing restart notice for active keys
86
+ *
87
+ * @param keyManager - The key manager instance
88
+ * @param existingKey - If provided, updates this key; otherwise adds a new key
89
+ * @param ui - UI utilities (chalk, icons, etc.)
90
+ * @param spinner - Spinner for loading states
91
+ * @param options - Optional configuration
92
+ * - advanced: if true, shows permission checkboxes; if false/undefined, uses secure defaults
93
+ * - skipRestartNotice: skip showing restart notice at the end
94
+ * - prefilledApiKey: if provided, skip API key prompt and use this value
95
+ * - autoActivate: if true, automatically activates new keys (for installer); if false, prompts user
96
+ * @returns The saved key's ID
97
+ */
98
+ export async function saveKeyInteractive(keyManager, existingKeyArg, ui, spinner, options) {
99
+ const { chalk, icons, showError, showSuccess, showInfo, formatKeyValue, linkColor, showBox, } = ui;
100
+ // Use local variables so we can reassign if duplicate detected
101
+ let isUpdate = !!existingKeyArg;
102
+ let existingKey = existingKeyArg;
103
+ // Helper to activate a key after selecting or updating a key
104
+ const maybeActivateKey = async (key) => {
105
+ if (key.isActive) {
106
+ console.log();
107
+ showSuccess(`"${key.name}" is already your active key`);
108
+ return key;
109
+ }
110
+ let shouldActivate = true;
111
+ if (!options?.autoActivate) {
112
+ const { activateNow } = await inquirer.prompt([
113
+ {
114
+ type: "confirm",
115
+ name: "activateNow",
116
+ message: `Set "${key.name}" as your active API key now?`,
117
+ default: !isUpdate, // Default to yes for new keys, no for updates
118
+ },
119
+ ]);
120
+ shouldActivate = activateNow;
121
+ }
122
+ if (shouldActivate) {
123
+ spinner.start("Activating key...");
124
+ await keyManager.setActiveKey(key.id);
125
+ spinner.succeed("Key activated successfully");
126
+ console.log();
127
+ showSuccess(`"${key.name}" is now your active API key`);
128
+ const updatedKey = (await keyManager.getKeyMetadata(key.id));
129
+ if (!options?.skipRestartNotice) {
130
+ console.log();
131
+ showBox("Action Required", [
132
+ chalk.yellow("Restart your AI tools to use this key"),
133
+ "",
134
+ chalk.gray("The MCP server will automatically load the active key when it starts"),
135
+ ], { icon: icons.zap, theme: "warning" });
136
+ }
137
+ return updatedKey;
138
+ }
139
+ else {
140
+ console.log();
141
+ showInfo(`Keeping your current active key. Run '${COMMAND_NAME} keys activate "${key.name}"' to switch later.`);
142
+ return key;
143
+ }
144
+ };
145
+ // Step 1: Get API key value (prompt, use prefilled, or retrieve existing)
146
+ let apiKey;
147
+ if (isUpdate) {
148
+ const { updateApiKey } = await inquirer.prompt([
149
+ {
150
+ type: "confirm",
151
+ name: "updateApiKey",
152
+ message: "Update the API key value?",
153
+ default: false,
154
+ },
155
+ ]);
156
+ if (updateApiKey) {
157
+ console.log();
158
+ apiKey = await promptForApiKey(icons.lock + " Enter your new Iterable API key: ");
159
+ }
160
+ else {
161
+ spinner.start("Retrieving existing API key...");
162
+ try {
163
+ const existingApiKey = await keyManager.getKey(existingKey.id);
164
+ if (!existingApiKey) {
165
+ spinner.fail("Failed to retrieve existing API key");
166
+ showError("Could not access the existing API key value");
167
+ console.log();
168
+ showInfo("You'll need to enter a new API key value");
169
+ apiKey = await promptForApiKey(icons.lock + " Enter your Iterable API key: ");
170
+ }
171
+ else {
172
+ spinner.succeed("Using existing API key");
173
+ apiKey = existingApiKey;
174
+ }
175
+ }
176
+ catch (error) {
177
+ spinner.fail("Failed to retrieve existing API key");
178
+ showError(error instanceof Error ? error.message : "Could not access key");
179
+ console.log();
180
+ showInfo("You'll need to enter a new API key value");
181
+ apiKey = await promptForApiKey(icons.lock + " Enter your Iterable API key: ");
182
+ }
183
+ }
184
+ }
185
+ else {
186
+ if (options?.prefilledApiKey) {
187
+ apiKey = options.prefilledApiKey;
188
+ showSuccess("Using API key from environment variable");
189
+ }
190
+ else {
191
+ apiKey = await promptForApiKey(icons.lock + " Enter your Iterable API key: ");
192
+ }
193
+ }
194
+ // Step 2: For new keys, check if this API key value already exists
195
+ if (!isUpdate) {
196
+ spinner.start("Checking for duplicate keys...");
197
+ const existingKeyWithValue = await keyManager.findKeyByValue(apiKey);
198
+ spinner.stop();
199
+ if (existingKeyWithValue) {
200
+ console.log();
201
+ showInfo(`This API key is already stored as "${existingKeyWithValue.name}"`);
202
+ console.log();
203
+ displayKeyDetails(existingKeyWithValue, {
204
+ chalk,
205
+ formatKeyValue,
206
+ linkColor,
207
+ });
208
+ console.log();
209
+ const { updateExisting } = await inquirer.prompt([
210
+ {
211
+ type: "confirm",
212
+ name: "updateExisting",
213
+ message: `Update this key with new settings?`,
214
+ default: true,
215
+ },
216
+ ]);
217
+ if (!updateExisting) {
218
+ console.log();
219
+ await maybeActivateKey(existingKeyWithValue);
220
+ console.log();
221
+ return existingKeyWithValue.id;
222
+ }
223
+ // User wants to update - convert this to an update operation
224
+ console.log();
225
+ showInfo(`Updating existing key: "${existingKeyWithValue.name}"`);
226
+ console.log();
227
+ isUpdate = true;
228
+ existingKey = existingKeyWithValue;
229
+ }
230
+ }
231
+ // Step 3: Prompt for endpoint
232
+ let baseUrl;
233
+ try {
234
+ baseUrl = await promptForIterableBaseUrl({
235
+ inquirer,
236
+ icons,
237
+ chalk,
238
+ showError,
239
+ ...(existingKey?.baseUrl ? { defaultBaseUrl: existingKey.baseUrl } : {}),
240
+ });
241
+ }
242
+ catch {
243
+ process.exit(1);
244
+ return "";
245
+ }
246
+ if (isUpdate && existingKey.baseUrl !== baseUrl) {
247
+ console.log();
248
+ showInfo(`Endpoint changing from ${existingKey.baseUrl} to ${baseUrl}`);
249
+ console.log();
250
+ }
251
+ // Step 4: Prompt for name
252
+ const validateKeyName = async (input) => {
253
+ if (!input)
254
+ return "Name is required";
255
+ if (input.length > 50)
256
+ return "Name must be 50 characters or less";
257
+ // Check for duplicate names (skip if updating and keeping the same name)
258
+ if (input !== existingKey?.name) {
259
+ const keys = await keyManager.listKeys();
260
+ if (keys.some((k) => k.name === input)) {
261
+ return `A key named "${input}" already exists. Please choose a different name.`;
262
+ }
263
+ }
264
+ return true;
265
+ };
266
+ const { name } = await inquirer.prompt([
267
+ {
268
+ type: "input",
269
+ name: "name",
270
+ message: isUpdate
271
+ ? "Enter a new name, or press Enter to keep current name:"
272
+ : "Enter a name for this API key:",
273
+ default: existingKey?.name || "default",
274
+ validate: validateKeyName,
275
+ },
276
+ ]);
277
+ // Step 5: Configure permissions
278
+ const currentEnv = existingKey?.env || {};
279
+ let selectedEnv = {
280
+ ITERABLE_USER_PII: currentEnv.ITERABLE_USER_PII || "false",
281
+ ITERABLE_ENABLE_WRITES: currentEnv.ITERABLE_ENABLE_WRITES || "false",
282
+ ITERABLE_ENABLE_SENDS: currentEnv.ITERABLE_ENABLE_SENDS || "false",
283
+ };
284
+ if (options?.advanced) {
285
+ const currentPerms = [];
286
+ if (selectedEnv.ITERABLE_USER_PII === "true")
287
+ currentPerms.push("pii");
288
+ if (selectedEnv.ITERABLE_ENABLE_WRITES === "true")
289
+ currentPerms.push("writes");
290
+ if (selectedEnv.ITERABLE_ENABLE_SENDS === "true")
291
+ currentPerms.push("sends");
292
+ const { permFlags } = await inquirer.prompt([
293
+ {
294
+ type: "checkbox",
295
+ name: "permFlags",
296
+ message: "Select permissions to enable (default: none):",
297
+ choices: [
298
+ { name: "View PII (access user data)", value: "pii" },
299
+ {
300
+ name: "Write operations (create/update/delete)",
301
+ value: "writes",
302
+ },
303
+ { name: "Sends (campaigns/journeys/events)", value: "sends" },
304
+ ],
305
+ pageSize: 6,
306
+ default: currentPerms,
307
+ },
308
+ ]);
309
+ selectedEnv = {
310
+ ITERABLE_USER_PII: permFlags.includes("pii")
311
+ ? "true"
312
+ : "false",
313
+ ITERABLE_ENABLE_WRITES: permFlags.includes("writes")
314
+ ? "true"
315
+ : "false",
316
+ ITERABLE_ENABLE_SENDS: permFlags.includes("sends")
317
+ ? "true"
318
+ : "false",
319
+ };
320
+ }
321
+ else {
322
+ console.log();
323
+ if (isUpdate) {
324
+ showInfo("Keeping existing permissions");
325
+ }
326
+ else {
327
+ showInfo("Using secure default permissions (PII, Writes, and Sends disabled)");
328
+ }
329
+ showInfo(`Run with --advanced to configure advanced permissions`);
330
+ console.log();
331
+ }
332
+ // Step 6: Save the key
333
+ spinner.start(isUpdate ? "Updating API key..." : "Storing API key securely...");
334
+ try {
335
+ const id = isUpdate
336
+ ? await keyManager.updateKey(existingKey.id, name, apiKey, baseUrl, selectedEnv)
337
+ : await keyManager.addKey(name, apiKey, baseUrl, selectedEnv);
338
+ spinner.succeed(isUpdate
339
+ ? "API key updated successfully!"
340
+ : "API key stored successfully!");
341
+ console.log();
342
+ console.log(formatKeyValue("Name", name, chalk.white.bold));
343
+ console.log(formatKeyValue("ID", id, chalk.gray));
344
+ console.log(formatKeyValue("Endpoint", baseUrl, linkColor()));
345
+ console.log(formatKeyValue("User PII", selectedEnv.ITERABLE_USER_PII === "true"
346
+ ? chalk.green("Enabled")
347
+ : chalk.gray("Disabled")));
348
+ console.log(formatKeyValue("Writes", selectedEnv.ITERABLE_ENABLE_WRITES === "true"
349
+ ? chalk.green("Enabled")
350
+ : chalk.gray("Disabled")));
351
+ console.log(formatKeyValue("Sends", selectedEnv.ITERABLE_ENABLE_SENDS === "true"
352
+ ? chalk.green("Enabled")
353
+ : chalk.gray("Disabled")));
354
+ console.log();
355
+ showSuccess(isUpdate
356
+ ? "Your API key has been updated (encrypted at rest)"
357
+ : "Your API key is now securely stored (encrypted at rest)");
358
+ const savedKey = await keyManager.getKeyMetadata(id);
359
+ if (savedKey) {
360
+ await maybeActivateKey(savedKey);
361
+ }
362
+ else {
363
+ showError("Failed to get key metadata after saving");
364
+ process.exit(1);
365
+ }
366
+ return id;
367
+ }
368
+ catch (error) {
369
+ spinner.fail(isUpdate ? "Failed to update API key" : "Failed to add API key");
370
+ const msg = error instanceof Error ? sanitizeString(error.message) : "Unknown error";
371
+ showError(msg);
372
+ process.exit(1);
373
+ }
374
+ }
15
375
  /**
16
376
  * Handle the 'keys' command and its subcommands
17
377
  */
18
378
  export async function handleKeysCommand() {
19
379
  const args = process.argv.slice(2);
20
380
  const subCommand = args[1];
381
+ // Parse common flags and get positional arguments
382
+ const hasAdvancedFlag = args.includes("--advanced");
383
+ const positionalArgs = args.filter((arg) => !arg.startsWith("--"));
21
384
  const chalk = (await import("chalk")).default;
22
385
  const { createTable, formatKeyValue, icons, showBox, showError, showInfo, showIterableLogo, showSection, showSuccess, linkColor, } = await loadUi();
23
386
  const spinner = await getSpinner();
387
+ // Dynamic import for testability (Jest mocks need runtime imports)
24
388
  const { getKeyManager } = await import("./key-manager.js");
25
389
  const keyManager = getKeyManager();
26
390
  try {
@@ -46,7 +410,7 @@ export async function handleKeysCommand() {
46
410
  chalk.gray("You haven't added any API keys yet."),
47
411
  "",
48
412
  chalk.cyan("Get started by running:"),
49
- chalk.bold.white(" iterable-mcp setup"),
413
+ chalk.bold.white(` ${COMMAND_NAME} setup`),
50
414
  ], { icon: icons.key, theme: "info" });
51
415
  }
52
416
  else {
@@ -60,7 +424,7 @@ export async function handleKeysCommand() {
60
424
  "View PII?",
61
425
  "Writes?",
62
426
  "Sends?",
63
- "Created",
427
+ "Modified",
64
428
  "Status",
65
429
  ],
66
430
  style: "normal",
@@ -69,7 +433,9 @@ export async function handleKeysCommand() {
69
433
  const statusBadge = key.isActive
70
434
  ? chalk.bgGreen.black(" ACTIVE ")
71
435
  : chalk.gray("INACTIVE");
72
- const createdDate = new Date(key.created).toLocaleDateString("en-US", {
436
+ // Show updated date if available, otherwise created date
437
+ const dateToShow = key.updated || key.created;
438
+ const formattedDate = new Date(dateToShow).toLocaleDateString("en-US", {
73
439
  year: "numeric",
74
440
  month: "short",
75
441
  day: "numeric",
@@ -91,22 +457,19 @@ export async function handleKeysCommand() {
91
457
  pii,
92
458
  writes,
93
459
  sends,
94
- chalk.gray(createdDate),
460
+ chalk.gray(formattedDate),
95
461
  statusBadge,
96
462
  ]);
97
463
  }
98
464
  console.log(table.toString());
99
465
  console.log();
100
- // Show key management tips
101
- const tips = [
102
- "Use " +
103
- chalk.cyan("keys activate <name>") +
104
- " to switch between keys",
105
- "Use " + chalk.cyan("keys add") + " to add a new API key",
466
+ // Show key management commands
467
+ showBox("Key Management", [
468
+ ...KEYS_COMMAND_TABLE.map(([cmd, desc]) => `${chalk.cyan(cmd)} - ${chalk.gray(desc)}`),
469
+ "",
106
470
  getKeyStorageMessage(),
107
- ];
108
- showBox("Quick Tips", tips, {
109
- icon: icons.bulb,
471
+ ], {
472
+ icon: icons.key,
110
473
  theme: "info",
111
474
  padding: 1,
112
475
  });
@@ -116,289 +479,86 @@ export async function handleKeysCommand() {
116
479
  case "add": {
117
480
  console.clear();
118
481
  showIterableLogo(packageJson.version);
119
- // Interactive add flow (no flags; all prompts)
120
- const { name } = await inquirer.prompt([
121
- {
122
- type: "input",
123
- name: "name",
124
- message: "Enter a name for this API key:",
125
- default: "default",
126
- validate: (input) => {
127
- if (!input)
128
- return "Name is required";
129
- if (input.length > 50)
130
- return "Name must be 50 characters or less";
131
- return true;
132
- },
133
- },
134
- ]);
135
- const { promptForIterableBaseUrl } = await import("./utils/endpoint-prompt.js");
136
- let baseUrl;
137
- try {
138
- baseUrl = await promptForIterableBaseUrl({
139
- inquirer,
140
- icons,
141
- chalk,
142
- showError,
143
- });
144
- }
145
- catch {
146
- process.exit(1);
147
- return;
148
- }
149
- const apiKey = await promptForApiKey("\n " + icons.lock + " Enter your Iterable API key: ");
150
- // Basic by default: conservative env flags; optional advanced profile
151
- let selectedEnv = {
152
- ITERABLE_USER_PII: "false",
153
- ITERABLE_ENABLE_WRITES: "false",
154
- ITERABLE_ENABLE_SENDS: "false",
155
- };
156
- const { doAdvanced } = await inquirer.prompt([
157
- {
158
- type: "confirm",
159
- name: "doAdvanced",
160
- message: "Configure advanced permissions now?",
161
- default: false,
162
- },
163
- ]);
164
- if (doAdvanced) {
165
- const { permFlags } = await inquirer.prompt([
166
- {
167
- type: "checkbox",
168
- name: "permFlags",
169
- message: "Select permissions to enable (default: none):",
170
- choices: [
171
- { name: "View PII (access user data)", value: "pii" },
172
- {
173
- name: "Write operations (create/update/delete)",
174
- value: "writes",
175
- },
176
- { name: "Sends (campaigns/journeys/events)", value: "sends" },
177
- ],
178
- pageSize: 6,
179
- default: [],
180
- },
181
- ]);
182
- selectedEnv = {
183
- ITERABLE_USER_PII: permFlags.includes("pii")
184
- ? "true"
185
- : "false",
186
- ITERABLE_ENABLE_WRITES: permFlags.includes("writes")
187
- ? "true"
188
- : "false",
189
- ITERABLE_ENABLE_SENDS: permFlags.includes("sends")
190
- ? "true"
191
- : "false",
192
- };
193
- }
194
- // Check if this API key value already exists
195
- spinner.start("Checking for duplicate keys...");
196
- const existingKeyWithValue = await keyManager.findKeyByValue(apiKey);
197
- spinner.stop();
198
- if (existingKeyWithValue) {
199
- console.log();
200
- showInfo(`This API key is already stored as "${existingKeyWithValue.name}"`);
201
- if (!existingKeyWithValue.isActive) {
202
- console.log();
203
- console.log(chalk.gray(" To activate this key, run:"));
204
- console.log(chalk.cyan(` iterable-mcp keys activate "${existingKeyWithValue.name}"`));
205
- }
206
- else {
207
- console.log();
208
- showSuccess(`"${existingKeyWithValue.name}" is already your active key`);
209
- }
210
- console.log();
211
- break;
212
- }
213
- // Add the key
214
- spinner.start("Storing API key securely...");
215
- try {
216
- const id = await keyManager.addKey(name, apiKey, baseUrl, {
217
- ...selectedEnv,
218
- });
219
- spinner.succeed("API key stored successfully!");
220
- console.log();
221
- console.log(formatKeyValue("Name", name, chalk.white.bold));
222
- console.log(formatKeyValue("ID", id, chalk.gray));
223
- console.log(formatKeyValue("Endpoint", baseUrl, linkColor()));
224
- console.log(formatKeyValue("User PII", selectedEnv.ITERABLE_USER_PII === "true"
225
- ? chalk.green("Enabled")
226
- : chalk.gray("Disabled")));
227
- console.log(formatKeyValue("Writes", selectedEnv.ITERABLE_ENABLE_WRITES === "true"
228
- ? chalk.green("Enabled")
229
- : chalk.gray("Disabled")));
230
- console.log(formatKeyValue("Sends", selectedEnv.ITERABLE_ENABLE_SENDS === "true"
231
- ? chalk.green("Enabled")
232
- : chalk.gray("Disabled")));
233
- console.log();
234
- showSuccess("Your API key is now securely stored (encrypted at rest)");
235
- // Offer to set newly added key as active
236
- const { activateNow } = await inquirer.prompt([
237
- {
238
- type: "confirm",
239
- name: "activateNow",
240
- message: `Set "${name}" as your active API key now?`,
241
- default: true,
242
- },
243
- ]);
244
- if (activateNow) {
245
- spinner.start("Activating key...");
246
- await keyManager.setActiveKey(id);
247
- spinner.succeed("Key activated");
248
- showSuccess(`"${name}" is now your active key`);
249
- }
250
- else {
251
- showInfo(`Keeping your current active key. Run 'iterable-mcp keys activate "${name}"' to switch later.`);
252
- }
253
- }
254
- catch (error) {
255
- const { sanitizeString } = await import("./utils/sanitize.js");
256
- spinner.fail("Failed to add API key");
257
- const msg = error instanceof Error
258
- ? sanitizeString(error.message)
259
- : "Unknown error";
260
- showError(msg);
261
- process.exit(1);
262
- }
482
+ await saveKeyInteractive(keyManager, null, {
483
+ chalk,
484
+ icons,
485
+ showError,
486
+ showSuccess,
487
+ showInfo,
488
+ formatKeyValue,
489
+ linkColor,
490
+ showBox,
491
+ }, spinner, hasAdvancedFlag ? { advanced: true } : undefined);
492
+ break;
493
+ }
494
+ case "update": {
495
+ console.clear();
496
+ showIterableLogo(packageJson.version);
497
+ const existingKey = await findKeyOrExit(keyManager, positionalArgs[2], "update", {
498
+ chalk,
499
+ showError,
500
+ showInfo,
501
+ });
502
+ console.log();
503
+ showInfo(`Updating key: "${existingKey.name}"`);
504
+ console.log();
505
+ await saveKeyInteractive(keyManager, existingKey, {
506
+ chalk,
507
+ icons,
508
+ showError,
509
+ showSuccess,
510
+ showInfo,
511
+ formatKeyValue,
512
+ linkColor,
513
+ showBox,
514
+ }, spinner, { advanced: hasAdvancedFlag });
263
515
  break;
264
516
  }
265
517
  case "activate": {
266
518
  console.clear();
267
519
  showIterableLogo(packageJson.version);
268
- const idOrName = args.slice(2).join(" ").trim();
269
- if (!idOrName) {
520
+ // Find the key first to get better error messages
521
+ const keyToActivate = await findKeyOrExit(keyManager, positionalArgs[2], "activate", { chalk, showError, showInfo });
522
+ spinner.start(`Activating key "${keyToActivate.name}"...`);
523
+ try {
524
+ await keyManager.getKey(keyToActivate.id);
525
+ }
526
+ catch (error) {
527
+ spinner.fail("Failed to activate key");
528
+ showError(error instanceof Error ? error.message : "Failed to access key");
270
529
  console.log();
271
- showError("Missing key name or ID");
530
+ showInfo(`This key's value is not accessible. Update it with: ${COMMAND_NAME} keys update <name-or-id>`);
531
+ process.exit(1);
532
+ }
533
+ await keyManager.setActiveKey(keyToActivate.id);
534
+ spinner.stop();
535
+ const meta = await keyManager.getActiveKeyMetadata();
536
+ if (meta) {
272
537
  console.log();
273
- console.log(chalk.white.bold(" USAGE"));
274
- console.log(chalk.white(" iterable-mcp keys activate <name-or-id>"));
538
+ showSuccess(`Switched to "${meta.name}"`);
275
539
  console.log();
276
- console.log(chalk.white.bold(" EXAMPLE"));
277
- console.log(chalk.cyan(" iterable-mcp keys activate production"));
540
+ displayKeyDetails(meta, { chalk, formatKeyValue, linkColor });
278
541
  console.log();
279
- process.exit(1);
280
542
  }
281
- try {
282
- spinner.start(`Activating key "${idOrName}"...`);
283
- // First check if the key value is accessible
284
- try {
285
- await keyManager.getKey(idOrName);
286
- }
287
- catch (error) {
288
- spinner.fail("Failed to activate key");
289
- showError(error instanceof Error ? error.message : "Failed to access key");
290
- console.log();
291
- showInfo("This key's value is not accessible. Delete and re-add it with: iterable-mcp keys delete <id> && iterable-mcp keys add");
292
- process.exit(1);
293
- }
294
- await keyManager.setActiveKey(idOrName);
295
- spinner.stop();
296
- const meta = await keyManager.getActiveKeyMetadata();
297
- if (meta) {
298
- console.log();
299
- showSuccess(`Switched to "${meta.name}"`);
300
- console.log();
301
- console.log(formatKeyValue("Name", meta.name, chalk.white.bold));
302
- console.log(formatKeyValue("ID", meta.id, chalk.gray));
303
- console.log(formatKeyValue("Endpoint", meta.baseUrl, linkColor()));
304
- const pii = meta.env?.ITERABLE_USER_PII === "true"
305
- ? chalk.green("Enabled")
306
- : chalk.gray("Disabled");
307
- const writes = meta.env?.ITERABLE_ENABLE_WRITES === "true"
308
- ? chalk.green("Enabled")
309
- : chalk.gray("Disabled");
310
- const sends = meta.env?.ITERABLE_ENABLE_SENDS === "true"
311
- ? chalk.green("Enabled")
312
- : chalk.gray("Disabled");
313
- console.log(formatKeyValue("User PII", pii));
314
- console.log(formatKeyValue("Writes", writes));
315
- console.log(formatKeyValue("Sends", sends));
316
- console.log();
317
- }
318
- else {
319
- console.log();
320
- showSuccess(`"${idOrName}" is now your active API key`);
321
- }
322
- showBox("Action Required", [
323
- chalk.yellow("Restart your AI tools to use this key"),
324
- "",
325
- chalk.gray("The MCP server will automatically load the active key when it starts"),
326
- ], { icon: icons.zap, theme: "warning" });
327
- }
328
- catch (_error) {
329
- // This only catches "key not found" errors from setActiveKey
330
- // (API key inaccessibility is handled in the inner try-catch above)
331
- spinner.fail("Failed to activate key");
332
- const keys = await keyManager.listKeys();
333
- const suggestions = keys
334
- .filter((k) => k.name.toLowerCase().includes(idOrName.toLowerCase()) ||
335
- k.id.toLowerCase().includes(idOrName.toLowerCase()))
336
- .slice(0, 5);
337
- showError(`Key not found: ${idOrName}`);
338
- if (suggestions.length) {
339
- console.log();
340
- console.log(chalk.white.bold(" Did you mean one of these?"));
341
- console.log();
342
- for (const s of suggestions) {
343
- console.log(chalk.cyan(` • "${s.name}"`) + chalk.gray(` (ID: ${s.id})`));
344
- }
345
- console.log();
346
- }
347
- showInfo("If your key name has spaces, wrap it in quotes");
348
- console.log(chalk.gray(' Example: iterable-mcp keys activate "My Prod Key"'));
543
+ else {
349
544
  console.log();
350
- process.exit(1);
545
+ showSuccess(`"${keyToActivate.name}" is now your active API key`);
351
546
  }
547
+ showBox("Action Required", [
548
+ chalk.yellow("Restart your AI tools to use this key"),
549
+ "",
550
+ chalk.gray("The MCP server will automatically load the active key when it starts"),
551
+ ], { icon: icons.zap, theme: "warning" });
352
552
  break;
353
553
  }
354
554
  case "delete": {
355
555
  console.clear();
356
556
  showIterableLogo(packageJson.version);
357
- const idOrName = args[2];
358
- if (!idOrName) {
359
- console.log();
360
- showError("Missing key name or ID");
361
- console.log();
362
- console.log(chalk.white.bold(" USAGE"));
363
- console.log(chalk.white(" iterable-mcp keys delete <id-or-name>"));
364
- console.log();
365
- showInfo("Use the key ID (not name) for deletion to ensure uniqueness");
366
- console.log(chalk.gray(" Run 'iterable-mcp keys list' to see key IDs"));
367
- console.log();
368
- process.exit(1);
369
- }
370
- // Resolve id or name to an ID for deletion
371
- let resolved = idOrName;
372
- let resolvedMeta = null;
373
- try {
374
- const keys = await keyManager.listKeys();
375
- const meta = keys.find((k) => k.id === idOrName || k.name === idOrName);
376
- if (!meta) {
377
- // Provide suggestions
378
- const suggestions = keys
379
- .filter((k) => k.name.toLowerCase().includes(idOrName.toLowerCase()) ||
380
- k.id.toLowerCase().includes(idOrName.toLowerCase()))
381
- .slice(0, 5);
382
- showError(`Key not found: ${idOrName}`);
383
- if (suggestions.length) {
384
- console.log();
385
- console.log(chalk.white.bold(" Did you mean one of these?"));
386
- console.log();
387
- for (const s of suggestions) {
388
- console.log(chalk.cyan(` • "${s.name}"`) + chalk.gray(` (ID: ${s.id})`));
389
- }
390
- console.log();
391
- }
392
- showInfo("Run 'iterable-mcp keys list' to view all keys");
393
- process.exit(1);
394
- }
395
- resolved = meta.id;
396
- resolvedMeta = { id: meta.id, name: meta.name };
397
- }
398
- catch (_error) {
399
- showError("Unable to resolve key by name or ID");
400
- process.exit(1);
401
- }
557
+ const keyToDelete = await findKeyOrExit(keyManager, positionalArgs[2], "delete", {
558
+ chalk,
559
+ showError,
560
+ showInfo,
561
+ });
402
562
  // Confirm deletion (non-interactive in tests)
403
563
  let confirmDelete = false;
404
564
  if (process.env.NODE_ENV !== "test") {
@@ -406,7 +566,7 @@ export async function handleKeysCommand() {
406
566
  {
407
567
  type: "confirm",
408
568
  name: "confirmDelete",
409
- message: `Permanently delete key "${resolvedMeta?.name ?? idOrName}" (ID: ${resolved})?`,
569
+ message: `Permanently delete key "${keyToDelete.name}" (ID: ${keyToDelete.id})?`,
410
570
  default: false,
411
571
  },
412
572
  ]));
@@ -417,10 +577,10 @@ export async function handleKeysCommand() {
417
577
  }
418
578
  spinner.start("Deleting API key...");
419
579
  try {
420
- await keyManager.deleteKey(resolved);
580
+ await keyManager.deleteKey(keyToDelete.id);
421
581
  spinner.succeed("API key deleted successfully");
422
582
  console.log();
423
- console.log(formatKeyValue("ID", resolved, chalk.gray));
583
+ console.log(formatKeyValue("ID", keyToDelete.id, chalk.gray));
424
584
  console.log();
425
585
  showSuccess("Key securely removed");
426
586
  }
@@ -438,10 +598,11 @@ export async function handleKeysCommand() {
438
598
  console.log();
439
599
  const commandsTable = createTable({
440
600
  head: ["Command", "Description"],
441
- colWidths: [30, 50],
601
+ colWidths: [45, 40],
442
602
  style: "normal",
443
603
  });
444
- commandsTable.push(["list", chalk.gray("View all your stored API keys")], ["add", chalk.gray("Add a new API key (interactive)")], ["activate <name-or-id>", chalk.gray("Switch to a different key")], ["delete <id-or-name>", chalk.gray("Remove a key by ID or name")]);
604
+ // Apply chalk gray to descriptions for styling
605
+ commandsTable.push(...KEYS_COMMAND_TABLE.map(([cmd, desc]) => [cmd, chalk.gray(desc)]));
445
606
  console.log(commandsTable.toString());
446
607
  console.log();
447
608
  console.log();
@@ -450,24 +611,23 @@ export async function handleKeysCommand() {
450
611
  console.log(chalk.white.bold(" Add API keys"));
451
612
  console.log(chalk.gray(" (Interactive prompts: name, region, API key, PII, Writes, Sends)"));
452
613
  console.log();
453
- console.log(chalk.cyan(" iterable-mcp keys add"));
614
+ console.log(chalk.cyan(` ${COMMAND_NAME} keys add`));
454
615
  console.log();
455
616
  console.log();
456
617
  console.log(chalk.white.bold(" Manage your keys"));
457
618
  console.log();
458
- console.log(chalk.cyan(" iterable-mcp keys list"));
459
- console.log(chalk.cyan(" iterable-mcp keys activate production"));
460
- console.log(chalk.cyan(" iterable-mcp keys delete 3f5d2b07-5b1c-4e86-8f3c-9a1b2c3d4e5f"));
619
+ console.log(chalk.cyan(` ${COMMAND_NAME} keys list`));
620
+ console.log(chalk.cyan(` ${COMMAND_NAME} keys add`));
621
+ console.log(chalk.cyan(` ${COMMAND_NAME} keys update production`));
622
+ console.log(chalk.cyan(` ${COMMAND_NAME} keys activate production`));
623
+ console.log(chalk.cyan(` ${COMMAND_NAME} keys delete 3f5d2b07-5b1c-4e86-8f3c-9a1b2c3d4e5f`));
461
624
  console.log();
462
- const tips = [
625
+ showBox("Important Notes", [
463
626
  "API keys are prompted interactively - never stored in shell history",
464
627
  "Each API key is tightly coupled to its endpoint (US/EU/custom)",
465
628
  getKeyStorageMessage(),
466
- "Use 'keys list' to see all your keys and their details",
467
629
  "The active key (● ACTIVE) is what your AI tools will use",
468
- "To update a key: delete the old one and add a new one",
469
- ];
470
- showBox("Tips & Best Practices", tips, {
630
+ ], {
471
631
  icon: icons.bulb,
472
632
  theme: "info",
473
633
  padding: 1,