@iterable/mcp 1.0.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.
Files changed (140) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +318 -0
  3. package/TOOLS.md +133 -0
  4. package/dist/client-detection.d.ts +7 -0
  5. package/dist/client-detection.d.ts.map +1 -0
  6. package/dist/client-detection.js +22 -0
  7. package/dist/client-detection.js.map +1 -0
  8. package/dist/config.d.ts +25 -0
  9. package/dist/config.d.ts.map +1 -0
  10. package/dist/config.js +74 -0
  11. package/dist/config.js.map +1 -0
  12. package/dist/http-headers.d.ts +5 -0
  13. package/dist/http-headers.d.ts.map +1 -0
  14. package/dist/http-headers.js +5 -0
  15. package/dist/http-headers.js.map +1 -0
  16. package/dist/index.d.ts +3 -0
  17. package/dist/index.d.ts.map +1 -0
  18. package/dist/index.js +43 -0
  19. package/dist/index.js.map +1 -0
  20. package/dist/install.d.ts +41 -0
  21. package/dist/install.d.ts.map +1 -0
  22. package/dist/install.js +725 -0
  23. package/dist/install.js.map +1 -0
  24. package/dist/key-manager.d.ts +236 -0
  25. package/dist/key-manager.d.ts.map +1 -0
  26. package/dist/key-manager.js +776 -0
  27. package/dist/key-manager.js.map +1 -0
  28. package/dist/keys-cli.d.ts +8 -0
  29. package/dist/keys-cli.d.ts.map +1 -0
  30. package/dist/keys-cli.js +487 -0
  31. package/dist/keys-cli.js.map +1 -0
  32. package/dist/prompts.d.ts +15 -0
  33. package/dist/prompts.d.ts.map +1 -0
  34. package/dist/prompts.js +66 -0
  35. package/dist/prompts.js.map +1 -0
  36. package/dist/schema-utils.d.ts +22 -0
  37. package/dist/schema-utils.d.ts.map +1 -0
  38. package/dist/schema-utils.js +65 -0
  39. package/dist/schema-utils.js.map +1 -0
  40. package/dist/server.d.ts +13 -0
  41. package/dist/server.d.ts.map +1 -0
  42. package/dist/server.js +172 -0
  43. package/dist/server.js.map +1 -0
  44. package/dist/shutdown.d.ts +5 -0
  45. package/dist/shutdown.d.ts.map +1 -0
  46. package/dist/shutdown.js +68 -0
  47. package/dist/shutdown.js.map +1 -0
  48. package/dist/tool-filter.d.ts +19 -0
  49. package/dist/tool-filter.d.ts.map +1 -0
  50. package/dist/tool-filter.js +144 -0
  51. package/dist/tool-filter.js.map +1 -0
  52. package/dist/tools/campaigns.d.ts +7 -0
  53. package/dist/tools/campaigns.d.ts.map +1 -0
  54. package/dist/tools/campaigns.js +88 -0
  55. package/dist/tools/campaigns.js.map +1 -0
  56. package/dist/tools/catalogs.d.ts +7 -0
  57. package/dist/tools/catalogs.d.ts.map +1 -0
  58. package/dist/tools/catalogs.js +85 -0
  59. package/dist/tools/catalogs.js.map +1 -0
  60. package/dist/tools/events.d.ts +7 -0
  61. package/dist/tools/events.d.ts.map +1 -0
  62. package/dist/tools/events.js +34 -0
  63. package/dist/tools/events.js.map +1 -0
  64. package/dist/tools/experiments.d.ts +7 -0
  65. package/dist/tools/experiments.d.ts.map +1 -0
  66. package/dist/tools/experiments.js +16 -0
  67. package/dist/tools/experiments.js.map +1 -0
  68. package/dist/tools/export.d.ts +7 -0
  69. package/dist/tools/export.d.ts.map +1 -0
  70. package/dist/tools/export.js +41 -0
  71. package/dist/tools/export.js.map +1 -0
  72. package/dist/tools/index.d.ts +19 -0
  73. package/dist/tools/index.d.ts.map +1 -0
  74. package/dist/tools/index.js +44 -0
  75. package/dist/tools/index.js.map +1 -0
  76. package/dist/tools/journeys.d.ts +10 -0
  77. package/dist/tools/journeys.d.ts.map +1 -0
  78. package/dist/tools/journeys.js +25 -0
  79. package/dist/tools/journeys.js.map +1 -0
  80. package/dist/tools/lists.d.ts +7 -0
  81. package/dist/tools/lists.d.ts.map +1 -0
  82. package/dist/tools/lists.js +58 -0
  83. package/dist/tools/lists.js.map +1 -0
  84. package/dist/tools/messaging.d.ts +11 -0
  85. package/dist/tools/messaging.d.ts.map +1 -0
  86. package/dist/tools/messaging.js +103 -0
  87. package/dist/tools/messaging.js.map +1 -0
  88. package/dist/tools/snippets.d.ts +7 -0
  89. package/dist/tools/snippets.d.ts.map +1 -0
  90. package/dist/tools/snippets.js +46 -0
  91. package/dist/tools/snippets.js.map +1 -0
  92. package/dist/tools/subscriptions.d.ts +4 -0
  93. package/dist/tools/subscriptions.d.ts.map +1 -0
  94. package/dist/tools/subscriptions.js +37 -0
  95. package/dist/tools/subscriptions.js.map +1 -0
  96. package/dist/tools/templates.d.ts +7 -0
  97. package/dist/tools/templates.d.ts.map +1 -0
  98. package/dist/tools/templates.js +118 -0
  99. package/dist/tools/templates.js.map +1 -0
  100. package/dist/tools/users.d.ts +10 -0
  101. package/dist/tools/users.d.ts.map +1 -0
  102. package/dist/tools/users.js +74 -0
  103. package/dist/tools/users.js.map +1 -0
  104. package/dist/tools/webhooks.d.ts +7 -0
  105. package/dist/tools/webhooks.d.ts.map +1 -0
  106. package/dist/tools/webhooks.js +22 -0
  107. package/dist/tools/webhooks.js.map +1 -0
  108. package/dist/utils/cli-env.d.ts +11 -0
  109. package/dist/utils/cli-env.d.ts.map +1 -0
  110. package/dist/utils/cli-env.js +32 -0
  111. package/dist/utils/cli-env.js.map +1 -0
  112. package/dist/utils/endpoint-prompt.d.ts +14 -0
  113. package/dist/utils/endpoint-prompt.d.ts.map +1 -0
  114. package/dist/utils/endpoint-prompt.js +73 -0
  115. package/dist/utils/endpoint-prompt.js.map +1 -0
  116. package/dist/utils/formatting.d.ts +10 -0
  117. package/dist/utils/formatting.d.ts.map +1 -0
  118. package/dist/utils/formatting.js +12 -0
  119. package/dist/utils/formatting.js.map +1 -0
  120. package/dist/utils/password-prompt.d.ts +19 -0
  121. package/dist/utils/password-prompt.d.ts.map +1 -0
  122. package/dist/utils/password-prompt.js +37 -0
  123. package/dist/utils/password-prompt.js.map +1 -0
  124. package/dist/utils/sanitize.d.ts +13 -0
  125. package/dist/utils/sanitize.d.ts.map +1 -0
  126. package/dist/utils/sanitize.js +23 -0
  127. package/dist/utils/sanitize.js.map +1 -0
  128. package/dist/utils/tool-config.d.ts +13 -0
  129. package/dist/utils/tool-config.d.ts.map +1 -0
  130. package/dist/utils/tool-config.js +45 -0
  131. package/dist/utils/tool-config.js.map +1 -0
  132. package/dist/utils/ui.d.ts +90 -0
  133. package/dist/utils/ui.d.ts.map +1 -0
  134. package/dist/utils/ui.js +298 -0
  135. package/dist/utils/ui.js.map +1 -0
  136. package/dist/utils/url.d.ts +20 -0
  137. package/dist/utils/url.d.ts.map +1 -0
  138. package/dist/utils/url.js +53 -0
  139. package/dist/utils/url.js.map +1 -0
  140. package/package.json +105 -0
@@ -0,0 +1,725 @@
1
+ /* eslint-disable no-console */
2
+ import { exec, spawn } from "child_process";
3
+ import { existsSync, readFileSync } from "fs";
4
+ import inquirer from "inquirer";
5
+ import os from "os";
6
+ import path from "path";
7
+ import { fileURLToPath } from "url";
8
+ import { promisify } from "util";
9
+ const { dirname, join } = path;
10
+ // IMPORTANT: UI imports are loaded lazily inside functions to avoid ESM issues in Jest/CommonJS.
11
+ // Executable/package names
12
+ const LOCAL_BINARY_NAME = "iterable-mcp";
13
+ const NPX_PACKAGE_NAME = "@iterable/mcp";
14
+ // Get package version
15
+ const packageJson = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), "..", "package.json"), "utf-8"));
16
+ const TOOL_CONFIGS = {
17
+ "claude-desktop": (() => {
18
+ switch (process.platform) {
19
+ case "darwin":
20
+ return path.join(os.homedir(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
21
+ case "win32":
22
+ return path.join(process.env.APPDATA || "", "Claude", "claude_desktop_config.json");
23
+ default:
24
+ return path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config"), "Claude", "claude_desktop_config.json");
25
+ }
26
+ })(),
27
+ cursor: path.join(os.homedir(), ".cursor", "mcp.json"),
28
+ };
29
+ // Cross-platform command finder
30
+ const findCommand = async (command) => {
31
+ const finder = process.platform === "win32" ? "where" : "which";
32
+ const { stdout } = await promisify(exec)(`${finder} ${command}`);
33
+ const lines = stdout.trim().split("\n");
34
+ if (!lines?.[0])
35
+ throw new Error(`${command} not found`);
36
+ return lines[0];
37
+ };
38
+ // Auto-detect if running from a local development build
39
+ const isLocalDevelopmentBuild = () => {
40
+ const scriptPath = process.argv[1] || "";
41
+ if (scriptPath.includes("/dist/index.js")) {
42
+ return true;
43
+ }
44
+ try {
45
+ const distPath = path.resolve(process.cwd(), "dist", "index.js");
46
+ return existsSync(distPath);
47
+ }
48
+ catch {
49
+ return false;
50
+ }
51
+ };
52
+ // Build MCP config - exported for testing
53
+ export const buildMcpConfig = (options) => {
54
+ const isLocal = options.isLocal ?? isLocalDevelopmentBuild();
55
+ const nodePath = options.nodePath || process.env.ITERABLE_MCP_NODE_PATH || "node";
56
+ const npxPath = options.npxPath || process.env.ITERABLE_MCP_NPX_PATH || "npx";
57
+ return {
58
+ type: "stdio",
59
+ command: isLocal ? nodePath : npxPath,
60
+ args: isLocal
61
+ ? [path.resolve(process.cwd(), "dist", "index.js")]
62
+ : ["-y", NPX_PACKAGE_NAME],
63
+ env: options.env,
64
+ };
65
+ };
66
+ /**
67
+ * Merge selected env flags with key metadata env (if provided), preferring the key's
68
+ * persisted values. This ensures the written MCP configs reflect the authoritative
69
+ * values saved to the key after any updates in the setup flow.
70
+ */
71
+ export function resolveFinalMcpEnv(selectedEnv, keyEnv) {
72
+ const normalize = (v) => (v === "true" ? "true" : "false");
73
+ const result = { ...selectedEnv };
74
+ if (keyEnv) {
75
+ result.ITERABLE_USER_PII = normalize(keyEnv.ITERABLE_USER_PII ?? result.ITERABLE_USER_PII);
76
+ result.ITERABLE_ENABLE_WRITES = normalize(keyEnv.ITERABLE_ENABLE_WRITES ?? result.ITERABLE_ENABLE_WRITES);
77
+ result.ITERABLE_ENABLE_SENDS = normalize(keyEnv.ITERABLE_ENABLE_SENDS ?? result.ITERABLE_ENABLE_SENDS);
78
+ }
79
+ result.ITERABLE_USER_PII = normalize(result.ITERABLE_USER_PII);
80
+ result.ITERABLE_ENABLE_WRITES = normalize(result.ITERABLE_ENABLE_WRITES);
81
+ result.ITERABLE_ENABLE_SENDS = normalize(result.ITERABLE_ENABLE_SENDS);
82
+ return result;
83
+ }
84
+ /**
85
+ * Pick only permission-related env flags for persistence into key metadata.
86
+ */
87
+ export function pickPersistablePermissionEnv(env) {
88
+ const normalize = (v) => (v === "true" ? "true" : "false");
89
+ return {
90
+ ITERABLE_USER_PII: normalize(env.ITERABLE_USER_PII),
91
+ ITERABLE_ENABLE_WRITES: normalize(env.ITERABLE_ENABLE_WRITES),
92
+ ITERABLE_ENABLE_SENDS: normalize(env.ITERABLE_ENABLE_SENDS),
93
+ };
94
+ }
95
+ /**
96
+ * Enforce that Sends require Writes. If Sends is enabled but Writes is disabled,
97
+ * disable Sends and optionally emit a warning.
98
+ */
99
+ export function enforceSendsRequiresWrites(env, warn) {
100
+ if (env.ITERABLE_ENABLE_SENDS === "true" &&
101
+ env.ITERABLE_ENABLE_WRITES !== "true") {
102
+ env.ITERABLE_ENABLE_SENDS = "false";
103
+ if (warn)
104
+ warn("Sends require Writes. Disabling Sends.");
105
+ }
106
+ return env;
107
+ }
108
+ /**
109
+ * Security defaults used for Privacy & Security prompts.
110
+ * Always conservative: all disabled by default.
111
+ */
112
+ export function getSecurityDefaults(_activeMeta) {
113
+ return { defaultPii: false, defaultWrites: false, defaultSends: false };
114
+ }
115
+ export const setupMcpServer = async () => {
116
+ const args = process.argv.slice(2);
117
+ const showHelp = args.includes("--help") || args.includes("-h");
118
+ const advanced = args.includes("--advanced");
119
+ let tools = [
120
+ ...(args.includes("--claude-desktop") ? ["claude-desktop"] : []),
121
+ ...(args.includes("--cursor") ? ["cursor"] : []),
122
+ ...(args.includes("--claude-code") ? ["claude-code"] : []),
123
+ ];
124
+ // Detect how the command was invoked
125
+ const isNpx = process.argv[1]?.includes("npx") ||
126
+ process.env.npm_execpath?.includes("npx");
127
+ const commandName = isNpx ? `npx ${NPX_PACKAGE_NAME}` : LOCAL_BINARY_NAME;
128
+ if (showHelp) {
129
+ const { createTable, icons, showBox, showIterableLogo, showSection } = await import("./utils/ui.js");
130
+ const chalk = (await import("chalk")).default;
131
+ console.clear();
132
+ showIterableLogo(packageJson.version);
133
+ showSection("Setup Commands", icons.rocket);
134
+ console.log();
135
+ const setupTable = createTable({
136
+ head: ["Command", "Description"],
137
+ colWidths: [45, 40],
138
+ style: "normal",
139
+ });
140
+ setupTable.push([`${commandName} setup --claude-desktop`, "Configure for Claude Desktop"], [`${commandName} setup --cursor`, "Configure for Cursor"], [`${commandName} setup --claude-code`, "Configure for Claude Code"], [
141
+ `${commandName} setup --cursor --claude-desktop`,
142
+ "Configure multiple tools",
143
+ ], [`${commandName} setup --cursor --debug`, "Enable debug logging"]);
144
+ console.log(setupTable.toString());
145
+ console.log();
146
+ console.log();
147
+ showSection("Key Management" + chalk.gray(" (macOS only)"), icons.key);
148
+ console.log();
149
+ const keysTable = createTable({
150
+ head: ["Command", "Description"],
151
+ colWidths: [45, 40],
152
+ style: "normal",
153
+ });
154
+ keysTable.push([`${commandName} keys list`, "View all stored API keys"], [`${commandName} keys add`, "Add a new API key"], [`${commandName} keys activate <name>`, "Switch to a different key"], [`${commandName} keys delete <id>`, "Remove a key by ID"]);
155
+ console.log(keysTable.toString());
156
+ console.log();
157
+ console.log();
158
+ showSection("Environment Variables", icons.globe);
159
+ console.log();
160
+ const envTable = createTable({
161
+ head: ["Variable", "Description"],
162
+ colWidths: [30, 50],
163
+ style: "compact",
164
+ });
165
+ envTable.push(["ITERABLE_API_KEY", chalk.gray("Your Iterable API key")], [
166
+ "ITERABLE_BASE_URL",
167
+ chalk.gray("API endpoint (default: api.iterable.com)"),
168
+ ], [
169
+ "ITERABLE_USER_PII",
170
+ chalk.gray("Enable user PII access (default: false)"),
171
+ ], [
172
+ "ITERABLE_ENABLE_WRITES",
173
+ chalk.gray("Enable write operations (default: false)"),
174
+ ], [
175
+ "ITERABLE_ENABLE_SENDS",
176
+ chalk.gray("Enable message sending (default: false)"),
177
+ ], ["ITERABLE_MCP_NODE_PATH", chalk.gray("Custom node executable path")], ["ITERABLE_MCP_NPX_PATH", chalk.gray("Custom npx executable path")]);
178
+ console.log(envTable.toString());
179
+ console.log();
180
+ showBox("Quick Start", [
181
+ chalk.white.bold("1. Run setup for your AI tool:"),
182
+ chalk.cyan(` ${commandName} setup --cursor`),
183
+ "",
184
+ chalk.white.bold("2. Restart your AI tool"),
185
+ "",
186
+ chalk.white.bold("3. Start building!"),
187
+ chalk.gray(" Try: 'list my Iterable campaigns'"),
188
+ ], { icon: icons.zap, theme: "success" });
189
+ showBox("Security Features", [
190
+ "• API keys prompted interactively (never in shell history)",
191
+ "• macOS: Keys stored securely in Keychain",
192
+ "• Each key coupled to its endpoint (US/EU/custom)",
193
+ "• Windows/Linux: Use ITERABLE_API_KEY environment variable",
194
+ ], { icon: icons.lock, theme: "info", padding: 1 });
195
+ return;
196
+ }
197
+ // New behavior: if no tools were provided as flags, prompt user to select one or more
198
+ if (tools.length === 0) {
199
+ // Show logo first when no flags provided
200
+ const { showIterableLogo } = await import("./utils/ui.js");
201
+ console.clear();
202
+ showIterableLogo(packageJson.version);
203
+ const { selectedTools } = await inquirer.prompt([
204
+ {
205
+ type: "checkbox",
206
+ name: "selectedTools",
207
+ message: "Select your AI tools to configure:",
208
+ choices: [
209
+ { name: "Cursor", value: "cursor" },
210
+ { name: "Claude Desktop", value: "claude-desktop" },
211
+ { name: "Claude Code (CLI)", value: "claude-code" },
212
+ ],
213
+ validate: (arr) => Array.isArray(arr) && arr.length > 0
214
+ ? true
215
+ : "Please select at least one tool",
216
+ },
217
+ ]);
218
+ tools = selectedTools;
219
+ }
220
+ // Start the setup flow
221
+ console.clear();
222
+ const { formatKeyValue, icons, showCompletion, showError, showInfo, showIterableLogo, showSection, showSuccess, showWarning, } = await import("./utils/ui.js");
223
+ const chalk = (await import("chalk")).default;
224
+ showIterableLogo(packageJson.version);
225
+ // Succinct overview of what will be configured
226
+ const prettyName = (t) => t === "cursor"
227
+ ? "Cursor"
228
+ : t === "claude-desktop"
229
+ ? "Claude Desktop"
230
+ : "Claude Code";
231
+ console.log(chalk.gray(`Running setup to configure: ${tools.map(prettyName).join(", ")}`));
232
+ console.log();
233
+ const ora = (await import("ora")).default;
234
+ const spinner = ora();
235
+ try {
236
+ // Step 1: API Key Configuration
237
+ console.log();
238
+ showSection("API Key Configuration", icons.key);
239
+ console.log();
240
+ let apiKey;
241
+ let keyName;
242
+ let baseUrl;
243
+ let usedKeyName;
244
+ let usedExistingKey = false;
245
+ let selectedExistingMeta;
246
+ // 1) Offer using API key from environment
247
+ if (process.env.ITERABLE_API_KEY) {
248
+ const { useEnvKey } = await inquirer.prompt([
249
+ {
250
+ type: "confirm",
251
+ name: "useEnvKey",
252
+ message: "Found API key in ITERABLE_API_KEY environment variable. Import and use this key?",
253
+ default: true,
254
+ },
255
+ ]);
256
+ if (useEnvKey) {
257
+ apiKey = process.env.ITERABLE_API_KEY;
258
+ showSuccess("Using API key from environment variable");
259
+ }
260
+ }
261
+ // 2) If not using env key, offer using an existing Keychain key (macOS)
262
+ if (!apiKey && process.platform === "darwin") {
263
+ const { getKeyManager } = await import("./key-manager.js");
264
+ const km = getKeyManager();
265
+ const keys = await km.listKeys().catch(() => []);
266
+ if (Array.isArray(keys) && keys.length > 0) {
267
+ const { useExisting } = await inquirer.prompt([
268
+ {
269
+ type: "confirm",
270
+ name: "useExisting",
271
+ message: `Found ${keys.length} existing Iterable API key${keys.length === 1 ? "" : "s"} in macOS Keychain. Use one of these?`,
272
+ default: true,
273
+ },
274
+ ]);
275
+ if (useExisting) {
276
+ const { formatKeychainChoiceLabel } = await import("./utils/ui.js");
277
+ const { chosenId } = await inquirer.prompt([
278
+ {
279
+ type: "list",
280
+ name: "chosenId",
281
+ message: "Select a stored key:",
282
+ choices: keys.map((k) => ({
283
+ name: formatKeychainChoiceLabel(k.name, k.baseUrl, k.isActive, k.env),
284
+ value: k.id,
285
+ short: k.name,
286
+ })),
287
+ pageSize: Math.min(10, keys.length),
288
+ },
289
+ ]);
290
+ try {
291
+ const meta = keys.find((k) => k.id === chosenId);
292
+ if (!meta)
293
+ throw new Error("Selected key metadata not found");
294
+ baseUrl = meta.baseUrl;
295
+ usedKeyName = meta.name;
296
+ usedExistingKey = true;
297
+ selectedExistingMeta = meta;
298
+ showSuccess(`Using existing key "${meta.name}"`);
299
+ }
300
+ catch (e) {
301
+ showError(e instanceof Error
302
+ ? e.message
303
+ : "Failed to load selected key from Keychain");
304
+ process.exit(1);
305
+ }
306
+ }
307
+ }
308
+ }
309
+ // 3) If neither env nor existing key chosen, prompt for a new key
310
+ if (!apiKey && !usedExistingKey) {
311
+ const { newApiKey } = await inquirer.prompt([
312
+ {
313
+ type: "password",
314
+ name: "newApiKey",
315
+ message: "Enter your Iterable API key:",
316
+ validate: (input) => {
317
+ if (!input)
318
+ return "API key is required";
319
+ if (!/^[a-f0-9]{32}$/.test(input))
320
+ return "API key must be a 32-character lowercase hexadecimal string";
321
+ return true;
322
+ },
323
+ mask: "*",
324
+ },
325
+ ]);
326
+ apiKey = newApiKey;
327
+ }
328
+ // Step 2: API Endpoint Selection
329
+ console.log();
330
+ showSection("API Endpoint Configuration", icons.globe);
331
+ console.log();
332
+ if (!baseUrl) {
333
+ if (process.env.ITERABLE_BASE_URL) {
334
+ baseUrl = process.env.ITERABLE_BASE_URL;
335
+ showSuccess(`Using API endpoint from environment: ${baseUrl}`);
336
+ }
337
+ else {
338
+ const { promptForIterableBaseUrl } = await import("./utils/endpoint-prompt.js");
339
+ try {
340
+ baseUrl = await promptForIterableBaseUrl({
341
+ inquirer,
342
+ icons,
343
+ chalk,
344
+ showError,
345
+ });
346
+ }
347
+ catch {
348
+ process.exit(1);
349
+ }
350
+ }
351
+ }
352
+ else {
353
+ showSuccess(`Using API endpoint from selected key: ${baseUrl}`);
354
+ }
355
+ console.log();
356
+ console.log(formatKeyValue("Selected endpoint", baseUrl, chalk.cyan));
357
+ console.log();
358
+ // Step 3: Store the key securely (macOS only). On other platforms, use env vars.
359
+ let existingKeyWithValue = null;
360
+ if (process.platform === "darwin") {
361
+ const { getKeyManager } = await import("./key-manager.js");
362
+ const keyManager = getKeyManager();
363
+ spinner.start("Initializing secure key storage...");
364
+ await keyManager.initialize();
365
+ spinner.succeed("Key storage ready");
366
+ // Check if key already exists
367
+ if (!usedExistingKey && apiKey) {
368
+ spinner.start("Checking for existing keys...");
369
+ existingKeyWithValue = await keyManager.findKeyByValue(apiKey);
370
+ spinner.stop();
371
+ }
372
+ if (usedExistingKey) {
373
+ // Offer to activate the selected existing key if not already active
374
+ if (!selectedExistingMeta?.isActive) {
375
+ const { activateExisting } = await inquirer.prompt([
376
+ {
377
+ type: "confirm",
378
+ name: "activateExisting",
379
+ message: `Set "${selectedExistingMeta?.name}" as your active API key?`,
380
+ default: true,
381
+ },
382
+ ]);
383
+ if (activateExisting) {
384
+ await keyManager.setActiveKey(selectedExistingMeta.id);
385
+ showSuccess(`"${selectedExistingMeta?.name}" is now your active key`);
386
+ }
387
+ else {
388
+ showInfo("Keeping your current active key");
389
+ }
390
+ }
391
+ else {
392
+ showSuccess(`"${selectedExistingMeta?.name}" is already your active key`);
393
+ }
394
+ }
395
+ else if (existingKeyWithValue) {
396
+ showInfo(`This API key is already stored as "${existingKeyWithValue.name}"`);
397
+ usedKeyName = existingKeyWithValue.name;
398
+ if (!existingKeyWithValue.isActive) {
399
+ const { activateExisting } = await inquirer.prompt([
400
+ {
401
+ type: "confirm",
402
+ name: "activateExisting",
403
+ message: `Set "${existingKeyWithValue.name}" as your active API key?`,
404
+ default: true,
405
+ },
406
+ ]);
407
+ if (activateExisting) {
408
+ await keyManager.setActiveKey(existingKeyWithValue.id);
409
+ showSuccess(`"${existingKeyWithValue.name}" is now your active key`);
410
+ }
411
+ else {
412
+ showInfo("Keeping your current active key");
413
+ }
414
+ }
415
+ else {
416
+ showSuccess(`"${existingKeyWithValue.name}" is already your active key`);
417
+ }
418
+ }
419
+ else {
420
+ // New key - prompt for name
421
+ const { newKeyName } = await inquirer.prompt([
422
+ {
423
+ type: "input",
424
+ name: "newKeyName",
425
+ message: "Enter a name for this API key:",
426
+ default: "default",
427
+ validate: (input) => {
428
+ if (input && input.length > 50)
429
+ return "Key name must be 50 characters or less";
430
+ return true;
431
+ },
432
+ },
433
+ ]);
434
+ keyName = newKeyName || "default";
435
+ spinner.start("Storing API key securely in macOS Keychain...");
436
+ try {
437
+ const newKeyId = await keyManager.addKey(keyName, apiKey, baseUrl);
438
+ spinner.succeed("API key stored successfully!");
439
+ console.log();
440
+ console.log(formatKeyValue("Name", keyName, chalk.white.bold));
441
+ console.log(formatKeyValue("ID", newKeyId, chalk.gray));
442
+ console.log(formatKeyValue("Endpoint", baseUrl, chalk.cyan));
443
+ console.log();
444
+ usedKeyName = keyName;
445
+ // Check if we should activate this key
446
+ const allKeys = await keyManager.listKeys();
447
+ if (allKeys.length > 1) {
448
+ const { activateNew } = await inquirer.prompt([
449
+ {
450
+ type: "confirm",
451
+ name: "activateNew",
452
+ message: `Set "${keyName}" as your active API key?`,
453
+ default: true,
454
+ },
455
+ ]);
456
+ if (activateNew) {
457
+ await keyManager.setActiveKey(newKeyId);
458
+ showSuccess(`"${keyName}" is now your active key`);
459
+ }
460
+ else {
461
+ showInfo(`Keeping your current active key. Run 'iterable-mcp keys activate "${keyName}"' to switch later.`);
462
+ }
463
+ }
464
+ }
465
+ catch (error) {
466
+ const { sanitizeString } = await import("./utils/sanitize.js");
467
+ spinner.fail("Failed to store API key");
468
+ console.log();
469
+ const msg = error instanceof Error
470
+ ? sanitizeString(error.message)
471
+ : "Unknown error";
472
+ showError(msg);
473
+ console.log();
474
+ showInfo("Your API key was not stored. You can re-run 'iterable-mcp setup' to try again.");
475
+ showInfo("If the problem persists, verify Keychain access and disk permissions.");
476
+ process.exit(1);
477
+ }
478
+ }
479
+ }
480
+ else {
481
+ // Non-macOS: do not use Keychain; rely on environment variables instead
482
+ usedKeyName =
483
+ usedKeyName ||
484
+ (process.env.ITERABLE_API_KEY ? "Environment" : undefined);
485
+ }
486
+ // Step 4: Privacy & Security Settings (Basic by default; Advanced via --advanced)
487
+ console.log();
488
+ showSection("Privacy & Security Settings", icons.lock);
489
+ console.log();
490
+ // Default conservative env flags
491
+ let selectedEnv = {
492
+ ITERABLE_USER_PII: "false",
493
+ ITERABLE_ENABLE_WRITES: "false",
494
+ ITERABLE_ENABLE_SENDS: "false",
495
+ };
496
+ if (advanced) {
497
+ const { permFlags } = await inquirer.prompt([
498
+ {
499
+ type: "checkbox",
500
+ name: "permFlags",
501
+ message: "Select permissions to enable (default: none):",
502
+ choices: [
503
+ { name: "View PII (access user data)", value: "pii" },
504
+ {
505
+ name: "Write operations (create/update/delete)",
506
+ value: "writes",
507
+ },
508
+ { name: "Sends (campaigns/journeys/events)", value: "sends" },
509
+ ],
510
+ pageSize: 6,
511
+ default: [],
512
+ },
513
+ ]);
514
+ selectedEnv = {
515
+ ITERABLE_USER_PII: permFlags.includes("pii")
516
+ ? "true"
517
+ : "false",
518
+ ITERABLE_ENABLE_WRITES: permFlags.includes("writes")
519
+ ? "true"
520
+ : "false",
521
+ ITERABLE_ENABLE_SENDS: permFlags.includes("sends")
522
+ ? "true"
523
+ : "false",
524
+ };
525
+ }
526
+ let mcpEnv = {
527
+ ITERABLE_USER_PII: selectedEnv.ITERABLE_USER_PII,
528
+ ITERABLE_ENABLE_WRITES: selectedEnv.ITERABLE_ENABLE_WRITES,
529
+ ITERABLE_ENABLE_SENDS: selectedEnv.ITERABLE_ENABLE_SENDS,
530
+ };
531
+ // On non-macOS, include API key and base URL in the written config
532
+ if (process.platform !== "darwin") {
533
+ if (apiKey)
534
+ mcpEnv.ITERABLE_API_KEY = apiKey;
535
+ if (baseUrl)
536
+ mcpEnv.ITERABLE_BASE_URL = baseUrl;
537
+ }
538
+ if (args.includes("--debug")) {
539
+ mcpEnv.ITERABLE_DEBUG = "true";
540
+ mcpEnv.LOG_LEVEL = "debug";
541
+ }
542
+ // Enforce permission invariants before displaying
543
+ mcpEnv = enforceSendsRequiresWrites(mcpEnv, (msg) => showWarning(msg));
544
+ console.log();
545
+ console.log(formatKeyValue("User PII Access", mcpEnv.ITERABLE_USER_PII === "true"
546
+ ? chalk.green("Enabled")
547
+ : chalk.gray("Disabled")));
548
+ console.log(formatKeyValue("Write Operations", mcpEnv.ITERABLE_ENABLE_WRITES === "true"
549
+ ? chalk.green("Enabled")
550
+ : chalk.gray("Disabled")));
551
+ console.log(formatKeyValue("Sends", mcpEnv.ITERABLE_ENABLE_SENDS === "true"
552
+ ? chalk.green("Enabled")
553
+ : chalk.gray("Disabled")));
554
+ // Only show the default note when all flags are disabled
555
+ if (mcpEnv.ITERABLE_USER_PII !== "true" &&
556
+ mcpEnv.ITERABLE_ENABLE_WRITES !== "true" &&
557
+ mcpEnv.ITERABLE_ENABLE_SENDS !== "true") {
558
+ console.log();
559
+ console.log(chalk.gray("Note: These are disabled by default. If you absolutely need to enable these, run 'iterable-mcp setup --advanced'"));
560
+ }
561
+ else {
562
+ console.log();
563
+ showWarning("Warning: Elevated capabilities enabled. Sends, writes, and/or PII access may now occur.");
564
+ }
565
+ console.log();
566
+ // If we used an existing key, update its env overrides with chosen settings
567
+ if (usedKeyName && process.platform === "darwin") {
568
+ try {
569
+ const { getKeyManager } = await import("./key-manager.js");
570
+ const keyManager = getKeyManager();
571
+ await keyManager.initialize();
572
+ await keyManager.updateKeyEnv(usedKeyName, pickPersistablePermissionEnv(mcpEnv));
573
+ // Re-read the key metadata and prefer its persisted env when writing
574
+ // tool configurations. This addresses cases where the key was activated
575
+ // during setup and ensures configs reflect the updated values.
576
+ const keys = await keyManager.listKeys();
577
+ const updatedMeta = keys.find((k) => k.name === usedKeyName || k.id === usedKeyName);
578
+ if (updatedMeta?.env) {
579
+ mcpEnv = resolveFinalMcpEnv(mcpEnv, updatedMeta.env);
580
+ }
581
+ }
582
+ catch (err) {
583
+ if (process.env.ITERABLE_DEBUG === "true") {
584
+ console.warn("Non-fatal: failed to persist per-key env settings", err);
585
+ }
586
+ }
587
+ }
588
+ // Enforce again after merging persisted key env
589
+ mcpEnv = enforceSendsRequiresWrites(mcpEnv, (msg) => showWarning(msg));
590
+ // Step 5: Configure AI Tools
591
+ console.log();
592
+ showSection("Configuring AI Tools", icons.zap);
593
+ console.log();
594
+ const iterableMcpConfig = buildMcpConfig({
595
+ ...(args.includes("--local") && { isLocal: true }),
596
+ env: mcpEnv,
597
+ });
598
+ // Preflight confirmation summary before applying changes
599
+ const prettyName = (t) => t === "cursor"
600
+ ? "Cursor"
601
+ : t === "claude-desktop"
602
+ ? "Claude Desktop"
603
+ : "Claude Code";
604
+ console.log(chalk.gray("Summary:"));
605
+ console.log(formatKeyValue("Tools", tools.map(prettyName).join(", "), chalk.white));
606
+ if (usedKeyName) {
607
+ console.log(formatKeyValue("API Key", usedKeyName, chalk.white));
608
+ }
609
+ console.log(formatKeyValue("Endpoint", baseUrl, chalk.cyan));
610
+ console.log(formatKeyValue("User PII", mcpEnv.ITERABLE_USER_PII === "true"
611
+ ? chalk.green("Enabled")
612
+ : chalk.gray("Disabled")));
613
+ console.log(formatKeyValue("Writes", mcpEnv.ITERABLE_ENABLE_WRITES === "true"
614
+ ? chalk.green("Enabled")
615
+ : chalk.gray("Disabled")));
616
+ console.log(formatKeyValue("Sends", mcpEnv.ITERABLE_ENABLE_SENDS === "true"
617
+ ? chalk.green("Enabled")
618
+ : chalk.gray("Disabled")));
619
+ console.log();
620
+ const { proceed } = await inquirer.prompt([
621
+ {
622
+ type: "confirm",
623
+ name: "proceed",
624
+ message: "Proceed with configuration?",
625
+ default: true,
626
+ },
627
+ ]);
628
+ if (!proceed) {
629
+ showInfo("Setup cancelled by user. No changes were made.");
630
+ return;
631
+ }
632
+ const fileBasedTools = tools.filter((tool) => tool !== "claude-code");
633
+ const needsClaudeCode = tools.includes("claude-code");
634
+ if (fileBasedTools.length > 0) {
635
+ const { updateToolConfig } = await import("./utils/tool-config.js");
636
+ for (const tool of fileBasedTools) {
637
+ const configPath = TOOL_CONFIGS[tool];
638
+ const toolName = tool === "claude-desktop" ? "Claude Desktop" : "Cursor";
639
+ spinner.start(`Configuring ${toolName}...`);
640
+ try {
641
+ await updateToolConfig(configPath, iterableMcpConfig);
642
+ spinner.succeed(`${toolName} configured successfully`);
643
+ }
644
+ catch (error) {
645
+ spinner.fail(`Failed to configure ${toolName}`);
646
+ throw error;
647
+ }
648
+ }
649
+ }
650
+ if (needsClaudeCode) {
651
+ spinner.start("Checking for Claude CLI...");
652
+ try {
653
+ await findCommand("claude");
654
+ spinner.succeed("Claude CLI found");
655
+ }
656
+ catch {
657
+ spinner.fail("Claude CLI not found");
658
+ showError("Please install Claude Code first: https://docs.claude.com/en/docs/claude-code/overview");
659
+ console.log();
660
+ showInfo("After installing, re-run: iterable-mcp setup --claude-code");
661
+ process.exit(1);
662
+ }
663
+ const configJson = JSON.stringify(iterableMcpConfig);
664
+ // Remove existing config
665
+ try {
666
+ await promisify(exec)("claude mcp remove iterable");
667
+ }
668
+ catch {
669
+ // Ignore errors
670
+ }
671
+ spinner.start("Configuring Claude Code...");
672
+ await new Promise((resolve, reject) => {
673
+ const child = spawn("claude", ["mcp", "add-json", "iterable", configJson], {
674
+ stdio: "inherit",
675
+ });
676
+ child.on("close", (code) => {
677
+ if (code === 0) {
678
+ resolve();
679
+ }
680
+ else {
681
+ reject(new Error(`claude mcp add-json exited with code ${code ?? "unknown"}`));
682
+ }
683
+ });
684
+ child.on("error", reject);
685
+ });
686
+ spinner.succeed("Claude Code configured successfully");
687
+ }
688
+ // Build configured tools list
689
+ const configuredTools = [];
690
+ if (fileBasedTools.includes("cursor"))
691
+ configuredTools.push("Cursor");
692
+ if (fileBasedTools.includes("claude-desktop"))
693
+ configuredTools.push("Claude Desktop");
694
+ if (needsClaudeCode)
695
+ configuredTools.push("Claude Code");
696
+ const toolsList = configuredTools.length === 1
697
+ ? configuredTools[0]
698
+ : configuredTools.length === 2
699
+ ? `${configuredTools[0]} and ${configuredTools[1]}`
700
+ : configuredTools.slice(0, -1).join(", ") +
701
+ ", and " +
702
+ configuredTools[configuredTools.length - 1];
703
+ // Success!
704
+ console.log();
705
+ const nextSteps = [
706
+ `Restart ${toolsList} to load the new configuration`,
707
+ "Start using Iterable MCP tools in your conversations",
708
+ ...(needsClaudeCode
709
+ ? ["Run 'claude mcp list' to verify your setup"]
710
+ : []),
711
+ ];
712
+ const tips = [
713
+ `Try: 'list my Iterable campaigns' in ${toolsList}`,
714
+ "Manage keys with 'iterable-mcp keys list'",
715
+ "Switch keys with 'iterable-mcp keys activate <name>'",
716
+ ];
717
+ showCompletion("Setup Complete!", nextSteps, tips);
718
+ }
719
+ catch (error) {
720
+ spinner.stop();
721
+ showError(error instanceof Error ? error.message : "Setup failed");
722
+ process.exit(1);
723
+ }
724
+ };
725
+ //# sourceMappingURL=install.js.map