@iterable/mcp 1.2.1 → 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/README.md +28 -3
- package/dist/config.d.ts +4 -4
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +2 -1
- package/dist/config.js.map +1 -1
- package/dist/install.d.ts +0 -6
- package/dist/install.d.ts.map +1 -1
- package/dist/install.js +121 -346
- package/dist/install.js.map +1 -1
- package/dist/key-manager.d.ts +37 -3
- package/dist/key-manager.d.ts.map +1 -1
- package/dist/key-manager.js +109 -53
- package/dist/key-manager.js.map +1 -1
- package/dist/keys-cli.d.ts +38 -0
- package/dist/keys-cli.d.ts.map +1 -1
- package/dist/keys-cli.js +454 -373
- package/dist/keys-cli.js.map +1 -1
- package/dist/server.js +1 -1
- package/dist/server.js.map +1 -1
- package/dist/tools/snippets.d.ts.map +1 -1
- package/dist/tools/snippets.js +23 -12
- package/dist/tools/snippets.js.map +1 -1
- package/dist/tools/templates.js +2 -2
- package/dist/tools/templates.js.map +1 -1
- package/dist/utils/cli-env.d.ts.map +1 -1
- package/dist/utils/cli-env.js +2 -1
- package/dist/utils/cli-env.js.map +1 -1
- package/dist/utils/command-info.d.ts +11 -0
- package/dist/utils/command-info.d.ts.map +1 -0
- package/dist/utils/command-info.js +25 -0
- package/dist/utils/command-info.js.map +1 -0
- package/dist/utils/endpoint-prompt.d.ts +1 -0
- package/dist/utils/endpoint-prompt.d.ts.map +1 -1
- package/dist/utils/endpoint-prompt.js +20 -4
- package/dist/utils/endpoint-prompt.js.map +1 -1
- package/dist/utils/tool-config.d.ts +1 -0
- package/dist/utils/tool-config.d.ts.map +1 -1
- package/dist/utils/tool-config.js +3 -0
- package/dist/utils/tool-config.js.map +1 -1
- package/package.json +4 -4
package/dist/keys-cli.js
CHANGED
|
@@ -2,29 +2,389 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* CLI commands for API key management with beautiful modern UI
|
|
4
4
|
*/
|
|
5
|
-
import {
|
|
6
|
-
import { promises as fs, readFileSync } from "fs";
|
|
5
|
+
import { readFileSync } from "fs";
|
|
7
6
|
import inquirer from "inquirer";
|
|
8
|
-
import os from "os";
|
|
9
7
|
import path from "path";
|
|
10
8
|
import { fileURLToPath } from "url";
|
|
11
|
-
import {
|
|
9
|
+
import { COMMAND_NAME, KEYS_COMMAND_TABLE } from "./utils/command-info.js";
|
|
12
10
|
const { dirname, join } = path;
|
|
13
11
|
import { getSpinner, loadUi } from "./utils/cli-env.js";
|
|
12
|
+
import { promptForIterableBaseUrl } from "./utils/endpoint-prompt.js";
|
|
14
13
|
import { getKeyStorageMessage } from "./utils/formatting.js";
|
|
15
14
|
import { promptForApiKey } from "./utils/password-prompt.js";
|
|
16
|
-
|
|
15
|
+
import { sanitizeString } from "./utils/sanitize.js";
|
|
17
16
|
// Get package version
|
|
18
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
|
+
}
|
|
19
375
|
/**
|
|
20
376
|
* Handle the 'keys' command and its subcommands
|
|
21
377
|
*/
|
|
22
378
|
export async function handleKeysCommand() {
|
|
23
379
|
const args = process.argv.slice(2);
|
|
24
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("--"));
|
|
25
384
|
const chalk = (await import("chalk")).default;
|
|
26
385
|
const { createTable, formatKeyValue, icons, showBox, showError, showInfo, showIterableLogo, showSection, showSuccess, linkColor, } = await loadUi();
|
|
27
386
|
const spinner = await getSpinner();
|
|
387
|
+
// Dynamic import for testability (Jest mocks need runtime imports)
|
|
28
388
|
const { getKeyManager } = await import("./key-manager.js");
|
|
29
389
|
const keyManager = getKeyManager();
|
|
30
390
|
try {
|
|
@@ -50,7 +410,7 @@ export async function handleKeysCommand() {
|
|
|
50
410
|
chalk.gray("You haven't added any API keys yet."),
|
|
51
411
|
"",
|
|
52
412
|
chalk.cyan("Get started by running:"),
|
|
53
|
-
chalk.bold.white(
|
|
413
|
+
chalk.bold.white(` ${COMMAND_NAME} setup`),
|
|
54
414
|
], { icon: icons.key, theme: "info" });
|
|
55
415
|
}
|
|
56
416
|
else {
|
|
@@ -64,7 +424,7 @@ export async function handleKeysCommand() {
|
|
|
64
424
|
"View PII?",
|
|
65
425
|
"Writes?",
|
|
66
426
|
"Sends?",
|
|
67
|
-
"
|
|
427
|
+
"Modified",
|
|
68
428
|
"Status",
|
|
69
429
|
],
|
|
70
430
|
style: "normal",
|
|
@@ -73,7 +433,9 @@ export async function handleKeysCommand() {
|
|
|
73
433
|
const statusBadge = key.isActive
|
|
74
434
|
? chalk.bgGreen.black(" ACTIVE ")
|
|
75
435
|
: chalk.gray("INACTIVE");
|
|
76
|
-
|
|
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", {
|
|
77
439
|
year: "numeric",
|
|
78
440
|
month: "short",
|
|
79
441
|
day: "numeric",
|
|
@@ -95,22 +457,19 @@ export async function handleKeysCommand() {
|
|
|
95
457
|
pii,
|
|
96
458
|
writes,
|
|
97
459
|
sends,
|
|
98
|
-
chalk.gray(
|
|
460
|
+
chalk.gray(formattedDate),
|
|
99
461
|
statusBadge,
|
|
100
462
|
]);
|
|
101
463
|
}
|
|
102
464
|
console.log(table.toString());
|
|
103
465
|
console.log();
|
|
104
|
-
// Show key management
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
" to switch between keys",
|
|
109
|
-
"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
|
+
"",
|
|
110
470
|
getKeyStorageMessage(),
|
|
111
|
-
]
|
|
112
|
-
|
|
113
|
-
icon: icons.bulb,
|
|
471
|
+
], {
|
|
472
|
+
icon: icons.key,
|
|
114
473
|
theme: "info",
|
|
115
474
|
padding: 1,
|
|
116
475
|
});
|
|
@@ -120,364 +479,86 @@ export async function handleKeysCommand() {
|
|
|
120
479
|
case "add": {
|
|
121
480
|
console.clear();
|
|
122
481
|
showIterableLogo(packageJson.version);
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
]
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
ITERABLE_USER_PII: "false",
|
|
157
|
-
ITERABLE_ENABLE_WRITES: "false",
|
|
158
|
-
ITERABLE_ENABLE_SENDS: "false",
|
|
159
|
-
};
|
|
160
|
-
const { doAdvanced } = await inquirer.prompt([
|
|
161
|
-
{
|
|
162
|
-
type: "confirm",
|
|
163
|
-
name: "doAdvanced",
|
|
164
|
-
message: "Configure advanced permissions now?",
|
|
165
|
-
default: false,
|
|
166
|
-
},
|
|
167
|
-
]);
|
|
168
|
-
if (doAdvanced) {
|
|
169
|
-
const { permFlags } = await inquirer.prompt([
|
|
170
|
-
{
|
|
171
|
-
type: "checkbox",
|
|
172
|
-
name: "permFlags",
|
|
173
|
-
message: "Select permissions to enable (default: none):",
|
|
174
|
-
choices: [
|
|
175
|
-
{ name: "View PII (access user data)", value: "pii" },
|
|
176
|
-
{
|
|
177
|
-
name: "Write operations (create/update/delete)",
|
|
178
|
-
value: "writes",
|
|
179
|
-
},
|
|
180
|
-
{ name: "Sends (campaigns/journeys/events)", value: "sends" },
|
|
181
|
-
],
|
|
182
|
-
pageSize: 6,
|
|
183
|
-
default: [],
|
|
184
|
-
},
|
|
185
|
-
]);
|
|
186
|
-
selectedEnv = {
|
|
187
|
-
ITERABLE_USER_PII: permFlags.includes("pii")
|
|
188
|
-
? "true"
|
|
189
|
-
: "false",
|
|
190
|
-
ITERABLE_ENABLE_WRITES: permFlags.includes("writes")
|
|
191
|
-
? "true"
|
|
192
|
-
: "false",
|
|
193
|
-
ITERABLE_ENABLE_SENDS: permFlags.includes("sends")
|
|
194
|
-
? "true"
|
|
195
|
-
: "false",
|
|
196
|
-
};
|
|
197
|
-
}
|
|
198
|
-
// Check if this API key value already exists
|
|
199
|
-
spinner.start("Checking for duplicate keys...");
|
|
200
|
-
const existingKeyWithValue = await keyManager.findKeyByValue(apiKey);
|
|
201
|
-
spinner.stop();
|
|
202
|
-
if (existingKeyWithValue) {
|
|
203
|
-
console.log();
|
|
204
|
-
showInfo(`This API key is already stored as "${existingKeyWithValue.name}"`);
|
|
205
|
-
if (!existingKeyWithValue.isActive) {
|
|
206
|
-
console.log();
|
|
207
|
-
console.log(chalk.gray(" To activate this key, run:"));
|
|
208
|
-
console.log(chalk.cyan(` iterable-mcp keys activate "${existingKeyWithValue.name}"`));
|
|
209
|
-
}
|
|
210
|
-
else {
|
|
211
|
-
console.log();
|
|
212
|
-
showSuccess(`"${existingKeyWithValue.name}" is already your active key`);
|
|
213
|
-
}
|
|
214
|
-
console.log();
|
|
215
|
-
break;
|
|
216
|
-
}
|
|
217
|
-
// Add the key
|
|
218
|
-
spinner.start("Storing API key securely...");
|
|
219
|
-
try {
|
|
220
|
-
const id = await keyManager.addKey(name, apiKey, baseUrl, {
|
|
221
|
-
...selectedEnv,
|
|
222
|
-
});
|
|
223
|
-
spinner.succeed("API key stored successfully!");
|
|
224
|
-
console.log();
|
|
225
|
-
console.log(formatKeyValue("Name", name, chalk.white.bold));
|
|
226
|
-
console.log(formatKeyValue("ID", id, chalk.gray));
|
|
227
|
-
console.log(formatKeyValue("Endpoint", baseUrl, linkColor()));
|
|
228
|
-
console.log(formatKeyValue("User PII", selectedEnv.ITERABLE_USER_PII === "true"
|
|
229
|
-
? chalk.green("Enabled")
|
|
230
|
-
: chalk.gray("Disabled")));
|
|
231
|
-
console.log(formatKeyValue("Writes", selectedEnv.ITERABLE_ENABLE_WRITES === "true"
|
|
232
|
-
? chalk.green("Enabled")
|
|
233
|
-
: chalk.gray("Disabled")));
|
|
234
|
-
console.log(formatKeyValue("Sends", selectedEnv.ITERABLE_ENABLE_SENDS === "true"
|
|
235
|
-
? chalk.green("Enabled")
|
|
236
|
-
: chalk.gray("Disabled")));
|
|
237
|
-
console.log();
|
|
238
|
-
showSuccess("Your API key is now securely stored (encrypted at rest)");
|
|
239
|
-
// Offer to set newly added key as active
|
|
240
|
-
const { activateNow } = await inquirer.prompt([
|
|
241
|
-
{
|
|
242
|
-
type: "confirm",
|
|
243
|
-
name: "activateNow",
|
|
244
|
-
message: `Set "${name}" as your active API key now?`,
|
|
245
|
-
default: true,
|
|
246
|
-
},
|
|
247
|
-
]);
|
|
248
|
-
if (activateNow) {
|
|
249
|
-
spinner.start("Activating key...");
|
|
250
|
-
await keyManager.setActiveKey(id);
|
|
251
|
-
spinner.succeed("Key activated");
|
|
252
|
-
showSuccess(`"${name}" is now your active key`);
|
|
253
|
-
}
|
|
254
|
-
else {
|
|
255
|
-
showInfo(`Keeping your current active key. Run 'iterable-mcp keys activate "${name}"' to switch later.`);
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
catch (error) {
|
|
259
|
-
const { sanitizeString } = await import("./utils/sanitize.js");
|
|
260
|
-
spinner.fail("Failed to add API key");
|
|
261
|
-
const msg = error instanceof Error
|
|
262
|
-
? sanitizeString(error.message)
|
|
263
|
-
: "Unknown error";
|
|
264
|
-
showError(msg);
|
|
265
|
-
process.exit(1);
|
|
266
|
-
}
|
|
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 });
|
|
267
515
|
break;
|
|
268
516
|
}
|
|
269
517
|
case "activate": {
|
|
270
518
|
console.clear();
|
|
271
519
|
showIterableLogo(packageJson.version);
|
|
272
|
-
|
|
273
|
-
|
|
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");
|
|
274
529
|
console.log();
|
|
275
|
-
|
|
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) {
|
|
276
537
|
console.log();
|
|
277
|
-
|
|
278
|
-
console.log(chalk.white(" iterable-mcp keys activate <name-or-id>"));
|
|
538
|
+
showSuccess(`Switched to "${meta.name}"`);
|
|
279
539
|
console.log();
|
|
280
|
-
|
|
281
|
-
console.log(chalk.cyan(" iterable-mcp keys activate production"));
|
|
540
|
+
displayKeyDetails(meta, { chalk, formatKeyValue, linkColor });
|
|
282
541
|
console.log();
|
|
283
|
-
process.exit(1);
|
|
284
|
-
}
|
|
285
|
-
try {
|
|
286
|
-
spinner.start(`Activating key "${idOrName}"...`);
|
|
287
|
-
// First check if the key value is accessible
|
|
288
|
-
try {
|
|
289
|
-
await keyManager.getKey(idOrName);
|
|
290
|
-
}
|
|
291
|
-
catch (error) {
|
|
292
|
-
spinner.fail("Failed to activate key");
|
|
293
|
-
showError(error instanceof Error ? error.message : "Failed to access key");
|
|
294
|
-
console.log();
|
|
295
|
-
showInfo("This key's value is not accessible. Delete and re-add it with: iterable-mcp keys delete <id> && iterable-mcp keys add");
|
|
296
|
-
process.exit(1);
|
|
297
|
-
}
|
|
298
|
-
await keyManager.setActiveKey(idOrName);
|
|
299
|
-
spinner.stop();
|
|
300
|
-
const meta = await keyManager.getActiveKeyMetadata();
|
|
301
|
-
if (meta) {
|
|
302
|
-
console.log();
|
|
303
|
-
showSuccess(`Switched to "${meta.name}"`);
|
|
304
|
-
console.log();
|
|
305
|
-
console.log(formatKeyValue("Name", meta.name, chalk.white.bold));
|
|
306
|
-
console.log(formatKeyValue("ID", meta.id, chalk.gray));
|
|
307
|
-
console.log(formatKeyValue("Endpoint", meta.baseUrl, linkColor()));
|
|
308
|
-
const pii = meta.env?.ITERABLE_USER_PII === "true"
|
|
309
|
-
? chalk.green("Enabled")
|
|
310
|
-
: chalk.gray("Disabled");
|
|
311
|
-
const writes = meta.env?.ITERABLE_ENABLE_WRITES === "true"
|
|
312
|
-
? chalk.green("Enabled")
|
|
313
|
-
: chalk.gray("Disabled");
|
|
314
|
-
const sends = meta.env?.ITERABLE_ENABLE_SENDS === "true"
|
|
315
|
-
? chalk.green("Enabled")
|
|
316
|
-
: chalk.gray("Disabled");
|
|
317
|
-
console.log(formatKeyValue("User PII", pii));
|
|
318
|
-
console.log(formatKeyValue("Writes", writes));
|
|
319
|
-
console.log(formatKeyValue("Sends", sends));
|
|
320
|
-
// Sync configured AI tool JSON files to reflect the active key's flags
|
|
321
|
-
try {
|
|
322
|
-
const { resolveFinalMcpEnv, enforceSendsRequiresWrites, buildMcpConfig, } = await import("./install.js");
|
|
323
|
-
let mcpEnv = resolveFinalMcpEnv({
|
|
324
|
-
ITERABLE_USER_PII: "false",
|
|
325
|
-
ITERABLE_ENABLE_WRITES: "false",
|
|
326
|
-
ITERABLE_ENABLE_SENDS: "false",
|
|
327
|
-
}, meta.env);
|
|
328
|
-
mcpEnv = enforceSendsRequiresWrites(mcpEnv);
|
|
329
|
-
// Determine file-based tool config locations (Cursor, Claude Desktop)
|
|
330
|
-
const cursorPath = path.join(os.homedir(), ".cursor", "mcp.json");
|
|
331
|
-
// macOS-only path (we already guard the command to run only on darwin)
|
|
332
|
-
const claudeDesktopPath = path.join(os.homedir(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
333
|
-
const targets = [
|
|
334
|
-
{ name: "Cursor", file: cursorPath },
|
|
335
|
-
{ name: "Claude Desktop", file: claudeDesktopPath },
|
|
336
|
-
];
|
|
337
|
-
const { updateToolConfig } = await import("./utils/tool-config.js");
|
|
338
|
-
for (const t of targets) {
|
|
339
|
-
try {
|
|
340
|
-
const raw = await fs.readFile(t.file, "utf8").catch(() => "");
|
|
341
|
-
if (!raw)
|
|
342
|
-
continue;
|
|
343
|
-
const existing = JSON.parse(raw || "{}");
|
|
344
|
-
if (!existing?.mcpServers?.iterable)
|
|
345
|
-
continue;
|
|
346
|
-
const iterableMcpConfig = buildMcpConfig({
|
|
347
|
-
env: {
|
|
348
|
-
...(existing.mcpServers.iterable.env || {}),
|
|
349
|
-
...mcpEnv,
|
|
350
|
-
},
|
|
351
|
-
});
|
|
352
|
-
await updateToolConfig(t.file, iterableMcpConfig);
|
|
353
|
-
showSuccess(`${t.name} configuration synced to active key permissions`);
|
|
354
|
-
}
|
|
355
|
-
catch {
|
|
356
|
-
// Non-fatal: skip if cannot read/parse/write
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
// Update Claude Code CLI registry if available
|
|
360
|
-
try {
|
|
361
|
-
await execFileAsync("claude", ["--version"]);
|
|
362
|
-
// Build config using existing helper (keeps local/npx logic consistent)
|
|
363
|
-
const iterableMcpConfig = buildMcpConfig({ env: mcpEnv });
|
|
364
|
-
const configJson = JSON.stringify(iterableMcpConfig);
|
|
365
|
-
// Remove existing registration (ignore errors)
|
|
366
|
-
await execFileAsync("claude", [
|
|
367
|
-
"mcp",
|
|
368
|
-
"remove",
|
|
369
|
-
"iterable",
|
|
370
|
-
]).catch(() => { });
|
|
371
|
-
// Add new registration with inherited stdio to show Claude CLI output
|
|
372
|
-
await new Promise((resolve, reject) => {
|
|
373
|
-
const child = spawn("claude", ["mcp", "add-json", "iterable", configJson], {
|
|
374
|
-
stdio: "inherit",
|
|
375
|
-
});
|
|
376
|
-
child.on("close", (code) => {
|
|
377
|
-
if (code === 0)
|
|
378
|
-
resolve();
|
|
379
|
-
else
|
|
380
|
-
reject(new Error(`claude mcp add-json exited with code ${code ?? "unknown"}`));
|
|
381
|
-
});
|
|
382
|
-
child.on("error", reject);
|
|
383
|
-
});
|
|
384
|
-
showSuccess("Claude Code configuration synced to active key permissions");
|
|
385
|
-
}
|
|
386
|
-
catch {
|
|
387
|
-
// If Claude CLI not installed or update fails, skip silently
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
catch {
|
|
391
|
-
// Non-fatal: if syncing fails, continue
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
else {
|
|
395
|
-
console.log();
|
|
396
|
-
showSuccess(`"${idOrName}" is now your active API key`);
|
|
397
|
-
}
|
|
398
|
-
showBox("Action Required", [
|
|
399
|
-
chalk.yellow("Restart your AI tools to use this key"),
|
|
400
|
-
"",
|
|
401
|
-
chalk.gray("The new key will be used after restarting:"),
|
|
402
|
-
chalk.white(" • Cursor"),
|
|
403
|
-
chalk.white(" • Claude Desktop"),
|
|
404
|
-
chalk.white(" • Claude Code"),
|
|
405
|
-
], { icon: icons.zap, theme: "warning" });
|
|
406
542
|
}
|
|
407
|
-
|
|
408
|
-
// This only catches "key not found" errors from setActiveKey
|
|
409
|
-
// (API key inaccessibility is handled in the inner try-catch above)
|
|
410
|
-
spinner.fail("Failed to activate key");
|
|
411
|
-
const keys = await keyManager.listKeys();
|
|
412
|
-
const suggestions = keys
|
|
413
|
-
.filter((k) => k.name.toLowerCase().includes(idOrName.toLowerCase()) ||
|
|
414
|
-
k.id.toLowerCase().includes(idOrName.toLowerCase()))
|
|
415
|
-
.slice(0, 5);
|
|
416
|
-
showError(`Key not found: ${idOrName}`);
|
|
417
|
-
if (suggestions.length) {
|
|
418
|
-
console.log();
|
|
419
|
-
console.log(chalk.white.bold(" Did you mean one of these?"));
|
|
420
|
-
console.log();
|
|
421
|
-
for (const s of suggestions) {
|
|
422
|
-
console.log(chalk.cyan(` • "${s.name}"`) + chalk.gray(` (ID: ${s.id})`));
|
|
423
|
-
}
|
|
424
|
-
console.log();
|
|
425
|
-
}
|
|
426
|
-
showInfo("If your key name has spaces, wrap it in quotes");
|
|
427
|
-
console.log(chalk.gray(' Example: iterable-mcp keys activate "My Prod Key"'));
|
|
543
|
+
else {
|
|
428
544
|
console.log();
|
|
429
|
-
|
|
545
|
+
showSuccess(`"${keyToActivate.name}" is now your active API key`);
|
|
430
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" });
|
|
431
552
|
break;
|
|
432
553
|
}
|
|
433
554
|
case "delete": {
|
|
434
555
|
console.clear();
|
|
435
556
|
showIterableLogo(packageJson.version);
|
|
436
|
-
const
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
console.log(chalk.white.bold(" USAGE"));
|
|
442
|
-
console.log(chalk.white(" iterable-mcp keys delete <id-or-name>"));
|
|
443
|
-
console.log();
|
|
444
|
-
showInfo("Use the key ID (not name) for deletion to ensure uniqueness");
|
|
445
|
-
console.log(chalk.gray(" Run 'iterable-mcp keys list' to see key IDs"));
|
|
446
|
-
console.log();
|
|
447
|
-
process.exit(1);
|
|
448
|
-
}
|
|
449
|
-
// Resolve id or name to an ID for deletion
|
|
450
|
-
let resolved = idOrName;
|
|
451
|
-
let resolvedMeta = null;
|
|
452
|
-
try {
|
|
453
|
-
const keys = await keyManager.listKeys();
|
|
454
|
-
const meta = keys.find((k) => k.id === idOrName || k.name === idOrName);
|
|
455
|
-
if (!meta) {
|
|
456
|
-
// Provide suggestions
|
|
457
|
-
const suggestions = keys
|
|
458
|
-
.filter((k) => k.name.toLowerCase().includes(idOrName.toLowerCase()) ||
|
|
459
|
-
k.id.toLowerCase().includes(idOrName.toLowerCase()))
|
|
460
|
-
.slice(0, 5);
|
|
461
|
-
showError(`Key not found: ${idOrName}`);
|
|
462
|
-
if (suggestions.length) {
|
|
463
|
-
console.log();
|
|
464
|
-
console.log(chalk.white.bold(" Did you mean one of these?"));
|
|
465
|
-
console.log();
|
|
466
|
-
for (const s of suggestions) {
|
|
467
|
-
console.log(chalk.cyan(` • "${s.name}"`) + chalk.gray(` (ID: ${s.id})`));
|
|
468
|
-
}
|
|
469
|
-
console.log();
|
|
470
|
-
}
|
|
471
|
-
showInfo("Run 'iterable-mcp keys list' to view all keys");
|
|
472
|
-
process.exit(1);
|
|
473
|
-
}
|
|
474
|
-
resolved = meta.id;
|
|
475
|
-
resolvedMeta = { id: meta.id, name: meta.name };
|
|
476
|
-
}
|
|
477
|
-
catch (_error) {
|
|
478
|
-
showError("Unable to resolve key by name or ID");
|
|
479
|
-
process.exit(1);
|
|
480
|
-
}
|
|
557
|
+
const keyToDelete = await findKeyOrExit(keyManager, positionalArgs[2], "delete", {
|
|
558
|
+
chalk,
|
|
559
|
+
showError,
|
|
560
|
+
showInfo,
|
|
561
|
+
});
|
|
481
562
|
// Confirm deletion (non-interactive in tests)
|
|
482
563
|
let confirmDelete = false;
|
|
483
564
|
if (process.env.NODE_ENV !== "test") {
|
|
@@ -485,7 +566,7 @@ export async function handleKeysCommand() {
|
|
|
485
566
|
{
|
|
486
567
|
type: "confirm",
|
|
487
568
|
name: "confirmDelete",
|
|
488
|
-
message: `Permanently delete key "${
|
|
569
|
+
message: `Permanently delete key "${keyToDelete.name}" (ID: ${keyToDelete.id})?`,
|
|
489
570
|
default: false,
|
|
490
571
|
},
|
|
491
572
|
]));
|
|
@@ -496,10 +577,10 @@ export async function handleKeysCommand() {
|
|
|
496
577
|
}
|
|
497
578
|
spinner.start("Deleting API key...");
|
|
498
579
|
try {
|
|
499
|
-
await keyManager.deleteKey(
|
|
580
|
+
await keyManager.deleteKey(keyToDelete.id);
|
|
500
581
|
spinner.succeed("API key deleted successfully");
|
|
501
582
|
console.log();
|
|
502
|
-
console.log(formatKeyValue("ID",
|
|
583
|
+
console.log(formatKeyValue("ID", keyToDelete.id, chalk.gray));
|
|
503
584
|
console.log();
|
|
504
585
|
showSuccess("Key securely removed");
|
|
505
586
|
}
|
|
@@ -517,10 +598,11 @@ export async function handleKeysCommand() {
|
|
|
517
598
|
console.log();
|
|
518
599
|
const commandsTable = createTable({
|
|
519
600
|
head: ["Command", "Description"],
|
|
520
|
-
colWidths: [
|
|
601
|
+
colWidths: [45, 40],
|
|
521
602
|
style: "normal",
|
|
522
603
|
});
|
|
523
|
-
|
|
604
|
+
// Apply chalk gray to descriptions for styling
|
|
605
|
+
commandsTable.push(...KEYS_COMMAND_TABLE.map(([cmd, desc]) => [cmd, chalk.gray(desc)]));
|
|
524
606
|
console.log(commandsTable.toString());
|
|
525
607
|
console.log();
|
|
526
608
|
console.log();
|
|
@@ -529,24 +611,23 @@ export async function handleKeysCommand() {
|
|
|
529
611
|
console.log(chalk.white.bold(" Add API keys"));
|
|
530
612
|
console.log(chalk.gray(" (Interactive prompts: name, region, API key, PII, Writes, Sends)"));
|
|
531
613
|
console.log();
|
|
532
|
-
console.log(chalk.cyan(
|
|
614
|
+
console.log(chalk.cyan(` ${COMMAND_NAME} keys add`));
|
|
533
615
|
console.log();
|
|
534
616
|
console.log();
|
|
535
617
|
console.log(chalk.white.bold(" Manage your keys"));
|
|
536
618
|
console.log();
|
|
537
|
-
console.log(chalk.cyan(
|
|
538
|
-
console.log(chalk.cyan(
|
|
539
|
-
console.log(chalk.cyan(
|
|
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`));
|
|
540
624
|
console.log();
|
|
541
|
-
|
|
625
|
+
showBox("Important Notes", [
|
|
542
626
|
"API keys are prompted interactively - never stored in shell history",
|
|
543
627
|
"Each API key is tightly coupled to its endpoint (US/EU/custom)",
|
|
544
628
|
getKeyStorageMessage(),
|
|
545
|
-
"Use 'keys list' to see all your keys and their details",
|
|
546
629
|
"The active key (● ACTIVE) is what your AI tools will use",
|
|
547
|
-
|
|
548
|
-
];
|
|
549
|
-
showBox("Tips & Best Practices", tips, {
|
|
630
|
+
], {
|
|
550
631
|
icon: icons.bulb,
|
|
551
632
|
theme: "info",
|
|
552
633
|
padding: 1,
|