@iterable/mcp 1.2.2 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -3
- 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.map +1 -1
- package/dist/install.js +127 -318
- 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 +453 -293
- package/dist/keys-cli.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 +2 -0
- package/dist/utils/tool-config.d.ts.map +1 -1
- package/dist/utils/tool-config.js +6 -0
- package/dist/utils/tool-config.js.map +1 -1
- package/package.json +8 -4
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(
|
|
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
|
-
"
|
|
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
|
-
|
|
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(
|
|
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
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
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
|
-
|
|
269
|
-
|
|
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
|
-
|
|
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
|
-
|
|
274
|
-
console.log(chalk.white(" iterable-mcp keys activate <name-or-id>"));
|
|
538
|
+
showSuccess(`Switched to "${meta.name}"`);
|
|
275
539
|
console.log();
|
|
276
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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 "${
|
|
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(
|
|
580
|
+
await keyManager.deleteKey(keyToDelete.id);
|
|
421
581
|
spinner.succeed("API key deleted successfully");
|
|
422
582
|
console.log();
|
|
423
|
-
console.log(formatKeyValue("ID",
|
|
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: [
|
|
601
|
+
colWidths: [45, 40],
|
|
442
602
|
style: "normal",
|
|
443
603
|
});
|
|
444
|
-
|
|
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(
|
|
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(
|
|
459
|
-
console.log(chalk.cyan(
|
|
460
|
-
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`));
|
|
461
624
|
console.log();
|
|
462
|
-
|
|
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
|
-
|
|
469
|
-
];
|
|
470
|
-
showBox("Tips & Best Practices", tips, {
|
|
630
|
+
], {
|
|
471
631
|
icon: icons.bulb,
|
|
472
632
|
theme: "info",
|
|
473
633
|
padding: 1,
|